Last updated: 2021-09-08

Checks: 7 0

Knit directory: myTidyTuesday/

This reproducible R Markdown analysis was created with workflowr (version 1.6.2). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


Great! Since the R Markdown file has been committed to the Git repository, you know the exact version of the code that produced these results.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(20210907) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version e231131. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    .Rhistory
    Ignored:    .Rproj.user/

Untracked files:
    Untracked:  catboost_info/
    Untracked:  data/2021-09-08/

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (analysis/2021_08_17_sliced.Rmd) and HTML (docs/2021_08_17_sliced.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
Rmd e231131 opus1993 2021-09-08 wflow_publish(“analysis/2021_08_17_sliced.Rmd”)
html 7605fc7 opus1993 2021-09-08 Build site.
Rmd 31a4cc9 opus1993 2021-09-08 Classification EDA titles, aspect ratios
html 4e38044 opus1993 2021-09-08 Build site.
Rmd 3fc8f4f opus1993 2021-09-08 Add DALEX variable importance
html 379ed14 opus1993 2021-09-07 Build site.
Rmd 990bb59 opus1993 2021-09-07 wflow_publish(“analysis/2021_08_17_sliced.Rmd”)

The Season 1 Finale of #SLICED featured a challenge to predict default exposure in a portfolio of bank loans in a dataset from the U.S. Small Business Administration (SBA).

Small Business Administration

The Small Business Administration (SBA) was founded in 1953 to assist small businesses in securing loans. The argument is that, as small businesses are a primary source of employment in the United States, then helping small businesses helps with job creation, reducing unemployment. One of the ways the SBA helps small businesses is by guaranteeing bank loans. This guarantee reduces the risk to individual banks and encourages them to lend in situations that they otherwise might not. If the loan defaults, the SBA covers the amount guaranteed, and the bank suffers a loss for the remaining balance.

There have been a few small business success stories like FedEx and Apple. However, the rate of default is somewhat higher than most commercial banks would find attractive. Some economists believe the banking market works better without the assistance of the SBA. On the other hand, supporters claim that the social benefits and job creation outweigh the financial costs to the taxpayer in defaulted loans.

My capstone project in grad school a few years back involved a study of regional banks and account policies across the US financial network. Regulatory submissions to SBA, FDIC, the USDA, and even state banking authorities are a great opportunity to model for decisions for economists and finance. In this case, modeling the amount of default exposure could be useful in providing information to government policymakers or insurers.

The source data set was from the U.S. SBA loan database and includes information on whether the loan was paid off in full or if the SBA had to charge off any amount in default, and how much that amount was. More information on this data set is availble here.

SLICED is like the TV Show Chopped but for data science. The competitors get a never-before-seen dataset and two-hours to code a solution to a prediction challenge. Contestants get points for the best model plus bonus points for data visualization, votes from the audience, and more. The evaluation metric is Mean Absolute Error.

The audience is invited to participate as well. This type of problem is a big challenge. 64k of the 83k loans in the SLICED training dataset did not default at all, so show zero default amount. Separating the signal from the noise here is tough.

Clearly there is a large imbalance here between good and bad loans. This is probably a good thing for the banks, but poses an interesting issue for us because we want to ensure we are sensitive to the bad loans and are not overwhelmed by the number of good ones. One approach that we might take is to downsample the number of good loans so that the total number of them is more in line with the number of bad loans.

There are likely confounders and other feature transformations required. I also wondered whether approaching this as a classification problem first, and building a regression second might be better. Eben Esterhuizen was kind enough to share his top-scoring Python model code at Kaggle. Interestingly, this was exactly his approach.

tweetrmd::include_tweet("https://twitter.com/EbenEsterhuizen/status/1427837303423184900")

Still in disbelief, but I won modeling in #SLICED #datascience championship round :) Massive thanks to @MeganRisdal and @nickwan for an amazing show and data science education. Here is my notebook using stacked models: https://t.co/nYUmZLE9AA

— Eben Esterhuizen (@EbenEsterhuizen) August 18, 2021

My plan here is to re-write his approach in R, make a number of improvements, and explain how it all works. Let’s load up R packages:

suppressPackageStartupMessages({
library(tidyverse) # clean and transform rectangular data

library(hrbrthemes) # plot theming
library(ggthemes)
library(ggforce)
library(treemapify)

library(tidymodels) # machine learning tools
library(finetune) # racing methods for accelerating hyperparameter tuning

library(discrim)
library(probably)
library(treesnip) # a boost tree adaptor for catboost and other engines
library(catboost) # our boosted tree engine
library(embed)
  
library(bestNormalize) # has a step that can enforce a symmetric distribution for the predictors. We’ll use this to mitigate the issue of skewed distributions.
library(themis) # ml prep tools for handling unbalanced datasets

  
})

source(here::here("code","_common.R"),
       verbose = FALSE,
       local = knitr::knit_global())

theme_set(theme_jim(base_size = 14))

#create a data directory
data_dir <- here::here("data",Sys.Date())
if (!file.exists(data_dir)) dir.create(data_dir)

# set a competition metric
mset <- metric_set(mae)

# set the competition name from the web address
competition_name <- "sliced-s01e12-championship"

zipfile <- paste0(data_dir,"/", competition_name, ".zip")

path_export <- here::here("data",Sys.Date(),paste0(competition_name,".csv"))

Get the Data

A quick reminder before downloading the dataset: Go to the Kaggle web site and accept the competition terms!!!

If you pip install kaggle, there are (Windows) shell commands available to interact with Kaggle here:

# from the Kaggle api https://github.com/Kaggle/kaggle-api

# the leaderboard
shell(glue::glue('kaggle competitions leaderboard { competition_name } -s'))

# the files to download
shell(glue::glue('kaggle competitions files -c { competition_name }'))

# the command to download files
shell(glue::glue('kaggle competitions download -c { competition_name } -p { data_dir }'))

# unzip the files received
shell(glue::glue('unzip { zipfile } -d { data_dir }'))

Read in the contents of the files to dataframes here. I’ve written an import function to cast the data types and create new features in the same way, both from the provided train data file as well as from the un-labeled file labeled test, that I will call competition_submission_df. In this case, the competition_submission_df appears to be a random sample selection across all years and states. We will likely discover that the competition_submission_df includes years or factor levels, like states or zip codes, that were not present in the training set.

cast_transform <- function(tbl){
  tbl %>% 
   mutate(FranchiseCode = if_else(FranchiseCode %in% c(0,1),
                                 "noFranchise",
                                 "Franchise"),
    NAICS2 = str_sub(NAICS, 1L, 2L), #Industry Class Hierarchy
    NAICS3 = str_sub(NAICS, 1L, 3L), #Industry Class Hierarchy
    NAICS4 = str_sub(NAICS, 1L, 4L), #Industry Class Hierarchy
    NewExist = if_else(is.na(NewExist), "Unknown", NewExist)
    ) %>% 
  mutate(across(
    c("Name",
      "City",
      "State",
      "Zip",
      "Bank",
      "Sector",
      "NAICS",
      "BankState",
      "UrbanRural",
      "NewExist",
      "FranchiseCode", 
      "NAICS2", "NAICS3", "NAICS4"
    ),
    as_factor
  )) %>%
  mutate(
    UrbanRural = fct_recode(
      UrbanRural,
      "Urban" = "1",
      "Rural" = "2",
      "Unknown" = "0"
    ),
    NewExist = fct_recode(NewExist,
                          "Existing" = "1.0",
                          "New" = "2.0",
                          "Unknown" = "0.0"),
    loan_per_employee = GrAppv / (NoEmp + 1),
    SBA_Appv_vs_Gross = SBA_Appv / GrAppv,
    Gross_minus_SBA_Appv = GrAppv - SBA_Appv,
    Gross_minus_SBA_Appv_vs_Employee = Gross_minus_SBA_Appv / (NoEmp + 1),
    same_state = State == BankState
   )
}

train_df <-
  read_csv(
    file = glue::glue({
      data_dir
    }, "/train.csv"),
    col_types = "ccccccccciiciiccdddd",
    show_col_types = FALSE
  ) %>%
  mutate(default_flag = as_factor(if_else(
    default_amount > 0,
    "default",
    "nondefault"
  )),
  default_proportion = default_amount / GrAppv) %>%
  cast_transform() %>%
  dplyr::select(default_amount,
                default_flag,
                default_proportion,
                everything())

competition_submission_df <-
  read_csv(
    file = glue::glue({
      data_dir
    }, "/test.csv"),
    col_types = "ccccccccciiciiccddd",
    show_col_types = FALSE
  ) %>%
  cast_transform()

Exploratory Data Analysis (EDA)

Some questions to answer upfront:

In competition I use skimr::skim() for quick checks. We will craft much nicer plots below.

skimr::skim(train_df)

The Outcome Variable

Lets characterize the default_amount as a histogram for all data in the set, and as a treemap to better understand the distributions by a few of the category factor levels.

train_df %>% 
  group_by(State, Sector = fct_lump(Sector, 10)) %>% 
  summarise(n = n(), 
            mean_default = mean(default_amount > 0, na.rm = TRUE),
            .groups = "drop") %>% 
  ggplot(aes(area = n, 
             fill = mean_default, 
             label = Sector, 
             subgroup = State)) +
  geom_treemap() +
  geom_treemap_subgroup_border() +
  geom_treemap_text(color = "black", place = "top", reflow = TRUE) +
  geom_treemap_subgroup_text(
    color = "gray40", 
    place = "bottomright",
    fontface = "italic", 
    min.size = 0, 
    alpha = 0.3
  )  +
  scale_fill_fermenter(
    palette = "PuRd", 
    direction = 1,
    guide = "colorsteps", 
    labels = scales::percent_format(accuracy = 1),
    breaks = seq(0,1,0.1)
  ) +
  labs(
    fill = "Loans\nThat Default", 
    title = "US Small Business Admin Loans 2003-2010",
    subtitle = "Box area is # of loans for sectors within ea State.\nThe vast majority of individual loans do not default.",
    caption = "Data Source: Kaggle"
  )

train_df %>% 
  dplyr::select(GrAppv, default_flag ) %>% 
  bind_rows(dplyr::select(competition_submission_df, GrAppv) %>% mutate(default_flag = "holdout set")) %>% 
  ggplot(aes(GrAppv, fill = default_flag)) +
  geom_histogram(bins = 50) +
  scale_x_log10(labels = scales::dollar_format()) +
  labs(title = "Loans tend to fall between $10k to 1M",
       fill = NULL, x = "Gross Approved Loan Value",
       subtitle = "SBA must have value threshold limits")


Categorical Variables

summarize_defaults <- function(tbl){
  tbl %>% 
  summarize(n_loans = n(),
            pct_default = mean(default_amount > 0),
            total_gr_appv = sum(GrAppv),
            total_default_amount = sum(default_amount ),
            pct_default_amount = total_default_amount / total_gr_appv,
            .groups = "drop") %>% 
  arrange(desc(n_loans))
}

withfreq <- function(x){
  tibble(x) %>% 
    add_count(x) %>% 
    mutate(combined = glue::glue("{ str_sub(x, end = 18) } ({ n })")) %>% 
    pull(combined)
}

plot_category <- function(tbl, category, n_categories = 7){
  tbl %>% 
  group_by({{ category }} := withfreq(fct_lump({{ category }}, n_categories))) %>% 
  summarize_defaults() %>% 
  mutate({{ category }} := fct_reorder({{ category }}, pct_default)) %>% 
  ggplot(aes(pct_default, {{ category }})) +
  geom_point(aes(color = pct_default_amount)) +
  geom_text(aes(x = pct_default - 0.02, 
                label = glue::glue("$ {round(total_default_amount/10^6,0)} M")),
            hjust = 1) +
    scale_color_viridis_b(
      name = "% of Loan Amount\nin Default",
      labels = scales::percent_format(accuracy = 1),
      breaks = c(0, 0.05, 0.1, 0.15),
      option = "H"
    ) +
    guides(color = guide_bins()) +
    scale_x_continuous(labels = scales::percent_format(accuracy = 1)) +
    expand_limits(x = 0, size = 0) +
    labs(x = "% of Loans Originated that Defaulted", y = NULL,
         subtitle = "(# of loans) & Total Defaulted Amount on Labels")
  
}

train_df %>% 
   plot_category(State) +
   labs(title = "Florida, Illinois, New York")

train_df %>% 
  plot_category(City) +
  labs(title = "Miami leads")

train_df %>% 
  plot_category(Sector) +
  labs(title = "Construction leads")

train_df %>% 
  plot_category(Bank) +
  labs(title = "BBCN is the leader")

train_df %>% 
  mutate(same_state = if_else(BankState == State, 
                              "Same State",
                              "Different")) %>% 
  plot_category(same_state) +
  labs(title = "Banks in States different than the Borrower")

train_df %>% 
  plot_category(NewExist) +
  labs(title = "New Borrowers")

train_df %>% 
  plot_category(FranchiseCode) +
  labs(title = "When they do default, Franchises aren't much different")

train_df %>% 
  plot_category(NAICS2) +
  labs(title = "NAICS codes starting with 23")

train_df %>% 
  plot_category(NAICS3) +
  labs(title = "NAICS codes starting with 238")

train_df %>% 
  plot_category(NAICS4) +
  labs(title = "NAICS codes starting with 8121")

train_df %>% 
  plot_category(NAICS) +
  labs(title = "NAICS code 722211")

train_df %>% 
  plot_category(UrbanRural) +
  labs(title = "Urban borrowers")

train_df %>% 
  plot_category(Name) +
  labs(title = "The bad business names")


Time Series

train_df %>% 
  group_by(ApprovalFY, State = fct_lump(State, 5)) %>% 
  summarize_defaults() %>% 
  ggplot(aes(ApprovalFY, pct_default, color = State)) +
  geom_line(size = 2) +
  scale_y_continuous(labels = scales::percent_format()) +
  scale_size_continuous(labels = scales::dollar_format()) +
  expand_limits(y = 0, size = 0) +
  labs(x = "year approved", y = "Default Percentage",
       title = "SBA Loans in the 2008 financial crisis",
       size = "Loan Approvals",
       caption = "Based on David Robinson @drob example")


Numeric Features

train_numeric <- train_df %>% keep(is.numeric) %>% colnames()

train_df %>%
  bind_rows(competition_submission_df) %>% 
  select_at(all_of(train_numeric)) %>%
  dplyr::select(-default_amount) %>%
  pivot_longer(
    cols = everything(),
    names_to = "key",
    values_to = "value"
  ) %>%
  filter(!is.na(value)) %>%
  ggplot(mapping = aes(value, fill = key)) +
  geom_histogram(
    position = "identity",
    bins = 30,
    show.legend = FALSE
  ) +
  scale_y_continuous(labels = scales::comma_format(),
                     n.breaks = 3) +
  scale_x_continuous(n.breaks = 3) + 
  facet_wrap( ~ key, scales = "free", ncol = 3) +
  labs(
    title = "Numeric Feature Histogram Distributions",
    subtitle = "Training and Holdout datasets combined",
    x = NULL,
    y = "Count"
  ) +
  theme(aspect.ratio = 1.3) +
  theme_jim(base_size = 8)

train_df %>% 
  filter(default_amount > 1) %>% 
  select_at(all_of(train_numeric)) %>%
  pivot_longer(cols = -c(default_amount, ApprovalFY),
               names_to = "key",
               values_to = "value") %>% 
  ggplot(aes(ApprovalFY, value, z = default_amount)) +
  stat_summary_hex(alpha = 0.9, bins = 10) +
  scale_fill_viridis_c(option = "H",labels = scales::dollar_format()) +
  scale_x_continuous(n.breaks = 3) +
  scale_y_continuous(n.breaks = 3) +
  facet_wrap(~ key, scales = "free", ncol = 3 ) +
  labs(subtitle = "Mean Default Value Where > $1", 
       title = "Numeric Features by Loan Origination Year",
       y = NULL, fill = "Default $") +
  theme(aspect.ratio = 1.3) +
  theme_jim(base_size = 8)

tmwr_cols <- viridis::viridis_pal(option = "H")

train_df %>% 
    select_at(all_of(train_numeric)) %>%
    cor() %>% 
    heatmap(main = "Numeric Correlations")

The highly correlated numeric variables are going to have to be dealt with, either through some sort of regularization or pre-processing.


Maps

by_state <- train_df %>% 
  group_by(State) %>% 
  summarize_defaults()

map_data("state") %>% 
  as_tibble() %>% 
  mutate(State = state.abb[match(region, str_to_lower(state.name))]) %>% 
  inner_join(by_state, by = "State") %>% 
  ggplot(aes(long, lat, group = group, fill = pct_default)) +
  geom_polygon() +
  theme_map() +
  coord_map() +
  scale_fill_viridis_c(option = "H", labels = scales::percent_format()) +
  labs(title = "SBA Loan Defaults 2003-2010: Florida, Georgia, Illinois, Nevada, & Michigan lead",
       fill = "Percent of Loan\nOriginations Ending\nin Default",
       caption = "Data Source: Kaggle. Based on David Robinson's example.")

So, yes, we will first model to predict for the default_flag class. If it performs well, we will then go on to model the default_amount separately for the loans predicted to default.

Machine Learning: Classification

Here’s an often overlooked piece of advice for all ML practitioners, & especially newcomers:

👉 Build a non-ML baseline first.

You’ll be surprised how often you cannot easily improve a basic solution by throwing ML at it, and you’ll learn something about the problem too.

Your best alternative to any complex model isn’t “no model.” Your alternative is a simple model. I see this over and over. Before you build a Rube Goldberg model of the universe, build a brain dead simple model. THAT parsimonious piece of junk is your bogie. Out perform THAT in a way that justifies your complexity and time.

Once you’re comparing a complicated model to a parsimonious model you can intelligently discuss the “marginal benefit (or lift) from complexity.”

round(100*prop.table(table(train_df$default_flag)),1) 

nondefault    default 
      77.1       22.9 

In the loan default dataset, predicting a nondefault class for every case results in 77.1% accuracy, doing nothing. Let’s call this the NULL model.


Spending the Data

We will split train_df randomly into a training portion (90%) and a labeled testing portion (10%). The training portion is then divided into 5-fold cross validation sets and stratified on the default_flag to ensure that each CV fold has some proportion of the imbalanced outcomes.

set.seed(2021)

split <- initial_split(train_df, prop = 0.9)

training <- training(split)
testing <- testing(split)

class_folds <- vfold_cv(training, v = 5, strata = default_flag )

The Recipes

We are going to start by taking a survey of a handful of models that train fast, have little tuning required, and are generally interpretable. The choice of pre-processor, or tidymodels recipe() steps, transforms the dataframe into formats that machine learning algorithms can work with.

Our strategy here is to pre-process at three levels. The first, classification_rec is a raw dataset with all categorical predictors intact. The second, dummies_classification_rec tunes a threshold for lumping categorical levels together and then creates dummy variables so everything is numeric. With so many category levels, this training set could be thousands of columns wide, and sparse. There is a cardinality tradeoff here, where each additional column may not contribute much to the model. The third pre-processor, pls_classification_rec, works as a sort of supervised learning version of principal components analysis, called Partial Least Squares, that simultaneously maximize the variation in the predictors while also maximizing the relationship between those components and the outcome.

classification_rec <-
  recipe(
    default_flag ~ LoanNr_ChkDgt + Name + Sector + City + State + Zip +
      Bank + BankState + NAICS + ApprovalFY + NoEmp +
      NewExist + CreateJob + RetainedJob + FranchiseCode + UrbanRural +
      DisbursementGross + GrAppv + SBA_Appv + NAICS2 + NAICS3 + NAICS4 +
      loan_per_employee + SBA_Appv_vs_Gross + Gross_minus_SBA_Appv +
      Gross_minus_SBA_Appv_vs_Employee + same_state,
    data = training
  ) %>%
  update_role(LoanNr_ChkDgt, new_role = "ID") 

dummies_classification_rec <-
  classification_rec %>%
  step_novel(
    Name,
    Sector,
    City,
    State,
    Zip,
    Bank,
    BankState,
    NAICS,
    NAICS2,
    NAICS3,
    NAICS4
  ) %>%
  step_other(
    Name,
    Sector,
    City,
    State,
    Zip,
    Bank,
    BankState,
    NAICS,
    NAICS2,
    NAICS3,
    NAICS4,
    threshold = tune()) %>%
  step_dummy(all_nominal_predictors()) %>%
  step_interact(~ starts_with("Bank"):ApprovalFY) %>% # Add important interactions
  step_orderNorm(all_numeric_predictors()) %>%
  step_normalize(all_numeric_predictors()) 

pls_classification_rec <- dummies_classification_rec %>%
  step_pls(all_numeric_predictors(),
           num_comp = tune(),
           outcome = "default_flag") %>%
  step_normalize(all_numeric_predictors()) 

Feature Extraction

plot_validation_results <- function(recipe, 
                                    dat = train_df, 
                                    outcome = default_flag) {
  recipe %>%
    # Estimate any additional steps
    prep() %>%
    # Process the data (the validation set by default)
    bake(new_data = dat) %>%
    dplyr::select({{ outcome }}, starts_with("PLS")) %>% 
    # Create the scatterplot matrix
    ggplot(aes(x = .panel_x, 
               y = .panel_y, 
               col = {{ outcome }}, 
               fill = {{ outcome }})) +
    geom_point(alpha = 0.4, size = 0.5) +
    geom_autodensity(alpha = .3) +
    facet_matrix(vars(-{{ outcome }}), layer.diag = 2) 
}

Let’s look for separation of the classes in the new, transformed features from the PLS preprocessor. Our modeling system will tune() both for the number of factors to be lumped together and for the number of PLS components.

pls_classification_rec %>%
  finalize_recipe(list(num_comp = 4,
                       threshold = 0.01)) %>% 
  plot_validation_results() +
  ggtitle("Partial Least Squares")

pls_classification_rec %>% 
    finalize_recipe(list(num_comp = 4,
                       threshold = 0.01)) %>% 
    prep() %>% 
    learntidymodels::plot_top_loadings(component_number <= 4, 
                                      n = 6, type = "pls") +
  ggtitle("Partial Least Squares Components") +
  theme(aspect.ratio = 0.9) +
  theme_jim(base_size = 7)

Each component consists of combinations of the original features. Depending upon the threshold for lumping categorical levels together, the PLS pre-processing may help to separate the signal from the noise. On the other hand, we will use machine learning techniques that include regularization, another technique for dealing with having too many noisy variables.

The size of the loan and approval year drive the first PLS component. Others PLS components are driven by groups of states and a few of the banks where the loan was originated.

Baseline Model Specifications

Our modeling engines will first include a Generalized Linear Model with Regularization to constrain the number of features and constrain the out of sample error. This approach, with Lasso and Ridge penalty parameters, is sometimes called Elastic Net or GLMNET.

The second builds a discriminant analysis model that uses nonlinear features created using multivariate adaptive regression splines, or MARS. It is a non-parametric regression technique and can be seen as an extension of linear models that automatically models nonlinearities and interactions between variables. The term “MARS” is trademarked and licensed to Salford Systems. In order to avoid trademark infringements, open-source implementations of MARS are called “Earth”.

The third, regularized discriminant analysis, builds a model that estimates a multivariate distribution for the predictors separately for the data in each class.

logistic_reg_glm_spec <-
  logistic_reg(penalty = tune(),
               mixture = tune()) %>%
  set_engine('glmnet')

fda_spec <-
  discrim_flexible(prod_degree = tune()) %>%
  set_engine('earth')

rda_spec <-
  discrim_regularized(frac_common_cov = tune(),
                      frac_identity = tune()) %>%
  set_engine('klaR')

GLM Model Tune

We are going to quickly race through a grid of 6 parameter combinations with workflowsets. I will set reasonable parameters ranges in advance for the categorical variable lumping and the number of PLS components.

ctrl <- control_race(parallel_over = "everything",
                     save_pred = TRUE)

all_cores <- parallelly::availableCores(omit = 1)
all_cores

future::plan("multisession", workers = all_cores) # on Windows
workflows <-  workflow_set(
    preproc = list(factor = dummies_classification_rec,
                   pls = pls_classification_rec), 
    models = list(glmnet = logistic_reg_glm_spec,
                  fda = fda_spec,
                  rda = rda_spec)
  ) 

factor_glmnet_params <- workflows %>% 
  extract_workflow("factor_glmnet") %>% 
  parameters() %>% 
  update(penalty = penalty(),
         mixture = mixture(),
         threshold = threshold(range = c(0.1, 0.005)))

factor_fda_params <- workflows %>% 
  extract_workflow("factor_fda") %>% 
  parameters() %>% 
  update(prod_degree = prod_degree(),
         threshold = threshold(range = c(0.1, 0.005)))

factor_rda_params <- workflows %>% 
  extract_workflow("factor_rda") %>% 
  parameters() %>% 
  update(frac_common_cov = frac_common_cov(),
         frac_identity = frac_identity(),
         threshold = threshold(range = c(0.1, 0.005)))

pls_glmnet_params <- workflows %>% 
  extract_workflow("pls_glmnet") %>% 
  parameters() %>% 
  update(penalty = penalty(),
         mixture = mixture(),
         threshold = threshold(range = c(0.1, 0.005)),
         num_comp = num_comp(range = c(6L,11L)))

pls_fda_params <- workflows %>% 
  extract_workflow("pls_fda") %>% 
  parameters() %>% 
  update(prod_degree = prod_degree(),
         threshold = threshold(range = c(0.1, 0.005)),
         num_comp = num_comp(range = c(6L,11L)))

pls_rda_params <- workflows %>% 
  extract_workflow("pls_rda") %>% 
  parameters() %>% 
  update(frac_common_cov = frac_common_cov(),
         frac_identity = frac_identity(),
         threshold = threshold(range = c(0.1, 0.005)),
         num_comp = num_comp(range = c(6L,11L)))

workflows <- workflows %>% 
  option_add(param_info = factor_glmnet_params, id = "factor_glmnet") %>%
  option_add(param_info = factor_fda_params, id = "factor_fda") %>% 
  option_add(param_info = factor_rda_params, id = "factor_rda") %>% 
  option_add(param_info = pls_glmnet_params, id = "pls_glmnet") %>%
  option_add(param_info = pls_fda_params, id = "pls_fda") %>% 
  option_add(param_info = pls_rda_params, id = "pls_rda") 

We then tune each of the models, running across all validation folds for every grid combination. We are using a time-saving technique here with tune_race_anova(), where poor performing candidates are dropped. For 5-fold cross validation, the reduction in time is as much as 40%.

default_res <- workflows %>% 
  workflow_map(
    fn = "tune_race_anova",
    verbose = TRUE,
    seed = 2021,
    resamples = class_folds,
    grid = 21,
    control = ctrl,
    metrics = metric_set(mn_log_loss)
  )

i 1 of 6 tuning: factor_glmnet

v 1 of 6 tuning: factor_glmnet (1h 1m 15.9s)

i 2 of 6 tuning: factor_fda

v 2 of 6 tuning: factor_fda (57m 56.7s)

i 3 of 6 tuning: factor_rda

v 3 of 6 tuning: factor_rda (13m 13.9s)

i 4 of 6 tuning: pls_glmnet

v 4 of 6 tuning: pls_glmnet (9m 10.9s)

i 5 of 6 tuning: pls_fda

v 5 of 6 tuning: pls_fda (10m 18.3s)

i 6 of 6 tuning: pls_rda

v 6 of 6 tuning: pls_rda (11m 49.3s)

rankings <- 
  rank_results(default_res, select_best = TRUE) %>% 
  mutate(method = map_chr(wflow_id, ~ str_split(.x, "_", simplify = TRUE)[1])) 

filter(rankings, rank <= 5) %>% dplyr::select(rank, mean, model, method)
rankings %>% 
  ggplot(aes(x = rank, y = mean, pch = method, col = model)) + 
  geom_point(cex = 3) + 
  theme(legend.position = "right") +
  labs(y = "Mean Log Loss")

The best of the best were the regularlized glm with the simple dummy preprocessor. We can visualize the benefit of tune_race_anova(), where only the best combinations were run through the entire tuning routine.

plot_race(default_res %>% extract_workflow_set_result("factor_glmnet"))

Let’s extract the hyperparameter settings for that top model.

(best_params <- default_res %>% 
  extract_workflow_set_result("factor_glmnet") %>% 
  select_best())
best_wf <- default_res %>% 
  extract_workflow("factor_glmnet") %>% 
  finalize_workflow(best_params)

And take a close look at the classification performance of that best glmnet model on the 10% testing data that was held out.

lastFit <- last_fit(best_wf, split, metrics = metric_set(mn_log_loss))

collect_metrics(lastFit) 

Note here the the mean log loss classification error is comparable to that of the cross validation figure above, so we have more confidence that the model works on unseen data. Let’s move forward then.

Fit

Recall that our original dataset was imbalanced. There are far more non-defaults than defaults. Because of this, dialing in the cutoff, balancing between specificity and sensitivity, warrants more attention. Let’s fit the best model on all of the training data (omitting the cross validation folds).

glm_classification_fit <- fit(best_wf, data = training)

Let’s simply predict the probabilities on the labeled testing data.

lending_test_pred <- glm_classification_fit %>%
    predict(new_data = testing, type = "prob") %>% 
    bind_cols(testing)

With our class probabilities in hand, we can use make_two_class_pred() to convert these probabilities into hard predictions using a threshold. A threshold of 0.5 just says that if the predicted probability is above 0.5, then classify this prediction as a “nondefault” loan, otherwise, default.

lending_test_pred %>%
  mutate(.pred = make_two_class_pred(.pred_nondefault, levels(default_flag), threshold = .5)) %>%
  dplyr::select(default_flag, contains(".pred")) %>% 
  conf_mat(truth = default_flag, estimate = .pred) %>% 
  autoplot() +
  labs(title = "GLM Confusion Matrix at 0.5 Cutoff")

With a 0.5 threshold, almost all of the loans were predicted as “nondefault”. Perhaps this has something to do with the large class imbalance. On the other hand, the bank might want to be more stringent with what is classified as a “good” loan, and might require a probability of 0.75 as the threshold.

lending_test_pred %>%
  mutate(.pred = make_two_class_pred(.pred_nondefault, levels(default_flag), threshold = .75)) %>%
  dplyr::select(default_flag, contains(".pred")) %>% 
  conf_mat(truth = default_flag, estimate = .pred) %>% 
  autoplot() +
  labs(title = "GLM Confusion Matrix at 0.75 Cutoff")

In this case, many the bad loans were correctly classified as bad, but more of the good loans were also misclassified as bad now. There is a tradeoff here, which can be somewhat captured by the metrics sensitivity and specificity.

In this example, as we increased specificity, we lowered sensitivity. It would be nice to have some combination of these metrics to represent this tradeoff. Luckily, j_index is exactly that.

Now, this is not the only way to optimize things. If you care about low false positives, you might be more interested in keeping sensitivity high, and this wouldn’t be the best way to tackle this problem. There are other ways to weigh economic consequences. For now, let’s see how we can use probably to optimize the j_index.

With ggplot2, we can easily visualize this varying performance to find our optimal threshold for maximizing j_index.

threshold_data <- lending_test_pred %>%
  threshold_perf(default_flag, 
                 .pred_nondefault, 
                 thresholds = seq(0.5, 1, by = 0.0025)) %>%
  filter(.metric != "distance") %>%
  mutate(group = case_when(
    .metric == "sens" | .metric == "spec" ~ "1",
    TRUE ~ "2"
  ))

max_j_index_threshold <- threshold_data %>%
  filter(.metric == "j_index") %>%
  filter(.estimate == max(.estimate)) %>%
  pull(.threshold)

ggplot(threshold_data,
       aes(
         x = .threshold,
         y = .estimate,
         color = .metric,
         alpha = group
       )) +
  geom_line() +
  scale_alpha_manual(values = c(.7, 1), guide = "none") +
  geom_vline(xintercept = max_j_index_threshold,
             alpha = .6, size = 2,
             color = "grey30") +
  labs(
    x = "'Good' Threshold (above this value is considered 'non-default')",
    y = "Metric Estimate",
    title = "Balancing performance by varying the threshold",
    subtitle = "Sensitivity or specificity alone might not be enough!\nVertical line = Max J-Index"
  )

From this visual, the optimal threshold is exactly 0.78. As an interesting view, let’s run that GLM model on all of the train_df file with the new cutoff.

augment(glm_classification_fit, train_df, type = "prob") %>% 
   mutate(.pred = make_two_class_pred(.pred_nondefault,
                                      levels(default_flag), 
                                      threshold = max_j_index_threshold)) %>% 
  conf_mat(truth = default_flag, estimate = .pred_class) %>% 
  autoplot() +
  labs(title = "GLM Confusion Matrix on Entire Training Set",
       subtitle = glue::glue("{ augment(glm_classification_fit, train_df, type = 'prob') %>% mutate(.pred = make_two_class_pred(.pred_nondefault, levels(default_flag), threshold = max_j_index_threshold)) %>%  accuracy(truth = default_flag, estimate = .pred_class) %>% pull(.estimate) %>% round(2)*100 }% ", "Accuracy with regularized general linear model.\nOnly 4 accuracy points better than the NULL model."))

After all of that work, the results are disappointing, as the accuracy is not much better than the NULL model.

glm_classification_fit %>% 
  extract_fit_parsnip() %>% 
  vip::vip(metric = "mn_log_loss", num_features = 25L) +
  labs(title = "Regularized GLM Variable Importance Scores")

Let’s use the insights gained above, including the variable importance scores, to build a better boosted model.

Catboost Model Tune

Catboost is a high-performance open source library for gradient boosting on decision trees. The algorithm has advantages in automatically handling deep categorical and hieararchies of categorical features and leveraging the residuals of the weak learners to boost additional rounds of model training. The catboost specification requires five tuning parameters, so it often takes more time to find optimal combinations.

catboost_classification_spec <- boost_tree(
  trees = tune(),
  mtry = tune(),
  min_n = tune(),
  learn_rate = tune(),
  tree_depth = tune()
) %>% 
set_engine("catboost") %>%
  set_mode("classification") %>% 
  step_novel(all_nominal_predictors())

Given the variable importance scores above, let’s build a new pre-processor recipe().

catboost_class_rec <- recipe(
  default_flag ~ GrAppv + SBA_Appv + ApprovalFY + Bank + BankState +
    Name + NAICS + NAICS2 + NAICS3 + NAICS4 + UrbanRural + State + City,
    data = training
    ) 

Let’s set boundaries for the hyper-parameter search and build a grid of possible values.

catboost_classification_wf <-
  workflow() %>%
  add_recipe(catboost_class_rec) %>%
  add_model(catboost_classification_spec)

set.seed(2021)

(race_grid <-
   grid_latin_hypercube(
     finalize(mtry(), catboost_class_rec %>% prep() %>% juice()),
     trees(range = c(500,1500)),
     learn_rate(range = c(-2, -1)),
     tree_depth(range = c(3L, 10L)),
     min_n(range = c(10L, 40L)),
    size = 5
   ))
catboost_classification_rs <- tune_race_anova(
   catboost_classification_wf,
   resamples = class_folds,
   grid = race_grid,
   metrics = metric_set(mn_log_loss),
   control = control_race(verbose = TRUE,
                          save_pred = TRUE,
                          save_workflow = TRUE,
                          extract = extract_model,
                          parallel_over = "resamples")
    )

         
autoplot(catboost_classification_rs)

collect_metrics(catboost_classification_rs) %>%
  dplyr::select(mtry, 
         trees, 
         min_n, 
         tree_depth, 
         learn_rate, 
         mn_log_loss = mean) %>%
  arrange(mn_log_loss)

After modeling on 5 resample sets for every combination of parameters, we collect metrics on the model’s performance on the samples of data held out in cross validation. This is a substantial improvement.

Performance Checks

catboost_classification_best_wf <-   
  catboost_classification_wf %>% 
  finalize_workflow(select_best(catboost_classification_rs))

catboost_classification_last_fit <- 
      catboost_classification_best_wf %>% 
      last_fit(split)

collect_metrics(catboost_classification_last_fit)

The accuracy and area under the roc curve are moving in the right direction, but 81% is not much better than the 80% that we achieved with the GLM PLS model above.

collect_predictions(catboost_classification_last_fit) %>%
  conf_mat(default_flag, .pred_class) %>% 
  autoplot() +
  labs(title = "Confusion Matrix on Resamples Held Out")

Here again, the algorithm is predicting “nondefault” when it should be finding defaults. Let’s press ahead explore a more suitable j_index.

classification_fit <- fit(catboost_classification_best_wf, 
                          data = training)

lending_test_pred <- classification_fit %>%
    predict(new_data = testing, type = "prob") %>% 
    bind_cols(testing)

threshold_data <- lending_test_pred %>%
  threshold_perf(default_flag, 
                 .pred_nondefault, 
                 thresholds = seq(0.5, 1, by = 0.0025)) %>%
  filter(.metric != "distance") %>%
  mutate(group = case_when(
    .metric == "sens" | .metric == "spec" ~ "1",
    TRUE ~ "2"
  ))

max_j_index_threshold <- threshold_data %>%
  filter(.metric == "j_index") %>%
  filter(.estimate == max(.estimate)) %>%
  pull(.threshold)

ggplot(threshold_data,
       aes(
         x = .threshold,
         y = .estimate,
         color = .metric,
         alpha = group
       )) +
  geom_line() +
  scale_color_viridis_d(end = 0.9) +
  scale_alpha_manual(values = c(.4, 1), guide = "none") +
  geom_vline(xintercept = max_j_index_threshold,
             alpha = .6,
             color = "grey30") +
  labs(
    x = "'Good' Threshold\n(above this value is considered 'good')",
    y = "Metric Estimate",
    title = "Balancing performance by varying the threshold",
    subtitle = "Sensitivity or specificity alone might not be enough!\nVertical line = Max J-Index"
  )

From this visual, the optimal threshold is exactly 0.785. As an interesting view, let’s run that GLM model on all of the train_df file with the new cutoff.

augment(classification_fit, train_df, type = "prob") %>% 
   mutate(.pred = make_two_class_pred(.pred_nondefault,
                                      levels(default_flag), 
                                      threshold = max_j_index_threshold)) %>% 
  conf_mat(truth = default_flag, estimate = .pred_class) %>% 
  autoplot() +
  labs(title = "Catboost Confusion Matrix on Entire Training Set",
       subtitle = glue::glue("{ augment(classification_fit, train_df, type = 'prob') %>% mutate(.pred = make_two_class_pred(.pred_nondefault, levels(default_flag), threshold = max_j_index_threshold)) %>%  accuracy(truth = default_flag, estimate = .pred_class) %>% pull(.estimate) %>% round(2)*100 }% ", "Accuracy with catboost classification model.\nMuch better than the NULL model."))

Good enough to move forward. Loans predicted in the classifier to default will be pushed through the regression model below.

Variable Importance

Let’s look at the features that contributed the most to this classification model.

classification_fit %>% 
  extract_fit_engine() %>% 
  catboost.get_feature_importance(pool = NULL, 
                                  type = 'FeatureImportance',
                                  thread_count = -1) %>% 
  as_tibble(rownames = "Feature") %>% 
  mutate(Feature = fct_reorder(Feature, V1)) %>% 
  ggplot(aes(V1, Feature)) +
    geom_col() +
  labs(title = "Catboost SBA Default Classification Feature Importance",
       y = NULL, x = NULL)

The interaction feature that built by binding the bank to the approval year ended up as the most important feature, and the second most important is the ration between SBA_Approval value and the Gross Loan value.


Machine Learning: Regression

👉 Build a non-ML baseline first.

Recall that our goal is to predict the default_amount. For our NULL model, if we apply our classifier above, and then simply take the mean of all default_amounts, the Mean Average Error is:

augment(classification_fit, train_df) %>% 
  mutate(.pred_default_amount = if_else(default_flag == "default", default_amount,0)) %>%   
  filter(.pred_class == "default") %>% 
  mutate(.pred_mean = mean(.pred_default_amount)) %>% 
  mae(truth = default_amount, estimate = .pred_mean) 

As an aside: the train_df we used above contains 51 loans where the default_amount was greater than the Loan Approval amount. Most of them are small value loans, for which there are thousands of others in the dataset.

EDA

Let’s make more exploratory plots to learn what features might yield good numeric predictions.

train_df_regression <- augment(classification_fit, train_df) %>% 
    filter(.pred_class == "default")

summarize_default_amounts <- function(tbl){
  tbl %>% 
  summarize(n_loans = n(),
            mean_default = mean(default_amount),
            total_gr_appv = sum(GrAppv),
            total_default_amount = sum(default_amount ),
            pct_default_amount = total_default_amount / total_gr_appv,
            .groups = "drop") %>% 
  arrange(desc(n_loans))
}

plot_category_amounts <- function(tbl, category, n_categories = 7){
  tbl %>% 
  group_by({{ category }} := withfreq(fct_lump({{ category }}, n_categories))) %>% 
  summarize_default_amounts() %>% 
  mutate({{ category }} := fct_reorder({{ category }}, mean_default)) %>% 
  ggplot(aes(mean_default, {{ category }})) +
  geom_point(aes(color = pct_default_amount,
                 size = pct_default_amount)) +
  geom_text(aes(x = mean_default - 6000, 
                label = glue::glue("$ {round(total_default_amount/10^6,0)} M")),
            hjust = 1) +
    scale_color_viridis_b(
      name = "% of Loan Amount\nin Default",
      labels = scales::percent_format(accuracy = 1),
      breaks = seq(0,1,0.1),
      option = "H"
    ) +
    scale_size_binned(
      name = "% of Loan\nin Default",
      labels = scales::percent_format(accuracy = 1),
      breaks = seq(0,1,0.1),
      guide = "none"
    ) +
    guides(color = guide_bins()) +
    scale_x_continuous(labels = scales::dollar_format()) +
    expand_limits(x = 0, size = 0) +
    labs(x = "Mean Value of the Defaults", y = NULL,
         title = "US Small Business Administration Loans that defaulted 2003-2010",
         subtitle = "# of loans in parenthesis and Total Defaulted $ on Labels")
  
}


train_df_regression %>% 
  plot_category_amounts(State) +
   labs(title = "When they do default, Arizona has the highest mean Default Value")

train_df_regression %>% 
  plot_category_amounts(City) +
   labs(title = "When they do default, Phoenix has the highest mean Default Value")

train_df_regression %>% 
  plot_category_amounts(Sector) +
   labs(title = "When they do default, Accomodation have the highest mean Default Value")

train_df_regression %>% 
  plot_category_amounts(Bank) +
   labs(title = "When they do default, ReadyCap Lending has the highest mean Default Value")

train_df_regression %>% 
  plot_category_amounts(NewExist) +
   labs(title = "When they do default, Unknown have the highest mean Default Value")

train_df_regression %>% 
  plot_category_amounts(FranchiseCode) +
   labs(title = "When they do default, Franchises have mean Default Value")

train_df_regression %>% 
  plot_category_amounts(NAICS2) +
   labs(title = "When they do default, NAICS group 72 have mean Default Value")

train_df_regression %>% 
  plot_category_amounts(NAICS3) +
   labs(title = "When they do default, NAICS group 722 have mean Default Value")

train_df_regression %>% 
  plot_category_amounts(NAICS4) +
   labs(title = "When they do default, NAICS group 7222 have mean Default Value")

train_df_regression %>% 
  plot_category_amounts(NAICS) +
   labs(title = "When they do default, NAICS group 722211 have mean Default Value")

train_df_regression %>% 
  plot_category_amounts(UrbanRural) +
   labs(title = "When they do default, Unknown businesses have the highest mean Default Value")

train_df_regression %>% 
  plot_category_amounts(Name) +
   labs(title = "When they do default, Domino's Pizza have the highest mean Default Value")

train_df_regression %>% 
    select_at(all_of(train_numeric)) %>%
    cor() %>% 
    heatmap(main = "Numeric Correlations")

As with the classification, the regression will rely heavily on categorical features, and some of the numeric features are highly correlated with one another.

We will use 5-fold cross validation and stratify on the default_amount this time.

set.seed(2021)

regression_split <- initial_split(augment(classification_fit, train_df) %>% 
                         filter(.pred_class == "default") , 
                       prop = 0.9)

regression_training <- training(split)
regression_testing <- testing(split)

(regression_folds <-
    vfold_cv(
      regression_training,
      v = 5,
      strata = default_amount
    ))

The Recipe

For the regression, our outcome variable is the default_amount. All other features are the same as above.

regression_rec <- recipe(
    default_amount ~ LoanNr_ChkDgt + Name + Sector + City + State + 
      Bank +  NAICS + ApprovalFY + NoEmp +
      NewExist + CreateJob + RetainedJob + FranchiseCode + UrbanRural +
      DisbursementGross + GrAppv + SBA_Appv + NAICS2 + NAICS3 + NAICS4 +
      loan_per_employee + SBA_Appv_vs_Gross + Gross_minus_SBA_Appv +
      Gross_minus_SBA_Appv_vs_Employee + same_state ,
    data = regression_training
  ) %>%
  update_role(LoanNr_ChkDgt, new_role = "ID") %>%
  step_zv(all_numeric_predictors()) %>% 
  step_novel(
    Name,
    Sector,
    City,
    State,
    Bank,
    NAICS,
    NAICS2,
    NAICS3,
    NAICS4 ) %>%
  step_other(all_nominal_predictors(), threshold = tune::tune()) %>%
  step_dummy(all_nominal_predictors()) %>%
  step_interact(~ starts_with("Bank"):ApprovalFY) %>% # Add important interactions
  step_orderNorm(all_numeric_predictors()) %>%
  step_normalize(all_numeric_predictors()) 

Linear Regression

linear_reg_lm_spec <-
  linear_reg(penalty = tune(), 
             mixture = tune()) %>%
  set_engine('glmnet')

linear_regression_wf <-
  workflow() %>%
  add_recipe(regression_rec) %>%
  add_model(linear_reg_lm_spec)
linear_regression_rs <- tune_race_anova(
   linear_regression_wf,
   resamples = regression_folds,
   grid = 30,
   metrics = mset,
   control = control_race(verbose = TRUE,
                          save_pred = TRUE,
                          save_workflow = TRUE,
                          extract = extract_model,
                          parallel_over = "everything")
    )

collect_metrics(linear_regression_rs) %>% 
  arrange(mean)
linear_regression_best_wf <-   
  linear_regression_wf %>% 
  finalize_workflow(select_best(linear_regression_rs))

glm_regression_fit <- fit(linear_regression_best_wf, data = regression_training)

glm_regression_fit %>% 
  extract_fit_parsnip() %>% 
  vip::vip(metric = "mae", num_features = 25L) +
  labs(title = "Regularized GLM Variable Importance Scores")

Linear regression yields an Mean Average Error on cross validation samples held out that is about 1/3 less than the NULL model.

Catboost

Much like above, but now for regression.

catboost_regression_rec <- recipe(
    default_amount ~ LoanNr_ChkDgt + Name + Sector + City + State + 
      Bank +  NAICS + ApprovalFY + NoEmp +
      NewExist + CreateJob + RetainedJob + FranchiseCode + UrbanRural +
      DisbursementGross + GrAppv + SBA_Appv + NAICS2 + NAICS3 + NAICS4 +
      loan_per_employee + SBA_Appv_vs_Gross + Gross_minus_SBA_Appv +
      Gross_minus_SBA_Appv_vs_Employee + same_state ,
    data = regression_training
  ) %>%
  update_role(LoanNr_ChkDgt, new_role = "ID") %>%
  step_zv(all_numeric_predictors()) 

catboost_regression_spec <- boost_tree(trees = tune(),
                            mtry = tune(),
                            min_n = tune(),
                            learn_rate =  tune(),
                            tree_depth = tune()) %>% 
  set_engine("catboost") %>% 
  set_mode("regression")
catboost_regression_wf <-
  workflow() %>%
  add_recipe(catboost_regression_rec) %>%
  add_model(catboost_regression_spec)

set.seed(2021)
race_grid <-
   grid_latin_hypercube(
     finalize(mtry(), catboost_regression_rec %>% prep() %>% juice()),
     trees(range = c(500,1800)),
     learn_rate(range = c(-2, -1)),
     tree_depth(range = c(6L, 12L)),
     min_n(range = c(10L, 45L)),
    size = 15
   )
catboost_regression_rs <- tune_race_anova(
   catboost_regression_wf,
   resamples = regression_folds,
   grid = race_grid,
   metrics = mset,
   control = control_race(verbose = FALSE,
                            save_pred = FALSE, 
                            save_workflow = TRUE,
                            extract = extract_model,
                            parallel_over = "everything")
    )

autoplot(catboost_regression_rs)

collect_metrics(catboost_regression_rs) %>%
  select(mtry, 
         trees, 
         min_n, 
         tree_depth, 
         learn_rate, 
         "MAE" = mean) %>%
  arrange(MAE)

Not bad. Let’s make a final fit at these hyperparameter settings.

catboost_regression_best_wf <-   
  catboost_regression_wf %>% 
  finalize_workflow(select_best(catboost_regression_rs))

regression_fit <- fit(catboost_regression_best_wf, 
                      data = augment(classification_fit, train_df) %>% 
                      filter(.pred_class == "default"))

Performance Checks

First, a Mean Absolute Error on the labeled testing data that was held out:

augment(regression_fit, augment(classification_fit, testing)) %>% 
  mutate(.pred = if_else(default_flag == "default", .pred,0)) %>% 
  mae(truth = default_amount, estimate = .pred) 

Next, a MAE on the predictions on training, to assess whether we have over-fit:

augment(regression_fit, augment(classification_fit, training)) %>% 
  mutate(.pred = if_else(default_flag == "default", .pred,0)) %>% 
  mae(truth = default_amount, estimate = .pred) 

Yep, we did. Given more time, increasing the number of folds and adding more repeats would help resolve this. A visual of our predictions of default_amounts versus the truth:

augment(regression_fit, augment(classification_fit, train_df)) %>%
  mutate(.pred = if_else(default_flag == "default", .pred, 0)) %>% 
  ggplot(aes(default_amount, .pred)) +
  geom_point(alpha = 0.2, color = "blue") +
  geom_abline(slope = 1, color = "red", alpha = 0.5) +
  scale_x_continuous(labels = scales::dollar) +
  scale_y_continuous(labels = scales::dollar) +
  labs(title = "Catboost Model Performance",
       x = "Truth",
       y = "Prediction")

Variable Importance

I like to come back to either these scores, or partial dependence plots, to be able to tell the story of what the model is doing and ask questions about the incoming data sample. If, for example, the SBA changes policies like the Approval thresholds, we would certainly expect the model to move dramatially.

vip_regressor <- 
  DALEXtra::explain_tidymodels(
    regression_fit,
    data = augment(regression_fit, augment(classification_fit, train_df)) %>% 
                        mutate(.pred = if_else(default_flag == "default", .pred, 0)) %>% 
                        select(-default_amount),
    y = augment(regression_fit, augment(classification_fit, train_df)) %>% 
                        mutate(.pred = if_else(default_flag == "default", .pred, 0)) %>% 
                        select(default_amount),
    label = "Catboost Regression",
    verbose = FALSE
  ) %>% 
  DALEX::model_parts()

ggplot_imp(vip_regressor) +
  theme(aspect.ratio = 1.2) +
  labs(title = "SBA Default Amount Regression Feature Importance")

With all models built and fit, we can now pipeline the whole process for the holdout and make the Kaggle submission.

shell(glue::glue('kaggle competitions submit -c { competition_name } -f { path_export } -m "Catboost with advanced preprocessing model 2"'))

sessionInfo()
R version 4.1.1 (2021-08-10)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 19043)

Matrix products: default

locale:
[1] LC_COLLATE=English_United States.1252 
[2] LC_CTYPE=English_United States.1252   
[3] LC_MONETARY=English_United States.1252
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.1252    

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

other attached packages:
 [1] glmnet_4.1-2        Matrix_1.3-4        vctrs_0.3.8        
 [4] rlang_0.4.11        themis_0.1.4        bestNormalize_1.8.1
 [7] embed_0.1.4.9000    catboost_0.26       treesnip_0.1.0.9000
[10] probably_0.0.6      discrim_0.1.3       finetune_0.1.0     
[13] yardstick_0.0.8     workflowsets_0.1.0  workflows_0.2.3    
[16] tune_0.1.6          rsample_0.1.0       recipes_0.1.16     
[19] parsnip_0.1.7.900   modeldata_0.1.1     infer_1.0.0        
[22] dials_0.0.9.9000    scales_1.1.1        broom_0.7.9        
[25] tidymodels_0.1.3    treemapify_2.5.5    ggforce_0.3.3      
[28] ggthemes_4.2.4      hrbrthemes_0.8.0    forcats_0.5.1      
[31] stringr_1.4.0       dplyr_1.0.7         purrr_0.3.4        
[34] readr_2.0.1         tidyr_1.1.3         tibble_3.1.4       
[37] ggplot2_3.3.5       tidyverse_1.3.1     workflowr_1.6.2    

loaded via a namespace (and not attached):
  [1] DALEXtra_2.1.1             ragg_1.1.3                
  [3] bit64_4.0.5                knitr_1.33                
  [5] styler_1.5.1               data.table_1.14.0         
  [7] rpart_4.1-15               hardhat_0.1.6             
  [9] doParallel_1.0.16          generics_0.1.0            
 [11] GPfit_1.0-8                usethis_2.0.1             
 [13] mixOmics_6.16.2            RANN_2.6.1                
 [15] future_1.22.1              ggfittext_0.9.1           
 [17] conflicted_1.0.4           tensorflow_2.6.0          
 [19] bit_4.0.4                  tzdb_0.1.2                
 [21] tweetrmd_0.0.9             xml2_1.3.2                
 [23] lubridate_1.7.10           httpuv_1.6.2              
 [25] assertthat_0.2.1           viridis_0.6.1             
 [27] gower_0.2.2                xfun_0.25                 
 [29] hms_1.1.0                  jquerylib_0.1.4           
 [31] evaluate_0.14              promises_1.2.0.1          
 [33] fansi_0.5.0                dbplyr_2.1.1              
 [35] readxl_1.3.1               igraph_1.2.6              
 [37] DBI_1.1.1                  rARPACK_0.11-0            
 [39] ellipsis_0.3.2             RSpectra_0.16-0           
 [41] backports_1.2.1            prismatic_1.0.0           
 [43] here_1.0.1                 cachem_1.0.6              
 [45] withr_2.4.2                checkmate_2.0.0           
 [47] vroom_1.5.4                crayon_1.4.1              
 [49] ellipse_0.4.2              pkgconfig_2.0.3           
 [51] labeling_0.4.2             tweenr_1.0.2              
 [53] nnet_7.3-16                globals_0.14.0            
 [55] lifecycle_1.0.0            sysfonts_0.8.5            
 [57] extrafontdb_1.0            ingredients_2.2.0         
 [59] unbalanced_2.0             modelr_0.1.8              
 [61] cellranger_1.1.0           rprojroot_2.0.2           
 [63] polyclip_1.10-0            matrixStats_0.60.1        
 [65] rngtools_1.5               showtextdb_3.0            
 [67] reprex_2.0.1               base64enc_0.1-3           
 [69] whisker_0.4                png_0.1-7                 
 [71] viridisLite_0.4.0          ROSE_0.0-4                
 [73] pROC_1.18.0                mlr_2.19.0                
 [75] doRNG_1.8.2                shape_1.4.6               
 [77] parallelly_1.27.0          magrittr_2.0.1            
 [79] ParamHelpers_1.14          plyr_1.8.6                
 [81] hexbin_1.28.2              compiler_4.1.1            
 [83] RColorBrewer_1.1-2         cli_3.0.1                 
 [85] DiceDesign_1.9             listenv_0.8.0             
 [87] MASS_7.3-54                tidyselect_1.1.1          
 [89] stringi_1.7.4              textshaping_0.3.5         
 [91] butcher_0.1.5              highr_0.9                 
 [93] yaml_2.2.1                 ggrepel_0.9.1             
 [95] grid_4.1.1                 sass_0.4.0                
 [97] fastmatch_1.1-3            tools_4.1.1               
 [99] future.apply_1.8.1         parallel_4.1.1            
[101] rstudioapi_0.13            foreach_1.5.1             
[103] git2r_0.28.0               gridExtra_2.3             
[105] showtext_0.9-4             prodlim_2019.11.13        
[107] farver_2.1.0               digest_0.6.27             
[109] FNN_1.1.3                  lava_1.6.10               
[111] proto_1.0.0                nortest_1.0-4             
[113] Rcpp_1.0.7                 later_1.3.0               
[115] httr_1.4.2                 gdtools_0.2.3             
[117] learntidymodels_0.0.0.9001 colorspace_2.0-2          
[119] rvest_1.0.1                fs_1.5.0                  
[121] reticulate_1.20            splines_4.1.1             
[123] uwot_0.1.10                vip_0.3.2                 
[125] mapproj_1.2.7              systemfonts_1.0.2         
[127] jsonlite_1.7.2             BBmisc_1.11               
[129] corpcor_1.6.9              timeDate_3043.102         
[131] zeallot_0.1.0              keras_2.6.0               
[133] DALEX_2.3.0                ipred_0.9-11              
[135] R6_2.5.1                   lhs_1.1.1                 
[137] pillar_1.6.2               htmltools_0.5.2           
[139] glue_1.4.2                 fastmap_1.1.0             
[141] BiocParallel_1.26.1        class_7.3-19              
[143] codetools_0.2-18           maps_3.3.0                
[145] furrr_0.2.3                utf8_1.2.2                
[147] lattice_0.20-44            bslib_0.3.0               
[149] curl_4.3.2                 tfruns_1.5.0              
[151] Rttf2pt1_1.3.9             survival_3.2-11           
[153] parallelMap_1.5.1          rmarkdown_2.10            
[155] munsell_0.5.0              iterators_1.0.13          
[157] haven_2.4.3                reshape2_1.4.4            
[159] gtable_0.3.0               emojifont_0.5.5           
[161] extrafont_0.17            
LS0tDQp0aXRsZTogIlNsaWNlZCBTZWFzb24gMSBGaW5hbGU6IFNCQSBMb2FuIERlZmF1bHRzIg0KYXV0aG9yOiAiSmltIEdydW1hbiINCmRhdGU6ICJBdWd1c3QgMTcsIDIwMjEiDQpvdXRwdXQ6DQogIHdvcmtmbG93cjo6d2Zsb3dfaHRtbDoNCiAgICB0b2M6IG5vDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGRmX3ByaW50OiBwYWdlZA0KZWRpdG9yX29wdGlvbnM6DQogIGNodW5rX291dHB1dF90eXBlOiBjb25zb2xlDQotLS0NCg0KVGhlIFtTZWFzb24gMSBGaW5hbGVdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vYy9zbGljZWQtczAxZTEyLWNoYW1waW9uc2hpcCkgb2YgI1NMSUNFRCBmZWF0dXJlZCBhIGNoYWxsZW5nZSB0byBwcmVkaWN0IGRlZmF1bHQgZXhwb3N1cmUgaW4gYSBwb3J0Zm9saW8gb2YgYmFuayBsb2FucyBpbiBhIGRhdGFzZXQgZnJvbSB0aGUgW1UuUy4gU21hbGwgQnVzaW5lc3MgQWRtaW5pc3RyYXRpb25dKGh0dHBzOi8vZGF0YS5zYmEuZ292L29yZ2FuaXphdGlvbi9vY2lvKSAoU0JBKS4gDQoNCjxpbWcgYWx0PSJTbWFsbCBCdXNpbmVzcyBBZG1pbmlzdHJhdGlvbiIgYXJpYS1sYWJlbD0iaG9tZXBhZ2UiIHNyYz0iaHR0cHM6Ly93d3cuc2JhLmdvdi9idWlsZC9lMGUxOTlhY2QxNWIyYzU0YTE3ZGU2YmU1MDdjZjdmNy5wbmciPg0KDQpUaGUgU21hbGwgQnVzaW5lc3MgQWRtaW5pc3RyYXRpb24gKFNCQSkgd2FzIGZvdW5kZWQgaW4gMTk1MyB0byBhc3Npc3Qgc21hbGwgYnVzaW5lc3NlcyBpbiBzZWN1cmluZyBsb2Fucy4gVGhlIGFyZ3VtZW50IGlzIHRoYXQsIGFzIHNtYWxsIGJ1c2luZXNzZXMgYXJlIGEgcHJpbWFyeSBzb3VyY2Ugb2YgZW1wbG95bWVudCBpbiB0aGUgVW5pdGVkIFN0YXRlcywgdGhlbiBoZWxwaW5nIHNtYWxsIGJ1c2luZXNzZXMgaGVscHMgd2l0aCBqb2IgY3JlYXRpb24sIHJlZHVjaW5nIHVuZW1wbG95bWVudC4gT25lIG9mIHRoZSB3YXlzIHRoZSBTQkEgaGVscHMgc21hbGwgYnVzaW5lc3NlcyBpcyBieSBndWFyYW50ZWVpbmcgYmFuayBsb2Fucy4gVGhpcyBndWFyYW50ZWUgcmVkdWNlcyB0aGUgcmlzayB0byBpbmRpdmlkdWFsIGJhbmtzIGFuZCBlbmNvdXJhZ2VzIHRoZW0gdG8gbGVuZCBpbiBzaXR1YXRpb25zIHRoYXQgdGhleSBvdGhlcndpc2UgbWlnaHQgbm90LiBJZiB0aGUgbG9hbiBkZWZhdWx0cywgdGhlIFNCQSBjb3ZlcnMgdGhlIGFtb3VudCBndWFyYW50ZWVkLCBhbmQgdGhlIGJhbmsgc3VmZmVycyBhIGxvc3MgZm9yIHRoZSByZW1haW5pbmcgYmFsYW5jZS4NCg0KVGhlcmUgaGF2ZSBiZWVuIGEgZmV3IHNtYWxsIGJ1c2luZXNzIHN1Y2Nlc3Mgc3RvcmllcyBsaWtlIEZlZEV4IGFuZCBBcHBsZS4gSG93ZXZlciwgdGhlIHJhdGUgb2YgZGVmYXVsdCBpcyBzb21ld2hhdCBoaWdoZXIgdGhhbiBtb3N0IGNvbW1lcmNpYWwgYmFua3Mgd291bGQgZmluZCBhdHRyYWN0aXZlLiBTb21lIGVjb25vbWlzdHMgYmVsaWV2ZSB0aGUgYmFua2luZyBtYXJrZXQgd29ya3MgYmV0dGVyIHdpdGhvdXQgdGhlIGFzc2lzdGFuY2Ugb2YgdGhlIFNCQS4gT24gdGhlIG90aGVyIGhhbmQsIHN1cHBvcnRlcnMgY2xhaW0gdGhhdCB0aGUgc29jaWFsIGJlbmVmaXRzIGFuZCBqb2IgY3JlYXRpb24gb3V0d2VpZ2ggdGhlIGZpbmFuY2lhbCBjb3N0cyB0byB0aGUgdGF4cGF5ZXIgaW4gZGVmYXVsdGVkIGxvYW5zLg0KDQpNeSBjYXBzdG9uZSBwcm9qZWN0IGluIGdyYWQgc2Nob29sIGEgZmV3IHllYXJzIGJhY2sgaW52b2x2ZWQgYSBzdHVkeSBvZiByZWdpb25hbCBiYW5rcyBhbmQgYWNjb3VudCBwb2xpY2llcyBhY3Jvc3MgdGhlIFVTIGZpbmFuY2lhbCBuZXR3b3JrLiBSZWd1bGF0b3J5IHN1Ym1pc3Npb25zIHRvIFNCQSwgRkRJQywgdGhlIFVTREEsIGFuZCBldmVuIHN0YXRlIGJhbmtpbmcgYXV0aG9yaXRpZXMgYXJlIGEgZ3JlYXQgb3Bwb3J0dW5pdHkgdG8gbW9kZWwgZm9yIGRlY2lzaW9ucyBmb3IgZWNvbm9taXN0cyBhbmQgZmluYW5jZS4gSW4gdGhpcyBjYXNlLCBtb2RlbGluZyB0aGUgYW1vdW50IG9mIGRlZmF1bHQgZXhwb3N1cmUgY291bGQgYmUgdXNlZnVsIGluIHByb3ZpZGluZyBpbmZvcm1hdGlvbiB0byBnb3Zlcm5tZW50IHBvbGljeW1ha2VycyBvciBpbnN1cmVycy4NCg0KVGhlIHNvdXJjZSBkYXRhIHNldCB3YXMgZnJvbSB0aGUgVS5TLiBTQkEgbG9hbiBkYXRhYmFzZSBhbmQgaW5jbHVkZXMgaW5mb3JtYXRpb24gb24gd2hldGhlciB0aGUgbG9hbiB3YXMgcGFpZCBvZmYgaW4gZnVsbCBvciBpZiB0aGUgU0JBIGhhZCB0byBjaGFyZ2Ugb2ZmIGFueSBhbW91bnQgaW4gZGVmYXVsdCwgYW5kIGhvdyBtdWNoIHRoYXQgYW1vdW50IHdhcy4gTW9yZSBpbmZvcm1hdGlvbiBvbiB0aGlzIFtkYXRhIHNldCBpcyBhdmFpbGJsZSBoZXJlXShodHRwczovL2Ftc3RhdC50YW5kZm9ubGluZS5jb20vZG9pL2Z1bGwvMTAuMTA4MC8xMDY5MTg5OC4yMDE4LjE0MzQzNDIpLg0KDQpbU0xJQ0VEXShodHRwczovL3d3dy5ub3Rpb24uc28vU0xJQ0VELVNob3ctYzdiZDI2MzU2ZTNhNDIyNzllMmRmYmFmYjA0ODAwNzMpIGlzIGxpa2UgdGhlIFRWIFNob3cgQ2hvcHBlZCBidXQgZm9yIGRhdGEgc2NpZW5jZS4gVGhlIGNvbXBldGl0b3JzIGdldCBhIG5ldmVyLWJlZm9yZS1zZWVuIGRhdGFzZXQgYW5kIHR3by1ob3VycyB0byBjb2RlIGEgc29sdXRpb24gdG8gYSBwcmVkaWN0aW9uIGNoYWxsZW5nZS4gQ29udGVzdGFudHMgZ2V0IHBvaW50cyBmb3IgdGhlIGJlc3QgbW9kZWwgcGx1cyBib251cyBwb2ludHMgZm9yIGRhdGEgdmlzdWFsaXphdGlvbiwgdm90ZXMgZnJvbSB0aGUgYXVkaWVuY2UsIGFuZCBtb3JlLiBUaGUgZXZhbHVhdGlvbiBtZXRyaWMgaXMgYE1lYW4gQWJzb2x1dGUgRXJyb3JgLg0KDQpUaGUgYXVkaWVuY2UgaXMgaW52aXRlZCB0byBwYXJ0aWNpcGF0ZSBhcyB3ZWxsLiBUaGlzIHR5cGUgb2YgcHJvYmxlbSBpcyBhIGJpZyBjaGFsbGVuZ2UuIDY0ayBvZiB0aGUgODNrIGxvYW5zIGluIHRoZSBTTElDRUQgdHJhaW5pbmcgZGF0YXNldCBkaWQgbm90IGRlZmF1bHQgYXQgYWxsLCBzbyBzaG93IHplcm8gZGVmYXVsdCBhbW91bnQuIFNlcGFyYXRpbmcgdGhlIHNpZ25hbCBmcm9tIHRoZSBub2lzZSBoZXJlIGlzIHRvdWdoLiANCg0KIVtdKGh0dHBzOi8vd3d3Lm5vdGlvbi5zby9pbWFnZS9odHRwcyUzQSUyRiUyRnMzLXVzLXdlc3QtMi5hbWF6b25hd3MuY29tJTJGc2VjdXJlLm5vdGlvbi1zdGF0aWMuY29tJTJGN2Y3YmE1ZjktZDdiZC00MTAxLTg5MzMtYTExMmI0Zjc4NTcwJTJGRnJhbWVfMy5wbmc/dGFibGU9YmxvY2smaWQ9YzdiZDI2MzUtNmUzYS00MjI3LTllMmQtZmJhZmIwNDgwMDczJnNwYWNlSWQ9MmNjNDA0ZTYtZmUyMC00ODNkLTllYTUtNWQ0NGViM2RkNTg2JndpZHRoPTE1MTAmdXNlcklkPSZjYWNoZT12MikNCg0KQ2xlYXJseSB0aGVyZSBpcyBhIGxhcmdlIGltYmFsYW5jZSBoZXJlIGJldHdlZW4gZ29vZCBhbmQgYmFkIGxvYW5zLiBUaGlzIGlzIHByb2JhYmx5IGEgZ29vZCB0aGluZyBmb3IgdGhlIGJhbmtzLCBidXQgcG9zZXMgYW4gaW50ZXJlc3RpbmcgaXNzdWUgZm9yIHVzIGJlY2F1c2Ugd2Ugd2FudCB0byBlbnN1cmUgd2UgYXJlIHNlbnNpdGl2ZSB0byB0aGUgYmFkIGxvYW5zIGFuZCBhcmUgbm90IG92ZXJ3aGVsbWVkIGJ5IHRoZSBudW1iZXIgb2YgZ29vZCBvbmVzLiBPbmUgYXBwcm9hY2ggdGhhdCB3ZSBtaWdodCB0YWtlIGlzIHRvIGBkb3duc2FtcGxlYCB0aGUgbnVtYmVyIG9mIGdvb2QgbG9hbnMgc28gdGhhdCB0aGUgdG90YWwgbnVtYmVyIG9mIHRoZW0gaXMgbW9yZSBpbiBsaW5lIHdpdGggdGhlIG51bWJlciBvZiBiYWQgbG9hbnMuDQoNClRoZXJlIGFyZSBsaWtlbHkgY29uZm91bmRlcnMgYW5kIG90aGVyIGZlYXR1cmUgdHJhbnNmb3JtYXRpb25zIHJlcXVpcmVkLiAgSSBhbHNvIHdvbmRlcmVkIHdoZXRoZXIgYXBwcm9hY2hpbmcgdGhpcyBhcyBhIGNsYXNzaWZpY2F0aW9uIHByb2JsZW0gZmlyc3QsIGFuZCBidWlsZGluZyBhIHJlZ3Jlc3Npb24gc2Vjb25kIG1pZ2h0IGJlIGJldHRlci4gRWJlbiBFc3Rlcmh1aXplbiB3YXMga2luZCBlbm91Z2ggdG8gc2hhcmUgaGlzIHRvcC1zY29yaW5nIFB5dGhvbiBtb2RlbCBjb2RlIGF0IEthZ2dsZS4gSW50ZXJlc3RpbmdseSwgdGhpcyB3YXMgZXhhY3RseSBoaXMgYXBwcm9hY2guDQoNCmBgYHtyfQ0KdHdlZXRybWQ6OmluY2x1ZGVfdHdlZXQoImh0dHBzOi8vdHdpdHRlci5jb20vRWJlbkVzdGVyaHVpemVuL3N0YXR1cy8xNDI3ODM3MzAzNDIzMTg0OTAwIikNCg0KYGBgDQoNCk15IHBsYW4gaGVyZSBpcyB0byByZS13cml0ZSBoaXMgYXBwcm9hY2ggaW4gUiwgbWFrZSBhIG51bWJlciBvZiBpbXByb3ZlbWVudHMsIGFuZCBleHBsYWluIGhvdyBpdCBhbGwgd29ya3MuIExldCdzIGxvYWQgdXAgUiBwYWNrYWdlczoNCg0KYGBge3Igc2V0dXB9DQoNCnN1cHByZXNzUGFja2FnZVN0YXJ0dXBNZXNzYWdlcyh7DQpsaWJyYXJ5KHRpZHl2ZXJzZSkgIyBjbGVhbiBhbmQgdHJhbnNmb3JtIHJlY3Rhbmd1bGFyIGRhdGENCg0KbGlicmFyeShocmJydGhlbWVzKSAjIHBsb3QgdGhlbWluZw0KbGlicmFyeShnZ3RoZW1lcykNCmxpYnJhcnkoZ2dmb3JjZSkNCmxpYnJhcnkodHJlZW1hcGlmeSkNCg0KbGlicmFyeSh0aWR5bW9kZWxzKSAjIG1hY2hpbmUgbGVhcm5pbmcgdG9vbHMNCmxpYnJhcnkoZmluZXR1bmUpICMgcmFjaW5nIG1ldGhvZHMgZm9yIGFjY2VsZXJhdGluZyBoeXBlcnBhcmFtZXRlciB0dW5pbmcNCg0KbGlicmFyeShkaXNjcmltKQ0KbGlicmFyeShwcm9iYWJseSkNCmxpYnJhcnkodHJlZXNuaXApICMgYSBib29zdCB0cmVlIGFkYXB0b3IgZm9yIGNhdGJvb3N0IGFuZCBvdGhlciBlbmdpbmVzDQpsaWJyYXJ5KGNhdGJvb3N0KSAjIG91ciBib29zdGVkIHRyZWUgZW5naW5lDQpsaWJyYXJ5KGVtYmVkKQ0KICANCmxpYnJhcnkoYmVzdE5vcm1hbGl6ZSkgIyBoYXMgYSBzdGVwIHRoYXQgY2FuIGVuZm9yY2UgYSBzeW1tZXRyaWMgZGlzdHJpYnV0aW9uIGZvciB0aGUgcHJlZGljdG9ycy4gV2XigJlsbCB1c2UgdGhpcyB0byBtaXRpZ2F0ZSB0aGUgaXNzdWUgb2Ygc2tld2VkIGRpc3RyaWJ1dGlvbnMuDQpsaWJyYXJ5KHRoZW1pcykgIyBtbCBwcmVwIHRvb2xzIGZvciBoYW5kbGluZyB1bmJhbGFuY2VkIGRhdGFzZXRzDQoNCiAgDQp9KQ0KDQpzb3VyY2UoaGVyZTo6aGVyZSgiY29kZSIsIl9jb21tb24uUiIpLA0KICAgICAgIHZlcmJvc2UgPSBGQUxTRSwNCiAgICAgICBsb2NhbCA9IGtuaXRyOjprbml0X2dsb2JhbCgpKQ0KDQp0aGVtZV9zZXQodGhlbWVfamltKGJhc2Vfc2l6ZSA9IDE0KSkNCg0KI2NyZWF0ZSBhIGRhdGEgZGlyZWN0b3J5DQpkYXRhX2RpciA8LSBoZXJlOjpoZXJlKCJkYXRhIixTeXMuRGF0ZSgpKQ0KaWYgKCFmaWxlLmV4aXN0cyhkYXRhX2RpcikpIGRpci5jcmVhdGUoZGF0YV9kaXIpDQoNCiMgc2V0IGEgY29tcGV0aXRpb24gbWV0cmljDQptc2V0IDwtIG1ldHJpY19zZXQobWFlKQ0KDQojIHNldCB0aGUgY29tcGV0aXRpb24gbmFtZSBmcm9tIHRoZSB3ZWIgYWRkcmVzcw0KY29tcGV0aXRpb25fbmFtZSA8LSAic2xpY2VkLXMwMWUxMi1jaGFtcGlvbnNoaXAiDQoNCnppcGZpbGUgPC0gcGFzdGUwKGRhdGFfZGlyLCIvIiwgY29tcGV0aXRpb25fbmFtZSwgIi56aXAiKQ0KDQpwYXRoX2V4cG9ydCA8LSBoZXJlOjpoZXJlKCJkYXRhIixTeXMuRGF0ZSgpLHBhc3RlMChjb21wZXRpdGlvbl9uYW1lLCIuY3N2IikpDQpgYGANCg0KIyMgR2V0IHRoZSBEYXRhDQoNCkEgcXVpY2sgcmVtaW5kZXIgYmVmb3JlIGRvd25sb2FkaW5nIHRoZSBkYXRhc2V0OiAgR28gdG8gdGhlIEthZ2dsZSB3ZWIgc2l0ZSBhbmQgYWNjZXB0IHRoZSBjb21wZXRpdGlvbiB0ZXJtcyEhIQ0KDQpJZiB5b3UgYHBpcCBpbnN0YWxsIGthZ2dsZWAsIHRoZXJlIGFyZSAoV2luZG93cykgc2hlbGwgY29tbWFuZHMgYXZhaWxhYmxlIHRvIGludGVyYWN0IHdpdGggS2FnZ2xlIGhlcmU6DQoNCmBgYHtyIGthZ2dsZSBjb21wZXRpdGlvbnMgdGVybWluYWwgY29tbWFuZHMsIGV2YWw9RkFMU0UsIGNsYXNzLm91dHB1dD0ic2Nyb2xsLTEwMCJ9DQojIGZyb20gdGhlIEthZ2dsZSBhcGkgaHR0cHM6Ly9naXRodWIuY29tL0thZ2dsZS9rYWdnbGUtYXBpDQoNCiMgdGhlIGxlYWRlcmJvYXJkDQpzaGVsbChnbHVlOjpnbHVlKCdrYWdnbGUgY29tcGV0aXRpb25zIGxlYWRlcmJvYXJkIHsgY29tcGV0aXRpb25fbmFtZSB9IC1zJykpDQoNCiMgdGhlIGZpbGVzIHRvIGRvd25sb2FkDQpzaGVsbChnbHVlOjpnbHVlKCdrYWdnbGUgY29tcGV0aXRpb25zIGZpbGVzIC1jIHsgY29tcGV0aXRpb25fbmFtZSB9JykpDQoNCiMgdGhlIGNvbW1hbmQgdG8gZG93bmxvYWQgZmlsZXMNCnNoZWxsKGdsdWU6OmdsdWUoJ2thZ2dsZSBjb21wZXRpdGlvbnMgZG93bmxvYWQgLWMgeyBjb21wZXRpdGlvbl9uYW1lIH0gLXAgeyBkYXRhX2RpciB9JykpDQoNCiMgdW56aXAgdGhlIGZpbGVzIHJlY2VpdmVkDQpzaGVsbChnbHVlOjpnbHVlKCd1bnppcCB7IHppcGZpbGUgfSAtZCB7IGRhdGFfZGlyIH0nKSkNCg0KYGBgDQoNClJlYWQgaW4gdGhlIGNvbnRlbnRzIG9mIHRoZSBmaWxlcyB0byBkYXRhZnJhbWVzIGhlcmUuIEkndmUgd3JpdHRlbiBhbiBpbXBvcnQgZnVuY3Rpb24gdG8gY2FzdCB0aGUgZGF0YSB0eXBlcyBhbmQgY3JlYXRlIG5ldyBmZWF0dXJlcyBpbiB0aGUgc2FtZSB3YXksIGJvdGggZnJvbSB0aGUgcHJvdmlkZWQgdHJhaW4gZGF0YSBmaWxlIGFzIHdlbGwgYXMgZnJvbSB0aGUgdW4tbGFiZWxlZCBmaWxlIGxhYmVsZWQgdGVzdCwgdGhhdCBJIHdpbGwgY2FsbCBgY29tcGV0aXRpb25fc3VibWlzc2lvbl9kZmAuICBJbiB0aGlzIGNhc2UsIHRoZSBgY29tcGV0aXRpb25fc3VibWlzc2lvbl9kZmAgYXBwZWFycyB0byBiZSBhIHJhbmRvbSBzYW1wbGUgc2VsZWN0aW9uIGFjcm9zcyBhbGwgeWVhcnMgYW5kIHN0YXRlcy4gV2Ugd2lsbCBsaWtlbHkgZGlzY292ZXIgdGhhdCB0aGUgYGNvbXBldGl0aW9uX3N1Ym1pc3Npb25fZGZgIGluY2x1ZGVzIHllYXJzIG9yIGZhY3RvciBsZXZlbHMsIGxpa2Ugc3RhdGVzIG9yIHppcCBjb2RlcywgdGhhdCB3ZXJlIG5vdCBwcmVzZW50IGluIHRoZSB0cmFpbmluZyBzZXQuDQoNCmBgYHtyIHJlYWQga2FnZ2xlIGZpbGVzLCBjbGFzcy5vdXRwdXQ9InNjcm9sbC0xMDAifQ0KDQpjYXN0X3RyYW5zZm9ybSA8LSBmdW5jdGlvbih0Ymwpew0KICB0YmwgJT4lIA0KICAgbXV0YXRlKEZyYW5jaGlzZUNvZGUgPSBpZl9lbHNlKEZyYW5jaGlzZUNvZGUgJWluJSBjKDAsMSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibm9GcmFuY2hpc2UiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIkZyYW5jaGlzZSIpLA0KICAgIE5BSUNTMiA9IHN0cl9zdWIoTkFJQ1MsIDFMLCAyTCksICNJbmR1c3RyeSBDbGFzcyBIaWVyYXJjaHkNCiAgICBOQUlDUzMgPSBzdHJfc3ViKE5BSUNTLCAxTCwgM0wpLCAjSW5kdXN0cnkgQ2xhc3MgSGllcmFyY2h5DQogICAgTkFJQ1M0ID0gc3RyX3N1YihOQUlDUywgMUwsIDRMKSwgI0luZHVzdHJ5IENsYXNzIEhpZXJhcmNoeQ0KICAgIE5ld0V4aXN0ID0gaWZfZWxzZShpcy5uYShOZXdFeGlzdCksICJVbmtub3duIiwgTmV3RXhpc3QpDQogICAgKSAlPiUgDQogIG11dGF0ZShhY3Jvc3MoDQogICAgYygiTmFtZSIsDQogICAgICAiQ2l0eSIsDQogICAgICAiU3RhdGUiLA0KICAgICAgIlppcCIsDQogICAgICAiQmFuayIsDQogICAgICAiU2VjdG9yIiwNCiAgICAgICJOQUlDUyIsDQogICAgICAiQmFua1N0YXRlIiwNCiAgICAgICJVcmJhblJ1cmFsIiwNCiAgICAgICJOZXdFeGlzdCIsDQogICAgICAiRnJhbmNoaXNlQ29kZSIsIA0KICAgICAgIk5BSUNTMiIsICJOQUlDUzMiLCAiTkFJQ1M0Ig0KICAgICksDQogICAgYXNfZmFjdG9yDQogICkpICU+JQ0KICBtdXRhdGUoDQogICAgVXJiYW5SdXJhbCA9IGZjdF9yZWNvZGUoDQogICAgICBVcmJhblJ1cmFsLA0KICAgICAgIlVyYmFuIiA9ICIxIiwNCiAgICAgICJSdXJhbCIgPSAiMiIsDQogICAgICAiVW5rbm93biIgPSAiMCINCiAgICApLA0KICAgIE5ld0V4aXN0ID0gZmN0X3JlY29kZShOZXdFeGlzdCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIkV4aXN0aW5nIiA9ICIxLjAiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAiTmV3IiA9ICIyLjAiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAiVW5rbm93biIgPSAiMC4wIiksDQogICAgbG9hbl9wZXJfZW1wbG95ZWUgPSBHckFwcHYgLyAoTm9FbXAgKyAxKSwNCiAgICBTQkFfQXBwdl92c19Hcm9zcyA9IFNCQV9BcHB2IC8gR3JBcHB2LA0KICAgIEdyb3NzX21pbnVzX1NCQV9BcHB2ID0gR3JBcHB2IC0gU0JBX0FwcHYsDQogICAgR3Jvc3NfbWludXNfU0JBX0FwcHZfdnNfRW1wbG95ZWUgPSBHcm9zc19taW51c19TQkFfQXBwdiAvIChOb0VtcCArIDEpLA0KICAgIHNhbWVfc3RhdGUgPSBTdGF0ZSA9PSBCYW5rU3RhdGUNCiAgICkNCn0NCg0KdHJhaW5fZGYgPC0NCiAgcmVhZF9jc3YoDQogICAgZmlsZSA9IGdsdWU6OmdsdWUoew0KICAgICAgZGF0YV9kaXINCiAgICB9LCAiL3RyYWluLmNzdiIpLA0KICAgIGNvbF90eXBlcyA9ICJjY2NjY2NjY2NpaWNpaWNjZGRkZCIsDQogICAgc2hvd19jb2xfdHlwZXMgPSBGQUxTRQ0KICApICU+JQ0KICBtdXRhdGUoZGVmYXVsdF9mbGFnID0gYXNfZmFjdG9yKGlmX2Vsc2UoDQogICAgZGVmYXVsdF9hbW91bnQgPiAwLA0KICAgICJkZWZhdWx0IiwNCiAgICAibm9uZGVmYXVsdCINCiAgKSksDQogIGRlZmF1bHRfcHJvcG9ydGlvbiA9IGRlZmF1bHRfYW1vdW50IC8gR3JBcHB2KSAlPiUNCiAgY2FzdF90cmFuc2Zvcm0oKSAlPiUNCiAgZHBseXI6OnNlbGVjdChkZWZhdWx0X2Ftb3VudCwNCiAgICAgICAgICAgICAgICBkZWZhdWx0X2ZsYWcsDQogICAgICAgICAgICAgICAgZGVmYXVsdF9wcm9wb3J0aW9uLA0KICAgICAgICAgICAgICAgIGV2ZXJ5dGhpbmcoKSkNCg0KY29tcGV0aXRpb25fc3VibWlzc2lvbl9kZiA8LQ0KICByZWFkX2NzdigNCiAgICBmaWxlID0gZ2x1ZTo6Z2x1ZSh7DQogICAgICBkYXRhX2Rpcg0KICAgIH0sICIvdGVzdC5jc3YiKSwNCiAgICBjb2xfdHlwZXMgPSAiY2NjY2NjY2NjaWljaWljY2RkZCIsDQogICAgc2hvd19jb2xfdHlwZXMgPSBGQUxTRQ0KICApICU+JQ0KICBjYXN0X3RyYW5zZm9ybSgpDQoNCmBgYA0KDQotLS0tDQoNCiMjIEV4cGxvcmF0b3J5IERhdGEgQW5hbHlzaXMgKEVEQSkNCg0KU29tZSBxdWVzdGlvbnMgdG8gYW5zd2VyIHVwZnJvbnQ6DQoNCi0gV2hhdCBmZWF0dXJlcyBoYXZlIG1pc3NpbmcgZGF0YSwgYW5kIGltcHV0YXRpb25zIG1heSBiZSByZXF1aXJlZD8NCg0KLSBXaGF0IGRvZXMgdGhlIG91dGNvbWUgdmFyaWFibGUgbG9vayBsaWtlLCBpbiB0ZXJtcyBvZiBpbWJhbGFuY2U/DQoNCkluIGNvbXBldGl0aW9uIEkgdXNlIGBza2ltcjo6c2tpbSgpYCBmb3IgcXVpY2sgY2hlY2tzLiBXZSB3aWxsIGNyYWZ0IG11Y2ggbmljZXIgcGxvdHMgYmVsb3cuDQoNCmBgYHtyIHNraW0sIGV2YWwgPSBGQUxTRSwgY2xhc3Mub3V0cHV0PSJzY3JvbGwtMTAwIn0NCnNraW1yOjpza2ltKHRyYWluX2RmKQ0KYGBgDQoNCiMjIyBUaGUgT3V0Y29tZSBWYXJpYWJsZQ0KDQpMZXRzIGNoYXJhY3Rlcml6ZSB0aGUgYGRlZmF1bHRfYW1vdW50YCBhcyBhIGhpc3RvZ3JhbSBmb3IgYWxsIGRhdGEgaW4gdGhlIHNldCwgYW5kIGFzIGEgdHJlZW1hcCB0byBiZXR0ZXIgdW5kZXJzdGFuZCB0aGUgZGlzdHJpYnV0aW9ucyBieSBhIGZldyBvZiB0aGUgY2F0ZWdvcnkgZmFjdG9yIGxldmVscy4NCg0KYGBge3Igb3V0Y29tZSB2YXJpYWJsZSBlZGEsIG9wdHMuTGFiZWw9J2ZpZy5sYXJnZSd9DQp0cmFpbl9kZiAlPiUgDQogIGdyb3VwX2J5KFN0YXRlLCBTZWN0b3IgPSBmY3RfbHVtcChTZWN0b3IsIDEwKSkgJT4lIA0KICBzdW1tYXJpc2UobiA9IG4oKSwgDQogICAgICAgICAgICBtZWFuX2RlZmF1bHQgPSBtZWFuKGRlZmF1bHRfYW1vdW50ID4gMCwgbmEucm0gPSBUUlVFKSwNCiAgICAgICAgICAgIC5ncm91cHMgPSAiZHJvcCIpICU+JSANCiAgZ2dwbG90KGFlcyhhcmVhID0gbiwgDQogICAgICAgICAgICAgZmlsbCA9IG1lYW5fZGVmYXVsdCwgDQogICAgICAgICAgICAgbGFiZWwgPSBTZWN0b3IsIA0KICAgICAgICAgICAgIHN1Ymdyb3VwID0gU3RhdGUpKSArDQogIGdlb21fdHJlZW1hcCgpICsNCiAgZ2VvbV90cmVlbWFwX3N1Ymdyb3VwX2JvcmRlcigpICsNCiAgZ2VvbV90cmVlbWFwX3RleHQoY29sb3IgPSAiYmxhY2siLCBwbGFjZSA9ICJ0b3AiLCByZWZsb3cgPSBUUlVFKSArDQogIGdlb21fdHJlZW1hcF9zdWJncm91cF90ZXh0KA0KICAgIGNvbG9yID0gImdyYXk0MCIsIA0KICAgIHBsYWNlID0gImJvdHRvbXJpZ2h0IiwNCiAgICBmb250ZmFjZSA9ICJpdGFsaWMiLCANCiAgICBtaW4uc2l6ZSA9IDAsIA0KICAgIGFscGhhID0gMC4zDQogICkgICsNCiAgc2NhbGVfZmlsbF9mZXJtZW50ZXIoDQogICAgcGFsZXR0ZSA9ICJQdVJkIiwgDQogICAgZGlyZWN0aW9uID0gMSwNCiAgICBndWlkZSA9ICJjb2xvcnN0ZXBzIiwgDQogICAgbGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpLA0KICAgIGJyZWFrcyA9IHNlcSgwLDEsMC4xKQ0KICApICsNCiAgbGFicygNCiAgICBmaWxsID0gIkxvYW5zXG5UaGF0IERlZmF1bHQiLCANCiAgICB0aXRsZSA9ICJVUyBTbWFsbCBCdXNpbmVzcyBBZG1pbiBMb2FucyAyMDAzLTIwMTAiLA0KICAgIHN1YnRpdGxlID0gIkJveCBhcmVhIGlzICMgb2YgbG9hbnMgZm9yIHNlY3RvcnMgd2l0aGluIGVhIFN0YXRlLlxuVGhlIHZhc3QgbWFqb3JpdHkgb2YgaW5kaXZpZHVhbCBsb2FucyBkbyBub3QgZGVmYXVsdC4iLA0KICAgIGNhcHRpb24gPSAiRGF0YSBTb3VyY2U6IEthZ2dsZSINCiAgKQ0KYGBgDQoNCmBgYHtyIGdyb3NzIGFwcHJvdmVkIHZhbHVlIGhpc3RvZ3JhbX0NCnRyYWluX2RmICU+JSANCiAgZHBseXI6OnNlbGVjdChHckFwcHYsIGRlZmF1bHRfZmxhZyApICU+JSANCiAgYmluZF9yb3dzKGRwbHlyOjpzZWxlY3QoY29tcGV0aXRpb25fc3VibWlzc2lvbl9kZiwgR3JBcHB2KSAlPiUgbXV0YXRlKGRlZmF1bHRfZmxhZyA9ICJob2xkb3V0IHNldCIpKSAlPiUgDQogIGdncGxvdChhZXMoR3JBcHB2LCBmaWxsID0gZGVmYXVsdF9mbGFnKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShiaW5zID0gNTApICsNCiAgc2NhbGVfeF9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OmRvbGxhcl9mb3JtYXQoKSkgKw0KICBsYWJzKHRpdGxlID0gIkxvYW5zIHRlbmQgdG8gZmFsbCBiZXR3ZWVuICQxMGsgdG8gMU0iLA0KICAgICAgIGZpbGwgPSBOVUxMLCB4ID0gIkdyb3NzIEFwcHJvdmVkIExvYW4gVmFsdWUiLA0KICAgICAgIHN1YnRpdGxlID0gIlNCQSBtdXN0IGhhdmUgdmFsdWUgdGhyZXNob2xkIGxpbWl0cyIpDQpgYGANCg0KLS0tLQ0KDQojIyMgQ2F0ZWdvcmljYWwgVmFyaWFibGVzDQoNCmBgYHtyIHN1bW1hcml6ZSBjYXRlZ29yaWNhbCB2YXJpYWJsZXMsIG9wdHMuTGFiZWw9J2ZpZy5sYXJnZSd9DQpzdW1tYXJpemVfZGVmYXVsdHMgPC0gZnVuY3Rpb24odGJsKXsNCiAgdGJsICU+JSANCiAgc3VtbWFyaXplKG5fbG9hbnMgPSBuKCksDQogICAgICAgICAgICBwY3RfZGVmYXVsdCA9IG1lYW4oZGVmYXVsdF9hbW91bnQgPiAwKSwNCiAgICAgICAgICAgIHRvdGFsX2dyX2FwcHYgPSBzdW0oR3JBcHB2KSwNCiAgICAgICAgICAgIHRvdGFsX2RlZmF1bHRfYW1vdW50ID0gc3VtKGRlZmF1bHRfYW1vdW50ICksDQogICAgICAgICAgICBwY3RfZGVmYXVsdF9hbW91bnQgPSB0b3RhbF9kZWZhdWx0X2Ftb3VudCAvIHRvdGFsX2dyX2FwcHYsDQogICAgICAgICAgICAuZ3JvdXBzID0gImRyb3AiKSAlPiUgDQogIGFycmFuZ2UoZGVzYyhuX2xvYW5zKSkNCn0NCg0Kd2l0aGZyZXEgPC0gZnVuY3Rpb24oeCl7DQogIHRpYmJsZSh4KSAlPiUgDQogICAgYWRkX2NvdW50KHgpICU+JSANCiAgICBtdXRhdGUoY29tYmluZWQgPSBnbHVlOjpnbHVlKCJ7IHN0cl9zdWIoeCwgZW5kID0gMTgpIH0gKHsgbiB9KSIpKSAlPiUgDQogICAgcHVsbChjb21iaW5lZCkNCn0NCg0KcGxvdF9jYXRlZ29yeSA8LSBmdW5jdGlvbih0YmwsIGNhdGVnb3J5LCBuX2NhdGVnb3JpZXMgPSA3KXsNCiAgdGJsICU+JSANCiAgZ3JvdXBfYnkoe3sgY2F0ZWdvcnkgfX0gOj0gd2l0aGZyZXEoZmN0X2x1bXAoe3sgY2F0ZWdvcnkgfX0sIG5fY2F0ZWdvcmllcykpKSAlPiUgDQogIHN1bW1hcml6ZV9kZWZhdWx0cygpICU+JSANCiAgbXV0YXRlKHt7IGNhdGVnb3J5IH19IDo9IGZjdF9yZW9yZGVyKHt7IGNhdGVnb3J5IH19LCBwY3RfZGVmYXVsdCkpICU+JSANCiAgZ2dwbG90KGFlcyhwY3RfZGVmYXVsdCwge3sgY2F0ZWdvcnkgfX0pKSArDQogIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gcGN0X2RlZmF1bHRfYW1vdW50KSkgKw0KICBnZW9tX3RleHQoYWVzKHggPSBwY3RfZGVmYXVsdCAtIDAuMDIsIA0KICAgICAgICAgICAgICAgIGxhYmVsID0gZ2x1ZTo6Z2x1ZSgiJCB7cm91bmQodG90YWxfZGVmYXVsdF9hbW91bnQvMTBeNiwwKX0gTSIpKSwNCiAgICAgICAgICAgIGhqdXN0ID0gMSkgKw0KICAgIHNjYWxlX2NvbG9yX3ZpcmlkaXNfYigNCiAgICAgIG5hbWUgPSAiJSBvZiBMb2FuIEFtb3VudFxuaW4gRGVmYXVsdCIsDQogICAgICBsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KGFjY3VyYWN5ID0gMSksDQogICAgICBicmVha3MgPSBjKDAsIDAuMDUsIDAuMSwgMC4xNSksDQogICAgICBvcHRpb24gPSAiSCINCiAgICApICsNCiAgICBndWlkZXMoY29sb3IgPSBndWlkZV9iaW5zKCkpICsNCiAgICBzY2FsZV94X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpKSArDQogICAgZXhwYW5kX2xpbWl0cyh4ID0gMCwgc2l6ZSA9IDApICsNCiAgICBsYWJzKHggPSAiJSBvZiBMb2FucyBPcmlnaW5hdGVkIHRoYXQgRGVmYXVsdGVkIiwgeSA9IE5VTEwsDQogICAgICAgICBzdWJ0aXRsZSA9ICIoIyBvZiBsb2FucykgJiBUb3RhbCBEZWZhdWx0ZWQgQW1vdW50IG9uIExhYmVscyIpDQogIA0KfQ0KDQp0cmFpbl9kZiAlPiUgDQogICBwbG90X2NhdGVnb3J5KFN0YXRlKSArDQogICBsYWJzKHRpdGxlID0gIkZsb3JpZGEsIElsbGlub2lzLCBOZXcgWW9yayIpDQoNCnRyYWluX2RmICU+JSANCiAgcGxvdF9jYXRlZ29yeShDaXR5KSArDQogIGxhYnModGl0bGUgPSAiTWlhbWkgbGVhZHMiKQ0KDQp0cmFpbl9kZiAlPiUgDQogIHBsb3RfY2F0ZWdvcnkoU2VjdG9yKSArDQogIGxhYnModGl0bGUgPSAiQ29uc3RydWN0aW9uIGxlYWRzIikNCg0KdHJhaW5fZGYgJT4lIA0KICBwbG90X2NhdGVnb3J5KEJhbmspICsNCiAgbGFicyh0aXRsZSA9ICJCQkNOIGlzIHRoZSBsZWFkZXIiKQ0KDQp0cmFpbl9kZiAlPiUgDQogIG11dGF0ZShzYW1lX3N0YXRlID0gaWZfZWxzZShCYW5rU3RhdGUgPT0gU3RhdGUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlNhbWUgU3RhdGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIkRpZmZlcmVudCIpKSAlPiUgDQogIHBsb3RfY2F0ZWdvcnkoc2FtZV9zdGF0ZSkgKw0KICBsYWJzKHRpdGxlID0gIkJhbmtzIGluIFN0YXRlcyBkaWZmZXJlbnQgdGhhbiB0aGUgQm9ycm93ZXIiKQ0KDQp0cmFpbl9kZiAlPiUgDQogIHBsb3RfY2F0ZWdvcnkoTmV3RXhpc3QpICsNCiAgbGFicyh0aXRsZSA9ICJOZXcgQm9ycm93ZXJzIikNCg0KdHJhaW5fZGYgJT4lIA0KICBwbG90X2NhdGVnb3J5KEZyYW5jaGlzZUNvZGUpICsNCiAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgRnJhbmNoaXNlcyBhcmVuJ3QgbXVjaCBkaWZmZXJlbnQiKQ0KDQp0cmFpbl9kZiAlPiUgDQogIHBsb3RfY2F0ZWdvcnkoTkFJQ1MyKSArDQogIGxhYnModGl0bGUgPSAiTkFJQ1MgY29kZXMgc3RhcnRpbmcgd2l0aCAyMyIpDQoNCnRyYWluX2RmICU+JSANCiAgcGxvdF9jYXRlZ29yeShOQUlDUzMpICsNCiAgbGFicyh0aXRsZSA9ICJOQUlDUyBjb2RlcyBzdGFydGluZyB3aXRoIDIzOCIpDQoNCnRyYWluX2RmICU+JSANCiAgcGxvdF9jYXRlZ29yeShOQUlDUzQpICsNCiAgbGFicyh0aXRsZSA9ICJOQUlDUyBjb2RlcyBzdGFydGluZyB3aXRoIDgxMjEiKQ0KDQp0cmFpbl9kZiAlPiUgDQogIHBsb3RfY2F0ZWdvcnkoTkFJQ1MpICsNCiAgbGFicyh0aXRsZSA9ICJOQUlDUyBjb2RlIDcyMjIxMSIpDQoNCnRyYWluX2RmICU+JSANCiAgcGxvdF9jYXRlZ29yeShVcmJhblJ1cmFsKSArDQogIGxhYnModGl0bGUgPSAiVXJiYW4gYm9ycm93ZXJzIikNCg0KdHJhaW5fZGYgJT4lIA0KICBwbG90X2NhdGVnb3J5KE5hbWUpICsNCiAgbGFicyh0aXRsZSA9ICJUaGUgYmFkIGJ1c2luZXNzIG5hbWVzIikNCg0KYGBgDQoNCi0tLS0NCg0KIyMjIFRpbWUgU2VyaWVzDQoNCmBgYHtyIHRpbWUgc2VyaWVzLCBvcHRzLkxhYmVsPSdmaWcubG9uZyd9DQp0cmFpbl9kZiAlPiUgDQogIGdyb3VwX2J5KEFwcHJvdmFsRlksIFN0YXRlID0gZmN0X2x1bXAoU3RhdGUsIDUpKSAlPiUgDQogIHN1bW1hcml6ZV9kZWZhdWx0cygpICU+JSANCiAgZ2dwbG90KGFlcyhBcHByb3ZhbEZZLCBwY3RfZGVmYXVsdCwgY29sb3IgPSBTdGF0ZSkpICsNCiAgZ2VvbV9saW5lKHNpemUgPSAyKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgc2NhbGVfc2l6ZV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6ZG9sbGFyX2Zvcm1hdCgpKSArDQogIGV4cGFuZF9saW1pdHMoeSA9IDAsIHNpemUgPSAwKSArDQogIGxhYnMoeCA9ICJ5ZWFyIGFwcHJvdmVkIiwgeSA9ICJEZWZhdWx0IFBlcmNlbnRhZ2UiLA0KICAgICAgIHRpdGxlID0gIlNCQSBMb2FucyBpbiB0aGUgMjAwOCBmaW5hbmNpYWwgY3Jpc2lzIiwNCiAgICAgICBzaXplID0gIkxvYW4gQXBwcm92YWxzIiwNCiAgICAgICBjYXB0aW9uID0gIkJhc2VkIG9uIERhdmlkIFJvYmluc29uIEBkcm9iIGV4YW1wbGUiKQ0KICANCmBgYA0KDQotLS0tDQoNCiMjIyBOdW1lcmljIEZlYXR1cmVzDQoNCmBgYHtyIHN1bW1hcml6ZSBudW1lcmljIG91dGNvbWUsIG9wdHMuTGFiZWw9J2ZpZy5sb25nJ30NCnRyYWluX251bWVyaWMgPC0gdHJhaW5fZGYgJT4lIGtlZXAoaXMubnVtZXJpYykgJT4lIGNvbG5hbWVzKCkNCg0KdHJhaW5fZGYgJT4lDQogIGJpbmRfcm93cyhjb21wZXRpdGlvbl9zdWJtaXNzaW9uX2RmKSAlPiUgDQogIHNlbGVjdF9hdChhbGxfb2YodHJhaW5fbnVtZXJpYykpICU+JQ0KICBkcGx5cjo6c2VsZWN0KC1kZWZhdWx0X2Ftb3VudCkgJT4lDQogIHBpdm90X2xvbmdlcigNCiAgICBjb2xzID0gZXZlcnl0aGluZygpLA0KICAgIG5hbWVzX3RvID0gImtleSIsDQogICAgdmFsdWVzX3RvID0gInZhbHVlIg0KICApICU+JQ0KICBmaWx0ZXIoIWlzLm5hKHZhbHVlKSkgJT4lDQogIGdncGxvdChtYXBwaW5nID0gYWVzKHZhbHVlLCBmaWxsID0ga2V5KSkgKw0KICBnZW9tX2hpc3RvZ3JhbSgNCiAgICBwb3NpdGlvbiA9ICJpZGVudGl0eSIsDQogICAgYmlucyA9IDMwLA0KICAgIHNob3cubGVnZW5kID0gRkFMU0UNCiAgKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hX2Zvcm1hdCgpLA0KICAgICAgICAgICAgICAgICAgICAgbi5icmVha3MgPSAzKSArDQogIHNjYWxlX3hfY29udGludW91cyhuLmJyZWFrcyA9IDMpICsgDQogIGZhY2V0X3dyYXAoIH4ga2V5LCBzY2FsZXMgPSAiZnJlZSIsIG5jb2wgPSAzKSArDQogIGxhYnMoDQogICAgdGl0bGUgPSAiTnVtZXJpYyBGZWF0dXJlIEhpc3RvZ3JhbSBEaXN0cmlidXRpb25zIiwNCiAgICBzdWJ0aXRsZSA9ICJUcmFpbmluZyBhbmQgSG9sZG91dCBkYXRhc2V0cyBjb21iaW5lZCIsDQogICAgeCA9IE5VTEwsDQogICAgeSA9ICJDb3VudCINCiAgKSArDQogIHRoZW1lKGFzcGVjdC5yYXRpbyA9IDEuMykgKw0KICB0aGVtZV9qaW0oYmFzZV9zaXplID0gOCkNCg0KdHJhaW5fZGYgJT4lIA0KICBmaWx0ZXIoZGVmYXVsdF9hbW91bnQgPiAxKSAlPiUgDQogIHNlbGVjdF9hdChhbGxfb2YodHJhaW5fbnVtZXJpYykpICU+JQ0KICBwaXZvdF9sb25nZXIoY29scyA9IC1jKGRlZmF1bHRfYW1vdW50LCBBcHByb3ZhbEZZKSwNCiAgICAgICAgICAgICAgIG5hbWVzX3RvID0gImtleSIsDQogICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAidmFsdWUiKSAlPiUgDQogIGdncGxvdChhZXMoQXBwcm92YWxGWSwgdmFsdWUsIHogPSBkZWZhdWx0X2Ftb3VudCkpICsNCiAgc3RhdF9zdW1tYXJ5X2hleChhbHBoYSA9IDAuOSwgYmlucyA9IDEwKSArDQogIHNjYWxlX2ZpbGxfdmlyaWRpc19jKG9wdGlvbiA9ICJIIixsYWJlbHMgPSBzY2FsZXM6OmRvbGxhcl9mb3JtYXQoKSkgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMobi5icmVha3MgPSAzKSArDQogIHNjYWxlX3lfY29udGludW91cyhuLmJyZWFrcyA9IDMpICsNCiAgZmFjZXRfd3JhcCh+IGtleSwgc2NhbGVzID0gImZyZWUiLCBuY29sID0gMyApICsNCiAgbGFicyhzdWJ0aXRsZSA9ICJNZWFuIERlZmF1bHQgVmFsdWUgV2hlcmUgPiAkMSIsIA0KICAgICAgIHRpdGxlID0gIk51bWVyaWMgRmVhdHVyZXMgYnkgTG9hbiBPcmlnaW5hdGlvbiBZZWFyIiwNCiAgICAgICB5ID0gTlVMTCwgZmlsbCA9ICJEZWZhdWx0ICQiKSArDQogIHRoZW1lKGFzcGVjdC5yYXRpbyA9IDEuMykgKw0KICB0aGVtZV9qaW0oYmFzZV9zaXplID0gOCkNCg0KdG13cl9jb2xzIDwtIHZpcmlkaXM6OnZpcmlkaXNfcGFsKG9wdGlvbiA9ICJIIikNCg0KdHJhaW5fZGYgJT4lIA0KICAgIHNlbGVjdF9hdChhbGxfb2YodHJhaW5fbnVtZXJpYykpICU+JQ0KICAgIGNvcigpICU+JSANCiAgICBoZWF0bWFwKG1haW4gPSAiTnVtZXJpYyBDb3JyZWxhdGlvbnMiKQ0KDQpgYGANCg0KVGhlIGhpZ2hseSBjb3JyZWxhdGVkIG51bWVyaWMgdmFyaWFibGVzIGFyZSBnb2luZyB0byBoYXZlIHRvIGJlIGRlYWx0IHdpdGgsIGVpdGhlciB0aHJvdWdoIHNvbWUgc29ydCBvZiByZWd1bGFyaXphdGlvbiBvciBwcmUtcHJvY2Vzc2luZy4NCg0KLS0tLQ0KDQojIyMgTWFwcw0KDQpgYGB7ciBtYXAsIG9wdHMuTGFiZWw9J2ZpZy5sYXJnZSd9DQpieV9zdGF0ZSA8LSB0cmFpbl9kZiAlPiUgDQogIGdyb3VwX2J5KFN0YXRlKSAlPiUgDQogIHN1bW1hcml6ZV9kZWZhdWx0cygpDQoNCm1hcF9kYXRhKCJzdGF0ZSIpICU+JSANCiAgYXNfdGliYmxlKCkgJT4lIA0KICBtdXRhdGUoU3RhdGUgPSBzdGF0ZS5hYmJbbWF0Y2gocmVnaW9uLCBzdHJfdG9fbG93ZXIoc3RhdGUubmFtZSkpXSkgJT4lIA0KICBpbm5lcl9qb2luKGJ5X3N0YXRlLCBieSA9ICJTdGF0ZSIpICU+JSANCiAgZ2dwbG90KGFlcyhsb25nLCBsYXQsIGdyb3VwID0gZ3JvdXAsIGZpbGwgPSBwY3RfZGVmYXVsdCkpICsNCiAgZ2VvbV9wb2x5Z29uKCkgKw0KICB0aGVtZV9tYXAoKSArDQogIGNvb3JkX21hcCgpICsNCiAgc2NhbGVfZmlsbF92aXJpZGlzX2Mob3B0aW9uID0gIkgiLCBsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgbGFicyh0aXRsZSA9ICJTQkEgTG9hbiBEZWZhdWx0cyAyMDAzLTIwMTA6IEZsb3JpZGEsIEdlb3JnaWEsIElsbGlub2lzLCBOZXZhZGEsICYgTWljaGlnYW4gbGVhZCIsDQogICAgICAgZmlsbCA9ICJQZXJjZW50IG9mIExvYW5cbk9yaWdpbmF0aW9ucyBFbmRpbmdcbmluIERlZmF1bHQiLA0KICAgICAgIGNhcHRpb24gPSAiRGF0YSBTb3VyY2U6IEthZ2dsZS4gQmFzZWQgb24gRGF2aWQgUm9iaW5zb24ncyBleGFtcGxlLiIpDQoNCmBgYA0KDQpTbywgeWVzLCB3ZSB3aWxsIGZpcnN0IG1vZGVsIHRvIHByZWRpY3QgZm9yIHRoZSBgZGVmYXVsdF9mbGFnYCBjbGFzcy4gSWYgaXQgcGVyZm9ybXMgd2VsbCwgd2Ugd2lsbCB0aGVuIGdvIG9uIHRvIG1vZGVsIHRoZSBgZGVmYXVsdF9hbW91bnRgIHNlcGFyYXRlbHkgZm9yIHRoZSBsb2FucyBwcmVkaWN0ZWQgdG8gZGVmYXVsdC4NCg0KIyBNYWNoaW5lIExlYXJuaW5nOiBDbGFzc2lmaWNhdGlvbiB7LnRhYnNldCAudGFic2V0LXBpbGxzfQ0KDQpIZXJlJ3MgYW4gb2Z0ZW4gb3Zlcmxvb2tlZCBwaWVjZSBvZiBhZHZpY2UgZm9yIGFsbCBNTCBwcmFjdGl0aW9uZXJzLCAmIGVzcGVjaWFsbHkgbmV3Y29tZXJzOg0KDQpgciBlbW9qaWZvbnQ6OmVtb2ppKCJwb2ludF9yaWdodCIpYCAgQnVpbGQgYSBub24tTUwgYmFzZWxpbmUgZmlyc3QuDQoNCllvdSdsbCBiZSBzdXJwcmlzZWQgaG93IG9mdGVuIHlvdSBjYW5ub3QgZWFzaWx5IGltcHJvdmUgYSBiYXNpYyBzb2x1dGlvbiBieSB0aHJvd2luZyBNTCBhdCBpdCwgYW5kIHlvdSdsbCBsZWFybiBzb21ldGhpbmcgYWJvdXQgdGhlIHByb2JsZW0gdG9vLg0KDQpZb3VyIGJlc3QgYWx0ZXJuYXRpdmUgdG8gYW55IGNvbXBsZXggbW9kZWwgaXNu4oCZdCDigJxubyBtb2RlbC7igJ0gWW91ciBhbHRlcm5hdGl2ZSBpcyBhIHNpbXBsZSBtb2RlbC4gSSBzZWUgdGhpcyBvdmVyIGFuZCBvdmVyLiBCZWZvcmUgeW91IGJ1aWxkIGEgUnViZSBHb2xkYmVyZyBtb2RlbCBvZiB0aGUgdW5pdmVyc2UsIGJ1aWxkIGEgYnJhaW4gZGVhZCBzaW1wbGUgbW9kZWwuIFRIQVQgcGFyc2ltb25pb3VzIHBpZWNlIG9mIGp1bmsgaXMgeW91ciBib2dpZS4gT3V0IHBlcmZvcm0gVEhBVCBpbiBhIHdheSB0aGF0IGp1c3RpZmllcyB5b3VyIGNvbXBsZXhpdHkgYW5kIHRpbWUuDQoNCk9uY2UgeW914oCZcmUgY29tcGFyaW5nIGEgY29tcGxpY2F0ZWQgbW9kZWwgdG8gYSBwYXJzaW1vbmlvdXMgbW9kZWwgeW91IGNhbiBpbnRlbGxpZ2VudGx5IGRpc2N1c3MgdGhlIOKAnG1hcmdpbmFsIGJlbmVmaXQgKG9yIGxpZnQpIGZyb20gY29tcGxleGl0eS7igJ0NCg0KYGBge3IgbnVsbCBhY2N1cmFjeX0NCnJvdW5kKDEwMCpwcm9wLnRhYmxlKHRhYmxlKHRyYWluX2RmJGRlZmF1bHRfZmxhZykpLDEpIA0KYGBgDQoNCkluIHRoZSBsb2FuIGRlZmF1bHQgZGF0YXNldCwgcHJlZGljdGluZyBhIG5vbmRlZmF1bHQgY2xhc3MgZm9yIGV2ZXJ5IGNhc2UgcmVzdWx0cyBpbiA3Ny4xJSBhY2N1cmFjeSwgZG9pbmcgbm90aGluZy4gTGV0J3MgY2FsbCB0aGlzIHRoZSBOVUxMIG1vZGVsLg0KDQotLS0tDQoNCiMjIFNwZW5kaW5nIHRoZSBEYXRhDQoNCldlIHdpbGwgc3BsaXQgYHRyYWluX2RmYCByYW5kb21seSBpbnRvIGEgdHJhaW5pbmcgcG9ydGlvbiAoOTAlKSBhbmQgYSBsYWJlbGVkIHRlc3RpbmcgcG9ydGlvbiAoMTAlKS4gVGhlIHRyYWluaW5nIHBvcnRpb24gaXMgdGhlbiBkaXZpZGVkIGludG8gNS1mb2xkIGNyb3NzIHZhbGlkYXRpb24gc2V0cyBhbmQgc3RyYXRpZmllZCBvbiB0aGUgYGRlZmF1bHRfZmxhZ2AgdG8gZW5zdXJlIHRoYXQgZWFjaCBDViBmb2xkIGhhcyBzb21lIHByb3BvcnRpb24gb2YgdGhlIGltYmFsYW5jZWQgb3V0Y29tZXMuICANCg0KYGBge3Igc3BlbmRpbmcgdGhlIGRhdGF9DQpzZXQuc2VlZCgyMDIxKQ0KDQpzcGxpdCA8LSBpbml0aWFsX3NwbGl0KHRyYWluX2RmLCBwcm9wID0gMC45KQ0KDQp0cmFpbmluZyA8LSB0cmFpbmluZyhzcGxpdCkNCnRlc3RpbmcgPC0gdGVzdGluZyhzcGxpdCkNCg0KY2xhc3NfZm9sZHMgPC0gdmZvbGRfY3YodHJhaW5pbmcsIHYgPSA1LCBzdHJhdGEgPSBkZWZhdWx0X2ZsYWcgKQ0KDQpgYGANCg0KIyMgVGhlIFJlY2lwZXMNCg0KV2UgYXJlIGdvaW5nIHRvIHN0YXJ0IGJ5IHRha2luZyBhIHN1cnZleSBvZiBhIGhhbmRmdWwgb2YgbW9kZWxzIHRoYXQgdHJhaW4gZmFzdCwgaGF2ZSBsaXR0bGUgdHVuaW5nIHJlcXVpcmVkLCBhbmQgYXJlIGdlbmVyYWxseSBpbnRlcnByZXRhYmxlLiBUaGUgY2hvaWNlIG9mIHByZS1wcm9jZXNzb3IsIG9yIGB0aWR5bW9kZWxzYCBgcmVjaXBlKClgIHN0ZXBzLCB0cmFuc2Zvcm1zIHRoZSBkYXRhZnJhbWUgaW50byBmb3JtYXRzIHRoYXQgbWFjaGluZSBsZWFybmluZyBhbGdvcml0aG1zIGNhbiB3b3JrIHdpdGguIA0KDQpPdXIgc3RyYXRlZ3kgaGVyZSBpcyB0byBwcmUtcHJvY2VzcyBhdCB0aHJlZSBsZXZlbHMuIFRoZSBmaXJzdCwgYGNsYXNzaWZpY2F0aW9uX3JlY2AgaXMgYSByYXcgZGF0YXNldCB3aXRoIGFsbCBjYXRlZ29yaWNhbCBwcmVkaWN0b3JzIGludGFjdC4gVGhlIHNlY29uZCwgYGR1bW1pZXNfY2xhc3NpZmljYXRpb25fcmVjYCB0dW5lcyBhIHRocmVzaG9sZCBmb3IgbHVtcGluZyBjYXRlZ29yaWNhbCBsZXZlbHMgdG9nZXRoZXIgYW5kIHRoZW4gY3JlYXRlcyBkdW1teSB2YXJpYWJsZXMgc28gZXZlcnl0aGluZyBpcyBudW1lcmljLiBXaXRoIHNvIG1hbnkgY2F0ZWdvcnkgbGV2ZWxzLCB0aGlzIHRyYWluaW5nIHNldCBjb3VsZCBiZSB0aG91c2FuZHMgb2YgY29sdW1ucyB3aWRlLCBhbmQgc3BhcnNlLiBUaGVyZSBpcyBhIGNhcmRpbmFsaXR5IHRyYWRlb2ZmIGhlcmUsIHdoZXJlIGVhY2ggYWRkaXRpb25hbCBjb2x1bW4gbWF5IG5vdCBjb250cmlidXRlIG11Y2ggdG8gdGhlIG1vZGVsLiBUaGUgdGhpcmQgcHJlLXByb2Nlc3NvciwgYHBsc19jbGFzc2lmaWNhdGlvbl9yZWNgLCB3b3JrcyBhcyBhIHNvcnQgb2Ygc3VwZXJ2aXNlZCBsZWFybmluZyB2ZXJzaW9uIG9mIHByaW5jaXBhbCBjb21wb25lbnRzIGFuYWx5c2lzLCBjYWxsZWQgW1BhcnRpYWwgTGVhc3QgU3F1YXJlc10oaHR0cHM6Ly93d3cudG13ci5vcmcvZGltZW5zaW9uYWxpdHkuaHRtbCNwYXJ0aWFsLWxlYXN0LXNxdWFyZXMpLCB0aGF0IHNpbXVsdGFuZW91c2x5IG1heGltaXplIHRoZSB2YXJpYXRpb24gaW4gdGhlIHByZWRpY3RvcnMgd2hpbGUgYWxzbyBtYXhpbWl6aW5nIHRoZSByZWxhdGlvbnNoaXAgYmV0d2VlbiB0aG9zZSBjb21wb25lbnRzIGFuZCB0aGUgb3V0Y29tZS4NCg0KYGBge3IgY2xhc3NpZmljYXRpb24gcmVjaXBlc30NCg0KY2xhc3NpZmljYXRpb25fcmVjIDwtDQogIHJlY2lwZSgNCiAgICBkZWZhdWx0X2ZsYWcgfiBMb2FuTnJfQ2hrRGd0ICsgTmFtZSArIFNlY3RvciArIENpdHkgKyBTdGF0ZSArIFppcCArDQogICAgICBCYW5rICsgQmFua1N0YXRlICsgTkFJQ1MgKyBBcHByb3ZhbEZZICsgTm9FbXAgKw0KICAgICAgTmV3RXhpc3QgKyBDcmVhdGVKb2IgKyBSZXRhaW5lZEpvYiArIEZyYW5jaGlzZUNvZGUgKyBVcmJhblJ1cmFsICsNCiAgICAgIERpc2J1cnNlbWVudEdyb3NzICsgR3JBcHB2ICsgU0JBX0FwcHYgKyBOQUlDUzIgKyBOQUlDUzMgKyBOQUlDUzQgKw0KICAgICAgbG9hbl9wZXJfZW1wbG95ZWUgKyBTQkFfQXBwdl92c19Hcm9zcyArIEdyb3NzX21pbnVzX1NCQV9BcHB2ICsNCiAgICAgIEdyb3NzX21pbnVzX1NCQV9BcHB2X3ZzX0VtcGxveWVlICsgc2FtZV9zdGF0ZSwNCiAgICBkYXRhID0gdHJhaW5pbmcNCiAgKSAlPiUNCiAgdXBkYXRlX3JvbGUoTG9hbk5yX0Noa0RndCwgbmV3X3JvbGUgPSAiSUQiKSANCg0KZHVtbWllc19jbGFzc2lmaWNhdGlvbl9yZWMgPC0NCiAgY2xhc3NpZmljYXRpb25fcmVjICU+JQ0KICBzdGVwX25vdmVsKA0KICAgIE5hbWUsDQogICAgU2VjdG9yLA0KICAgIENpdHksDQogICAgU3RhdGUsDQogICAgWmlwLA0KICAgIEJhbmssDQogICAgQmFua1N0YXRlLA0KICAgIE5BSUNTLA0KICAgIE5BSUNTMiwNCiAgICBOQUlDUzMsDQogICAgTkFJQ1M0DQogICkgJT4lDQogIHN0ZXBfb3RoZXIoDQogICAgTmFtZSwNCiAgICBTZWN0b3IsDQogICAgQ2l0eSwNCiAgICBTdGF0ZSwNCiAgICBaaXAsDQogICAgQmFuaywNCiAgICBCYW5rU3RhdGUsDQogICAgTkFJQ1MsDQogICAgTkFJQ1MyLA0KICAgIE5BSUNTMywNCiAgICBOQUlDUzQsDQogICAgdGhyZXNob2xkID0gdHVuZSgpKSAlPiUNCiAgc3RlcF9kdW1teShhbGxfbm9taW5hbF9wcmVkaWN0b3JzKCkpICU+JQ0KICBzdGVwX2ludGVyYWN0KH4gc3RhcnRzX3dpdGgoIkJhbmsiKTpBcHByb3ZhbEZZKSAlPiUgIyBBZGQgaW1wb3J0YW50IGludGVyYWN0aW9ucw0KICBzdGVwX29yZGVyTm9ybShhbGxfbnVtZXJpY19wcmVkaWN0b3JzKCkpICU+JQ0KICBzdGVwX25vcm1hbGl6ZShhbGxfbnVtZXJpY19wcmVkaWN0b3JzKCkpIA0KDQpwbHNfY2xhc3NpZmljYXRpb25fcmVjIDwtIGR1bW1pZXNfY2xhc3NpZmljYXRpb25fcmVjICU+JQ0KICBzdGVwX3BscyhhbGxfbnVtZXJpY19wcmVkaWN0b3JzKCksDQogICAgICAgICAgIG51bV9jb21wID0gdHVuZSgpLA0KICAgICAgICAgICBvdXRjb21lID0gImRlZmF1bHRfZmxhZyIpICU+JQ0KICBzdGVwX25vcm1hbGl6ZShhbGxfbnVtZXJpY19wcmVkaWN0b3JzKCkpIA0KDQpgYGANCg0KIyMgRmVhdHVyZSBFeHRyYWN0aW9uDQoNCmBgYHtyIHBsb3QgdmFsaWRhdGlvbiByZXN1bHRzIGZ1bmN0aW9ufQ0KcGxvdF92YWxpZGF0aW9uX3Jlc3VsdHMgPC0gZnVuY3Rpb24ocmVjaXBlLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdCA9IHRyYWluX2RmLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG91dGNvbWUgPSBkZWZhdWx0X2ZsYWcpIHsNCiAgcmVjaXBlICU+JQ0KICAgICMgRXN0aW1hdGUgYW55IGFkZGl0aW9uYWwgc3RlcHMNCiAgICBwcmVwKCkgJT4lDQogICAgIyBQcm9jZXNzIHRoZSBkYXRhICh0aGUgdmFsaWRhdGlvbiBzZXQgYnkgZGVmYXVsdCkNCiAgICBiYWtlKG5ld19kYXRhID0gZGF0KSAlPiUNCiAgICBkcGx5cjo6c2VsZWN0KHt7IG91dGNvbWUgfX0sIHN0YXJ0c193aXRoKCJQTFMiKSkgJT4lIA0KICAgICMgQ3JlYXRlIHRoZSBzY2F0dGVycGxvdCBtYXRyaXgNCiAgICBnZ3Bsb3QoYWVzKHggPSAucGFuZWxfeCwgDQogICAgICAgICAgICAgICB5ID0gLnBhbmVsX3ksIA0KICAgICAgICAgICAgICAgY29sID0ge3sgb3V0Y29tZSB9fSwgDQogICAgICAgICAgICAgICBmaWxsID0ge3sgb3V0Y29tZSB9fSkpICsNCiAgICBnZW9tX3BvaW50KGFscGhhID0gMC40LCBzaXplID0gMC41KSArDQogICAgZ2VvbV9hdXRvZGVuc2l0eShhbHBoYSA9IC4zKSArDQogICAgZmFjZXRfbWF0cml4KHZhcnMoLXt7IG91dGNvbWUgfX0pLCBsYXllci5kaWFnID0gMikgDQp9DQpgYGANCg0KTGV0J3MgbG9vayBmb3Igc2VwYXJhdGlvbiBvZiB0aGUgY2xhc3NlcyBpbiB0aGUgbmV3LCB0cmFuc2Zvcm1lZCBmZWF0dXJlcyBmcm9tIHRoZSBQTFMgcHJlcHJvY2Vzc29yLiBPdXIgbW9kZWxpbmcgc3lzdGVtIHdpbGwgYHR1bmUoKWAgYm90aCBmb3IgdGhlIG51bWJlciBvZiBmYWN0b3JzIHRvIGJlIGx1bXBlZCB0b2dldGhlciBhbmQgZm9yIHRoZSBudW1iZXIgb2YgUExTIGNvbXBvbmVudHMuDQoNCmBgYHtyIHBscyBmZWF0dXJlIGV4dHJhY3Rpb24gcGxvdHMsIG9wdHMuTGFiZWw9J2ZpZy5sb25nJ30NCnBsc19jbGFzc2lmaWNhdGlvbl9yZWMgJT4lDQogIGZpbmFsaXplX3JlY2lwZShsaXN0KG51bV9jb21wID0gNCwNCiAgICAgICAgICAgICAgICAgICAgICAgdGhyZXNob2xkID0gMC4wMSkpICU+JSANCiAgcGxvdF92YWxpZGF0aW9uX3Jlc3VsdHMoKSArDQogIGdndGl0bGUoIlBhcnRpYWwgTGVhc3QgU3F1YXJlcyIpDQoNCnBsc19jbGFzc2lmaWNhdGlvbl9yZWMgJT4lIA0KICAgIGZpbmFsaXplX3JlY2lwZShsaXN0KG51bV9jb21wID0gNCwNCiAgICAgICAgICAgICAgICAgICAgICAgdGhyZXNob2xkID0gMC4wMSkpICU+JSANCiAgICBwcmVwKCkgJT4lIA0KICAgIGxlYXJudGlkeW1vZGVsczo6cGxvdF90b3BfbG9hZGluZ3MoY29tcG9uZW50X251bWJlciA8PSA0LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbiA9IDYsIHR5cGUgPSAicGxzIikgKw0KICBnZ3RpdGxlKCJQYXJ0aWFsIExlYXN0IFNxdWFyZXMgQ29tcG9uZW50cyIpICsNCiAgdGhlbWUoYXNwZWN0LnJhdGlvID0gMC45KSArDQogIHRoZW1lX2ppbShiYXNlX3NpemUgPSA3KQ0KDQpgYGANCg0KRWFjaCBjb21wb25lbnQgY29uc2lzdHMgb2YgY29tYmluYXRpb25zIG9mIHRoZSBvcmlnaW5hbCBmZWF0dXJlcy4gRGVwZW5kaW5nIHVwb24gdGhlIHRocmVzaG9sZCBmb3IgbHVtcGluZyBjYXRlZ29yaWNhbCBsZXZlbHMgdG9nZXRoZXIsIHRoZSBQTFMgcHJlLXByb2Nlc3NpbmcgbWF5IGhlbHAgdG8gc2VwYXJhdGUgdGhlIHNpZ25hbCBmcm9tIHRoZSBub2lzZS4gT24gdGhlIG90aGVyIGhhbmQsIHdlIHdpbGwgdXNlIG1hY2hpbmUgbGVhcm5pbmcgdGVjaG5pcXVlcyB0aGF0IGluY2x1ZGUgcmVndWxhcml6YXRpb24sIGFub3RoZXIgdGVjaG5pcXVlIGZvciBkZWFsaW5nIHdpdGggaGF2aW5nIHRvbyBtYW55IG5vaXN5IHZhcmlhYmxlcy4NCg0KVGhlIHNpemUgb2YgdGhlIGxvYW4gYW5kIGFwcHJvdmFsIHllYXIgZHJpdmUgdGhlIGZpcnN0IFBMUyBjb21wb25lbnQuIE90aGVycyBQTFMgY29tcG9uZW50cyBhcmUgZHJpdmVuIGJ5IGdyb3VwcyBvZiBzdGF0ZXMgYW5kIGEgZmV3IG9mIHRoZSBiYW5rcyB3aGVyZSB0aGUgbG9hbiB3YXMgb3JpZ2luYXRlZC4gDQoNCiMjIyBCYXNlbGluZSBNb2RlbCBTcGVjaWZpY2F0aW9ucw0KDQpPdXIgbW9kZWxpbmcgZW5naW5lcyB3aWxsIGZpcnN0IGluY2x1ZGUgYSBbR2VuZXJhbGl6ZWQgTGluZWFyIE1vZGVsIHdpdGggUmVndWxhcml6YXRpb25dKGh0dHBzOi8vYnJhZGxleWJvZWhta2UuZ2l0aHViLmlvL0hPTUwvcmVndWxhcml6ZWQtcmVncmVzc2lvbi5odG1sKSB0byBjb25zdHJhaW4gdGhlIG51bWJlciBvZiBmZWF0dXJlcyBhbmQgY29uc3RyYWluIHRoZSBvdXQgb2Ygc2FtcGxlIGVycm9yLiBUaGlzIGFwcHJvYWNoLCB3aXRoIExhc3NvIGFuZCBSaWRnZSBwZW5hbHR5IHBhcmFtZXRlcnMsIGlzIHNvbWV0aW1lcyBjYWxsZWQgRWxhc3RpYyBOZXQgb3IgR0xNTkVULg0KDQpUaGUgc2Vjb25kIGJ1aWxkcyBhIGRpc2NyaW1pbmFudCBhbmFseXNpcyBtb2RlbCB0aGF0IHVzZXMgbm9ubGluZWFyIGZlYXR1cmVzIGNyZWF0ZWQgdXNpbmcgbXVsdGl2YXJpYXRlIGFkYXB0aXZlIHJlZ3Jlc3Npb24gc3BsaW5lcywgb3IgTUFSUy4gSXQgaXMgYSBub24tcGFyYW1ldHJpYyByZWdyZXNzaW9uIHRlY2huaXF1ZSBhbmQgY2FuIGJlIHNlZW4gYXMgYW4gZXh0ZW5zaW9uIG9mIGxpbmVhciBtb2RlbHMgdGhhdCBhdXRvbWF0aWNhbGx5IG1vZGVscyBub25saW5lYXJpdGllcyBhbmQgaW50ZXJhY3Rpb25zIGJldHdlZW4gdmFyaWFibGVzLiBUaGUgdGVybSAiTUFSUyIgaXMgdHJhZGVtYXJrZWQgYW5kIGxpY2Vuc2VkIHRvIFNhbGZvcmQgU3lzdGVtcy4gSW4gb3JkZXIgdG8gYXZvaWQgdHJhZGVtYXJrIGluZnJpbmdlbWVudHMsIG9wZW4tc291cmNlIGltcGxlbWVudGF0aW9ucyBvZiBNQVJTIGFyZSBjYWxsZWQgIkVhcnRoIi4NCg0KVGhlIHRoaXJkLCByZWd1bGFyaXplZCBkaXNjcmltaW5hbnQgYW5hbHlzaXMsIGJ1aWxkcyBhIG1vZGVsIHRoYXQgZXN0aW1hdGVzIGEgbXVsdGl2YXJpYXRlIGRpc3RyaWJ1dGlvbiBmb3IgdGhlIHByZWRpY3RvcnMgc2VwYXJhdGVseSBmb3IgdGhlIGRhdGEgaW4gZWFjaCBjbGFzcy4gDQoNCmBgYHtyIGJhc2VsaW5lIGNsYXNzaWZpY2F0aW9uIHNwZWNzfQ0KbG9naXN0aWNfcmVnX2dsbV9zcGVjIDwtDQogIGxvZ2lzdGljX3JlZyhwZW5hbHR5ID0gdHVuZSgpLA0KICAgICAgICAgICAgICAgbWl4dHVyZSA9IHR1bmUoKSkgJT4lDQogIHNldF9lbmdpbmUoJ2dsbW5ldCcpDQoNCmZkYV9zcGVjIDwtDQogIGRpc2NyaW1fZmxleGlibGUocHJvZF9kZWdyZWUgPSB0dW5lKCkpICU+JQ0KICBzZXRfZW5naW5lKCdlYXJ0aCcpDQoNCnJkYV9zcGVjIDwtDQogIGRpc2NyaW1fcmVndWxhcml6ZWQoZnJhY19jb21tb25fY292ID0gdHVuZSgpLA0KICAgICAgICAgICAgICAgICAgICAgIGZyYWNfaWRlbnRpdHkgPSB0dW5lKCkpICU+JQ0KICBzZXRfZW5naW5lKCdrbGFSJykNCg0KYGBgDQoNCiMjIEdMTSBNb2RlbCBUdW5lDQoNCldlIGFyZSBnb2luZyB0byBxdWlja2x5IHJhY2UgdGhyb3VnaCBhIGdyaWQgb2YgNiBwYXJhbWV0ZXIgY29tYmluYXRpb25zIHdpdGggYHdvcmtmbG93c2V0c2AuIEkgd2lsbCBzZXQgcmVhc29uYWJsZSBwYXJhbWV0ZXJzIHJhbmdlcyBpbiBhZHZhbmNlIGZvciB0aGUgY2F0ZWdvcmljYWwgdmFyaWFibGUgbHVtcGluZyBhbmQgdGhlIG51bWJlciBvZiBQTFMgY29tcG9uZW50cy4NCg0KYGBge3IgcGFyYWxsZWwsIGV2YWwgPSBGQUxTRX0NCmN0cmwgPC0gY29udHJvbF9yYWNlKHBhcmFsbGVsX292ZXIgPSAiZXZlcnl0aGluZyIsDQogICAgICAgICAgICAgICAgICAgICBzYXZlX3ByZWQgPSBUUlVFKQ0KDQphbGxfY29yZXMgPC0gcGFyYWxsZWxseTo6YXZhaWxhYmxlQ29yZXMob21pdCA9IDEpDQphbGxfY29yZXMNCg0KZnV0dXJlOjpwbGFuKCJtdWx0aXNlc3Npb24iLCB3b3JrZXJzID0gYWxsX2NvcmVzKSAjIG9uIFdpbmRvd3MNCmBgYA0KDQpgYGB7ciBiYXNlbGluZSB3b3JrZmxvd3NldHN9DQp3b3JrZmxvd3MgPC0gIHdvcmtmbG93X3NldCgNCiAgICBwcmVwcm9jID0gbGlzdChmYWN0b3IgPSBkdW1taWVzX2NsYXNzaWZpY2F0aW9uX3JlYywNCiAgICAgICAgICAgICAgICAgICBwbHMgPSBwbHNfY2xhc3NpZmljYXRpb25fcmVjKSwgDQogICAgbW9kZWxzID0gbGlzdChnbG1uZXQgPSBsb2dpc3RpY19yZWdfZ2xtX3NwZWMsDQogICAgICAgICAgICAgICAgICBmZGEgPSBmZGFfc3BlYywNCiAgICAgICAgICAgICAgICAgIHJkYSA9IHJkYV9zcGVjKQ0KICApIA0KDQpmYWN0b3JfZ2xtbmV0X3BhcmFtcyA8LSB3b3JrZmxvd3MgJT4lIA0KICBleHRyYWN0X3dvcmtmbG93KCJmYWN0b3JfZ2xtbmV0IikgJT4lIA0KICBwYXJhbWV0ZXJzKCkgJT4lIA0KICB1cGRhdGUocGVuYWx0eSA9IHBlbmFsdHkoKSwNCiAgICAgICAgIG1peHR1cmUgPSBtaXh0dXJlKCksDQogICAgICAgICB0aHJlc2hvbGQgPSB0aHJlc2hvbGQocmFuZ2UgPSBjKDAuMSwgMC4wMDUpKSkNCg0KZmFjdG9yX2ZkYV9wYXJhbXMgPC0gd29ya2Zsb3dzICU+JSANCiAgZXh0cmFjdF93b3JrZmxvdygiZmFjdG9yX2ZkYSIpICU+JSANCiAgcGFyYW1ldGVycygpICU+JSANCiAgdXBkYXRlKHByb2RfZGVncmVlID0gcHJvZF9kZWdyZWUoKSwNCiAgICAgICAgIHRocmVzaG9sZCA9IHRocmVzaG9sZChyYW5nZSA9IGMoMC4xLCAwLjAwNSkpKQ0KDQpmYWN0b3JfcmRhX3BhcmFtcyA8LSB3b3JrZmxvd3MgJT4lIA0KICBleHRyYWN0X3dvcmtmbG93KCJmYWN0b3JfcmRhIikgJT4lIA0KICBwYXJhbWV0ZXJzKCkgJT4lIA0KICB1cGRhdGUoZnJhY19jb21tb25fY292ID0gZnJhY19jb21tb25fY292KCksDQogICAgICAgICBmcmFjX2lkZW50aXR5ID0gZnJhY19pZGVudGl0eSgpLA0KICAgICAgICAgdGhyZXNob2xkID0gdGhyZXNob2xkKHJhbmdlID0gYygwLjEsIDAuMDA1KSkpDQoNCnBsc19nbG1uZXRfcGFyYW1zIDwtIHdvcmtmbG93cyAlPiUgDQogIGV4dHJhY3Rfd29ya2Zsb3coInBsc19nbG1uZXQiKSAlPiUgDQogIHBhcmFtZXRlcnMoKSAlPiUgDQogIHVwZGF0ZShwZW5hbHR5ID0gcGVuYWx0eSgpLA0KICAgICAgICAgbWl4dHVyZSA9IG1peHR1cmUoKSwNCiAgICAgICAgIHRocmVzaG9sZCA9IHRocmVzaG9sZChyYW5nZSA9IGMoMC4xLCAwLjAwNSkpLA0KICAgICAgICAgbnVtX2NvbXAgPSBudW1fY29tcChyYW5nZSA9IGMoNkwsMTFMKSkpDQoNCnBsc19mZGFfcGFyYW1zIDwtIHdvcmtmbG93cyAlPiUgDQogIGV4dHJhY3Rfd29ya2Zsb3coInBsc19mZGEiKSAlPiUgDQogIHBhcmFtZXRlcnMoKSAlPiUgDQogIHVwZGF0ZShwcm9kX2RlZ3JlZSA9IHByb2RfZGVncmVlKCksDQogICAgICAgICB0aHJlc2hvbGQgPSB0aHJlc2hvbGQocmFuZ2UgPSBjKDAuMSwgMC4wMDUpKSwNCiAgICAgICAgIG51bV9jb21wID0gbnVtX2NvbXAocmFuZ2UgPSBjKDZMLDExTCkpKQ0KDQpwbHNfcmRhX3BhcmFtcyA8LSB3b3JrZmxvd3MgJT4lIA0KICBleHRyYWN0X3dvcmtmbG93KCJwbHNfcmRhIikgJT4lIA0KICBwYXJhbWV0ZXJzKCkgJT4lIA0KICB1cGRhdGUoZnJhY19jb21tb25fY292ID0gZnJhY19jb21tb25fY292KCksDQogICAgICAgICBmcmFjX2lkZW50aXR5ID0gZnJhY19pZGVudGl0eSgpLA0KICAgICAgICAgdGhyZXNob2xkID0gdGhyZXNob2xkKHJhbmdlID0gYygwLjEsIDAuMDA1KSksDQogICAgICAgICBudW1fY29tcCA9IG51bV9jb21wKHJhbmdlID0gYyg2TCwxMUwpKSkNCg0Kd29ya2Zsb3dzIDwtIHdvcmtmbG93cyAlPiUgDQogIG9wdGlvbl9hZGQocGFyYW1faW5mbyA9IGZhY3Rvcl9nbG1uZXRfcGFyYW1zLCBpZCA9ICJmYWN0b3JfZ2xtbmV0IikgJT4lDQogIG9wdGlvbl9hZGQocGFyYW1faW5mbyA9IGZhY3Rvcl9mZGFfcGFyYW1zLCBpZCA9ICJmYWN0b3JfZmRhIikgJT4lIA0KICBvcHRpb25fYWRkKHBhcmFtX2luZm8gPSBmYWN0b3JfcmRhX3BhcmFtcywgaWQgPSAiZmFjdG9yX3JkYSIpICU+JSANCiAgb3B0aW9uX2FkZChwYXJhbV9pbmZvID0gcGxzX2dsbW5ldF9wYXJhbXMsIGlkID0gInBsc19nbG1uZXQiKSAlPiUNCiAgb3B0aW9uX2FkZChwYXJhbV9pbmZvID0gcGxzX2ZkYV9wYXJhbXMsIGlkID0gInBsc19mZGEiKSAlPiUgDQogIG9wdGlvbl9hZGQocGFyYW1faW5mbyA9IHBsc19yZGFfcGFyYW1zLCBpZCA9ICJwbHNfcmRhIikgDQoNCmBgYA0KDQpXZSB0aGVuIHR1bmUgZWFjaCBvZiB0aGUgbW9kZWxzLCBydW5uaW5nIGFjcm9zcyBhbGwgdmFsaWRhdGlvbiBmb2xkcyBmb3IgZXZlcnkgZ3JpZCBjb21iaW5hdGlvbi4gV2UgYXJlIHVzaW5nIGEgdGltZS1zYXZpbmcgdGVjaG5pcXVlIGhlcmUgd2l0aCBgdHVuZV9yYWNlX2Fub3ZhKClgLCB3aGVyZSBwb29yIHBlcmZvcm1pbmcgY2FuZGlkYXRlcyBhcmUgZHJvcHBlZC4gRm9yIDUtZm9sZCBjcm9zcyB2YWxpZGF0aW9uLCB0aGUgcmVkdWN0aW9uIGluIHRpbWUgaXMgYXMgbXVjaCBhcyA0MCUuIA0KDQpgYGB7ciBiYXNlbGluZSB3b3JrZmxvd3NldHMgdHVuaW5nLCBldmFsPUZBTFNFLCBpbmNsdWRlPVRSVUV9DQoNCmRlZmF1bHRfcmVzIDwtIHdvcmtmbG93cyAlPiUgDQogIHdvcmtmbG93X21hcCgNCiAgICBmbiA9ICJ0dW5lX3JhY2VfYW5vdmEiLA0KICAgIHZlcmJvc2UgPSBUUlVFLA0KICAgIHNlZWQgPSAyMDIxLA0KICAgIHJlc2FtcGxlcyA9IGNsYXNzX2ZvbGRzLA0KICAgIGdyaWQgPSAyMSwNCiAgICBjb250cm9sID0gY3RybCwNCiAgICBtZXRyaWNzID0gbWV0cmljX3NldChtbl9sb2dfbG9zcykNCiAgKQ0KDQpgYGANCg0KYGBge3IgYmFzZWxpbmUgd29ya2Zsb3dzZXRzIHR1bmluZyBub0luY2x1ZGUsIGluY2x1ZGU9RkFMU0V9DQppZiAoZmlsZS5leGlzdHMoaGVyZTo6aGVyZShkYXRhX2RpciwgImJhc2VsaW5lcy5yZHMiKSkpIHsgDQogIA0KZGVmYXVsdF9yZXMgPC0gcmVhZF9yZHMoaGVyZTo6aGVyZShkYXRhX2RpciwgImJhc2VsaW5lcy5yZHMiKSkNCg0KfSBlbHNlIHsNCg0KZGVmYXVsdF9yZXMgPC0gd29ya2Zsb3dzICU+JSANCiAgd29ya2Zsb3dfbWFwKA0KICAgIGZuID0gInR1bmVfcmFjZV9hbm92YSIsDQogICAgdmVyYm9zZSA9IFRSVUUsDQogICAgc2VlZCA9IDIwMjEsDQogICAgcmVzYW1wbGVzID0gY2xhc3NfZm9sZHMsDQogICAgZ3JpZCA9IDIxLA0KICAgIGNvbnRyb2wgPSBjdHJsLA0KICAgIG1ldHJpY3MgPSBtZXRyaWNfc2V0KG1uX2xvZ19sb3NzKQ0KICApDQoNCndyaXRlX3JkcyhkZWZhdWx0X3JlcywgaGVyZTo6aGVyZShkYXRhX2RpciwgImJhc2VsaW5lcy5yZHMiKSkNCg0KfQ0KDQpgYGANCg0KYGkgMSBvZiA2IHR1bmluZzogICAgIGZhY3Rvcl9nbG1uZXRgDQoNCmB2IDEgb2YgNiB0dW5pbmc6ICAgICBmYWN0b3JfZ2xtbmV0ICgxaCAxbSAxNS45cylgDQoNCmBpIDIgb2YgNiB0dW5pbmc6ICAgICBmYWN0b3JfZmRhYA0KDQpgdiAyIG9mIDYgdHVuaW5nOiAgICAgZmFjdG9yX2ZkYSAoNTdtIDU2LjdzKWANCg0KYGkgMyBvZiA2IHR1bmluZzogICAgIGZhY3Rvcl9yZGFgDQoNCmB2IDMgb2YgNiB0dW5pbmc6ICAgICBmYWN0b3JfcmRhICgxM20gMTMuOXMpYA0KDQpgaSA0IG9mIDYgdHVuaW5nOiAgICAgcGxzX2dsbW5ldGANCg0KYHYgNCBvZiA2IHR1bmluZzogICAgIHBsc19nbG1uZXQgKDltIDEwLjlzKWANCg0KYGkgNSBvZiA2IHR1bmluZzogICAgIHBsc19mZGFgDQoNCmB2IDUgb2YgNiB0dW5pbmc6ICAgICBwbHNfZmRhICgxMG0gMTguM3MpYA0KDQpgaSA2IG9mIDYgdHVuaW5nOiAgICAgcGxzX3JkYWANCg0KYHYgNiBvZiA2IHR1bmluZzogICAgIHBsc19yZGEgKDExbSA0OS4zcylgDQoNCmBgYHtyIGJhc2VsaW5lIHdvcmtmbG93c2V0cyBjb2xsZWN0X21ldHJpY3N9DQpyYW5raW5ncyA8LSANCiAgcmFua19yZXN1bHRzKGRlZmF1bHRfcmVzLCBzZWxlY3RfYmVzdCA9IFRSVUUpICU+JSANCiAgbXV0YXRlKG1ldGhvZCA9IG1hcF9jaHIod2Zsb3dfaWQsIH4gc3RyX3NwbGl0KC54LCAiXyIsIHNpbXBsaWZ5ID0gVFJVRSlbMV0pKSANCg0KZmlsdGVyKHJhbmtpbmdzLCByYW5rIDw9IDUpICU+JSBkcGx5cjo6c2VsZWN0KHJhbmssIG1lYW4sIG1vZGVsLCBtZXRob2QpDQoNCnJhbmtpbmdzICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gcmFuaywgeSA9IG1lYW4sIHBjaCA9IG1ldGhvZCwgY29sID0gbW9kZWwpKSArIA0KICBnZW9tX3BvaW50KGNleCA9IDMpICsgDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJyaWdodCIpICsNCiAgbGFicyh5ID0gIk1lYW4gTG9nIExvc3MiKQ0KYGBgDQoNClRoZSBiZXN0IG9mIHRoZSBiZXN0IHdlcmUgdGhlIHJlZ3VsYXJsaXplZCBgZ2xtYCB3aXRoIHRoZSBzaW1wbGUgZHVtbXkgcHJlcHJvY2Vzc29yLiBXZSBjYW4gdmlzdWFsaXplIHRoZSBiZW5lZml0IG9mIGB0dW5lX3JhY2VfYW5vdmEoKWAsIHdoZXJlIG9ubHkgdGhlIGJlc3QgY29tYmluYXRpb25zIHdlcmUgcnVuIHRocm91Z2ggdGhlIGVudGlyZSB0dW5pbmcgcm91dGluZS4NCg0KYGBge3IgYmFzZWxpbmUgd29ya2Zsb3dzZXRzIHBsb3RfcmFjZX0NCnBsb3RfcmFjZShkZWZhdWx0X3JlcyAlPiUgZXh0cmFjdF93b3JrZmxvd19zZXRfcmVzdWx0KCJmYWN0b3JfZ2xtbmV0IikpDQpgYGANCg0KTGV0J3MgZXh0cmFjdCB0aGUgaHlwZXJwYXJhbWV0ZXIgc2V0dGluZ3MgZm9yIHRoYXQgdG9wIG1vZGVsLg0KDQpgYGB7ciBiYXNlbGluZSB3b3JrZmxvd3NldHMgc2VsZWN0IGJlc3R9DQooYmVzdF9wYXJhbXMgPC0gZGVmYXVsdF9yZXMgJT4lIA0KICBleHRyYWN0X3dvcmtmbG93X3NldF9yZXN1bHQoImZhY3Rvcl9nbG1uZXQiKSAlPiUgDQogIHNlbGVjdF9iZXN0KCkpDQoNCmJlc3Rfd2YgPC0gZGVmYXVsdF9yZXMgJT4lIA0KICBleHRyYWN0X3dvcmtmbG93KCJmYWN0b3JfZ2xtbmV0IikgJT4lIA0KICBmaW5hbGl6ZV93b3JrZmxvdyhiZXN0X3BhcmFtcykNCg0KYGBgDQoNCkFuZCB0YWtlIGEgY2xvc2UgbG9vayBhdCB0aGUgY2xhc3NpZmljYXRpb24gcGVyZm9ybWFuY2Ugb2YgdGhhdCBiZXN0IGBnbG1uZXRgIG1vZGVsIG9uIHRoZSAxMCUgdGVzdGluZyBkYXRhIHRoYXQgd2FzIGhlbGQgb3V0Lg0KDQpgYGB7cn0NCmxhc3RGaXQgPC0gbGFzdF9maXQoYmVzdF93Ziwgc3BsaXQsIG1ldHJpY3MgPSBtZXRyaWNfc2V0KG1uX2xvZ19sb3NzKSkNCg0KY29sbGVjdF9tZXRyaWNzKGxhc3RGaXQpIA0KYGBgDQoNCk5vdGUgaGVyZSB0aGUgdGhlIG1lYW4gbG9nIGxvc3MgY2xhc3NpZmljYXRpb24gZXJyb3IgaXMgY29tcGFyYWJsZSB0byB0aGF0IG9mIHRoZSBjcm9zcyB2YWxpZGF0aW9uIGZpZ3VyZSBhYm92ZSwgc28gd2UgaGF2ZSBtb3JlIGNvbmZpZGVuY2UgdGhhdCB0aGUgbW9kZWwgd29ya3Mgb24gdW5zZWVuIGRhdGEuIExldCdzIG1vdmUgZm9yd2FyZCB0aGVuLg0KDQojIyBGaXQNCg0KUmVjYWxsIHRoYXQgb3VyIG9yaWdpbmFsIGRhdGFzZXQgd2FzIGltYmFsYW5jZWQuIFRoZXJlIGFyZSBmYXIgbW9yZSBub24tZGVmYXVsdHMgdGhhbiBkZWZhdWx0cy4gQmVjYXVzZSBvZiB0aGlzLCBkaWFsaW5nIGluIHRoZSBjdXRvZmYsIGJhbGFuY2luZyBiZXR3ZWVuIHNwZWNpZmljaXR5IGFuZCBzZW5zaXRpdml0eSwgd2FycmFudHMgbW9yZSBhdHRlbnRpb24uIExldCdzIGZpdCB0aGUgYmVzdCBtb2RlbCBvbiBhbGwgb2YgdGhlIHRyYWluaW5nIGRhdGEgKG9taXR0aW5nIHRoZSBjcm9zcyB2YWxpZGF0aW9uIGZvbGRzKS4NCg0KYGBge3IgZml0IHRoZSBnbG0gdG8gdHJhaW5pbmd9DQoNCmdsbV9jbGFzc2lmaWNhdGlvbl9maXQgPC0gZml0KGJlc3Rfd2YsIGRhdGEgPSB0cmFpbmluZykNCg0KYGBgDQoNCkxldCdzIHNpbXBseSBwcmVkaWN0IHRoZSBwcm9iYWJpbGl0aWVzIG9uIHRoZSBsYWJlbGVkIHRlc3RpbmcgZGF0YS4NCg0KYGBge3IgbGVuZGluZyB0ZXN0IHByZWRpY3Rpb259DQpsZW5kaW5nX3Rlc3RfcHJlZCA8LSBnbG1fY2xhc3NpZmljYXRpb25fZml0ICU+JQ0KICAgIHByZWRpY3QobmV3X2RhdGEgPSB0ZXN0aW5nLCB0eXBlID0gInByb2IiKSAlPiUgDQogICAgYmluZF9jb2xzKHRlc3RpbmcpDQpgYGANCg0KV2l0aCBvdXIgY2xhc3MgcHJvYmFiaWxpdGllcyBpbiBoYW5kLCB3ZSBjYW4gdXNlIGBtYWtlX3R3b19jbGFzc19wcmVkKClgIHRvIGNvbnZlcnQgdGhlc2UgcHJvYmFiaWxpdGllcyBpbnRvIGhhcmQgcHJlZGljdGlvbnMgdXNpbmcgYSB0aHJlc2hvbGQuIEEgdGhyZXNob2xkIG9mIDAuNSBqdXN0IHNheXMgdGhhdCBpZiB0aGUgcHJlZGljdGVkIHByb2JhYmlsaXR5IGlzIGFib3ZlIDAuNSwgdGhlbiBjbGFzc2lmeSB0aGlzIHByZWRpY3Rpb24gYXMgYSDigJxub25kZWZhdWx04oCdIGxvYW4sIG90aGVyd2lzZSwgZGVmYXVsdC4NCg0KYGBge3IgZmlmdHkgdGhyZXNob2xkfQ0KbGVuZGluZ190ZXN0X3ByZWQgJT4lDQogIG11dGF0ZSgucHJlZCA9IG1ha2VfdHdvX2NsYXNzX3ByZWQoLnByZWRfbm9uZGVmYXVsdCwgbGV2ZWxzKGRlZmF1bHRfZmxhZyksIHRocmVzaG9sZCA9IC41KSkgJT4lDQogIGRwbHlyOjpzZWxlY3QoZGVmYXVsdF9mbGFnLCBjb250YWlucygiLnByZWQiKSkgJT4lIA0KICBjb25mX21hdCh0cnV0aCA9IGRlZmF1bHRfZmxhZywgZXN0aW1hdGUgPSAucHJlZCkgJT4lIA0KICBhdXRvcGxvdCgpICsNCiAgbGFicyh0aXRsZSA9ICJHTE0gQ29uZnVzaW9uIE1hdHJpeCBhdCAwLjUgQ3V0b2ZmIikNCg0KYGBgDQoNCldpdGggYSAwLjUgdGhyZXNob2xkLCBhbG1vc3QgYWxsIG9mIHRoZSBsb2FucyB3ZXJlIHByZWRpY3RlZCBhcyDigJxub25kZWZhdWx04oCdLiBQZXJoYXBzIHRoaXMgaGFzIHNvbWV0aGluZyB0byBkbyB3aXRoIHRoZSBsYXJnZSBjbGFzcyBpbWJhbGFuY2UuIE9uIHRoZSBvdGhlciBoYW5kLCB0aGUgYmFuayBtaWdodCB3YW50IHRvIGJlIG1vcmUgc3RyaW5nZW50IHdpdGggd2hhdCBpcyBjbGFzc2lmaWVkIGFzIGEg4oCcZ29vZOKAnSBsb2FuLCBhbmQgbWlnaHQgcmVxdWlyZSBhIHByb2JhYmlsaXR5IG9mIDAuNzUgYXMgdGhlIHRocmVzaG9sZC4NCg0KYGBge3Igc2V2ZW50eSBmaXZlIHRocmVzaG9sZH0NCmxlbmRpbmdfdGVzdF9wcmVkICU+JQ0KICBtdXRhdGUoLnByZWQgPSBtYWtlX3R3b19jbGFzc19wcmVkKC5wcmVkX25vbmRlZmF1bHQsIGxldmVscyhkZWZhdWx0X2ZsYWcpLCB0aHJlc2hvbGQgPSAuNzUpKSAlPiUNCiAgZHBseXI6OnNlbGVjdChkZWZhdWx0X2ZsYWcsIGNvbnRhaW5zKCIucHJlZCIpKSAlPiUgDQogIGNvbmZfbWF0KHRydXRoID0gZGVmYXVsdF9mbGFnLCBlc3RpbWF0ZSA9IC5wcmVkKSAlPiUgDQogIGF1dG9wbG90KCkgKw0KICBsYWJzKHRpdGxlID0gIkdMTSBDb25mdXNpb24gTWF0cml4IGF0IDAuNzUgQ3V0b2ZmIikNCmBgYA0KDQpJbiB0aGlzIGNhc2UsIG1hbnkgdGhlIGJhZCBsb2FucyB3ZXJlIGNvcnJlY3RseSBjbGFzc2lmaWVkIGFzIGJhZCwgYnV0IG1vcmUgb2YgdGhlIGdvb2QgbG9hbnMgd2VyZSBhbHNvIG1pc2NsYXNzaWZpZWQgYXMgYmFkIG5vdy4gVGhlcmUgaXMgYSB0cmFkZW9mZiBoZXJlLCB3aGljaCBjYW4gYmUgc29tZXdoYXQgY2FwdHVyZWQgYnkgdGhlIG1ldHJpY3MgYHNlbnNpdGl2aXR5YCBhbmQgYHNwZWNpZmljaXR5YC4NCg0KSW4gdGhpcyBleGFtcGxlLCBhcyB3ZSBpbmNyZWFzZWQgc3BlY2lmaWNpdHksIHdlIGxvd2VyZWQgc2Vuc2l0aXZpdHkuIEl0IHdvdWxkIGJlIG5pY2UgdG8gaGF2ZSBzb21lIGNvbWJpbmF0aW9uIG9mIHRoZXNlIG1ldHJpY3MgdG8gcmVwcmVzZW50IHRoaXMgdHJhZGVvZmYuIEx1Y2tpbHksIGBqX2luZGV4YCBpcyBleGFjdGx5IHRoYXQuDQoNCk5vdywgdGhpcyBpcyBub3QgdGhlIG9ubHkgd2F5IHRvIG9wdGltaXplIHRoaW5ncy4gSWYgeW91IGNhcmUgYWJvdXQgbG93IGZhbHNlIHBvc2l0aXZlcywgeW91IG1pZ2h0IGJlIG1vcmUgaW50ZXJlc3RlZCBpbiBrZWVwaW5nIHNlbnNpdGl2aXR5IGhpZ2gsIGFuZCB0aGlzIHdvdWxkbuKAmXQgYmUgdGhlIGJlc3Qgd2F5IHRvIHRhY2tsZSB0aGlzIHByb2JsZW0uIFRoZXJlIGFyZSBvdGhlciB3YXlzIHRvIHdlaWdoIGVjb25vbWljIGNvbnNlcXVlbmNlcy4gRm9yIG5vdywgbGV04oCZcyBzZWUgaG93IHdlIGNhbiB1c2UgcHJvYmFibHkgdG8gb3B0aW1pemUgdGhlIGBqX2luZGV4YC4NCg0KV2l0aCBgZ2dwbG90MmAsIHdlIGNhbiBlYXNpbHkgdmlzdWFsaXplIHRoaXMgdmFyeWluZyBwZXJmb3JtYW5jZSB0byBmaW5kIG91ciBvcHRpbWFsIHRocmVzaG9sZCBmb3IgbWF4aW1pemluZyBgal9pbmRleGAuDQoNCmBgYHtyIGpfaW5kZXh9DQp0aHJlc2hvbGRfZGF0YSA8LSBsZW5kaW5nX3Rlc3RfcHJlZCAlPiUNCiAgdGhyZXNob2xkX3BlcmYoZGVmYXVsdF9mbGFnLCANCiAgICAgICAgICAgICAgICAgLnByZWRfbm9uZGVmYXVsdCwgDQogICAgICAgICAgICAgICAgIHRocmVzaG9sZHMgPSBzZXEoMC41LCAxLCBieSA9IDAuMDAyNSkpICU+JQ0KICBmaWx0ZXIoLm1ldHJpYyAhPSAiZGlzdGFuY2UiKSAlPiUNCiAgbXV0YXRlKGdyb3VwID0gY2FzZV93aGVuKA0KICAgIC5tZXRyaWMgPT0gInNlbnMiIHwgLm1ldHJpYyA9PSAic3BlYyIgfiAiMSIsDQogICAgVFJVRSB+ICIyIg0KICApKQ0KDQptYXhfal9pbmRleF90aHJlc2hvbGQgPC0gdGhyZXNob2xkX2RhdGEgJT4lDQogIGZpbHRlcigubWV0cmljID09ICJqX2luZGV4IikgJT4lDQogIGZpbHRlciguZXN0aW1hdGUgPT0gbWF4KC5lc3RpbWF0ZSkpICU+JQ0KICBwdWxsKC50aHJlc2hvbGQpDQoNCmdncGxvdCh0aHJlc2hvbGRfZGF0YSwNCiAgICAgICBhZXMoDQogICAgICAgICB4ID0gLnRocmVzaG9sZCwNCiAgICAgICAgIHkgPSAuZXN0aW1hdGUsDQogICAgICAgICBjb2xvciA9IC5tZXRyaWMsDQogICAgICAgICBhbHBoYSA9IGdyb3VwDQogICAgICAgKSkgKw0KICBnZW9tX2xpbmUoKSArDQogIHNjYWxlX2FscGhhX21hbnVhbCh2YWx1ZXMgPSBjKC43LCAxKSwgZ3VpZGUgPSAibm9uZSIpICsNCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gbWF4X2pfaW5kZXhfdGhyZXNob2xkLA0KICAgICAgICAgICAgIGFscGhhID0gLjYsIHNpemUgPSAyLA0KICAgICAgICAgICAgIGNvbG9yID0gImdyZXkzMCIpICsNCiAgbGFicygNCiAgICB4ID0gIidHb29kJyBUaHJlc2hvbGQgKGFib3ZlIHRoaXMgdmFsdWUgaXMgY29uc2lkZXJlZCAnbm9uLWRlZmF1bHQnKSIsDQogICAgeSA9ICJNZXRyaWMgRXN0aW1hdGUiLA0KICAgIHRpdGxlID0gIkJhbGFuY2luZyBwZXJmb3JtYW5jZSBieSB2YXJ5aW5nIHRoZSB0aHJlc2hvbGQiLA0KICAgIHN1YnRpdGxlID0gIlNlbnNpdGl2aXR5IG9yIHNwZWNpZmljaXR5IGFsb25lIG1pZ2h0IG5vdCBiZSBlbm91Z2ghXG5WZXJ0aWNhbCBsaW5lID0gTWF4IEotSW5kZXgiDQogICkNCmBgYA0KDQpGcm9tIHRoaXMgdmlzdWFsLCB0aGUgb3B0aW1hbCB0aHJlc2hvbGQgaXMgZXhhY3RseSBgciBtYXhfal9pbmRleF90aHJlc2hvbGRgLiBBcyBhbiBpbnRlcmVzdGluZyB2aWV3LCBsZXQncyBydW4gdGhhdCBHTE0gbW9kZWwgb24gYWxsIG9mIHRoZSBgdHJhaW5fZGZgIGZpbGUgd2l0aCB0aGUgbmV3IGN1dG9mZi4NCg0KYGBge3IgY29mdXNpb24gbWF0cml4fQ0KYXVnbWVudChnbG1fY2xhc3NpZmljYXRpb25fZml0LCB0cmFpbl9kZiwgdHlwZSA9ICJwcm9iIikgJT4lIA0KICAgbXV0YXRlKC5wcmVkID0gbWFrZV90d29fY2xhc3NfcHJlZCgucHJlZF9ub25kZWZhdWx0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHMoZGVmYXVsdF9mbGFnKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRocmVzaG9sZCA9IG1heF9qX2luZGV4X3RocmVzaG9sZCkpICU+JSANCiAgY29uZl9tYXQodHJ1dGggPSBkZWZhdWx0X2ZsYWcsIGVzdGltYXRlID0gLnByZWRfY2xhc3MpICU+JSANCiAgYXV0b3Bsb3QoKSArDQogIGxhYnModGl0bGUgPSAiR0xNIENvbmZ1c2lvbiBNYXRyaXggb24gRW50aXJlIFRyYWluaW5nIFNldCIsDQogICAgICAgc3VidGl0bGUgPSBnbHVlOjpnbHVlKCJ7IGF1Z21lbnQoZ2xtX2NsYXNzaWZpY2F0aW9uX2ZpdCwgdHJhaW5fZGYsIHR5cGUgPSAncHJvYicpICU+JSBtdXRhdGUoLnByZWQgPSBtYWtlX3R3b19jbGFzc19wcmVkKC5wcmVkX25vbmRlZmF1bHQsIGxldmVscyhkZWZhdWx0X2ZsYWcpLCB0aHJlc2hvbGQgPSBtYXhfal9pbmRleF90aHJlc2hvbGQpKSAlPiUgIGFjY3VyYWN5KHRydXRoID0gZGVmYXVsdF9mbGFnLCBlc3RpbWF0ZSA9IC5wcmVkX2NsYXNzKSAlPiUgcHVsbCguZXN0aW1hdGUpICU+JSByb3VuZCgyKSoxMDAgfSUgIiwgIkFjY3VyYWN5IHdpdGggcmVndWxhcml6ZWQgZ2VuZXJhbCBsaW5lYXIgbW9kZWwuXG5Pbmx5IDQgYWNjdXJhY3kgcG9pbnRzIGJldHRlciB0aGFuIHRoZSBOVUxMIG1vZGVsLiIpKQ0KDQpgYGANCg0KQWZ0ZXIgYWxsIG9mIHRoYXQgd29yaywgdGhlIHJlc3VsdHMgYXJlIGRpc2FwcG9pbnRpbmcsIGFzIHRoZSBhY2N1cmFjeSBpcyBub3QgbXVjaCBiZXR0ZXIgdGhhbiB0aGUgTlVMTCBtb2RlbC4NCg0KYGBge3J9DQpnbG1fY2xhc3NpZmljYXRpb25fZml0ICU+JSANCiAgZXh0cmFjdF9maXRfcGFyc25pcCgpICU+JSANCiAgdmlwOjp2aXAobWV0cmljID0gIm1uX2xvZ19sb3NzIiwgbnVtX2ZlYXR1cmVzID0gMjVMKSArDQogIGxhYnModGl0bGUgPSAiUmVndWxhcml6ZWQgR0xNIFZhcmlhYmxlIEltcG9ydGFuY2UgU2NvcmVzIikNCg0KYGBgDQoNCkxldCdzIHVzZSB0aGUgaW5zaWdodHMgZ2FpbmVkIGFib3ZlLCBpbmNsdWRpbmcgdGhlIHZhcmlhYmxlIGltcG9ydGFuY2Ugc2NvcmVzLCB0byBidWlsZCBhIGJldHRlciBib29zdGVkIG1vZGVsLg0KDQojIyBDYXRib29zdCBNb2RlbCBUdW5lDQoNCltDYXRib29zdF0oaHR0cHM6Ly9jYXRib29zdC5haS8pIGlzIGEgaGlnaC1wZXJmb3JtYW5jZSBvcGVuIHNvdXJjZSBsaWJyYXJ5IGZvciBncmFkaWVudCBib29zdGluZyBvbiBkZWNpc2lvbiB0cmVlcy4gVGhlIGFsZ29yaXRobSBoYXMgYWR2YW50YWdlcyBpbiBhdXRvbWF0aWNhbGx5IGhhbmRsaW5nIGRlZXAgY2F0ZWdvcmljYWwgYW5kIGhpZWFyYXJjaGllcyBvZiBjYXRlZ29yaWNhbCBmZWF0dXJlcyBhbmQgbGV2ZXJhZ2luZyB0aGUgcmVzaWR1YWxzIG9mIHRoZSB3ZWFrIGxlYXJuZXJzIHRvIGJvb3N0IGFkZGl0aW9uYWwgcm91bmRzIG9mIG1vZGVsIHRyYWluaW5nLiBUaGUgY2F0Ym9vc3Qgc3BlY2lmaWNhdGlvbiByZXF1aXJlcyBmaXZlIHR1bmluZyBwYXJhbWV0ZXJzLCBzbyBpdCBvZnRlbiB0YWtlcyBtb3JlIHRpbWUgdG8gZmluZCBvcHRpbWFsIGNvbWJpbmF0aW9ucy4NCg0KYGBge3IgY2F0Ym9vc3QgY2xhc3NpZmljYXRpb24gc3BlY30NCmNhdGJvb3N0X2NsYXNzaWZpY2F0aW9uX3NwZWMgPC0gYm9vc3RfdHJlZSgNCiAgdHJlZXMgPSB0dW5lKCksDQogIG10cnkgPSB0dW5lKCksDQogIG1pbl9uID0gdHVuZSgpLA0KICBsZWFybl9yYXRlID0gdHVuZSgpLA0KICB0cmVlX2RlcHRoID0gdHVuZSgpDQopICU+JSANCnNldF9lbmdpbmUoImNhdGJvb3N0IikgJT4lDQogIHNldF9tb2RlKCJjbGFzc2lmaWNhdGlvbiIpICU+JSANCiAgc3RlcF9ub3ZlbChhbGxfbm9taW5hbF9wcmVkaWN0b3JzKCkpDQoNCmBgYA0KDQpHaXZlbiB0aGUgdmFyaWFibGUgaW1wb3J0YW5jZSBzY29yZXMgYWJvdmUsIGxldCdzIGJ1aWxkIGEgbmV3IHByZS1wcm9jZXNzb3IgYHJlY2lwZSgpYC4NCg0KYGBge3J9DQoNCmNhdGJvb3N0X2NsYXNzX3JlYyA8LSByZWNpcGUoDQogIGRlZmF1bHRfZmxhZyB+IEdyQXBwdiArIFNCQV9BcHB2ICsgQXBwcm92YWxGWSArIEJhbmsgKyBCYW5rU3RhdGUgKw0KICAgIE5hbWUgKyBOQUlDUyArIE5BSUNTMiArIE5BSUNTMyArIE5BSUNTNCArIFVyYmFuUnVyYWwgKyBTdGF0ZSArIENpdHksDQogICAgZGF0YSA9IHRyYWluaW5nDQogICAgKSANCg0KYGBgDQoNCkxldCdzIHNldCBib3VuZGFyaWVzIGZvciB0aGUgaHlwZXItcGFyYW1ldGVyIHNlYXJjaCBhbmQgYnVpbGQgYSBncmlkIG9mIHBvc3NpYmxlIHZhbHVlcy4gDQoNCmBgYHtyIGNhdGJvb3N0IGNsYXNzaWZpY2F0aW9uIHdmfQ0KY2F0Ym9vc3RfY2xhc3NpZmljYXRpb25fd2YgPC0NCiAgd29ya2Zsb3coKSAlPiUNCiAgYWRkX3JlY2lwZShjYXRib29zdF9jbGFzc19yZWMpICU+JQ0KICBhZGRfbW9kZWwoY2F0Ym9vc3RfY2xhc3NpZmljYXRpb25fc3BlYykNCg0Kc2V0LnNlZWQoMjAyMSkNCg0KKHJhY2VfZ3JpZCA8LQ0KICAgZ3JpZF9sYXRpbl9oeXBlcmN1YmUoDQogICAgIGZpbmFsaXplKG10cnkoKSwgY2F0Ym9vc3RfY2xhc3NfcmVjICU+JSBwcmVwKCkgJT4lIGp1aWNlKCkpLA0KICAgICB0cmVlcyhyYW5nZSA9IGMoNTAwLDE1MDApKSwNCiAgICAgbGVhcm5fcmF0ZShyYW5nZSA9IGMoLTIsIC0xKSksDQogICAgIHRyZWVfZGVwdGgocmFuZ2UgPSBjKDNMLCAxMEwpKSwNCiAgICAgbWluX24ocmFuZ2UgPSBjKDEwTCwgNDBMKSksDQogICAgc2l6ZSA9IDUNCiAgICkpDQoNCmBgYA0KDQoNCmBgYHtyIGZpdCBjYXRib29zdCBjbGFzc2lmaWNhdGlvbiB0dW5lIGdyaWQsIGV2YWwgPSBGQUxTRX0NCmNhdGJvb3N0X2NsYXNzaWZpY2F0aW9uX3JzIDwtIHR1bmVfcmFjZV9hbm92YSgNCiAgIGNhdGJvb3N0X2NsYXNzaWZpY2F0aW9uX3dmLA0KICAgcmVzYW1wbGVzID0gY2xhc3NfZm9sZHMsDQogICBncmlkID0gcmFjZV9ncmlkLA0KICAgbWV0cmljcyA9IG1ldHJpY19zZXQobW5fbG9nX2xvc3MpLA0KICAgY29udHJvbCA9IGNvbnRyb2xfcmFjZSh2ZXJib3NlID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV9wcmVkID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV93b3JrZmxvdyA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGV4dHJhY3QgPSBleHRyYWN0X21vZGVsLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbGxlbF9vdmVyID0gInJlc2FtcGxlcyIpDQogICAgKQ0KDQogICAgICAgICANCmF1dG9wbG90KGNhdGJvb3N0X2NsYXNzaWZpY2F0aW9uX3JzKQ0KDQpjb2xsZWN0X21ldHJpY3MoY2F0Ym9vc3RfY2xhc3NpZmljYXRpb25fcnMpICU+JQ0KICBkcGx5cjo6c2VsZWN0KG10cnksIA0KICAgICAgICAgdHJlZXMsIA0KICAgICAgICAgbWluX24sIA0KICAgICAgICAgdHJlZV9kZXB0aCwgDQogICAgICAgICBsZWFybl9yYXRlLCANCiAgICAgICAgIG1uX2xvZ19sb3NzID0gbWVhbikgJT4lDQogIGFycmFuZ2UobW5fbG9nX2xvc3MpDQoNCmBgYA0KDQpgYGB7ciBmaXQgY2F0Ym9vc3QgY2xhc3NpZmljYXRpb24gdHVuZSBncmlkIG5vSW5jbHVkZSwgaW5jbHVkZSA9IEZBTFNFfQ0KDQppZiAoZmlsZS5leGlzdHMoaGVyZTo6aGVyZShkYXRhX2RpciwgImNsYXNzaWZpZXIucmRzIikpKSB7IA0KICANCmNhdGJvb3N0X2NsYXNzaWZpY2F0aW9uX3JzIDwtIHJlYWRfcmRzKGhlcmU6OmhlcmUoZGF0YV9kaXIsICJjbGFzc2lmaWVyLnJkcyIpKQ0KDQp9IGVsc2Ugew0KDQpjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9ycyA8LSB0dW5lX3JhY2VfYW5vdmEoDQogICBjYXRib29zdF9jbGFzc2lmaWNhdGlvbl93ZiwNCiAgIHJlc2FtcGxlcyA9IGNsYXNzX2ZvbGRzLA0KICAgZ3JpZCA9IHJhY2VfZ3JpZCwNCiAgIG1ldHJpY3MgPSBtZXRyaWNfc2V0KG1uX2xvZ19sb3NzKSwNCiAgIGNvbnRyb2wgPSBjb250cm9sX3JhY2UodmVyYm9zZSA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVfcHJlZCA9IFRSVUUsICAgIyB3aWxsIGl0IHdvcmsgYXMgd2VsbCB0byBtYWtlIEZBTFNFPw0KICAgICAgICAgICAgICAgICAgICAgICAgICBzYXZlX3dvcmtmbG93ID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgZXh0cmFjdCA9IGV4dHJhY3RfbW9kZWwsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFsbGVsX292ZXIgPSAicmVzYW1wbGVzIikNCiAgICApDQoNCndyaXRlX3JkcyhjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9ycywgaGVyZTo6aGVyZShkYXRhX2RpciwgImNsYXNzaWZpZXIucmRzIikpDQoNCn0NCg0KYXV0b3Bsb3QoY2F0Ym9vc3RfY2xhc3NpZmljYXRpb25fcnMpDQoNCmNvbGxlY3RfbWV0cmljcyhjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9ycykgJT4lIA0KICBkcGx5cjo6c2VsZWN0KG10cnksIHRyZWVzLCBtaW5fbiwgdHJlZV9kZXB0aCwgbGVhcm5fcmF0ZSwgbW5fbG9nX2xvc3MgPSBtZWFuKSAlPiUgDQogIGFycmFuZ2UobW5fbG9nX2xvc3MpDQoNCmBgYA0KDQpBZnRlciBtb2RlbGluZyBvbiA1IHJlc2FtcGxlIHNldHMgZm9yIGV2ZXJ5IGNvbWJpbmF0aW9uIG9mIHBhcmFtZXRlcnMsIHdlIGNvbGxlY3QgbWV0cmljcyBvbiB0aGUgbW9kZWwncyBwZXJmb3JtYW5jZSBvbiB0aGUgc2FtcGxlcyBvZiBkYXRhIGhlbGQgb3V0IGluIGNyb3NzIHZhbGlkYXRpb24uICBUaGlzIGlzIGEgc3Vic3RhbnRpYWwgaW1wcm92ZW1lbnQuDQoNCiMjIFBlcmZvcm1hbmNlIENoZWNrcyB7LmFjdGl2ZX0NCg0KYGBge3IgY2F0Ym9vc3QgY2xhc3NpZmljYXRpb24gcGVyZm9ybWFuY2UgY2hlY2t9DQpjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9iZXN0X3dmIDwtICAgDQogIGNhdGJvb3N0X2NsYXNzaWZpY2F0aW9uX3dmICU+JSANCiAgZmluYWxpemVfd29ya2Zsb3coc2VsZWN0X2Jlc3QoY2F0Ym9vc3RfY2xhc3NpZmljYXRpb25fcnMpKQ0KDQpjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9sYXN0X2ZpdCA8LSANCiAgICAgIGNhdGJvb3N0X2NsYXNzaWZpY2F0aW9uX2Jlc3Rfd2YgJT4lIA0KICAgICAgbGFzdF9maXQoc3BsaXQpDQoNCmNvbGxlY3RfbWV0cmljcyhjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9sYXN0X2ZpdCkNCg0KYGBgDQoNClRoZSBhY2N1cmFjeSBhbmQgYXJlYSB1bmRlciB0aGUgcm9jIGN1cnZlIGFyZSBtb3ZpbmcgaW4gdGhlIHJpZ2h0IGRpcmVjdGlvbiwgYnV0IDgxJSBpcyBub3QgbXVjaCBiZXR0ZXIgdGhhbiB0aGUgODAlIHRoYXQgd2UgYWNoaWV2ZWQgd2l0aCB0aGUgR0xNIFBMUyBtb2RlbCBhYm92ZS4gDQoNCmBgYHtyIGNsYXNzaWZpY2F0aW9uIGNvbmZ1c2lvbiBtYXRyaXggb24gZm9sZCBob2xkb3V0c30NCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9sYXN0X2ZpdCkgJT4lDQogIGNvbmZfbWF0KGRlZmF1bHRfZmxhZywgLnByZWRfY2xhc3MpICU+JSANCiAgYXV0b3Bsb3QoKSArDQogIGxhYnModGl0bGUgPSAiQ29uZnVzaW9uIE1hdHJpeCBvbiBSZXNhbXBsZXMgSGVsZCBPdXQiKQ0KDQpgYGANCg0KSGVyZSBhZ2FpbiwgdGhlIGFsZ29yaXRobSBpcyBwcmVkaWN0aW5nICJub25kZWZhdWx0IiB3aGVuIGl0IHNob3VsZCBiZSBmaW5kaW5nIGRlZmF1bHRzLiBMZXQncyBwcmVzcyBhaGVhZCBleHBsb3JlIGEgbW9yZSBzdWl0YWJsZSBqX2luZGV4Lg0KDQpgYGB7ciBjYXRib29zdCBqX2luZGV4fQ0KY2xhc3NpZmljYXRpb25fZml0IDwtIGZpdChjYXRib29zdF9jbGFzc2lmaWNhdGlvbl9iZXN0X3dmLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluaW5nKQ0KDQpsZW5kaW5nX3Rlc3RfcHJlZCA8LSBjbGFzc2lmaWNhdGlvbl9maXQgJT4lDQogICAgcHJlZGljdChuZXdfZGF0YSA9IHRlc3RpbmcsIHR5cGUgPSAicHJvYiIpICU+JSANCiAgICBiaW5kX2NvbHModGVzdGluZykNCg0KdGhyZXNob2xkX2RhdGEgPC0gbGVuZGluZ190ZXN0X3ByZWQgJT4lDQogIHRocmVzaG9sZF9wZXJmKGRlZmF1bHRfZmxhZywgDQogICAgICAgICAgICAgICAgIC5wcmVkX25vbmRlZmF1bHQsIA0KICAgICAgICAgICAgICAgICB0aHJlc2hvbGRzID0gc2VxKDAuNSwgMSwgYnkgPSAwLjAwMjUpKSAlPiUNCiAgZmlsdGVyKC5tZXRyaWMgIT0gImRpc3RhbmNlIikgJT4lDQogIG11dGF0ZShncm91cCA9IGNhc2Vfd2hlbigNCiAgICAubWV0cmljID09ICJzZW5zIiB8IC5tZXRyaWMgPT0gInNwZWMiIH4gIjEiLA0KICAgIFRSVUUgfiAiMiINCiAgKSkNCg0KbWF4X2pfaW5kZXhfdGhyZXNob2xkIDwtIHRocmVzaG9sZF9kYXRhICU+JQ0KICBmaWx0ZXIoLm1ldHJpYyA9PSAial9pbmRleCIpICU+JQ0KICBmaWx0ZXIoLmVzdGltYXRlID09IG1heCguZXN0aW1hdGUpKSAlPiUNCiAgcHVsbCgudGhyZXNob2xkKQ0KDQpnZ3Bsb3QodGhyZXNob2xkX2RhdGEsDQogICAgICAgYWVzKA0KICAgICAgICAgeCA9IC50aHJlc2hvbGQsDQogICAgICAgICB5ID0gLmVzdGltYXRlLA0KICAgICAgICAgY29sb3IgPSAubWV0cmljLA0KICAgICAgICAgYWxwaGEgPSBncm91cA0KICAgICAgICkpICsNCiAgZ2VvbV9saW5lKCkgKw0KICBzY2FsZV9jb2xvcl92aXJpZGlzX2QoZW5kID0gMC45KSArDQogIHNjYWxlX2FscGhhX21hbnVhbCh2YWx1ZXMgPSBjKC40LCAxKSwgZ3VpZGUgPSAibm9uZSIpICsNCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gbWF4X2pfaW5kZXhfdGhyZXNob2xkLA0KICAgICAgICAgICAgIGFscGhhID0gLjYsDQogICAgICAgICAgICAgY29sb3IgPSAiZ3JleTMwIikgKw0KICBsYWJzKA0KICAgIHggPSAiJ0dvb2QnIFRocmVzaG9sZFxuKGFib3ZlIHRoaXMgdmFsdWUgaXMgY29uc2lkZXJlZCAnZ29vZCcpIiwNCiAgICB5ID0gIk1ldHJpYyBFc3RpbWF0ZSIsDQogICAgdGl0bGUgPSAiQmFsYW5jaW5nIHBlcmZvcm1hbmNlIGJ5IHZhcnlpbmcgdGhlIHRocmVzaG9sZCIsDQogICAgc3VidGl0bGUgPSAiU2Vuc2l0aXZpdHkgb3Igc3BlY2lmaWNpdHkgYWxvbmUgbWlnaHQgbm90IGJlIGVub3VnaCFcblZlcnRpY2FsIGxpbmUgPSBNYXggSi1JbmRleCINCiAgKQ0KYGBgDQoNCkZyb20gdGhpcyB2aXN1YWwsIHRoZSBvcHRpbWFsIHRocmVzaG9sZCBpcyBleGFjdGx5IGByIG1heF9qX2luZGV4X3RocmVzaG9sZGAuIEFzIGFuIGludGVyZXN0aW5nIHZpZXcsIGxldCdzIHJ1biB0aGF0IEdMTSBtb2RlbCBvbiBhbGwgb2YgdGhlIGB0cmFpbl9kZmAgZmlsZSB3aXRoIHRoZSBuZXcgY3V0b2ZmLg0KDQpgYGB7ciBjYXRib29zdCBjbGFzc2lmaWVyIGNvbmZ1c2lvbiBtYXRyaXh9DQphdWdtZW50KGNsYXNzaWZpY2F0aW9uX2ZpdCwgdHJhaW5fZGYsIHR5cGUgPSAicHJvYiIpICU+JSANCiAgIG11dGF0ZSgucHJlZCA9IG1ha2VfdHdvX2NsYXNzX3ByZWQoLnByZWRfbm9uZGVmYXVsdCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzKGRlZmF1bHRfZmxhZyksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aHJlc2hvbGQgPSBtYXhfal9pbmRleF90aHJlc2hvbGQpKSAlPiUgDQogIGNvbmZfbWF0KHRydXRoID0gZGVmYXVsdF9mbGFnLCBlc3RpbWF0ZSA9IC5wcmVkX2NsYXNzKSAlPiUgDQogIGF1dG9wbG90KCkgKw0KICBsYWJzKHRpdGxlID0gIkNhdGJvb3N0IENvbmZ1c2lvbiBNYXRyaXggb24gRW50aXJlIFRyYWluaW5nIFNldCIsDQogICAgICAgc3VidGl0bGUgPSBnbHVlOjpnbHVlKCJ7IGF1Z21lbnQoY2xhc3NpZmljYXRpb25fZml0LCB0cmFpbl9kZiwgdHlwZSA9ICdwcm9iJykgJT4lIG11dGF0ZSgucHJlZCA9IG1ha2VfdHdvX2NsYXNzX3ByZWQoLnByZWRfbm9uZGVmYXVsdCwgbGV2ZWxzKGRlZmF1bHRfZmxhZyksIHRocmVzaG9sZCA9IG1heF9qX2luZGV4X3RocmVzaG9sZCkpICU+JSAgYWNjdXJhY3kodHJ1dGggPSBkZWZhdWx0X2ZsYWcsIGVzdGltYXRlID0gLnByZWRfY2xhc3MpICU+JSBwdWxsKC5lc3RpbWF0ZSkgJT4lIHJvdW5kKDIpKjEwMCB9JSAiLCAiQWNjdXJhY3kgd2l0aCBjYXRib29zdCBjbGFzc2lmaWNhdGlvbiBtb2RlbC5cbk11Y2ggYmV0dGVyIHRoYW4gdGhlIE5VTEwgbW9kZWwuIikpDQpgYGANCg0KR29vZCBlbm91Z2ggdG8gbW92ZSBmb3J3YXJkLiBMb2FucyBwcmVkaWN0ZWQgaW4gdGhlIGNsYXNzaWZpZXIgdG8gZGVmYXVsdCB3aWxsIGJlIHB1c2hlZCB0aHJvdWdoIHRoZSByZWdyZXNzaW9uIG1vZGVsIGJlbG93LiANCg0KIyMgVmFyaWFibGUgSW1wb3J0YW5jZQ0KDQpMZXQncyBsb29rIGF0IHRoZSBmZWF0dXJlcyB0aGF0IGNvbnRyaWJ1dGVkIHRoZSBtb3N0IHRvIHRoaXMgY2xhc3NpZmljYXRpb24gbW9kZWwuIA0KDQpgYGB7ciBjbGFzc2lmaWVyIHZhcmlhYmxlIGltcG9ydGFuY2V9DQoNCmNsYXNzaWZpY2F0aW9uX2ZpdCAlPiUgDQogIGV4dHJhY3RfZml0X2VuZ2luZSgpICU+JSANCiAgY2F0Ym9vc3QuZ2V0X2ZlYXR1cmVfaW1wb3J0YW5jZShwb29sID0gTlVMTCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICdGZWF0dXJlSW1wb3J0YW5jZScsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhyZWFkX2NvdW50ID0gLTEpICU+JSANCiAgYXNfdGliYmxlKHJvd25hbWVzID0gIkZlYXR1cmUiKSAlPiUgDQogIG11dGF0ZShGZWF0dXJlID0gZmN0X3Jlb3JkZXIoRmVhdHVyZSwgVjEpKSAlPiUgDQogIGdncGxvdChhZXMoVjEsIEZlYXR1cmUpKSArDQogICAgZ2VvbV9jb2woKSArDQogIGxhYnModGl0bGUgPSAiQ2F0Ym9vc3QgU0JBIERlZmF1bHQgQ2xhc3NpZmljYXRpb24gRmVhdHVyZSBJbXBvcnRhbmNlIiwNCiAgICAgICB5ID0gTlVMTCwgeCA9IE5VTEwpDQoNCmBgYA0KDQpUaGUgaW50ZXJhY3Rpb24gZmVhdHVyZSB0aGF0IGJ1aWx0IGJ5IGJpbmRpbmcgdGhlIGJhbmsgdG8gdGhlIGFwcHJvdmFsIHllYXIgZW5kZWQgdXAgYXMgdGhlIG1vc3QgaW1wb3J0YW50IGZlYXR1cmUsIGFuZCB0aGUgc2Vjb25kIG1vc3QgaW1wb3J0YW50IGlzIHRoZSByYXRpb24gYmV0d2VlbiBTQkFfQXBwcm92YWwgdmFsdWUgYW5kIHRoZSBHcm9zcyBMb2FuIHZhbHVlLiANCg0KIyB7LX0NCg0KLS0tLQ0KDQojIE1hY2hpbmUgTGVhcm5pbmc6IFJlZ3Jlc3Npb24gey50YWJzZXQgLnRhYnNldC1waWxsc30NCg0KYHIgZW1vamlmb250OjplbW9qaSgicG9pbnRfcmlnaHQiKWAgIEJ1aWxkIGEgbm9uLU1MIGJhc2VsaW5lIGZpcnN0Lg0KDQpSZWNhbGwgdGhhdCBvdXIgZ29hbCBpcyB0byBwcmVkaWN0IHRoZSBgZGVmYXVsdF9hbW91bnRgLiBGb3Igb3VyIGBOVUxMYCBtb2RlbCwgaWYgd2UgYXBwbHkgb3VyIGNsYXNzaWZpZXIgYWJvdmUsIGFuZCB0aGVuIHNpbXBseSB0YWtlIHRoZSBtZWFuIG9mIGFsbCBgZGVmYXVsdF9hbW91bnRzYCwgdGhlIE1lYW4gQXZlcmFnZSBFcnJvciBpczoNCg0KYGBge3J9DQoNCmF1Z21lbnQoY2xhc3NpZmljYXRpb25fZml0LCB0cmFpbl9kZikgJT4lIA0KICBtdXRhdGUoLnByZWRfZGVmYXVsdF9hbW91bnQgPSBpZl9lbHNlKGRlZmF1bHRfZmxhZyA9PSAiZGVmYXVsdCIsIGRlZmF1bHRfYW1vdW50LDApKSAlPiUgICANCiAgZmlsdGVyKC5wcmVkX2NsYXNzID09ICJkZWZhdWx0IikgJT4lIA0KICBtdXRhdGUoLnByZWRfbWVhbiA9IG1lYW4oLnByZWRfZGVmYXVsdF9hbW91bnQpKSAlPiUgDQogIG1hZSh0cnV0aCA9IGRlZmF1bHRfYW1vdW50LCBlc3RpbWF0ZSA9IC5wcmVkX21lYW4pIA0KDQpgYGANCg0KQXMgYW4gYXNpZGU6IHRoZSBgdHJhaW5fZGZgIHdlIHVzZWQgYWJvdmUgY29udGFpbnMgNTEgbG9hbnMgd2hlcmUgdGhlIGBkZWZhdWx0X2Ftb3VudGAgd2FzIGdyZWF0ZXIgdGhhbiB0aGUgTG9hbiBBcHByb3ZhbCBhbW91bnQuIE1vc3Qgb2YgdGhlbSBhcmUgc21hbGwgdmFsdWUgbG9hbnMsIGZvciB3aGljaCB0aGVyZSBhcmUgdGhvdXNhbmRzIG9mIG90aGVycyBpbiB0aGUgZGF0YXNldC4gDQoNCiMjIEVEQQ0KDQpMZXQncyBtYWtlIG1vcmUgZXhwbG9yYXRvcnkgcGxvdHMgdG8gbGVhcm4gd2hhdCBmZWF0dXJlcyBtaWdodCB5aWVsZCBnb29kIG51bWVyaWMgcHJlZGljdGlvbnMuDQoNCmBgYHtyIHJlZ3Jlc3Npb24gbnVtZXJpYyBlZGEsIG9wdHMuTGFiZWw9J2ZpZy5sb25nJ30NCnRyYWluX2RmX3JlZ3Jlc3Npb24gPC0gYXVnbWVudChjbGFzc2lmaWNhdGlvbl9maXQsIHRyYWluX2RmKSAlPiUgDQogICAgZmlsdGVyKC5wcmVkX2NsYXNzID09ICJkZWZhdWx0IikNCg0Kc3VtbWFyaXplX2RlZmF1bHRfYW1vdW50cyA8LSBmdW5jdGlvbih0Ymwpew0KICB0YmwgJT4lIA0KICBzdW1tYXJpemUobl9sb2FucyA9IG4oKSwNCiAgICAgICAgICAgIG1lYW5fZGVmYXVsdCA9IG1lYW4oZGVmYXVsdF9hbW91bnQpLA0KICAgICAgICAgICAgdG90YWxfZ3JfYXBwdiA9IHN1bShHckFwcHYpLA0KICAgICAgICAgICAgdG90YWxfZGVmYXVsdF9hbW91bnQgPSBzdW0oZGVmYXVsdF9hbW91bnQgKSwNCiAgICAgICAgICAgIHBjdF9kZWZhdWx0X2Ftb3VudCA9IHRvdGFsX2RlZmF1bHRfYW1vdW50IC8gdG90YWxfZ3JfYXBwdiwNCiAgICAgICAgICAgIC5ncm91cHMgPSAiZHJvcCIpICU+JSANCiAgYXJyYW5nZShkZXNjKG5fbG9hbnMpKQ0KfQ0KDQpwbG90X2NhdGVnb3J5X2Ftb3VudHMgPC0gZnVuY3Rpb24odGJsLCBjYXRlZ29yeSwgbl9jYXRlZ29yaWVzID0gNyl7DQogIHRibCAlPiUgDQogIGdyb3VwX2J5KHt7IGNhdGVnb3J5IH19IDo9IHdpdGhmcmVxKGZjdF9sdW1wKHt7IGNhdGVnb3J5IH19LCBuX2NhdGVnb3JpZXMpKSkgJT4lIA0KICBzdW1tYXJpemVfZGVmYXVsdF9hbW91bnRzKCkgJT4lIA0KICBtdXRhdGUoe3sgY2F0ZWdvcnkgfX0gOj0gZmN0X3Jlb3JkZXIoe3sgY2F0ZWdvcnkgfX0sIG1lYW5fZGVmYXVsdCkpICU+JSANCiAgZ2dwbG90KGFlcyhtZWFuX2RlZmF1bHQsIHt7IGNhdGVnb3J5IH19KSkgKw0KICBnZW9tX3BvaW50KGFlcyhjb2xvciA9IHBjdF9kZWZhdWx0X2Ftb3VudCwNCiAgICAgICAgICAgICAgICAgc2l6ZSA9IHBjdF9kZWZhdWx0X2Ftb3VudCkpICsNCiAgZ2VvbV90ZXh0KGFlcyh4ID0gbWVhbl9kZWZhdWx0IC0gNjAwMCwgDQogICAgICAgICAgICAgICAgbGFiZWwgPSBnbHVlOjpnbHVlKCIkIHtyb3VuZCh0b3RhbF9kZWZhdWx0X2Ftb3VudC8xMF42LDApfSBNIikpLA0KICAgICAgICAgICAgaGp1c3QgPSAxKSArDQogICAgc2NhbGVfY29sb3JfdmlyaWRpc19iKA0KICAgICAgbmFtZSA9ICIlIG9mIExvYW4gQW1vdW50XG5pbiBEZWZhdWx0IiwNCiAgICAgIGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSwNCiAgICAgIGJyZWFrcyA9IHNlcSgwLDEsMC4xKSwNCiAgICAgIG9wdGlvbiA9ICJIIg0KICAgICkgKw0KICAgIHNjYWxlX3NpemVfYmlubmVkKA0KICAgICAgbmFtZSA9ICIlIG9mIExvYW5cbmluIERlZmF1bHQiLA0KICAgICAgbGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpLA0KICAgICAgYnJlYWtzID0gc2VxKDAsMSwwLjEpLA0KICAgICAgZ3VpZGUgPSAibm9uZSINCiAgICApICsNCiAgICBndWlkZXMoY29sb3IgPSBndWlkZV9iaW5zKCkpICsNCiAgICBzY2FsZV94X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpkb2xsYXJfZm9ybWF0KCkpICsNCiAgICBleHBhbmRfbGltaXRzKHggPSAwLCBzaXplID0gMCkgKw0KICAgIGxhYnMoeCA9ICJNZWFuIFZhbHVlIG9mIHRoZSBEZWZhdWx0cyIsIHkgPSBOVUxMLA0KICAgICAgICAgdGl0bGUgPSAiVVMgU21hbGwgQnVzaW5lc3MgQWRtaW5pc3RyYXRpb24gTG9hbnMgdGhhdCBkZWZhdWx0ZWQgMjAwMy0yMDEwIiwNCiAgICAgICAgIHN1YnRpdGxlID0gIiMgb2YgbG9hbnMgaW4gcGFyZW50aGVzaXMgYW5kIFRvdGFsIERlZmF1bHRlZCAkIG9uIExhYmVscyIpDQogIA0KfQ0KDQoNCnRyYWluX2RmX3JlZ3Jlc3Npb24gJT4lIA0KICBwbG90X2NhdGVnb3J5X2Ftb3VudHMoU3RhdGUpICsNCiAgIGxhYnModGl0bGUgPSAiV2hlbiB0aGV5IGRvIGRlZmF1bHQsIEFyaXpvbmEgaGFzIHRoZSBoaWdoZXN0IG1lYW4gRGVmYXVsdCBWYWx1ZSIpDQoNCnRyYWluX2RmX3JlZ3Jlc3Npb24gJT4lIA0KICBwbG90X2NhdGVnb3J5X2Ftb3VudHMoQ2l0eSkgKw0KICAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgUGhvZW5peCBoYXMgdGhlIGhpZ2hlc3QgbWVhbiBEZWZhdWx0IFZhbHVlIikNCg0KdHJhaW5fZGZfcmVncmVzc2lvbiAlPiUgDQogIHBsb3RfY2F0ZWdvcnlfYW1vdW50cyhTZWN0b3IpICsNCiAgIGxhYnModGl0bGUgPSAiV2hlbiB0aGV5IGRvIGRlZmF1bHQsIEFjY29tb2RhdGlvbiBoYXZlIHRoZSBoaWdoZXN0IG1lYW4gRGVmYXVsdCBWYWx1ZSIpDQoNCnRyYWluX2RmX3JlZ3Jlc3Npb24gJT4lIA0KICBwbG90X2NhdGVnb3J5X2Ftb3VudHMoQmFuaykgKw0KICAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgUmVhZHlDYXAgTGVuZGluZyBoYXMgdGhlIGhpZ2hlc3QgbWVhbiBEZWZhdWx0IFZhbHVlIikNCg0KdHJhaW5fZGZfcmVncmVzc2lvbiAlPiUgDQogIHBsb3RfY2F0ZWdvcnlfYW1vdW50cyhOZXdFeGlzdCkgKw0KICAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgVW5rbm93biBoYXZlIHRoZSBoaWdoZXN0IG1lYW4gRGVmYXVsdCBWYWx1ZSIpDQoNCnRyYWluX2RmX3JlZ3Jlc3Npb24gJT4lIA0KICBwbG90X2NhdGVnb3J5X2Ftb3VudHMoRnJhbmNoaXNlQ29kZSkgKw0KICAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgRnJhbmNoaXNlcyBoYXZlIG1lYW4gRGVmYXVsdCBWYWx1ZSIpDQoNCnRyYWluX2RmX3JlZ3Jlc3Npb24gJT4lIA0KICBwbG90X2NhdGVnb3J5X2Ftb3VudHMoTkFJQ1MyKSArDQogICBsYWJzKHRpdGxlID0gIldoZW4gdGhleSBkbyBkZWZhdWx0LCBOQUlDUyBncm91cCA3MiBoYXZlIG1lYW4gRGVmYXVsdCBWYWx1ZSIpDQoNCnRyYWluX2RmX3JlZ3Jlc3Npb24gJT4lIA0KICBwbG90X2NhdGVnb3J5X2Ftb3VudHMoTkFJQ1MzKSArDQogICBsYWJzKHRpdGxlID0gIldoZW4gdGhleSBkbyBkZWZhdWx0LCBOQUlDUyBncm91cCA3MjIgaGF2ZSBtZWFuIERlZmF1bHQgVmFsdWUiKQ0KDQp0cmFpbl9kZl9yZWdyZXNzaW9uICU+JSANCiAgcGxvdF9jYXRlZ29yeV9hbW91bnRzKE5BSUNTNCkgKw0KICAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgTkFJQ1MgZ3JvdXAgNzIyMiBoYXZlIG1lYW4gRGVmYXVsdCBWYWx1ZSIpDQogIA0KdHJhaW5fZGZfcmVncmVzc2lvbiAlPiUgDQogIHBsb3RfY2F0ZWdvcnlfYW1vdW50cyhOQUlDUykgKw0KICAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgTkFJQ1MgZ3JvdXAgNzIyMjExIGhhdmUgbWVhbiBEZWZhdWx0IFZhbHVlIikNCg0KdHJhaW5fZGZfcmVncmVzc2lvbiAlPiUgDQogIHBsb3RfY2F0ZWdvcnlfYW1vdW50cyhVcmJhblJ1cmFsKSArDQogICBsYWJzKHRpdGxlID0gIldoZW4gdGhleSBkbyBkZWZhdWx0LCBVbmtub3duIGJ1c2luZXNzZXMgaGF2ZSB0aGUgaGlnaGVzdCBtZWFuIERlZmF1bHQgVmFsdWUiKQ0KICANCnRyYWluX2RmX3JlZ3Jlc3Npb24gJT4lIA0KICBwbG90X2NhdGVnb3J5X2Ftb3VudHMoTmFtZSkgKw0KICAgbGFicyh0aXRsZSA9ICJXaGVuIHRoZXkgZG8gZGVmYXVsdCwgRG9taW5vJ3MgUGl6emEgaGF2ZSB0aGUgaGlnaGVzdCBtZWFuIERlZmF1bHQgVmFsdWUiKQ0KDQp0cmFpbl9kZl9yZWdyZXNzaW9uICU+JSANCiAgICBzZWxlY3RfYXQoYWxsX29mKHRyYWluX251bWVyaWMpKSAlPiUNCiAgICBjb3IoKSAlPiUgDQogICAgaGVhdG1hcChtYWluID0gIk51bWVyaWMgQ29ycmVsYXRpb25zIikNCmBgYA0KDQpBcyB3aXRoIHRoZSBjbGFzc2lmaWNhdGlvbiwgdGhlIHJlZ3Jlc3Npb24gd2lsbCByZWx5IGhlYXZpbHkgb24gY2F0ZWdvcmljYWwgZmVhdHVyZXMsIGFuZCBzb21lIG9mIHRoZSBudW1lcmljIGZlYXR1cmVzIGFyZSBoaWdobHkgY29ycmVsYXRlZCB3aXRoIG9uZSBhbm90aGVyLg0KDQpXZSB3aWxsIHVzZSA1LWZvbGQgY3Jvc3MgdmFsaWRhdGlvbiBhbmQgc3RyYXRpZnkgb24gdGhlIGBkZWZhdWx0X2Ftb3VudGAgdGhpcyB0aW1lLiAgDQoNCmBgYHtyIHJlZ3Jlc3Npb24gY3Jvc3MgdmFsaWRhdGlvbn0NCnNldC5zZWVkKDIwMjEpDQoNCnJlZ3Jlc3Npb25fc3BsaXQgPC0gaW5pdGlhbF9zcGxpdChhdWdtZW50KGNsYXNzaWZpY2F0aW9uX2ZpdCwgdHJhaW5fZGYpICU+JSANCiAgICAgICAgICAgICAgICAgICAgICAgICBmaWx0ZXIoLnByZWRfY2xhc3MgPT0gImRlZmF1bHQiKSAsIA0KICAgICAgICAgICAgICAgICAgICAgICBwcm9wID0gMC45KQ0KDQpyZWdyZXNzaW9uX3RyYWluaW5nIDwtIHRyYWluaW5nKHNwbGl0KQ0KcmVncmVzc2lvbl90ZXN0aW5nIDwtIHRlc3Rpbmcoc3BsaXQpDQoNCihyZWdyZXNzaW9uX2ZvbGRzIDwtDQogICAgdmZvbGRfY3YoDQogICAgICByZWdyZXNzaW9uX3RyYWluaW5nLA0KICAgICAgdiA9IDUsDQogICAgICBzdHJhdGEgPSBkZWZhdWx0X2Ftb3VudA0KICAgICkpDQoNCmBgYA0KIyMgVGhlIFJlY2lwZQ0KDQpGb3IgdGhlIHJlZ3Jlc3Npb24sIG91ciBvdXRjb21lIHZhcmlhYmxlIGlzIHRoZSBgZGVmYXVsdF9hbW91bnRgLiBBbGwgb3RoZXIgZmVhdHVyZXMgYXJlIHRoZSBzYW1lIGFzIGFib3ZlLg0KDQpgYGB7ciByZWdyZXNzaW9uIHJlY2lwZX0NCnJlZ3Jlc3Npb25fcmVjIDwtIHJlY2lwZSgNCiAgICBkZWZhdWx0X2Ftb3VudCB+IExvYW5Ocl9DaGtEZ3QgKyBOYW1lICsgU2VjdG9yICsgQ2l0eSArIFN0YXRlICsgDQogICAgICBCYW5rICsgIE5BSUNTICsgQXBwcm92YWxGWSArIE5vRW1wICsNCiAgICAgIE5ld0V4aXN0ICsgQ3JlYXRlSm9iICsgUmV0YWluZWRKb2IgKyBGcmFuY2hpc2VDb2RlICsgVXJiYW5SdXJhbCArDQogICAgICBEaXNidXJzZW1lbnRHcm9zcyArIEdyQXBwdiArIFNCQV9BcHB2ICsgTkFJQ1MyICsgTkFJQ1MzICsgTkFJQ1M0ICsNCiAgICAgIGxvYW5fcGVyX2VtcGxveWVlICsgU0JBX0FwcHZfdnNfR3Jvc3MgKyBHcm9zc19taW51c19TQkFfQXBwdiArDQogICAgICBHcm9zc19taW51c19TQkFfQXBwdl92c19FbXBsb3llZSArIHNhbWVfc3RhdGUgLA0KICAgIGRhdGEgPSByZWdyZXNzaW9uX3RyYWluaW5nDQogICkgJT4lDQogIHVwZGF0ZV9yb2xlKExvYW5Ocl9DaGtEZ3QsIG5ld19yb2xlID0gIklEIikgJT4lDQogIHN0ZXBfenYoYWxsX251bWVyaWNfcHJlZGljdG9ycygpKSAlPiUgDQogIHN0ZXBfbm92ZWwoDQogICAgTmFtZSwNCiAgICBTZWN0b3IsDQogICAgQ2l0eSwNCiAgICBTdGF0ZSwNCiAgICBCYW5rLA0KICAgIE5BSUNTLA0KICAgIE5BSUNTMiwNCiAgICBOQUlDUzMsDQogICAgTkFJQ1M0ICkgJT4lDQogIHN0ZXBfb3RoZXIoYWxsX25vbWluYWxfcHJlZGljdG9ycygpLCB0aHJlc2hvbGQgPSB0dW5lOjp0dW5lKCkpICU+JQ0KICBzdGVwX2R1bW15KGFsbF9ub21pbmFsX3ByZWRpY3RvcnMoKSkgJT4lDQogIHN0ZXBfaW50ZXJhY3QofiBzdGFydHNfd2l0aCgiQmFuayIpOkFwcHJvdmFsRlkpICU+JSAjIEFkZCBpbXBvcnRhbnQgaW50ZXJhY3Rpb25zDQogIHN0ZXBfb3JkZXJOb3JtKGFsbF9udW1lcmljX3ByZWRpY3RvcnMoKSkgJT4lDQogIHN0ZXBfbm9ybWFsaXplKGFsbF9udW1lcmljX3ByZWRpY3RvcnMoKSkgDQoNCmBgYA0KDQojIyBMaW5lYXIgUmVncmVzc2lvbg0KDQpgYGB7ciBsaW5lYXIgcmVncmVzc2lvbiBzcGVjaWZpY2F0aW9ufQ0KbGluZWFyX3JlZ19sbV9zcGVjIDwtDQogIGxpbmVhcl9yZWcocGVuYWx0eSA9IHR1bmUoKSwgDQogICAgICAgICAgICAgbWl4dHVyZSA9IHR1bmUoKSkgJT4lDQogIHNldF9lbmdpbmUoJ2dsbW5ldCcpDQoNCmxpbmVhcl9yZWdyZXNzaW9uX3dmIDwtDQogIHdvcmtmbG93KCkgJT4lDQogIGFkZF9yZWNpcGUocmVncmVzc2lvbl9yZWMpICU+JQ0KICBhZGRfbW9kZWwobGluZWFyX3JlZ19sbV9zcGVjKQ0KYGBgDQoNCmBgYHtyIGxpbmVhciByZWdyZXNzaW9uIHR1bmUsIGV2YWwgPSBGQUxTRX0NCmxpbmVhcl9yZWdyZXNzaW9uX3JzIDwtIHR1bmVfcmFjZV9hbm92YSgNCiAgIGxpbmVhcl9yZWdyZXNzaW9uX3dmLA0KICAgcmVzYW1wbGVzID0gcmVncmVzc2lvbl9mb2xkcywNCiAgIGdyaWQgPSAzMCwNCiAgIG1ldHJpY3MgPSBtc2V0LA0KICAgY29udHJvbCA9IGNvbnRyb2xfcmFjZSh2ZXJib3NlID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV9wcmVkID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV93b3JrZmxvdyA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGV4dHJhY3QgPSBleHRyYWN0X21vZGVsLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbGxlbF9vdmVyID0gImV2ZXJ5dGhpbmciKQ0KICAgICkNCg0KY29sbGVjdF9tZXRyaWNzKGxpbmVhcl9yZWdyZXNzaW9uX3JzKSAlPiUgDQogIGFycmFuZ2UobWVhbikNCg0KYGBgDQoNCmBgYHtyIGxpbmVhciByZWdyZXNzaW9uIHR1bmUgbm9JbmNsdWRlLCBpbmNsdWRlID0gRkFMU0V9DQppZiAoZmlsZS5leGlzdHMoaGVyZTo6aGVyZShkYXRhX2RpciwgImxpbmVhcl9yZWdyZXNzb3IucmRzIikpKSB7DQogIA0KbGluZWFyX3JlZ3Jlc3Npb25fcnMgPC0gcmVhZF9yZHMoaGVyZTo6aGVyZShkYXRhX2RpciwgImxpbmVhcl9yZWdyZXNzb3IucmRzIikpDQogIA0KfSBlbHNlIHsNCg0KbGluZWFyX3JlZ3Jlc3Npb25fcnMgPC0gdHVuZV9yYWNlX2Fub3ZhKA0KICAgbGluZWFyX3JlZ3Jlc3Npb25fd2YsDQogICByZXNhbXBsZXMgPSByZWdyZXNzaW9uX2ZvbGRzLA0KICAgZ3JpZCA9IDMwLA0KICAgbWV0cmljcyA9IG1zZXQsDQogICBjb250cm9sID0gY29udHJvbF9yYWNlKHZlcmJvc2UgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzYXZlX3ByZWQgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzYXZlX3dvcmtmbG93ID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgZXh0cmFjdCA9IGV4dHJhY3RfbW9kZWwsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFsbGVsX292ZXIgPSAiZXZlcnl0aGluZyIpDQogICAgKQ0KDQp3cml0ZV9yZHMobGluZWFyX3JlZ3Jlc3Npb25fcnMsIGhlcmU6OmhlcmUoIGRhdGFfZGlyLCAibGluZWFyX3JlZ3Jlc3Nvci5yZHMiKSkNCn0NCg0KY29sbGVjdF9tZXRyaWNzKGxpbmVhcl9yZWdyZXNzaW9uX3JzKSAlPiUgDQogIGFycmFuZ2UobWVhbikNCg0KYGBgDQoNCg0KYGBge3J9DQoNCmxpbmVhcl9yZWdyZXNzaW9uX2Jlc3Rfd2YgPC0gICANCiAgbGluZWFyX3JlZ3Jlc3Npb25fd2YgJT4lIA0KICBmaW5hbGl6ZV93b3JrZmxvdyhzZWxlY3RfYmVzdChsaW5lYXJfcmVncmVzc2lvbl9ycykpDQoNCmdsbV9yZWdyZXNzaW9uX2ZpdCA8LSBmaXQobGluZWFyX3JlZ3Jlc3Npb25fYmVzdF93ZiwgZGF0YSA9IHJlZ3Jlc3Npb25fdHJhaW5pbmcpDQoNCmdsbV9yZWdyZXNzaW9uX2ZpdCAlPiUgDQogIGV4dHJhY3RfZml0X3BhcnNuaXAoKSAlPiUgDQogIHZpcDo6dmlwKG1ldHJpYyA9ICJtYWUiLCBudW1fZmVhdHVyZXMgPSAyNUwpICsNCiAgbGFicyh0aXRsZSA9ICJSZWd1bGFyaXplZCBHTE0gVmFyaWFibGUgSW1wb3J0YW5jZSBTY29yZXMiKQ0KYGBgDQoNCkxpbmVhciByZWdyZXNzaW9uIHlpZWxkcyBhbiBNZWFuIEF2ZXJhZ2UgRXJyb3Igb24gY3Jvc3MgdmFsaWRhdGlvbiBzYW1wbGVzIGhlbGQgb3V0IHRoYXQgaXMgYWJvdXQgMS8zIGxlc3MgdGhhbiB0aGUgTlVMTCBtb2RlbC4gDQoNCiMjIENhdGJvb3N0DQoNCk11Y2ggbGlrZSBhYm92ZSwgYnV0IG5vdyBmb3IgcmVncmVzc2lvbi4NCg0KYGBge3IgY2F0Ym9vc3QgcmVncmVzc2lvbiBzcGVjfQ0KDQpjYXRib29zdF9yZWdyZXNzaW9uX3JlYyA8LSByZWNpcGUoDQogICAgZGVmYXVsdF9hbW91bnQgfiBMb2FuTnJfQ2hrRGd0ICsgTmFtZSArIFNlY3RvciArIENpdHkgKyBTdGF0ZSArIA0KICAgICAgQmFuayArICBOQUlDUyArIEFwcHJvdmFsRlkgKyBOb0VtcCArDQogICAgICBOZXdFeGlzdCArIENyZWF0ZUpvYiArIFJldGFpbmVkSm9iICsgRnJhbmNoaXNlQ29kZSArIFVyYmFuUnVyYWwgKw0KICAgICAgRGlzYnVyc2VtZW50R3Jvc3MgKyBHckFwcHYgKyBTQkFfQXBwdiArIE5BSUNTMiArIE5BSUNTMyArIE5BSUNTNCArDQogICAgICBsb2FuX3Blcl9lbXBsb3llZSArIFNCQV9BcHB2X3ZzX0dyb3NzICsgR3Jvc3NfbWludXNfU0JBX0FwcHYgKw0KICAgICAgR3Jvc3NfbWludXNfU0JBX0FwcHZfdnNfRW1wbG95ZWUgKyBzYW1lX3N0YXRlICwNCiAgICBkYXRhID0gcmVncmVzc2lvbl90cmFpbmluZw0KICApICU+JQ0KICB1cGRhdGVfcm9sZShMb2FuTnJfQ2hrRGd0LCBuZXdfcm9sZSA9ICJJRCIpICU+JQ0KICBzdGVwX3p2KGFsbF9udW1lcmljX3ByZWRpY3RvcnMoKSkgDQoNCmNhdGJvb3N0X3JlZ3Jlc3Npb25fc3BlYyA8LSBib29zdF90cmVlKHRyZWVzID0gdHVuZSgpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIG10cnkgPSB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWluX24gPSB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgbGVhcm5fcmF0ZSA9ICB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJlZV9kZXB0aCA9IHR1bmUoKSkgJT4lIA0KICBzZXRfZW5naW5lKCJjYXRib29zdCIpICU+JSANCiAgc2V0X21vZGUoInJlZ3Jlc3Npb24iKQ0KDQpgYGANCg0KYGBge3IgZml0IGNhdGJvb3N0IHJlZ3Jlc3Npb24gd29ya2Zsb3d9DQpjYXRib29zdF9yZWdyZXNzaW9uX3dmIDwtDQogIHdvcmtmbG93KCkgJT4lDQogIGFkZF9yZWNpcGUoY2F0Ym9vc3RfcmVncmVzc2lvbl9yZWMpICU+JQ0KICBhZGRfbW9kZWwoY2F0Ym9vc3RfcmVncmVzc2lvbl9zcGVjKQ0KDQpzZXQuc2VlZCgyMDIxKQ0KcmFjZV9ncmlkIDwtDQogICBncmlkX2xhdGluX2h5cGVyY3ViZSgNCiAgICAgZmluYWxpemUobXRyeSgpLCBjYXRib29zdF9yZWdyZXNzaW9uX3JlYyAlPiUgcHJlcCgpICU+JSBqdWljZSgpKSwNCiAgICAgdHJlZXMocmFuZ2UgPSBjKDUwMCwxODAwKSksDQogICAgIGxlYXJuX3JhdGUocmFuZ2UgPSBjKC0yLCAtMSkpLA0KICAgICB0cmVlX2RlcHRoKHJhbmdlID0gYyg2TCwgMTJMKSksDQogICAgIG1pbl9uKHJhbmdlID0gYygxMEwsIDQ1TCkpLA0KICAgIHNpemUgPSAxNQ0KICAgKQ0KYGBgDQogDQpgYGB7ciBmaXQgdHVuZWdyaWQgcmVncmVzc2lvbiwgZXZhbCA9IEZBTFNFfQ0KY2F0Ym9vc3RfcmVncmVzc2lvbl9ycyA8LSB0dW5lX3JhY2VfYW5vdmEoDQogICBjYXRib29zdF9yZWdyZXNzaW9uX3dmLA0KICAgcmVzYW1wbGVzID0gcmVncmVzc2lvbl9mb2xkcywNCiAgIGdyaWQgPSByYWNlX2dyaWQsDQogICBtZXRyaWNzID0gbXNldCwNCiAgIGNvbnRyb2wgPSBjb250cm9sX3JhY2UodmVyYm9zZSA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVfcHJlZCA9IEZBTFNFLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzYXZlX3dvcmtmbG93ID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0ID0gZXh0cmFjdF9tb2RlbCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbGxlbF9vdmVyID0gImV2ZXJ5dGhpbmciKQ0KICAgICkNCg0KYXV0b3Bsb3QoY2F0Ym9vc3RfcmVncmVzc2lvbl9ycykNCg0KY29sbGVjdF9tZXRyaWNzKGNhdGJvb3N0X3JlZ3Jlc3Npb25fcnMpICU+JQ0KICBzZWxlY3QobXRyeSwgDQogICAgICAgICB0cmVlcywgDQogICAgICAgICBtaW5fbiwgDQogICAgICAgICB0cmVlX2RlcHRoLCANCiAgICAgICAgIGxlYXJuX3JhdGUsIA0KICAgICAgICAgIk1BRSIgPSBtZWFuKSAlPiUNCiAgYXJyYW5nZShNQUUpDQpgYGANCg0KDQpgYGB7ciBmaXQgY2F0Ym9vc3QgcmVncmVzc2lvbiB0dW5lIGdyaWQgbm9JbmNsdWRlLCBpbmNsdWRlID0gRkFMU0V9DQoNCmlmIChmaWxlLmV4aXN0cyhoZXJlOjpoZXJlKGRhdGFfZGlyLCAicmVncmVzc29yLnJkcyIpKSkgeyANCiAgDQpjYXRib29zdF9yZWdyZXNzaW9uX3JzIDwtIHJlYWRfcmRzKGhlcmU6OmhlcmUoZGF0YV9kaXIsICJyZWdyZXNzb3IucmRzIikpDQoNCn0gZWxzZSB7DQoNCmNhdGJvb3N0X3JlZ3Jlc3Npb25fcnMgPC0gdHVuZV9yYWNlX2Fub3ZhKA0KICAgY2F0Ym9vc3RfcmVncmVzc2lvbl93ZiwNCiAgIHJlc2FtcGxlcyA9IHJlZ3Jlc3Npb25fZm9sZHMsDQogICBncmlkID0gcmFjZV9ncmlkLA0KICAgbWV0cmljcyA9IG1zZXQsDQogICBjb250cm9sID0gY29udHJvbF9yYWNlKHZlcmJvc2UgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzYXZlX3ByZWQgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV93b3JrZmxvdyA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGV4dHJhY3QgPSBleHRyYWN0X21vZGVsLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbGxlbF9vdmVyID0gImV2ZXJ5dGhpbmciKQ0KICAgICkNCg0Kd3JpdGVfcmRzKGNhdGJvb3N0X3JlZ3Jlc3Npb25fcnMsIGhlcmU6OmhlcmUoZGF0YV9kaXIsICJyZWdyZXNzb3IucmRzIikpDQoNCn0NCg0KYXV0b3Bsb3QoY2F0Ym9vc3RfcmVncmVzc2lvbl9ycykNCg0KY29sbGVjdF9tZXRyaWNzKGNhdGJvb3N0X3JlZ3Jlc3Npb25fcnMpICU+JQ0KICBzZWxlY3QobXRyeSwgdHJlZXMsIG1pbl9uLCB0cmVlX2RlcHRoLCBsZWFybl9yYXRlLCAiTUFFIiA9IG1lYW4pICU+JQ0KICBhcnJhbmdlKE1BRSkNCg0KYGBgDQoNCk5vdCBiYWQuIExldCdzIG1ha2UgYSBmaW5hbCBmaXQgYXQgdGhlc2UgaHlwZXJwYXJhbWV0ZXIgc2V0dGluZ3MuDQoNCmBgYHtyIGZpbmFsIGZpdCByZWdyZXNzaW9ufQ0KY2F0Ym9vc3RfcmVncmVzc2lvbl9iZXN0X3dmIDwtICAgDQogIGNhdGJvb3N0X3JlZ3Jlc3Npb25fd2YgJT4lIA0KICBmaW5hbGl6ZV93b3JrZmxvdyhzZWxlY3RfYmVzdChjYXRib29zdF9yZWdyZXNzaW9uX3JzKSkNCg0KcmVncmVzc2lvbl9maXQgPC0gZml0KGNhdGJvb3N0X3JlZ3Jlc3Npb25fYmVzdF93ZiwgDQogICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IGF1Z21lbnQoY2xhc3NpZmljYXRpb25fZml0LCB0cmFpbl9kZikgJT4lIA0KICAgICAgICAgICAgICAgICAgICAgIGZpbHRlcigucHJlZF9jbGFzcyA9PSAiZGVmYXVsdCIpKQ0KYGBgDQoNCiMjIFBlcmZvcm1hbmNlIENoZWNrcyB7LmFjdGl2ZX0NCg0KRmlyc3QsIGEgTWVhbiBBYnNvbHV0ZSBFcnJvciBvbiB0aGUgbGFiZWxlZCBgdGVzdGluZ2AgZGF0YSB0aGF0IHdhcyBoZWxkIG91dDoNCg0KYGBge3IgZmluYWwgcGVyZm9ybWFuY2UgZXZhbDF9DQphdWdtZW50KHJlZ3Jlc3Npb25fZml0LCBhdWdtZW50KGNsYXNzaWZpY2F0aW9uX2ZpdCwgdGVzdGluZykpICU+JSANCiAgbXV0YXRlKC5wcmVkID0gaWZfZWxzZShkZWZhdWx0X2ZsYWcgPT0gImRlZmF1bHQiLCAucHJlZCwwKSkgJT4lIA0KICBtYWUodHJ1dGggPSBkZWZhdWx0X2Ftb3VudCwgZXN0aW1hdGUgPSAucHJlZCkgDQpgYGANCg0KTmV4dCwgYSBNQUUgb24gdGhlIHByZWRpY3Rpb25zIG9uIGB0cmFpbmluZ2AsIHRvIGFzc2VzcyB3aGV0aGVyIHdlIGhhdmUgb3Zlci1maXQ6DQoNCmBgYHtyIGZpbmFsIHBlcmZvcm1hbmNlIGV2YWwyfQ0KYXVnbWVudChyZWdyZXNzaW9uX2ZpdCwgYXVnbWVudChjbGFzc2lmaWNhdGlvbl9maXQsIHRyYWluaW5nKSkgJT4lIA0KICBtdXRhdGUoLnByZWQgPSBpZl9lbHNlKGRlZmF1bHRfZmxhZyA9PSAiZGVmYXVsdCIsIC5wcmVkLDApKSAlPiUgDQogIG1hZSh0cnV0aCA9IGRlZmF1bHRfYW1vdW50LCBlc3RpbWF0ZSA9IC5wcmVkKSANCmBgYA0KDQpZZXAsIHdlIGRpZC4gR2l2ZW4gbW9yZSB0aW1lLCBpbmNyZWFzaW5nIHRoZSBudW1iZXIgb2YgZm9sZHMgYW5kIGFkZGluZyBtb3JlIHJlcGVhdHMgd291bGQgaGVscCByZXNvbHZlIHRoaXMuICBBIHZpc3VhbCBvZiBvdXIgcHJlZGljdGlvbnMgb2YgYGRlZmF1bHRfYW1vdW50c2AgdmVyc3VzIHRoZSB0cnV0aDoNCg0KYGBge3IgZmluYWwgcGVyZm9ybWFuY2UgZXZhbDN9DQphdWdtZW50KHJlZ3Jlc3Npb25fZml0LCBhdWdtZW50KGNsYXNzaWZpY2F0aW9uX2ZpdCwgdHJhaW5fZGYpKSAlPiUNCiAgbXV0YXRlKC5wcmVkID0gaWZfZWxzZShkZWZhdWx0X2ZsYWcgPT0gImRlZmF1bHQiLCAucHJlZCwgMCkpICU+JSANCiAgZ2dwbG90KGFlcyhkZWZhdWx0X2Ftb3VudCwgLnByZWQpKSArDQogIGdlb21fcG9pbnQoYWxwaGEgPSAwLjIsIGNvbG9yID0gImJsdWUiKSArDQogIGdlb21fYWJsaW5lKHNsb3BlID0gMSwgY29sb3IgPSAicmVkIiwgYWxwaGEgPSAwLjUpICsNCiAgc2NhbGVfeF9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6ZG9sbGFyKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmRvbGxhcikgKw0KICBsYWJzKHRpdGxlID0gIkNhdGJvb3N0IE1vZGVsIFBlcmZvcm1hbmNlIiwNCiAgICAgICB4ID0gIlRydXRoIiwNCiAgICAgICB5ID0gIlByZWRpY3Rpb24iKQ0KDQpgYGANCg0KIyMgVmFyaWFibGUgSW1wb3J0YW5jZQ0KDQpJIGxpa2UgdG8gY29tZSBiYWNrIHRvIGVpdGhlciB0aGVzZSBzY29yZXMsIG9yIHBhcnRpYWwgZGVwZW5kZW5jZSBwbG90cywgdG8gYmUgYWJsZSB0byB0ZWxsIHRoZSBzdG9yeSBvZiB3aGF0IHRoZSBtb2RlbCBpcyBkb2luZyBhbmQgYXNrIHF1ZXN0aW9ucyBhYm91dCB0aGUgaW5jb21pbmcgZGF0YSBzYW1wbGUuIElmLCBmb3IgZXhhbXBsZSwgdGhlIFNCQSBjaGFuZ2VzIHBvbGljaWVzIGxpa2UgdGhlIEFwcHJvdmFsIHRocmVzaG9sZHMsIHdlIHdvdWxkIGNlcnRhaW5seSBleHBlY3QgdGhlIG1vZGVsIHRvIG1vdmUgZHJhbWF0aWFsbHkuDQoNCmBgYHtyIHJlZ3Jlc3Npb24gdmFyaWFibGUgaW1wb3J0YW5jZX0NCg0KdmlwX3JlZ3Jlc3NvciA8LSANCiAgREFMRVh0cmE6OmV4cGxhaW5fdGlkeW1vZGVscygNCiAgICByZWdyZXNzaW9uX2ZpdCwNCiAgICBkYXRhID0gYXVnbWVudChyZWdyZXNzaW9uX2ZpdCwgYXVnbWVudChjbGFzc2lmaWNhdGlvbl9maXQsIHRyYWluX2RmKSkgJT4lIA0KICAgICAgICAgICAgICAgICAgICAgICAgbXV0YXRlKC5wcmVkID0gaWZfZWxzZShkZWZhdWx0X2ZsYWcgPT0gImRlZmF1bHQiLCAucHJlZCwgMCkpICU+JSANCiAgICAgICAgICAgICAgICAgICAgICAgIHNlbGVjdCgtZGVmYXVsdF9hbW91bnQpLA0KICAgIHkgPSBhdWdtZW50KHJlZ3Jlc3Npb25fZml0LCBhdWdtZW50KGNsYXNzaWZpY2F0aW9uX2ZpdCwgdHJhaW5fZGYpKSAlPiUgDQogICAgICAgICAgICAgICAgICAgICAgICBtdXRhdGUoLnByZWQgPSBpZl9lbHNlKGRlZmF1bHRfZmxhZyA9PSAiZGVmYXVsdCIsIC5wcmVkLCAwKSkgJT4lIA0KICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0KGRlZmF1bHRfYW1vdW50KSwNCiAgICBsYWJlbCA9ICJDYXRib29zdCBSZWdyZXNzaW9uIiwNCiAgICB2ZXJib3NlID0gRkFMU0UNCiAgKSAlPiUgDQogIERBTEVYOjptb2RlbF9wYXJ0cygpDQoNCmdncGxvdF9pbXAodmlwX3JlZ3Jlc3NvcikgKw0KICB0aGVtZShhc3BlY3QucmF0aW8gPSAxLjIpICsNCiAgbGFicyh0aXRsZSA9ICJTQkEgRGVmYXVsdCBBbW91bnQgUmVncmVzc2lvbiBGZWF0dXJlIEltcG9ydGFuY2UiKQ0KDQpgYGANCg0KIyB7LX0NCg0KV2l0aCBhbGwgbW9kZWxzIGJ1aWx0IGFuZCBmaXQsIHdlIGNhbiBub3cgcGlwZWxpbmUgdGhlIHdob2xlIHByb2Nlc3MgZm9yIHRoZSBob2xkb3V0IGFuZCBtYWtlIHRoZSBLYWdnbGUgc3VibWlzc2lvbi4NCg0KDQpgYGB7ciBwb3N0IGNzdiB4Z2Jvb3N0MiwgZXZhbCA9IEZBTFNFfQ0Kc2hlbGwoZ2x1ZTo6Z2x1ZSgna2FnZ2xlIGNvbXBldGl0aW9ucyBzdWJtaXQgLWMgeyBjb21wZXRpdGlvbl9uYW1lIH0gLWYgeyBwYXRoX2V4cG9ydCB9IC1tICJDYXRib29zdCB3aXRoIGFkdmFuY2VkIHByZXByb2Nlc3NpbmcgbW9kZWwgMiInKSkNCmBgYA0KDQoNCg==