This R Notebook is the complement to my blog post Predicting And Mapping Arrest Types in San Francisco with LightGBM, R, ggplot2.

This notebook is licensed under the MIT License. If you use the code or data visualization designs contained within this notebook, it would be greatly appreciated if proper attribution is given back to this notebook and/or myself. Thanks! :)

1 Setup

Setup the R packages.

library(lightgbm)
library(Matrix)
library(caret)
library(viridis)
library(ggmap)
library(randomcoloR)
source("Rstart.R")
sessionInfo()
R version 3.3.2 (2016-10-31)
Platform: x86_64-apple-darwin13.4.0 (64-bit)
Running under: macOS Sierra 10.12.3

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

attached base packages:
[1] grid      stats     graphics  grDevices utils    
[6] datasets  methods   base     

other attached packages:
 [1] stringr_1.1.0      digest_0.6.11      RColorBrewer_1.1-2
 [4] scales_0.4.1       extrafont_0.17     dplyr_0.5.0       
 [7] readr_1.0.0        ggmap_2.7          viridis_0.3.4     
[10] caret_6.0-73       ggplot2_2.2.1      lattice_0.20-34   
[13] Matrix_1.2-7.1     lightgbm_0.1       R6_2.2.0          

loaded via a namespace (and not attached):
 [1] reshape2_1.4.2     splines_3.3.2      colorspace_1.3-2  
 [4] stats4_3.3.2       mgcv_1.8-16        ModelMetrics_1.1.0
 [7] nloptr_1.0.4       DBI_0.5-1          sp_1.2-4          
[10] jpeg_0.1-8         foreach_1.4.3      plyr_1.8.4        
[13] MatrixModels_0.4-1 munsell_0.4.3      gtable_0.2.0      
[16] RgoogleMaps_1.4.1  mapproj_1.2-4      codetools_0.2-15  
[19] knitr_1.15.1       SparseM_1.74       quantreg_5.29     
[22] pbkrtest_0.4-6     parallel_3.3.2     Rttf2pt1_1.3.4    
[25] proto_1.0.0        Rcpp_0.12.8        geosphere_1.5-5   
[28] lme4_1.1-12        gridExtra_2.2.1    rjson_0.2.15      
[31] png_0.1-7          stringi_1.1.2      tools_3.3.2       
[34] bitops_1.0-6       magrittr_1.5       maps_3.1.1        
[37] lazyeval_0.2.0     tibble_1.2         car_2.1-4         
[40] extrafontdb_1.0    MASS_7.3-45        data.table_1.10.0 
[43] assertthat_0.1     minqa_1.2.4        iterators_1.0.8   
[46] nnet_7.3-12        nlme_3.1-128      

Import data, and only keep relevant columns. Filter on Arrests only.

The data must be randomized for lightgbm to give unbiased scores. Can do with dplyr’s sample_frac.

# seed for sample_frac()
set.seed(123)
df <- df %>% sample_frac()
df %>% head()

There are 634,299 arrests in this dataset.

2 Feature Engineering

Engineer features for lightgbm.

2.1 Month, Hour, Year

Year is # years since the lowest year (in this case, 2003, as noted in the dataset title)

df <- df %>%
        mutate(month = factor(substring(Date, 1, 2)),
                hour = factor(substring(Time, 1, 2)),
                year = as.numeric(substring(Date, 7, 10)))
There were 50 or more warnings (use warnings() to see the first 50)
df %>% select(month, hour, year) %>% head()

2.2 Existing DayOfWeek to Factor

Change DayOfWeek to Factor.

Since column become encoded as numeric instead of categorical, encode order of numerals such that Saturday and Sunday are adjacent for proper lte/gte behavior.

dow_order <- c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
df <- df %>%
        mutate(DayOfWeek = factor(DayOfWeek, levels=dow_order))
df %>% select(DayOfWeek) %>% head()

2.3 Category Indices

Map the category to an index. Labels must be zero-indexed.

df <- df %>%
        mutate(category_index = as.numeric(factor(Category)) - 1)
df %>% select(category_index, Category) %>% head()

3 lightgbm Training

Use LightGBM’s categorical data feature for optimial performance.

Use caret for train/test splitting since createDataPartition ensures balanced distribution of categories between train and test.

# declare categorical feature names
categoricals <- NULL
# proportion of data to train on
split <- 0.7
set.seed(123)
trainIndex <- createDataPartition(df$category_index, p = split, list = FALSE, times = 1)
dtrain <- lgb.Dataset((df %>% select(X, Y, hour, month, year, DayOfWeek) %>% data.matrix())[trainIndex,],
                     colnames = c("X", "Y", "hour", "month", "year", "DayOfWeek"),
                     categorical_feature = categoricals,
                     label = df$category_index[trainIndex], free_raw_data=T)
dtest <- lgb.Dataset.create.valid(dtrain,
                                  (df %>% select(X, Y, hour, month, year, DayOfWeek) %>% data.matrix())[-trainIndex,],
                                  label = df$category_index[-trainIndex])
params <- list(objective = "multiclass", metric = "multi_logloss")
valids <- list(test=dtest)
num_classes <- length(unique(df$category_index))
# preformat sizes for use in data visualizations later
train_size_format <- length(trainIndex) %>% format(big.mark=",")
test_size_format <- (df %>% nrow() - length(trainIndex)) %>% format(big.mark=",")

The size of the training set is 444,011 and the size of the test set is 190,288.

# determine elapsed runtime 
system.time(
# training output not printed to notebook since spammy. (verbose = 0 + record = T)
bst <- lgb.train(params,
                dtrain,
                nrounds = 500,
                valids,
                num_threads = 4,
                num_class = num_classes,
                verbose = 0,
                record = T,
                early_stopping_rounds = 5,
                categorical_feature = categoricals
                )
)[3]
elapsed 
219.019 
# multilogloss of final iteration on test set
paste("# Rounds:", bst$current_iter())
[1] "# Rounds: 147"
paste("Multilogloss of best model:", bst$record_evals$test$multi_logloss$eval %>% unlist() %>% tail(1))
[1] "Multilogloss of best model: 1.97979254519616"

Calculate variable importance. (note: takes awhile since single-threaded)

df_imp <- tbl_df(lgb.importance(bst, percentage = TRUE))
Group 1 summed to more than type 'integer' can hold so the result has been coerced to 'numeric' automatically, for convenience.
df_imp

preds is a 1D vector of probabilities for each vector, of nrows x nclasses. Reshape accordingly and iterate through for the predicted label (label with the largest probability) and the corresponding probability.

test <- (df %>% select(X, Y, hour, month, year, DayOfWeek) %>% data.matrix())[-trainIndex,]
preds_matrix <- predict(bst, test, reshape=T)
preds_cor <- cor(preds_matrix)
preds_matrix[1:2,]
             [,1]       [,2]         [,3]         [,4]        [,5]       [,6]       [,7]
[1,] 0.0013608454 0.08942618 5.662892e-05 0.0004694346 0.056512186 0.03050866 0.00103404
[2,] 0.0008612873 0.13593373 2.485057e-05 0.0019040408 0.005572041 0.00338940 0.02117221
           [,8]        [,9]        [,10]        [,11]        [,12]       [,13]
[1,] 0.06979163 0.005465058 7.687412e-05 4.486161e-05 0.0002551846 0.004679881
[2,] 0.10628417 0.017717549 4.029097e-04 8.352999e-05 0.0009054910 0.004290717
           [,14]        [,15]       [,16]       [,17]        [,18]        [,19]
[1,] 0.004730996 1.015853e-04 0.001967209 0.030442149 0.0007290578 0.0125450954
[2,] 0.004323221 5.753834e-05 0.007895822 0.005553753 0.0027784636 0.0004657167
           [,20]       [,21]     [,22]        [,23]        [,24]        [,25]      [,26]
[1,] 0.002873131 0.002140685 0.2488873 8.046594e-05 0.0025070133 6.230313e-05 0.01176494
[2,] 0.003402504 0.004404593 0.4764304 4.563271e-05 0.0004264386 2.261202e-04 0.06097371
            [,27]       [,28]      [,29]        [,30]       [,31]        [,32]
[1,] 2.049359e-05 0.035559700 0.01084553 2.862660e-05 0.032270854 4.121095e-05
[2,] 2.255887e-05 0.007379504 0.00127614 9.229268e-05 0.007677787 5.485632e-05
           [,33]        [,34]       [,35]      [,36]       [,37]      [,38]       [,39]
[1,] 0.001107487 2.752509e-05 0.034151278 0.02489355 0.019603917 0.25434873 0.008587664
[2,] 0.001217908 1.290809e-05 0.004451146 0.01504172 0.009998015 0.07428322 0.012966080
# likely not most efficient method
results <- t(apply(preds_matrix, 1, function (x) {
  max_index = which(x==max(x))
  return (c(max_index-1, x[max_index]))
}))
df_results <- data.frame(results, label_act = df$category_index[-trainIndex]) %>%
                tbl_df() %>%
                transmute(label_pred = X1, prod_pred = X2, label_act)
df_results %>% arrange(desc(prod_pred)) %>% head(20)
rm(preds_matrix)

Confusion matrix:

cm <- confusionMatrix(df_results$label_pred, df_results$label_act)
longer object length is not a multiple of shorter object lengthLevels are not in the same order for reference and data. Refactoring data to match.
data.frame(cm$overall)

4 Visualizations

4.1 Importance Bar Chart

df_imp$Feature <- factor(df_imp$Feature, levels=rev(df_imp$Feature))
plot <- ggplot(df_imp, aes(x=Feature, y=Gain)) +
          geom_bar(stat="identity", fill="#34495e", alpha=0.9) +
          geom_text(aes(label=sprintf("%0.1f%%", Gain*100)), color="#34495e", hjust=-0.25, family="Open Sans Condensed Bold", size=2.5) +
          fte_theme() +
          coord_flip() +
          scale_y_continuous(limits = c(0, 0.4), labels=percent) +
   theme(plot.title=element_text(hjust=0.5), axis.title.y=element_blank()) +
          labs(title="Feature Importance for SF Arrest Type Model", y="% of Total Gain in LightGBM Model")
max_save(plot, "imp", "SF Open Data", h=2)

4.2 Confusion Matrix

Plot the confusion matrix. Fortunately, matrix is already in long format.

df_cm <- tbl_df(data.frame(cm$table))
df_cm %>% head(100)

Map the labels to the indices.

# create mapping df
df_labels <- df %>%
              select(category_index, Category) %>%
              group_by(category_index, Category) %>%
              summarize() %>%
              ungroup() %>%
              mutate(category_index = factor(category_index))
df_cm <- df_cm %>%
                left_join(df_labels, by = c("Prediction" = "category_index")) %>%
                left_join(df_labels, by = c("Reference" = "category_index")) %>%
                rename(label_pred = Category.x, label_act = Category.y)
df_cm %>% head(100)

Plot the confusion matrix. Since 39 labels, confusion matrix will be large to fit all labels. Will also need to log-scale.

# create a data frame of "correct values" to annotate
df_correct <- df_cm %>% filter(label_pred == label_act)
plot <- ggplot(df_cm, aes(x=label_act, y=label_pred, fill = Freq)) +
          geom_tile() +
          geom_point(data=df_correct, color="white", size=0.8) +
          fte_theme() +
          coord_equal() +
          scale_x_discrete() +
          scale_y_discrete() +
          theme(legend.title = element_text(size=7, family="Open Sans Condensed Bold"), legend.position="top", legend.direction="horizontal", legend.key.width=unit(1.25, "cm"), legend.key.height=unit(0.25, "cm"), legend.margin=unit(0,"cm"), axis.text.x=element_text(angle=-90, size=6, vjust=0.5, hjust=0), axis.text.y=element_text(size=6), plot.title = element_text(hjust=1)) +
            scale_fill_viridis(name="# of Preds", labels=comma, breaks=10^(0:4), trans="log10") +
            labs(title = sprintf("Confusion Matrix between %s Predicted SFPD Arrest Labels and Actual", test_size_format),
                 x = "Actual Label of Arrest",
                 y = "Predicted Label of Arrest")
`legend.margin` must be specified using `margin()`. For the old behavior use legend.spacing
max_save(plot, "confusionMatrix", "SF Open Data", h=6, w=5, tall=T)

4.3 Correlations

Covert the preds_cor matrix into long (adapted from http://stackoverflow.com/a/26838774)

Requires reordering correlations for cleaner chart: http://www.sthda.com/english/wiki/ggplot2-quick-correlation-matrix-heatmap-r-software-and-data-visualization)

dd <- as.dist((1-preds_cor)/2)
hc <- hclust(dd, "centroid")
label_order <- hc$order
preds_cor_reorder <- preds_cor[label_order, label_order]
df_corr <- tbl_df(data.frame(Var1=c(row(preds_cor_reorder))-1, Var2=c(col(preds_cor_reorder))-1, value = c(preds_cor_reorder))) %>%
            filter(Var1 <= Var2) %>%
            mutate(Var1 = factor(Var1), Var2=factor(Var2))
df_corr %>% head(100)

Plot similar chart to confusion matrix.

df_corr <- df_corr %>%
                left_join(df_labels, by = c("Var1" = "category_index")) %>%
                left_join(df_labels, by = c("Var2" = "category_index")) %>%
                mutate(label1 = factor(Category.x), label2 = factor(Category.y))
# fix the label order to the reordered order from the hclust
levels(df_corr$label1) <- levels(df_corr$label1)[label_order]
levels(df_corr$label2) <- levels(df_corr$label2)[label_order]
plot <- ggplot(df_corr, aes(x=label1, y=label2, fill=value)) +
          geom_tile() +
          fte_theme() +
          scale_x_discrete() +
          scale_y_discrete() +
          coord_fixed() +
          theme(legend.title = element_text(size=7, family="Open Sans Condensed Bold"), legend.position="top", legend.direction="horizontal", legend.key.width=unit(1.25, "cm"), legend.key.height=unit(0.25, "cm"), legend.margin=unit(0,"cm"), panel.margin=element_blank(), axis.text.x=element_text(angle=-90, vjust=0.5, hjust=0), axis.title.y=element_blank(), axis.title.x=element_blank(), plot.title=element_text(hjust=1, size=6)) +
            scale_fill_gradient2(high = "#2ecc71", low = "#e74c3c", mid = "white", 
   midpoint = 0, limit = c(-0.5,0.5), 
   name="Pearson\nCorrelation", breaks=pretty_breaks(8))  +
            labs(title = sprintf("Correlations between Predicted Multiclass Probabilities of %s SFPD Arrest Category Labels", test_size_format))
`panel.margin` is deprecated. Please use `panel.spacing` property instead`legend.margin` must be specified using `margin()`. For the old behavior use legend.spacing
max_save(plot, "correlationMatrix", "SF Open Data", h=6, w=5, tall=T)

5 Mapping Arrests

Reusing my mapping previous SF code: https://github.com/minimaxir/sf-arrests-when-where/blob/master/crime_data_sf.ipynb

bbox = c(-122.516441,37.702072,-122.37276,37.811818)
map <- get_map(location = bbox, source = "stamen", maptype = "toner-lite")
Source : http://tile.stamen.com/toner-lite/13/1308/3165.png
Source : http://tile.stamen.com/toner-lite/13/1309/3165.png
Source : http://tile.stamen.com/toner-lite/13/1310/3165.png
Source : http://tile.stamen.com/toner-lite/13/1311/3165.png
Source : http://tile.stamen.com/toner-lite/13/1308/3166.png
Source : http://tile.stamen.com/toner-lite/13/1309/3166.png
Source : http://tile.stamen.com/toner-lite/13/1310/3166.png
Source : http://tile.stamen.com/toner-lite/13/1311/3166.png
Source : http://tile.stamen.com/toner-lite/13/1308/3167.png
Source : http://tile.stamen.com/toner-lite/13/1309/3167.png
Source : http://tile.stamen.com/toner-lite/13/1310/3167.png
Source : http://tile.stamen.com/toner-lite/13/1311/3167.png
Source : http://tile.stamen.com/toner-lite/13/1308/3168.png
Source : http://tile.stamen.com/toner-lite/13/1309/3168.png
Source : http://tile.stamen.com/toner-lite/13/1310/3168.png
Source : http://tile.stamen.com/toner-lite/13/1311/3168.png

Create 40000 million latitude/longitude points in SF to simulate locations (200 points on x axis, 2000 points on y axis)

grid_size = 200
df_points <- data.frame(expand.grid(X=seq(bbox[1], bbox[3], length.out=grid_size),
                                    Y=seq(bbox[2], bbox[4], length.out=grid_size)
)
)
df_points %>% head()
df_points %>% nrow()
[1] 40000

Predict arrest types at each point on April 15th, 2017, at 8 PM.

Populate data with same format of data (i.e. add month, hour, year, DayOfWeek). Does not require much customization. (DayOfWeek is a Factor, however)

date_target <- as.POSIXct("2017-04-15 20:00:00")
df_points <- df_points %>%
              mutate(hour = format(date_target, "%H"),
                    month = format(date_target, "%m"),
                    year = format(date_target, "%Y"),
                    DayOfWeek = which(levels(df$DayOfWeek) == format(date_target, "%A"))) %>%
            data.matrix()
df_points %>% head()
             X        Y hour month year DayOfWeek
[1,] -122.5164 37.70207   20     4 2017         6
[2,] -122.5157 37.70207   20     4 2017         6
[3,] -122.5150 37.70207   20     4 2017         6
[4,] -122.5143 37.70207   20     4 2017         6
[5,] -122.5136 37.70207   20     4 2017         6
[6,] -122.5128 37.70207   20     4 2017         6
preds_matrix <- matrix(predict(bst, df_points), byrow=T, nrow(df_points), num_classes)
results <- t(apply(preds_matrix, 1, function (x) {
  max_index = which(x==max(x))
  return (c(max_index-1, x[max_index]))
}))
rm(preds_matrix)
df_results <- data.frame(X=df_points[,1], Y=df_points[,2], label=factor(results[,1]), prob=results[,2]) %>%
                tbl_df() %>%
                left_join(df_labels, by=c("label" = "category_index")) %>%
                mutate(Category = factor(Category))
joining factors with different levels, coercing to character vector
df_results %>% head(20)
plot <- ggmap(map) +
            geom_raster(data = df_results %>% filter(Category != "Other Offenses"), aes(x=X, y=Y, fill=Category), alpha=0.8, size=0) +
            coord_cartesian() +
            fte_theme() +
            scale_fill_brewer(palette = "Dark2") +
            theme(axis.text.x = element_blank(), axis.text.y = element_blank(), axis.title.x = element_blank(), axis.title.y = element_blank()) +
            theme(legend.title = element_text(size=7, family="Open Sans Condensed Bold"), legend.position="right", legend.key.width=unit(0.5, "cm"), legend.key.height=unit(2, "cm"), legend.margin=margin(1,0,1,0), plot.title=element_text(hjust=0, size=11)) +
            labs(title = sprintf("Locations of Predicted Types of Arrests in San Francisco on %s",
                 format(date_target, '%B %d, %Y at%l %p')))
Ignoring unknown parameters: size
max_save(plot, sprintf("crime-%s", format(date_target, '%Y-%m-%d-%H')), "SF Open Data", w = 6, h = 6, tall=T)

5.1 Map Animation

Set each label such that is has a consistent color.

set.seed(123)
cols <- distinctColorPalette(num_classes)
names(cols) <- df_labels$Category
cols
                      Arson                     Assault 
                  "#E44D3E"                   "#62E2B5" 
                 Bad Checks                     Bribery 
                  "#71EA49"                   "#947468" 
                   Burglary          Disorderly Conduct 
                  "#DE99E3"                   "#7957DB" 
Driving Under The Influence               Drug/Narcotic 
                  "#6DC0E4"                   "#E0BE43" 
                Drunkenness                Embezzlement 
                  "#D986A7"                   "#D9B97C" 
                  Extortion             Family Offenses 
                  "#DFE5B0"                   "#CA77E5" 
     Forgery/Counterfeiting                       Fraud 
                  "#5F9BDE"                   "#815493" 
                   Gambling                  Kidnapping 
                  "#66E4E2"                   "#7A9E53" 
              Larceny/Theft                 Liquor Laws 
                  "#E08F47"                   "#61E386" 
                  Loitering              Missing Person 
                  "#D7EA40"                   "#E14582" 
               Non-Criminal              Other Offenses 
                  "#ADE79E"                   "#7E35E8" 
    Pornography/Obscene Mat                Prostitution 
                  "#E348C4"                   "#AEDFE3" 
          Recovered Vehicle                     Robbery 
                  "#E16DBC"                   "#E4E9DD" 
                    Runaway             Secondary Codes 
                  "#6F7BDF"                   "#DA6B6D" 
     Sex Offenses, Forcible  Sex Offenses, Non Forcible 
                  "#D542E5"                   "#AEA3E2" 
            Stolen Property                     Suicide 
                  "#E3B5D9"                   "#D9C1B7" 
             Suspicious Occ                        Trea 
                  "#AEE8C8"                   "#CFCDE6" 
                   Trespass                   Vandalism 
                  "#DEE37D"                   "#E6A393" 
              Vehicle Theft                    Warrants 
                  "#7C8DA6"                   "#A4E165" 
                Weapon Laws 
                  "#70A596" 

Plot the map for 24 hours (convert to a GIF using external tools). Reuse code above to generate a map for given date/time + hour delta.

system("mkdir -p map_ani")
create_arrest_map <- function(hour_delta, date) {
  date_target <- date + hour_delta*60*60
  
grid_size <- 200
df_points <- data.frame(expand.grid(X=seq(bbox[1], bbox[3], length.out=grid_size),
                                    Y=seq(bbox[2], bbox[4], length.out=grid_size)
)
)
df_points <- df_points %>%
              mutate(hour = format(date_target, "%H"),
                    month = format(date_target, "%m"),
                    year = format(date_target, "%Y"),
                    DayOfWeek = which(levels(df$DayOfWeek) == format(date_target, "%A"))) %>%
            data.matrix()
preds_matrix <- matrix(predict(bst, df_points), byrow=T, nrow(df_points), num_classes)
results <- t(apply(preds_matrix, 1, function (x) {
  max_index = which(x==max(x))
  return (c(max_index-1, x[max_index]))
}))
rm(preds_matrix)
df_results <- data.frame(X=df_points[,1], Y=df_points[,2], label=factor(results[,1]), prob=results[,2]) %>%
                tbl_df() %>%
                left_join(df_labels, by=c("label" = "category_index")) %>%
                mutate(Category = factor(Category))
plot <- ggmap(map) +
            geom_raster(data = df_results %>% filter(Category != "Other Offenses"), aes(x=X, y=Y, fill=Category), alpha=0.9, size=0) +
            coord_cartesian() +
            fte_theme() +
            scale_fill_manual(values=cols) +
            theme(axis.text.x = element_blank(), axis.text.y = element_blank(), axis.title.x = element_blank(), axis.title.y = element_blank()) +
            theme(legend.title = element_text(family="Open Sans Condensed Bold"), legend.position="right", legend.key.width=unit(0.5, "cm"), legend.margin=margin(0,0,0,0, "cm"), legend.key.height=unit(1, "cm"), plot.title=element_text(hjust=0, size=11), legend.text.align=0) +
            labs(title = sprintf("Locations of Predicted Types of Arrests in San Francisco on %s",
                 format(date_target, '%B %d, %Y at %l %p')))
max_save(plot, sprintf("map_ani/crime-%s", format(date_target, '%Y-%m-%d-%H')), "SF Open Data", w = 6, h = 6, tall=T)
}
base_date <- as.POSIXct("2017-03-14 06:00:00")
hour_deltas <- 0:23
x <- lapply(hour_deltas, create_arrest_map, base_date)
joining factors with different levels, coercing to character vectorIgnoring unknown parameters: size

6 Code Which Did Not Work Out (One-Hot Encoding)

The categorical approach using LightGBM is better. Here is the former code using OHE.

Categorical Features must be factors for one-hot encoding.

Convert the factor variables into dummy variables: model.matrix() can do this in R natively. (via Stack Overflow)

# model.matrix() adds an Intercept column: the "-1" removes it.
# Matrix converts the dense matrix to sparse (reduces memory footprint to 25%).
train <- Matrix(model.matrix(~ X + Y + hour + month + year + DayOfWeek - 1, df))
num_classes <- length(unique(df$category_index))
num_rows <- nrow(train)

train[1:10,]

The objective is multi_logloss since there are many classes. The multiclass objective returns a probability for each class.

Demo: https://github.com/Microsoft/LightGBM/blob/master/R-package/tests/testthat/test_basic.R#L29

preds <- predict(bst, train[1:2,])
preds
length(preds)

7 LICENSE

The MIT License (MIT)

Copyright (c) 2017 Max Woolf

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

LS0tCnRpdGxlOiAiUHJlZGljdGluZyBBbmQgTWFwcGluZyBBcnJlc3QgVHlwZXMgaW4gU2FuIEZyYW5jaXNjbyB3aXRoIExpZ2h0R0JNLCBSLCBnZ3Bsb3QyIgphdXRob3I6ICJNYXggV29vbGYgKEBtaW5pbWF4aXIpIgpkYXRlOiAiMjAxNy0wMi0wOCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICBoaWdobGlnaHQ6IHRhbmdvCiAgICBtYXRoamF4OiBudWxsCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcwogICAgdGhlbWU6IHNwYWNlbGFiCiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB5ZXMKLS0tCgpUaGlzIFIgTm90ZWJvb2sgaXMgdGhlIGNvbXBsZW1lbnQgdG8gbXkgYmxvZyBwb3N0IFtQcmVkaWN0aW5nIEFuZCBNYXBwaW5nIEFycmVzdCBUeXBlcyBpbiBTYW4gRnJhbmNpc2NvIHdpdGggTGlnaHRHQk0sIFIsIGdncGxvdDJdKGh0dHA6Ly9taW5pbWF4aXIuY29tLzIwMTcvMDIvcHJlZGljdGluZy1hcnJlc3RzLykuCgpUaGlzIG5vdGVib29rIGlzIGxpY2Vuc2VkIHVuZGVyIHRoZSBNSVQgTGljZW5zZS4gSWYgeW91IHVzZSB0aGUgY29kZSBvciBkYXRhIHZpc3VhbGl6YXRpb24gZGVzaWducyBjb250YWluZWQgd2l0aGluIHRoaXMgbm90ZWJvb2ssIGl0IHdvdWxkIGJlIGdyZWF0bHkgYXBwcmVjaWF0ZWQgaWYgcHJvcGVyIGF0dHJpYnV0aW9uIGlzIGdpdmVuIGJhY2sgdG8gdGhpcyBub3RlYm9vayBhbmQvb3IgbXlzZWxmLiBUaGFua3MhIDopCgojIFNldHVwCgpTZXR1cCB0aGUgUiBwYWNrYWdlcy4KCmBgYHtyIHNldHVwfQpsaWJyYXJ5KGxpZ2h0Z2JtKQpsaWJyYXJ5KE1hdHJpeCkKbGlicmFyeShjYXJldCkKbGlicmFyeSh2aXJpZGlzKQpsaWJyYXJ5KGdnbWFwKQpsaWJyYXJ5KHJhbmRvbWNvbG9SKQoKc291cmNlKCJSc3RhcnQuUiIpCmBgYAoKYGBge3J9CnNlc3Npb25JbmZvKCkKYGBgCgpJbXBvcnQgZGF0YSwgYW5kIG9ubHkga2VlcCByZWxldmFudCBjb2x1bW5zLiBGaWx0ZXIgb24gQXJyZXN0cyBvbmx5LgoKVGhlIGRhdGEgbXVzdCBiZSByYW5kb21pemVkIGZvciBgbGlnaHRnYm1gIHRvIGdpdmUgdW5iaWFzZWQgc2NvcmVzLiBDYW4gZG8gd2l0aCBkcGx5cidzIGBzYW1wbGVfZnJhY2AuCgpgYGB7ciwgaW5jbHVkZT1GQUxTRX0KIyBDb252ZXJ0cyAiTEFSQ0VOWS9USEVGVCIgdG8gIkxhcmNlbnkvVGhlZnQiLCBldGMuCnByb3Blcl9jYXNlIDwtIGZ1bmN0aW9uKHgpIHsKICAgIHJldHVybiAoZ3N1YigiXFxiKFtBLVpdKShbQS1aXSspIiwgIlxcVVxcMVxcTFxcMiIgLCB4LCBwZXJsID0gVFJVRSkpCn0KCmZpbGVfcGF0aCA8LSAifi9Eb3dubG9hZHMvU0ZQRF9JbmNpZGVudHNfLV9mcm9tXzFfSmFudWFyeV8yMDAzLmNzdiIKCmRmIDwtIHJlYWRfY3N2KGZpbGVfcGF0aCwgY29sX3R5cGVzPSJfY19jY2NfY19ubl9fIikgJT4lCiAgICAgICAgZmlsdGVyKGdyZXBsKCJBUlJFU1QiLCBSZXNvbHV0aW9uKSkgJT4lCiAgICAgICAgbXV0YXRlKENhdGVnb3J5ID0gcHJvcGVyX2Nhc2UoQ2F0ZWdvcnkpKQpgYGAKCmBgYHtyfQojIHNlZWQgZm9yIHNhbXBsZV9mcmFjKCkKc2V0LnNlZWQoMTIzKQoKZGYgPC0gZGYgJT4lIHNhbXBsZV9mcmFjKCkKCmRmICU+JSBoZWFkKCkKYGBgCgpUaGVyZSBhcmUgKipgciBkZiAlPiUgbnJvdygpICU+JSBmb3JtYXQoYmlnLm1hcms9JywnKWAqKiBhcnJlc3RzIGluIHRoaXMgZGF0YXNldC4KCiMgRmVhdHVyZSBFbmdpbmVlcmluZwoKRW5naW5lZXIgZmVhdHVyZXMgZm9yIGBsaWdodGdibWAuCgojIyBNb250aCwgSG91ciwgWWVhcgoKWWVhciBpcyAjIHllYXJzIHNpbmNlIHRoZSBsb3dlc3QgeWVhciAoaW4gdGhpcyBjYXNlLCAyMDAzLCBhcyBub3RlZCBpbiB0aGUgZGF0YXNldCB0aXRsZSkKCmBgYHtyfQpkZiA8LSBkZiAlPiUKICAgICAgICBtdXRhdGUobW9udGggPSBmYWN0b3Ioc3Vic3RyaW5nKERhdGUsIDEsIDIpKSwKICAgICAgICAgICAgICAgIGhvdXIgPSBmYWN0b3Ioc3Vic3RyaW5nKFRpbWUsIDEsIDIpKSwKICAgICAgICAgICAgICAgIHllYXIgPSBhcy5udW1lcmljKHN1YnN0cmluZyhEYXRlLCA3LCAxMCkpKQoKZGYgJT4lIHNlbGVjdChtb250aCwgaG91ciwgeWVhcikgJT4lIGhlYWQoKQpgYGAKCiMjIEV4aXN0aW5nIERheU9mV2VlayB0byBGYWN0b3IKCkNoYW5nZSBEYXlPZldlZWsgdG8gRmFjdG9yLgoKU2luY2UgY29sdW1uIGJlY29tZSBlbmNvZGVkIGFzIG51bWVyaWMgaW5zdGVhZCBvZiBjYXRlZ29yaWNhbCwgZW5jb2RlIG9yZGVyIG9mIG51bWVyYWxzIHN1Y2ggdGhhdCBTYXR1cmRheSBhbmQgU3VuZGF5IGFyZSBhZGphY2VudCBmb3IgcHJvcGVyIGBsdGVgL2BndGVgIGJlaGF2aW9yLgoKYGBge3J9CmRvd19vcmRlciA8LSBjKCJNb25kYXkiLCAiVHVlc2RheSIsICJXZWRuZXNkYXkiLCAiVGh1cnNkYXkiLCAiRnJpZGF5IiwgIlNhdHVyZGF5IiwgIlN1bmRheSIpCgpkZiA8LSBkZiAlPiUKICAgICAgICBtdXRhdGUoRGF5T2ZXZWVrID0gZmFjdG9yKERheU9mV2VlaywgbGV2ZWxzPWRvd19vcmRlcikpCgpkZiAlPiUgc2VsZWN0KERheU9mV2VlaykgJT4lIGhlYWQoKQpgYGAKCiMjIENhdGVnb3J5IEluZGljZXMKCk1hcCB0aGUgY2F0ZWdvcnkgdG8gYW4gaW5kZXguIExhYmVscyBtdXN0IGJlIHplcm8taW5kZXhlZC4KCmBgYHtyfQpkZiA8LSBkZiAlPiUKICAgICAgICBtdXRhdGUoY2F0ZWdvcnlfaW5kZXggPSBhcy5udW1lcmljKGZhY3RvcihDYXRlZ29yeSkpIC0gMSkKCmRmICU+JSBzZWxlY3QoY2F0ZWdvcnlfaW5kZXgsIENhdGVnb3J5KSAlPiUgaGVhZCgpCmBgYAoKIyBsaWdodGdibSBUcmFpbmluZwoKVXNlIExpZ2h0R0JNJ3MgY2F0ZWdvcmljYWwgZGF0YSBmZWF0dXJlIGZvciBvcHRpbWlhbCBwZXJmb3JtYW5jZS4KClVzZSBgY2FyZXRgIGZvciB0cmFpbi90ZXN0IHNwbGl0dGluZyBzaW5jZSBgY3JlYXRlRGF0YVBhcnRpdGlvbmAgZW5zdXJlcyBiYWxhbmNlZCBkaXN0cmlidXRpb24gb2YgY2F0ZWdvcmllcyBiZXR3ZWVuIHRyYWluIGFuZCB0ZXN0LgoKYGBge3J9CiMgZGVjbGFyZSBjYXRlZ29yaWNhbCBmZWF0dXJlIG5hbWVzLCBpZiBhbnkKY2F0ZWdvcmljYWxzIDwtIE5VTEwKCiMgcHJvcG9ydGlvbiBvZiBkYXRhIHRvIHRyYWluIG9uCnNwbGl0IDwtIDAuNwoKc2V0LnNlZWQoMTIzKQp0cmFpbkluZGV4IDwtIGNyZWF0ZURhdGFQYXJ0aXRpb24oZGYkY2F0ZWdvcnlfaW5kZXgsIHAgPSBzcGxpdCwgbGlzdCA9IEZBTFNFLCB0aW1lcyA9IDEpCgpkdHJhaW4gPC0gbGdiLkRhdGFzZXQoKGRmICU+JSBzZWxlY3QoWCwgWSwgaG91ciwgbW9udGgsIHllYXIsIERheU9mV2VlaykgJT4lIGRhdGEubWF0cml4KCkpW3RyYWluSW5kZXgsXSwKICAgICAgICAgICAgICAgICAgICAgY29sbmFtZXMgPSBjKCJYIiwgIlkiLCAiaG91ciIsICJtb250aCIsICJ5ZWFyIiwgIkRheU9mV2VlayIpLAogICAgICAgICAgICAgICAgICAgICBjYXRlZ29yaWNhbF9mZWF0dXJlID0gY2F0ZWdvcmljYWxzLAogICAgICAgICAgICAgICAgICAgICBsYWJlbCA9IGRmJGNhdGVnb3J5X2luZGV4W3RyYWluSW5kZXhdLCBmcmVlX3Jhd19kYXRhPVQpCgpkdGVzdCA8LSBsZ2IuRGF0YXNldC5jcmVhdGUudmFsaWQoZHRyYWluLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKGRmICU+JSBzZWxlY3QoWCwgWSwgaG91ciwgbW9udGgsIHllYXIsIERheU9mV2VlaykgJT4lIGRhdGEubWF0cml4KCkpWy10cmFpbkluZGV4LF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsYWJlbCA9IGRmJGNhdGVnb3J5X2luZGV4Wy10cmFpbkluZGV4XSkKCnBhcmFtcyA8LSBsaXN0KG9iamVjdGl2ZSA9ICJtdWx0aWNsYXNzIiwgbWV0cmljID0gIm11bHRpX2xvZ2xvc3MiKQp2YWxpZHMgPC0gbGlzdCh0ZXN0PWR0ZXN0KQoKbnVtX2NsYXNzZXMgPC0gbGVuZ3RoKHVuaXF1ZShkZiRjYXRlZ29yeV9pbmRleCkpCgojIHByZWZvcm1hdCBzaXplcyBmb3IgdXNlIGluIGRhdGEgdmlzdWFsaXphdGlvbnMgbGF0ZXIKdHJhaW5fc2l6ZV9mb3JtYXQgPC0gbGVuZ3RoKHRyYWluSW5kZXgpICU+JSBmb3JtYXQoYmlnLm1hcms9IiwiKQp0ZXN0X3NpemVfZm9ybWF0IDwtIChkZiAlPiUgbnJvdygpIC0gbGVuZ3RoKHRyYWluSW5kZXgpKSAlPiUgZm9ybWF0KGJpZy5tYXJrPSIsIikKYGBgCgpUaGUgc2l6ZSBvZiB0aGUgdHJhaW5pbmcgc2V0IGlzICoqYHIgdHJhaW5fc2l6ZV9mb3JtYXRgKiogYW5kIHRoZSBzaXplIG9mIHRoZSB0ZXN0IHNldCBpcyAqKmByIHRlc3Rfc2l6ZV9mb3JtYXRgKiouCgpgYGB7cn0KIyBkZXRlcm1pbmUgZWxhcHNlZCBydW50aW1lIApzeXN0ZW0udGltZSgKCiMgdHJhaW5pbmcgb3V0cHV0IG5vdCBwcmludGVkIHRvIG5vdGVib29rIHNpbmNlIHNwYW1teS4gKHZlcmJvc2UgPSAwICsgcmVjb3JkID0gVCkKYnN0IDwtIGxnYi50cmFpbihwYXJhbXMsCiAgICAgICAgICAgICAgICBkdHJhaW4sCiAgICAgICAgICAgICAgICBucm91bmRzID0gNTAwLAogICAgICAgICAgICAgICAgdmFsaWRzLAogICAgICAgICAgICAgICAgbnVtX3RocmVhZHMgPSA0LAogICAgICAgICAgICAgICAgbnVtX2NsYXNzID0gbnVtX2NsYXNzZXMsCiAgICAgICAgICAgICAgICB2ZXJib3NlID0gMCwKICAgICAgICAgICAgICAgIHJlY29yZCA9IFQsCiAgICAgICAgICAgICAgICBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSA1LAogICAgICAgICAgICAgICAgY2F0ZWdvcmljYWxfZmVhdHVyZSA9IGNhdGVnb3JpY2FscwogICAgICAgICAgICAgICAgKQoKKVszXQoKIyBtdWx0aWxvZ2xvc3Mgb2YgZmluYWwgaXRlcmF0aW9uIG9uIHRlc3Qgc2V0CnBhc3RlKCIjIFJvdW5kczoiLCBic3QkY3VycmVudF9pdGVyKCkpCnBhc3RlKCJNdWx0aWxvZ2xvc3Mgb2YgYmVzdCBtb2RlbDoiLCBic3QkcmVjb3JkX2V2YWxzJHRlc3QkbXVsdGlfbG9nbG9zcyRldmFsICU+JSB1bmxpc3QoKSAlPiUgdGFpbCgxKSkKYGBgCkNhbGN1bGF0ZSB2YXJpYWJsZSBpbXBvcnRhbmNlLiAobm90ZTogdGFrZXMgYXdoaWxlIHNpbmNlIHNpbmdsZS10aHJlYWRlZCkKCmBgYHtyfQpkZl9pbXAgPC0gdGJsX2RmKGxnYi5pbXBvcnRhbmNlKGJzdCwgcGVyY2VudGFnZSA9IFRSVUUpKQpkZl9pbXAKYGBgCgoKYHByZWRzYCBpcyBhIDFEIHZlY3RvciBvZiBwcm9iYWJpbGl0aWVzIGZvciBlYWNoIHZlY3Rvciwgb2YgbnJvd3MgeCBuY2xhc3Nlcy4gUmVzaGFwZSBhY2NvcmRpbmdseSBhbmQgaXRlcmF0ZSB0aHJvdWdoIGZvciB0aGUgcHJlZGljdGVkIGxhYmVsIChsYWJlbCB3aXRoIHRoZSBsYXJnZXN0IHByb2JhYmlsaXR5KSBhbmQgdGhlIGNvcnJlc3BvbmRpbmcgcHJvYmFiaWxpdHkuCgpgYGB7cn0KdGVzdCA8LSAoZGYgJT4lIHNlbGVjdChYLCBZLCBob3VyLCBtb250aCwgeWVhciwgRGF5T2ZXZWVrKSAlPiUgZGF0YS5tYXRyaXgoKSlbLXRyYWluSW5kZXgsXQoKcHJlZHNfbWF0cml4IDwtIHByZWRpY3QoYnN0LCB0ZXN0LCByZXNoYXBlPVQpCgpwcmVkc19jb3IgPC0gY29yKHByZWRzX21hdHJpeCkKCnByZWRzX21hdHJpeFsxOjIsXQoKIyBsaWtlbHkgbm90IG1vc3QgZWZmaWNpZW50IG1ldGhvZApyZXN1bHRzIDwtIHQoYXBwbHkocHJlZHNfbWF0cml4LCAxLCBmdW5jdGlvbiAoeCkgewogIG1heF9pbmRleCA9IHdoaWNoKHg9PW1heCh4KSkKICByZXR1cm4gKGMobWF4X2luZGV4LTEsIHhbbWF4X2luZGV4XSkpCn0pKQpgYGAKCmBgYHtyfQpkZl9yZXN1bHRzIDwtIGRhdGEuZnJhbWUocmVzdWx0cywgbGFiZWxfYWN0ID0gZGYkY2F0ZWdvcnlfaW5kZXhbLXRyYWluSW5kZXhdKSAlPiUKICAgICAgICAgICAgICAgIHRibF9kZigpICU+JQogICAgICAgICAgICAgICAgdHJhbnNtdXRlKGxhYmVsX3ByZWQgPSBYMSwgcHJvZF9wcmVkID0gWDIsIGxhYmVsX2FjdCkKCmRmX3Jlc3VsdHMgJT4lIGFycmFuZ2UoZGVzYyhwcm9kX3ByZWQpKSAlPiUgaGVhZCgyMCkKCnJtKHByZWRzX21hdHJpeCkKYGBgCgoKQ29uZnVzaW9uIG1hdHJpeDoKCmBgYHtyfQpjbSA8LSBjb25mdXNpb25NYXRyaXgoZGZfcmVzdWx0cyRsYWJlbF9wcmVkLCBkZl9yZXN1bHRzJGxhYmVsX2FjdCkKCmRhdGEuZnJhbWUoY20kb3ZlcmFsbCkKYGBgCgojIFZpc3VhbGl6YXRpb25zCgojIyBJbXBvcnRhbmNlIEJhciBDaGFydAoKYGBge3J9CmRmX2ltcCRGZWF0dXJlIDwtIGZhY3RvcihkZl9pbXAkRmVhdHVyZSwgbGV2ZWxzPXJldihkZl9pbXAkRmVhdHVyZSkpCmBgYAoKCmBgYHtyfQpwbG90IDwtIGdncGxvdChkZl9pbXAsIGFlcyh4PUZlYXR1cmUsIHk9R2FpbikpICsKICAgICAgICAgIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5IiwgZmlsbD0iIzM0NDk1ZSIsIGFscGhhPTAuOSkgKwogICAgICAgICAgZ2VvbV90ZXh0KGFlcyhsYWJlbD1zcHJpbnRmKCIlMC4xZiUlIiwgR2FpbioxMDApKSwgY29sb3I9IiMzNDQ5NWUiLCBoanVzdD0tMC4yNSwgZmFtaWx5PSJPcGVuIFNhbnMgQ29uZGVuc2VkIEJvbGQiLCBzaXplPTIuNSkgKwogICAgICAgICAgZnRlX3RoZW1lKCkgKwogICAgICAgICAgY29vcmRfZmxpcCgpICsKICAgICAgICAgIHNjYWxlX3lfY29udGludW91cyhsaW1pdHMgPSBjKDAsIDAuNCksIGxhYmVscz1wZXJjZW50KSArCiAgIHRoZW1lKHBsb3QudGl0bGU9ZWxlbWVudF90ZXh0KGhqdXN0PTAuNSksIGF4aXMudGl0bGUueT1lbGVtZW50X2JsYW5rKCkpICsKICAgICAgICAgIGxhYnModGl0bGU9IkZlYXR1cmUgSW1wb3J0YW5jZSBmb3IgU0YgQXJyZXN0IFR5cGUgTW9kZWwiLCB5PSIlIG9mIFRvdGFsIEdhaW4gaW4gTGlnaHRHQk0gTW9kZWwiKQoKbWF4X3NhdmUocGxvdCwgImltcCIsICJTRiBPcGVuIERhdGEiLCBoPTIpCmBgYAoKIVtdKGltcC5wbmcpCgojIyBDb25mdXNpb24gTWF0cml4CgpQbG90IHRoZSBjb25mdXNpb24gbWF0cml4LiBGb3J0dW5hdGVseSwgbWF0cml4IGlzIGFscmVhZHkgaW4gbG9uZyBmb3JtYXQuCgpgYGB7cn0KZGZfY20gPC0gdGJsX2RmKGRhdGEuZnJhbWUoY20kdGFibGUpKQoKZGZfY20gJT4lIGhlYWQoMTAwKQpgYGAKCk1hcCB0aGUgbGFiZWxzIHRvIHRoZSBpbmRpY2VzLgoKYGBge3J9CiMgY3JlYXRlIG1hcHBpbmcgZGYKZGZfbGFiZWxzIDwtIGRmICU+JQogICAgICAgICAgICAgIHNlbGVjdChjYXRlZ29yeV9pbmRleCwgQ2F0ZWdvcnkpICU+JQogICAgICAgICAgICAgIGdyb3VwX2J5KGNhdGVnb3J5X2luZGV4LCBDYXRlZ29yeSkgJT4lCiAgICAgICAgICAgICAgc3VtbWFyaXplKCkgJT4lCiAgICAgICAgICAgICAgdW5ncm91cCgpICU+JQogICAgICAgICAgICAgIG11dGF0ZShjYXRlZ29yeV9pbmRleCA9IGZhY3RvcihjYXRlZ29yeV9pbmRleCkpCgpkZl9jbSA8LSBkZl9jbSAlPiUKICAgICAgICAgICAgICAgIGxlZnRfam9pbihkZl9sYWJlbHMsIGJ5ID0gYygiUHJlZGljdGlvbiIgPSAiY2F0ZWdvcnlfaW5kZXgiKSkgJT4lCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oZGZfbGFiZWxzLCBieSA9IGMoIlJlZmVyZW5jZSIgPSAiY2F0ZWdvcnlfaW5kZXgiKSkgJT4lCiAgICAgICAgICAgICAgICByZW5hbWUobGFiZWxfcHJlZCA9IENhdGVnb3J5LngsIGxhYmVsX2FjdCA9IENhdGVnb3J5LnkpCgpkZl9jbSAlPiUgaGVhZCgxMDApCmBgYAoKUGxvdCB0aGUgY29uZnVzaW9uIG1hdHJpeC4gU2luY2UgMzkgbGFiZWxzLCBjb25mdXNpb24gbWF0cml4IHdpbGwgYmUgbGFyZ2UgdG8gZml0IGFsbCBsYWJlbHMuIFdpbGwgYWxzbyBuZWVkIHRvIGxvZy1zY2FsZS4KCmBgYHtyfQojIGNyZWF0ZSBhIGRhdGEgZnJhbWUgb2YgImNvcnJlY3QgdmFsdWVzIiB0byBhbm5vdGF0ZQpkZl9jb3JyZWN0IDwtIGRmX2NtICU+JSBmaWx0ZXIobGFiZWxfcHJlZCA9PSBsYWJlbF9hY3QpCgpwbG90IDwtIGdncGxvdChkZl9jbSwgYWVzKHg9bGFiZWxfYWN0LCB5PWxhYmVsX3ByZWQsIGZpbGwgPSBGcmVxKSkgKwogICAgICAgICAgZ2VvbV90aWxlKCkgKwogICAgICAgICAgZ2VvbV9wb2ludChkYXRhPWRmX2NvcnJlY3QsIGNvbG9yPSJ3aGl0ZSIsIHNpemU9MC44KSArCiAgICAgICAgICBmdGVfdGhlbWUoKSArCiAgICAgICAgICBjb29yZF9lcXVhbCgpICsKICAgICAgICAgIHNjYWxlX3hfZGlzY3JldGUoKSArCiAgICAgICAgICBzY2FsZV95X2Rpc2NyZXRlKCkgKwogICAgICAgICAgdGhlbWUobGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemU9NywgZmFtaWx5PSJPcGVuIFNhbnMgQ29uZGVuc2VkIEJvbGQiKSwgbGVnZW5kLnBvc2l0aW9uPSJ0b3AiLCBsZWdlbmQuZGlyZWN0aW9uPSJob3Jpem9udGFsIiwgbGVnZW5kLmtleS53aWR0aD11bml0KDEuMjUsICJjbSIpLCBsZWdlbmQua2V5LmhlaWdodD11bml0KDAuMjUsICJjbSIpLCBsZWdlbmQubWFyZ2luPXVuaXQoMCwiY20iKSwgYXhpcy50ZXh0Lng9ZWxlbWVudF90ZXh0KGFuZ2xlPS05MCwgc2l6ZT02LCB2anVzdD0wLjUsIGhqdXN0PTApLCBheGlzLnRleHQueT1lbGVtZW50X3RleHQoc2l6ZT02KSwgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdD0xKSkgKwogICAgICAgICAgICBzY2FsZV9maWxsX3ZpcmlkaXMobmFtZT0iIyBvZiBQcmVkcyIsIGxhYmVscz1jb21tYSwgYnJlYWtzPTEwXigwOjQpLCB0cmFucz0ibG9nMTAiKSArCiAgICAgICAgICAgIGxhYnModGl0bGUgPSBzcHJpbnRmKCJDb25mdXNpb24gTWF0cml4IGJldHdlZW4gJXMgUHJlZGljdGVkIFNGUEQgQXJyZXN0IExhYmVscyBhbmQgQWN0dWFsIiwgdGVzdF9zaXplX2Zvcm1hdCksCiAgICAgICAgICAgICAgICAgeCA9ICJBY3R1YWwgTGFiZWwgb2YgQXJyZXN0IiwKICAgICAgICAgICAgICAgICB5ID0gIlByZWRpY3RlZCBMYWJlbCBvZiBBcnJlc3QiKQoKbWF4X3NhdmUocGxvdCwgImNvbmZ1c2lvbk1hdHJpeCIsICJTRiBPcGVuIERhdGEiLCBoPTYsIHc9NSwgdGFsbD1UKQpgYGAKCiFbXShjb25mdXNpb25NYXRyaXgucG5nKQoKIyMgQ29ycmVsYXRpb25zCgpDb3ZlcnQgdGhlIGBwcmVkc19jb3JgIG1hdHJpeCBpbnRvIGxvbmcgKGFkYXB0ZWQgZnJvbSBodHRwOi8vc3RhY2tvdmVyZmxvdy5jb20vYS8yNjgzODc3NCkKClJlcXVpcmVzIHJlb3JkZXJpbmcgY29ycmVsYXRpb25zIGZvciBjbGVhbmVyIGNoYXJ0OiBodHRwOi8vd3d3LnN0aGRhLmNvbS9lbmdsaXNoL3dpa2kvZ2dwbG90Mi1xdWljay1jb3JyZWxhdGlvbi1tYXRyaXgtaGVhdG1hcC1yLXNvZnR3YXJlLWFuZC1kYXRhLXZpc3VhbGl6YXRpb24pCgpgYGB7cn0KZGQgPC0gYXMuZGlzdCgoMS1wcmVkc19jb3IpLzIpICAgIyBuZWVkIHRvIGxvb2sgaW50byB3aHkgdGhpcyBpcyBuZWNlc3NhcnkKaGMgPC0gaGNsdXN0KGRkLCAiY2VudHJvaWQiKQpsYWJlbF9vcmRlciA8LSBoYyRvcmRlcgpwcmVkc19jb3JfcmVvcmRlciA8LSBwcmVkc19jb3JbbGFiZWxfb3JkZXIsIGxhYmVsX29yZGVyXQoKZGZfY29yciA8LSB0YmxfZGYoZGF0YS5mcmFtZShWYXIxPWMocm93KHByZWRzX2Nvcl9yZW9yZGVyKSktMSwgVmFyMj1jKGNvbChwcmVkc19jb3JfcmVvcmRlcikpLTEsIHZhbHVlID0gYyhwcmVkc19jb3JfcmVvcmRlcikpKSAlPiUKICAgICAgICAgICAgZmlsdGVyKFZhcjEgPD0gVmFyMikgJT4lCiAgICAgICAgICAgIG11dGF0ZShWYXIxID0gZmFjdG9yKFZhcjEpLCBWYXIyPWZhY3RvcihWYXIyKSkKCmRmX2NvcnIgJT4lIGhlYWQoMTAwKQpgYGAKClBsb3Qgc2ltaWxhciBjaGFydCB0byBjb25mdXNpb24gbWF0cml4LiAKCmBgYHtyfQpkZl9jb3JyIDwtIGRmX2NvcnIgJT4lCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oZGZfbGFiZWxzLCBieSA9IGMoIlZhcjEiID0gImNhdGVnb3J5X2luZGV4IikpICU+JQogICAgICAgICAgICAgICAgbGVmdF9qb2luKGRmX2xhYmVscywgYnkgPSBjKCJWYXIyIiA9ICJjYXRlZ29yeV9pbmRleCIpKSAlPiUKICAgICAgICAgICAgICAgIG11dGF0ZShsYWJlbDEgPSBmYWN0b3IoQ2F0ZWdvcnkueCksIGxhYmVsMiA9IGZhY3RvcihDYXRlZ29yeS55KSkKCiMgZml4IHRoZSBsYWJlbCBvcmRlciB0byB0aGUgcmVvcmRlcmVkIG9yZGVyIGZyb20gdGhlIGhjbHVzdApsZXZlbHMoZGZfY29yciRsYWJlbDEpIDwtIGxldmVscyhkZl9jb3JyJGxhYmVsMSlbbGFiZWxfb3JkZXJdCmxldmVscyhkZl9jb3JyJGxhYmVsMikgPC0gbGV2ZWxzKGRmX2NvcnIkbGFiZWwyKVtsYWJlbF9vcmRlcl0KCnBsb3QgPC0gZ2dwbG90KGRmX2NvcnIsIGFlcyh4PWxhYmVsMSwgeT1sYWJlbDIsIGZpbGw9dmFsdWUpKSArCiAgICAgICAgICBnZW9tX3RpbGUoKSArCiAgICAgICAgICBmdGVfdGhlbWUoKSArCiAgICAgICAgICBzY2FsZV94X2Rpc2NyZXRlKCkgKwogICAgICAgICAgc2NhbGVfeV9kaXNjcmV0ZSgpICsKICAgICAgICAgIGNvb3JkX2ZpeGVkKCkgKwogICAgICAgICAgdGhlbWUobGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemU9NywgZmFtaWx5PSJPcGVuIFNhbnMgQ29uZGVuc2VkIEJvbGQiKSwgbGVnZW5kLnBvc2l0aW9uPSJ0b3AiLCBsZWdlbmQuZGlyZWN0aW9uPSJob3Jpem9udGFsIiwgbGVnZW5kLmtleS53aWR0aD11bml0KDEuMjUsICJjbSIpLCBsZWdlbmQua2V5LmhlaWdodD11bml0KDAuMjUsICJjbSIpLCBsZWdlbmQubWFyZ2luPXVuaXQoMCwiY20iKSwgcGFuZWwubWFyZ2luPWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0Lng9ZWxlbWVudF90ZXh0KGFuZ2xlPS05MCwgdmp1c3Q9MC41LCBoanVzdD0wKSwgYXhpcy50aXRsZS55PWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aXRsZS54PWVsZW1lbnRfYmxhbmsoKSwgcGxvdC50aXRsZT1lbGVtZW50X3RleHQoaGp1c3Q9MSwgc2l6ZT02KSkgKwogICAgICAgICAgICBzY2FsZV9maWxsX2dyYWRpZW50MihoaWdoID0gIiMyZWNjNzEiLCBsb3cgPSAiI2U3NGMzYyIsIG1pZCA9ICJ3aGl0ZSIsIAogICBtaWRwb2ludCA9IDAsIGxpbWl0ID0gYygtMC41LDAuNSksIAogICBuYW1lPSJQZWFyc29uXG5Db3JyZWxhdGlvbiIsIGJyZWFrcz1wcmV0dHlfYnJlYWtzKDgpKSAgKwogICAgICAgICAgICBsYWJzKHRpdGxlID0gc3ByaW50ZigiQ29ycmVsYXRpb25zIGJldHdlZW4gUHJlZGljdGVkIE11bHRpY2xhc3MgUHJvYmFiaWxpdGllcyBvZiAlcyBTRlBEIEFycmVzdCBDYXRlZ29yeSBMYWJlbHMiLCB0ZXN0X3NpemVfZm9ybWF0KSkKCm1heF9zYXZlKHBsb3QsICJjb3JyZWxhdGlvbk1hdHJpeCIsICJTRiBPcGVuIERhdGEiLCBoPTYsIHc9NSwgdGFsbD1UKQpgYGAKCiFbXShjb3JyZWxhdGlvbk1hdHJpeC5wbmcpCgojIE1hcHBpbmcgQXJyZXN0cwoKUmV1c2luZyBteSBtYXBwaW5nIHByZXZpb3VzIFNGIGNvZGU6IGh0dHBzOi8vZ2l0aHViLmNvbS9taW5pbWF4aXIvc2YtYXJyZXN0cy13aGVuLXdoZXJlL2Jsb2IvbWFzdGVyL2NyaW1lX2RhdGFfc2YuaXB5bmIKCmBgYHtyfQpiYm94ID0gYygtMTIyLjUxNjQ0MSwzNy43MDIwNzIsLTEyMi4zNzI3NiwzNy44MTE4MTgpCm1hcCA8LSBnZXRfbWFwKGxvY2F0aW9uID0gYmJveCwgc291cmNlID0gInN0YW1lbiIsIG1hcHR5cGUgPSAidG9uZXItbGl0ZSIpCmBgYAoKQ3JlYXRlIDQwMDAwIG1pbGxpb24gbGF0aXR1ZGUvbG9uZ2l0dWRlIHBvaW50cyBpbiBTRiB0byBzaW11bGF0ZSBsb2NhdGlvbnMgKDIwMCBwb2ludHMgb24geCBheGlzLCAyMDAwIHBvaW50cyBvbiB5IGF4aXMpCgpgYGB7cn0KZ3JpZF9zaXplIDwtIDIwMAoKZGZfcG9pbnRzIDwtIGRhdGEuZnJhbWUoZXhwYW5kLmdyaWQoWD1zZXEoYmJveFsxXSwgYmJveFszXSwgbGVuZ3RoLm91dD1ncmlkX3NpemUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBZPXNlcShiYm94WzJdLCBiYm94WzRdLCBsZW5ndGgub3V0PWdyaWRfc2l6ZSkKKQopCgpkZl9wb2ludHMgJT4lIGhlYWQoKQpkZl9wb2ludHMgJT4lIG5yb3coKQpgYGAKClByZWRpY3QgYXJyZXN0IHR5cGVzIGF0IGVhY2ggcG9pbnQgb24gQXByaWwgMTV0aCwgMjAxNywgYXQgOCBQTS4KClBvcHVsYXRlIGRhdGEgd2l0aCBzYW1lIGZvcm1hdCBvZiBkYXRhIChpLmUuIGFkZCBtb250aCwgaG91ciwgeWVhciwgRGF5T2ZXZWVrKS4gRG9lcyBub3QgcmVxdWlyZSBtdWNoIGN1c3RvbWl6YXRpb24uIChEYXlPZldlZWsgaXMgYSBGYWN0b3IsIGhvd2V2ZXIpCgpgYGB7cn0KZGF0ZV90YXJnZXQgPC0gYXMuUE9TSVhjdCgiMjAxNy0wNC0xNSAyMDowMDowMCIpCgpkZl9wb2ludHMgPC0gZGZfcG9pbnRzICU+JQogICAgICAgICAgICAgIG11dGF0ZShob3VyID0gZm9ybWF0KGRhdGVfdGFyZ2V0LCAiJUgiKSwKICAgICAgICAgICAgICAgICAgICBtb250aCA9IGZvcm1hdChkYXRlX3RhcmdldCwgIiVtIiksCiAgICAgICAgICAgICAgICAgICAgeWVhciA9IGZvcm1hdChkYXRlX3RhcmdldCwgIiVZIiksCiAgICAgICAgICAgICAgICAgICAgRGF5T2ZXZWVrID0gd2hpY2gobGV2ZWxzKGRmJERheU9mV2VlaykgPT0gZm9ybWF0KGRhdGVfdGFyZ2V0LCAiJUEiKSkpICU+JQogICAgICAgICAgICBkYXRhLm1hdHJpeCgpCgpkZl9wb2ludHMgJT4lIGhlYWQoKQpgYGAKCmBgYHtyfQpwcmVkc19tYXRyaXggPC0gbWF0cml4KHByZWRpY3QoYnN0LCBkZl9wb2ludHMpLCBieXJvdz1ULCBucm93KGRmX3BvaW50cyksIG51bV9jbGFzc2VzKQoKcmVzdWx0cyA8LSB0KGFwcGx5KHByZWRzX21hdHJpeCwgMSwgZnVuY3Rpb24gKHgpIHsKICBtYXhfaW5kZXggPSB3aGljaCh4PT1tYXgoeCkpCiAgcmV0dXJuIChjKG1heF9pbmRleC0xLCB4W21heF9pbmRleF0pKQp9KSkKCnJtKHByZWRzX21hdHJpeCkKYGBgCgpgYGB7cn0KZGZfcmVzdWx0cyA8LSBkYXRhLmZyYW1lKFg9ZGZfcG9pbnRzWywxXSwgWT1kZl9wb2ludHNbLDJdLCBsYWJlbD1mYWN0b3IocmVzdWx0c1ssMV0pLCBwcm9iPXJlc3VsdHNbLDJdKSAlPiUKICAgICAgICAgICAgICAgIHRibF9kZigpICU+JQogICAgICAgICAgICAgICAgbGVmdF9qb2luKGRmX2xhYmVscywgYnk9YygibGFiZWwiID0gImNhdGVnb3J5X2luZGV4IikpICU+JQogICAgICAgICAgICAgICAgbXV0YXRlKENhdGVnb3J5ID0gZmFjdG9yKENhdGVnb3J5KSkKCmRmX3Jlc3VsdHMgJT4lIGhlYWQoMjApCmBgYAoKYGBge3J9CnBsb3QgPC0gZ2dtYXAobWFwKSArCiAgICAgICAgICAgIGdlb21fcmFzdGVyKGRhdGEgPSBkZl9yZXN1bHRzICU+JSBmaWx0ZXIoQ2F0ZWdvcnkgIT0gIk90aGVyIE9mZmVuc2VzIiksIGFlcyh4PVgsIHk9WSwgZmlsbD1DYXRlZ29yeSksIGFscGhhPTAuOCwgc2l6ZT0wKSArCiAgICAgICAgICAgIGNvb3JkX2NhcnRlc2lhbigpICsKICAgICAgICAgICAgZnRlX3RoZW1lKCkgKwogICAgICAgICAgICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlID0gIkRhcmsyIikgKwogICAgICAgICAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0LnkgPSBlbGVtZW50X2JsYW5rKCksIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aXRsZS55ID0gZWxlbWVudF9ibGFuaygpKSArCiAgICAgICAgICAgIHRoZW1lKGxlZ2VuZC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplPTcsIGZhbWlseT0iT3BlbiBTYW5zIENvbmRlbnNlZCBCb2xkIiksIGxlZ2VuZC5wb3NpdGlvbj0icmlnaHQiLCBsZWdlbmQua2V5LndpZHRoPXVuaXQoMC41LCAiY20iKSwgbGVnZW5kLmtleS5oZWlnaHQ9dW5pdCgyLCAiY20iKSwgbGVnZW5kLm1hcmdpbj1tYXJnaW4oMSwwLDEsMCksIHBsb3QudGl0bGU9ZWxlbWVudF90ZXh0KGhqdXN0PTAsIHNpemU9MTEpKSArCiAgICAgICAgICAgIGxhYnModGl0bGUgPSBzcHJpbnRmKCJMb2NhdGlvbnMgb2YgUHJlZGljdGVkIFR5cGVzIG9mIEFycmVzdHMgaW4gU2FuIEZyYW5jaXNjbyBvbiAlcyIsCiAgICAgICAgICAgICAgICAgZm9ybWF0KGRhdGVfdGFyZ2V0LCAnJUIgJWQsICVZIGF0JWwgJXAnKSkpCgptYXhfc2F2ZShwbG90LCBzcHJpbnRmKCJjcmltZS0lcyIsIGZvcm1hdChkYXRlX3RhcmdldCwgJyVZLSVtLSVkLSVIJykpLCAiU0YgT3BlbiBEYXRhIiwgdyA9IDYsIGggPSA2LCB0YWxsPVQpCmBgYAoKIVtdKGNyaW1lLTIwMTctMDQtMTUtMjAucG5nKQoKIyMgTWFwIEFuaW1hdGlvbgoKU2V0IGVhY2ggbGFiZWwgc3VjaCB0aGF0IGlzIGhhcyBhIGNvbnNpc3RlbnQgY29sb3IuCgpgYGB7cn0Kc2V0LnNlZWQoMTIzKQpjb2xzIDwtIGRpc3RpbmN0Q29sb3JQYWxldHRlKG51bV9jbGFzc2VzKQpuYW1lcyhjb2xzKSA8LSBkZl9sYWJlbHMkQ2F0ZWdvcnkKCmNvbHMKYGBgCgpQbG90IHRoZSBtYXAgZm9yIDI0IGhvdXJzIChjb252ZXJ0IHRvIGEgR0lGIHVzaW5nIGV4dGVybmFsIHRvb2xzKS4gUmV1c2UgY29kZSBhYm92ZSB0byBnZW5lcmF0ZSBhIG1hcCBmb3IgZ2l2ZW4gZGF0ZS90aW1lICsgaG91ciBkZWx0YS4KCgpgYGB7cn0Kc3lzdGVtKCJta2RpciAtcCBtYXBfYW5pIikKCmNyZWF0ZV9hcnJlc3RfbWFwIDwtIGZ1bmN0aW9uKGhvdXJfZGVsdGEsIGRhdGUpIHsKICBkYXRlX3RhcmdldCA8LSBkYXRlICsgaG91cl9kZWx0YSo2MCo2MAogIApncmlkX3NpemUgPC0gMjAwCgpkZl9wb2ludHMgPC0gZGF0YS5mcmFtZShleHBhbmQuZ3JpZChYPXNlcShiYm94WzFdLCBiYm94WzNdLCBsZW5ndGgub3V0PWdyaWRfc2l6ZSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFk9c2VxKGJib3hbMl0sIGJib3hbNF0sIGxlbmd0aC5vdXQ9Z3JpZF9zaXplKQopCikKCmRmX3BvaW50cyA8LSBkZl9wb2ludHMgJT4lCiAgICAgICAgICAgICAgbXV0YXRlKGhvdXIgPSBmb3JtYXQoZGF0ZV90YXJnZXQsICIlSCIpLAogICAgICAgICAgICAgICAgICAgIG1vbnRoID0gZm9ybWF0KGRhdGVfdGFyZ2V0LCAiJW0iKSwKICAgICAgICAgICAgICAgICAgICB5ZWFyID0gZm9ybWF0KGRhdGVfdGFyZ2V0LCAiJVkiKSwKICAgICAgICAgICAgICAgICAgICBEYXlPZldlZWsgPSB3aGljaChsZXZlbHMoZGYkRGF5T2ZXZWVrKSA9PSBmb3JtYXQoZGF0ZV90YXJnZXQsICIlQSIpKSkgJT4lCiAgICAgICAgICAgIGRhdGEubWF0cml4KCkKCgoKcHJlZHNfbWF0cml4IDwtIG1hdHJpeChwcmVkaWN0KGJzdCwgZGZfcG9pbnRzKSwgYnlyb3c9VCwgbnJvdyhkZl9wb2ludHMpLCBudW1fY2xhc3NlcykKCnJlc3VsdHMgPC0gdChhcHBseShwcmVkc19tYXRyaXgsIDEsIGZ1bmN0aW9uICh4KSB7CiAgbWF4X2luZGV4ID0gd2hpY2goeD09bWF4KHgpKQogIHJldHVybiAoYyhtYXhfaW5kZXgtMSwgeFttYXhfaW5kZXhdKSkKfSkpCgpybShwcmVkc19tYXRyaXgpCgpkZl9yZXN1bHRzIDwtIGRhdGEuZnJhbWUoWD1kZl9wb2ludHNbLDFdLCBZPWRmX3BvaW50c1ssMl0sIGxhYmVsPWZhY3RvcihyZXN1bHRzWywxXSksIHByb2I9cmVzdWx0c1ssMl0pICU+JQogICAgICAgICAgICAgICAgdGJsX2RmKCkgJT4lCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oZGZfbGFiZWxzLCBieT1jKCJsYWJlbCIgPSAiY2F0ZWdvcnlfaW5kZXgiKSkgJT4lCiAgICAgICAgICAgICAgICBtdXRhdGUoQ2F0ZWdvcnkgPSBmYWN0b3IoQ2F0ZWdvcnkpKQoKcGxvdCA8LSBnZ21hcChtYXApICsKICAgICAgICAgICAgZ2VvbV9yYXN0ZXIoZGF0YSA9IGRmX3Jlc3VsdHMgJT4lIGZpbHRlcihDYXRlZ29yeSAhPSAiT3RoZXIgT2ZmZW5zZXMiKSwgYWVzKHg9WCwgeT1ZLCBmaWxsPUNhdGVnb3J5KSwgYWxwaGE9MC45LCBzaXplPTApICsKICAgICAgICAgICAgY29vcmRfY2FydGVzaWFuKCkgKwogICAgICAgICAgICBmdGVfdGhlbWUoKSArCiAgICAgICAgICAgIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jb2xzKSArCiAgICAgICAgICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLCBheGlzLnRleHQueSA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpLCBheGlzLnRpdGxlLnkgPSBlbGVtZW50X2JsYW5rKCkpICsKICAgICAgICAgICAgdGhlbWUobGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KGZhbWlseT0iT3BlbiBTYW5zIENvbmRlbnNlZCBCb2xkIiksIGxlZ2VuZC5wb3NpdGlvbj0icmlnaHQiLCBsZWdlbmQua2V5LndpZHRoPXVuaXQoMC41LCAiY20iKSwgbGVnZW5kLm1hcmdpbj1tYXJnaW4oMCwwLDAsMCwgImNtIiksIGxlZ2VuZC5rZXkuaGVpZ2h0PXVuaXQoMSwgImNtIiksIHBsb3QudGl0bGU9ZWxlbWVudF90ZXh0KGhqdXN0PTAsIHNpemU9MTEpLCBsZWdlbmQudGV4dC5hbGlnbj0wKSArCiAgICAgICAgICAgIGxhYnModGl0bGUgPSBzcHJpbnRmKCJMb2NhdGlvbnMgb2YgUHJlZGljdGVkIFR5cGVzIG9mIEFycmVzdHMgaW4gU2FuIEZyYW5jaXNjbyBvbiAlcyIsCiAgICAgICAgICAgICAgICAgZm9ybWF0KGRhdGVfdGFyZ2V0LCAnJUIgJWQsICVZIGF0ICVsICVwJykpKQoKbWF4X3NhdmUocGxvdCwgc3ByaW50ZigibWFwX2FuaS9jcmltZS0lcyIsIGZvcm1hdChkYXRlX3RhcmdldCwgJyVZLSVtLSVkLSVIJykpLCAiU0YgT3BlbiBEYXRhIiwgdyA9IDYsIGggPSA2LCB0YWxsPVQpCgp9CmBgYAoKYGBge3J9CmJhc2VfZGF0ZSA8LSBhcy5QT1NJWGN0KCIyMDE3LTAzLTE0IDA2OjAwOjAwIikKaG91cl9kZWx0YXMgPC0gMDoyMwoKeCA8LSBsYXBwbHkoaG91cl9kZWx0YXMsIGNyZWF0ZV9hcnJlc3RfbWFwLCBiYXNlX2RhdGUpCmBgYAoKIVtdKG1hcF9hbmkuZ2lmKQoKIyBDb2RlIFdoaWNoIERpZCBOb3QgV29yayBPdXQgKE9uZS1Ib3QgRW5jb2RpbmcpCgpUaGUgY2F0ZWdvcmljYWwgYXBwcm9hY2ggdXNpbmcgTGlnaHRHQk0gaXMgYmV0dGVyLiBIZXJlIGlzIHRoZSBmb3JtZXIgY29kZSB1c2luZyBPSEUuCgpDYXRlZ29yaWNhbCBGZWF0dXJlcyBtdXN0IGJlIGZhY3RvcnMgZm9yIG9uZS1ob3QgZW5jb2RpbmcuCgoKQ29udmVydCB0aGUgZmFjdG9yIHZhcmlhYmxlcyBpbnRvIGR1bW15IHZhcmlhYmxlczogYG1vZGVsLm1hdHJpeCgpYCBjYW4gZG8gdGhpcyBpbiBSIG5hdGl2ZWx5LiAodmlhIFtTdGFjayBPdmVyZmxvd10oaHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL2EvNTA0ODcyNykpCgoKYGBge3J9CiMgbW9kZWwubWF0cml4KCkgYWRkcyBhbiBJbnRlcmNlcHQgY29sdW1uOiB0aGUgIi0xIiByZW1vdmVzIGl0LgojIE1hdHJpeCBjb252ZXJ0cyB0aGUgZGVuc2UgbWF0cml4IHRvIHNwYXJzZSAocmVkdWNlcyBtZW1vcnkgZm9vdHByaW50IHRvIDI1JSkuCnRyYWluIDwtIE1hdHJpeChtb2RlbC5tYXRyaXgofiBYICsgWSArIGhvdXIgKyBtb250aCArIHllYXIgKyBEYXlPZldlZWsgLSAxLCBkZikpCm51bV9jbGFzc2VzIDwtIGxlbmd0aCh1bmlxdWUoZGYkY2F0ZWdvcnlfaW5kZXgpKQpudW1fcm93cyA8LSBucm93KHRyYWluKQoKdHJhaW5bMToxMCxdCmBgYAoKVGhlIG9iamVjdGl2ZSBpcyBgbXVsdGlfbG9nbG9zc2Agc2luY2UgdGhlcmUgYXJlIG1hbnkgY2xhc3Nlcy4gVGhlIGBtdWx0aWNsYXNzYCBvYmplY3RpdmUgcmV0dXJucyBhIHByb2JhYmlsaXR5IGZvciBlYWNoIGNsYXNzLgoKRGVtbzogaHR0cHM6Ly9naXRodWIuY29tL01pY3Jvc29mdC9MaWdodEdCTS9ibG9iL21hc3Rlci9SLXBhY2thZ2UvdGVzdHMvdGVzdHRoYXQvdGVzdF9iYXNpYy5SI0wyOQoKYGBge3IsIGluY2x1ZGU9RkFMU0V9CnNldC5zZWVkKDEyMykKCmJzdCA8LSBsaWdodGdibShkYXRhID0gdHJhaW4sIGxhYmVsID0gZGYkY2F0ZWdvcnlfaW5kZXgsIG5yb3VuZHMgPSAyMDAsIG50aHJlYWRzPTgsIG9iamVjdGl2ZSA9ICJtdWx0aWNsYXNzIiwgbWV0cmljPSJtdWx0aV9sb2dsb3NzIiwgbnVtX2NsYXNzPW51bV9jbGFzc2VzLCBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSAzLCBuZm9sZHM9NSwgdmVyYm9zZSA9IDApCmBgYAoKYGBge3J9CnByZWRzIDwtIHByZWRpY3QoYnN0LCB0cmFpblsxOjIsXSkKcHJlZHMKbGVuZ3RoKHByZWRzKQpgYGAKCiMgTElDRU5TRQoKVGhlIE1JVCBMaWNlbnNlIChNSVQpCgpDb3B5cmlnaHQgKGMpIDIwMTcgTWF4IFdvb2xmCgpQZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYSBjb3B5IG9mIHRoaXMgc29mdHdhcmUgYW5kIGFzc29jaWF0ZWQgZG9jdW1lbnRhdGlvbiBmaWxlcyAodGhlIOKAnFNvZnR3YXJl4oCdKSwgdG8gZGVhbCBpbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCwgZGlzdHJpYnV0ZSwgc3VibGljZW5zZSwgYW5kL29yIHNlbGwgY29waWVzIG9mIHRoZSBTb2Z0d2FyZSwgYW5kIHRvIHBlcm1pdCBwZXJzb25zIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzIGZ1cm5pc2hlZCB0byBkbyBzbywgc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgpUaGUgYWJvdmUgY29weXJpZ2h0IG5vdGljZSBhbmQgdGhpcyBwZXJtaXNzaW9uIG5vdGljZSBzaGFsbCBiZSBpbmNsdWRlZCBpbiBhbGwgY29waWVzIG9yIHN1YnN0YW50aWFsIHBvcnRpb25zIG9mIHRoZSBTb2Z0d2FyZS4KClRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCDigJxBUyBJU+KAnSwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUiBJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIgTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUgU09GVFdBUkUu