🔎 How to download & run the codes?

All the source codes of the aggregation methods are available here . To run the codes, you can clone the repository directly or simply load the R script source file from the repository using devtools package in Rstudio as follow:

  1. Install devtools package using command:

    install.packages("devtools")

  2. Loading the source codes from GitHub repository using source_url function by:

    devtools::source_url("https://raw.githubusercontent.com/hassothea/AggregationMethods/main/KernelAggClassifier.R")


✎ Note: All codes contained in this Rmarkdown are built with recent version of (version \(>\) 4.1, available here) and Rstudio (version > 2022.02.2+485, available here). Note also that the code chucks are hidden by default.

To see the codes, you can:


1 Aggregation method & important packages

1.1 Aggregation method

This Rmarkdown provides the implementation of a kernel-based combined classification rule by Mojirsheibani (2020). Let \(\mathcal{D}_n=\{(x_1,y_1),...,(x_n,y_n)\}\) be a training data of size \(n\), where the input-output couples \((x_i, y_i)\in\mathbb{R}^d\times\{1,\dots,N\}\) for all \(i=1,...,n\), and here, \(N\) is the number of classes. \(\mathcal{D}_{n}\) is first randomly partitioned into \(\mathcal{D}_{k}\) and \(\mathcal{D}_{\ell}\) of size \(k\) and \(\ell\) respectively such that \(k+\ell=n\). We construct \(M\) classifiers (machines) \(c_1,...,c_M\) using only \(\mathcal{D}_{k}\). Let \({\bf c}(x)=(c_1(x),...,c_M(x))^T\in\{1,...,N\}^M\) be the vector of predicted classes of \(x\in\mathbb{R}^d\) (given by the classifiers \(c_1,...,c_M\)), the kernel-based combining method evaluated at point \(x\) is defined by

\[ g_n({\bf c}(x))=k^*=\text{arg}\max_{1\leq k\leq N}\sum_{i=1}^{\ell}K_h(d_{\cal H}({\bf c}(x),{\bf c}(x_i)))\mathbb{1}_{\{y_i=k\}}. \] Here,

  • \(K:\mathbb{R}_+\to\mathbb{R}_+\) is a non-increasing kernel function with \(K_h(x)=K(x/h)\) for some smoothing parameter \(h>0\) to be tuned
  • \(d_{\cal H}\) is the Hamming distance or the number of different classes among \(M\) coordinates.

In words, the predicted class is the class comprising heaviest total weight. Even though the formula of \(g_n\) is defined using only data points in \(\mathcal{D}_{\ell}\), it does depend on the whole training data \(\mathcal{D}_{n}\) as the basic machines \((c_i)_{i=1}^M\) are built using \(\mathcal{D}_{k}\).

1.2 Important packages

We prepare all the necessary tools for this Rmarkdown. pacman package allows us to load (if exists) or install (if does not exist) any available packages from The Comprehensive R Archive Network (CRAN) of .

# Check if package "pacman" is already installed 

lookup_packages <- installed.packages()[,1]
if(!("pacman" %in% lookup_packages))
  install.packages("pacman")


# To be installed or loaded
pacman::p_load(magrittr)
pacman::p_load(tidyverse)

## package for "generateMachines"
pacman::p_load(tree)
pacman::p_load(nnet)
pacman::p_load(e1071)
pacman::p_load(randomForest)
pacman::p_load(FNN)
pacman::p_load(xgboost)
pacman::p_load(adabag)
pacman::p_load(keras)
pacman::p_load(pracma)
pacman::p_load(latex2exp)
pacman::p_load(plotly)
rm(lookup_packages)

2 Basic machine generator

This section provides functions to generate basic machines (classifiers) to be aggregated.

2.1 Function : setBasicParameter

This function allows us to set the values of some key parameters of the basic machines.

  • Argument:

    • k : the parameter \(k\) of \(k\)NN (knn) classifiers and the default value is \(k=10\).
    • ntree : the number of trees in random forest (rf). By default, ntree = 300.
    • mtry : the number of random features chosen in each split of random forest procedure. By default, mtry = NULL and the default value of mtry of randomForest function from randomForest library is used.
    • ker_svm : kernel option in SVM. It should be a subset of {"linear", "polynomial", "radial", "sigmoid"}. By default, ker_svm = "radial".
    • deg_svm : degree of polynomial kernel in SVM. By default, deg_svm = 3.
    • breg_boost : Bregman divergence used in boosting function of maboost package. By default, breg_boost = "entrop" and KL divergence is used, resulting Adaboost-like algorithm.
    • iter_boost : number of boosting iterations to perform. By default iter_boost = 100.
    • eta_xgb : the learning rate \(\eta>0\) in gradient step of extreme gradient boosting method (xgb) of xgboost library.
    • nrounds_xgb : the parameter nrounds indicating the maximum number of boosting iterations. By default, nrounds_xgb = 100.
    • early_stop_xgb : the early stopping round criterion of xgboost function. By, default, early_stop_xgb = NULL and the early stopping function is not triggered.
    • max_depth_xgb : maximum depth of trees constructed in xgboost.
    • param_xgb : list of additional parameters of xgboost classifier. By default, param_xgb = NULL. For more information, read online documentation.
  • Value:

    This function returns a list of all the parameters given in its arguments, to be fed to the basicMachineParam argument of function generateMachines defined in the next section.


🧾 Remark.1: k, ntree, iter_boost and nrounds_xgb can be a single value or a vector. In other words, each type of models can be constructed several times according to the values of the hyperparameters (a single value or vector).


setBasicParameter <- function(k = 10,
                              ntree = 300,
                              mtry = NULL,
                              ker_svm = "radial",
                              deg_svm = 3,
                              mfinal_boost = 50,
                              boostrap = TRUE,
                              eta_xgb = 1, 
                              nrounds_xgb = 100, 
                              early_stop_xgb = NULL,
                              max_depth_xgb = 3,
                              param_xgb = NULL){
  return(list(
    k = k,
    ntree = ntree, 
    mtry = mtry, 
    ker_svm = ker_svm,
    deg_svm = deg_svm,
    mfinal_boost = mfinal_boost,
    boostrap = boostrap,
    eta_xgb = eta_xgb, 
    nrounds_xgb = nrounds_xgb, 
    early_stop_xgb = early_stop_xgb,
    max_depth_xgb = max_depth_xgb,
    param_xgb = param_xgb)
  )
}

2.2 Function : generateMachines

This function generates all the basic machines to be aggregated.

  • Argument:

    • train_input : a matrix or data frame of the training input data.
    • train_response : a vector of training response variable corresponding to the train_input.
    • scale_input : a logical value specifying whether to scale the input data (to be between \(0\) and \(1\)) or not. By default, scale_input = FALSE.
    • machines : types of basic machines to be constructed. It is a subset of {"knn", "tree", "rf", "logit", "svm", "xgb", "adaboost"}. By default, machines = NULL and all types of the basic machines are built.
    • splits : real number between \(0\) and \(1\) specifying the proportion of training data, used to train the basic machines (\(\mathcal{D}_k\)). The remaining proportion of (\(1-\) splits) is used for the aggregation (\(\mathcal{D}_{\ell}\)). By default, splits = 0.5.
    • basicMachineParam : an option used to set the values of parameters of each machines. One should feed the function setBasicParameter() defined above to this argument.
    • silent : a logical value specifying whether or not the progress of the method should be printed. Be default, silent = FALSE and the progress of the algorithm is printed.
  • Value:

    This function returns a list of the following objects.

    • predict2 : the predictions of the remaining part (\(\mathcal{D}_{\ell}\)) of the training data, used for the aggregation.
    • models : all the constructed basic machines (it contains only the values of the parameter \(k\) for knn).
    • id2 : a logical vector of size equals to the number of rows of the training data indicating the location of the training points used to build the basic machines (FALSE) and the remaining ones (TRUE).
    • train_data : a list of:
      • train_input : the trainnig input data.
      • train_response : the training response variable.
      • classes : the classes (unique) of response variable.
    • scale_max, scale_min : if the argument scale_input = TRUE, the maximun and minimun values of all columns are stored in these vectors.

✎ Note: You may need to modify the function accordingly if you want to build different types of basic machines.


generateMachines <- function(train_input, 
                             train_response,
                             scale_input = FALSE,
                             machines = NULL,
                             splits = 0.5, 
                             basicMachineParam = setBasicParameter(),
                             silent = FALSE){
  k <- basicMachineParam$k 
  ntree <- basicMachineParam$ntree 
  mtry <- basicMachineParam$mtry
  ker_svm <- basicMachineParam$ker_svm
  deg_svm <- basicMachineParam$deg_svm
  mfinal_boost = basicMachineParam$mfinal_boost
  boostrap = basicMachineParam$boostrap
  eta_xgb <- basicMachineParam$eta_xgb 
  nrounds_xgb <- basicMachineParam$nrounds_xgb
  early_stop_xgb <- basicMachineParam$early_stop_xgb
  max_depth_xgb <- basicMachineParam$max_depth_xgb
  param_xgb <- basicMachineParam$param_xgb
  class_xgb <- unique(train_response)
  numberOfClasses <- length(class_xgb)
  if(is.null(param_xgb)){
    param_xgb <- list("objective" = "multi:softmax",
                      "eval_metric" = "mlogloss",
                      "num_class" = numberOfClasses+1)
  }
  
  # Packages
  pacman::p_load(nnet)
  pacman::p_load(e1071)
  pacman::p_load(tree)
  pacman::p_load(randomForest)
  pacman::p_load(FNN)
  pacman::p_load(xgboost)
  pacman::p_load(maboost)
  
  # Preparing data
  input_names <- colnames(train_input)
  input_size <- dim(train_input)
  df_input <- train_input_scale <- train_input
  if(scale_input){
    maxs <- map_dbl(.x = df_input, .f = max)
    mins <- map_dbl(.x = df_input, .f = min)
    train_input_scale <- scale(train_input, 
                               center = mins, 
                               scale = maxs - mins)
  }
  if(is.matrix(train_input_scale)){
    df_input <- as_tibble(train_input_scale)
    matrix_input <- train_input_scale
  } else{
    df_input <- train_input_scale
    matrix_input <- as.matrix(train_input_scale)
  }
  
  # Machines
  svm_machine <- function(x, pa = NULL){
    mod <- svm(x = df_train_x1, 
               y = train_y1,
               kernel = ker_svm,
               degree = deg_svm,
               type = "C-classification")
    res <- predict(mod, 
                   newdata = x)
    return(list(pred = res,
                model = mod))
  }
  tree_machine <- function(x, pa = NULL) {
    mod <- tree(as.formula(paste("train_y1~", 
                                 paste(input_names, 
                                       sep = "", 
                                       collapse = "+"), 
                                 collapse = "", 
                                 sep = "")), 
                data = df_train_x1)
    res <- predict(mod, x, type = 'class')
    return(list(pred = res,
                model = mod))
  }
  knn_machine <- function(x, k0) {
    mod <- knn(train = matrix_train_x1, 
                   test = x, 
                   cl = train_y1, 
                   k = k0)
    return(list(pred = mod,
                model = k0))
  }
  RF_machine <- function(x, ntree0) {
    if(is.null(mtry)){
      mod <- randomForest(x = df_train_x1, 
                          y = train_y1, 
                          ntree = ntree0)
    }else{
      mod <- randomForest(x = df_train_x1, 
                          y = train_y1, 
                          ntree = ntree0, 
                          mtry = mtry)
    }
    res <- as.vector(predict(mod, x))
    return(list(pred = res,
                model = mod))
  }
  xgb_machine <- function(x, nrounds_xgb0){
    mod <- xgboost(data = matrix_train_x1,
                   label = train_y1,
                   params = param_xgb,
                   eta = eta_xgb,
                   early_stopping_rounds = early_stop_xgb,
                   max_depth = max_depth_xgb,
                   verbose = 0,
                   nrounds = nrounds_xgb0)
    res <- class_xgb[predict(mod, x)]
    return(list(pred = res,
                model = mod))
  }
  ada_machine <- function(x, mfinal0){
    data_tem <- cbind(df_train_x1, "target" = train_y1)
    mod_ <- boosting(target ~ ., 
                     data = data_tem,
                     mfinal = mfinal0,
                     boos = boostrap)
    res <- predict.boosting(mod_, 
                         newdata = as.data.frame(x))
    return(list(pred = res$class,
                model = mod_))
  }
  logit_machine <- function(x, pa = NULL){
    mod <- multinom(as.formula(paste("train_y1~", 
                                 paste(input_names, 
                                       sep = "", 
                                       collapse = "+"), 
                                 collapse = "", 
                                 sep = "")), 
                data = df_train_x1,
                trace = FALSE)
    res <- predict(mod, 
                   newdata = x)
    return(list(pred = res,
                model = mod))
  }
  # All machines
  all_machines <- list(knn = knn_machine, 
                       tree = tree_machine, 
                       rf = RF_machine,
                       logit = logit_machine,
                       svm = svm_machine,
                       adaboost = ada_machine,
                       xgb = xgb_machine)
  # All parameters
  all_parameters <- list(knn = k, 
                         tree = 1,
                         rf = ntree,
                         logit = NA,
                         svm = deg_svm,
                         adaboost = mfinal_boost,
                         xgb = nrounds_xgb)
  lookup_machines <- c("knn", "tree", "rf", "logit", "svm", "xgb", "adaboost")
  if(is.null(machines)){
    mach <- lookup_machines
  }else{
    mach <- map_chr(.x = machines,
                    .f = ~ match.arg(.x, lookup_machines))
  }
  # Extracting data
  M <- length(mach)
  size_D1 <- floor(splits*input_size[1])
  id_D1 <- logical(input_size[1])
  id_D1[sample(input_size[1], size_D1)] <- TRUE

  df_train_x1 <- df_input[id_D1,]
  matrix_train_x1 <- matrix_input[id_D1,]
  train_y1 <- train_response[id_D1]
  df_train_x2 <- df_input[!id_D1,]
  matrix_train_x2 <- matrix_input[!id_D1,]
  
  # Function to extract df and model from 'map' function
  extr_df <- function(x, nam, id){
    return(tibble("{nam}_{id}" := as.vector(pred_m[[x]]$pred)))
  }
  extr_mod <- function(x, id){
    return(pred_m[[x]]$model)
  }
  
  pred_D2 <- c()
  all_mod <- c()
  if(!silent){
    cat("\n* Building basic machines ...\n")
    cat("\t~ Progress:")
  }
  for(m in 1:M){
    if(mach[m] %in% c("knn", "xgb")){
      x0_test <-  matrix_train_x2
    } else {
      x0_test <- df_train_x2
    }
    if(is.null(all_parameters[[mach[m]]])){
      para_ <- 1
    }else{
      para_ <- all_parameters[[mach[m]]]
    }
    pred_m <-  map(para_,
                   .f = ~ all_machines[[mach[m]]](x0_test, .x))
    tem0 <- imap_dfc(.x = 1:length(para_), 
                     .f = ~ extr_df(x = .x, nam = mach[m], id = para_[.x]))
    tem1 <- map(.x = 1:length(para_), 
                 .f = extr_mod)
    names(tem0) <- names(tem1) <- paste0(mach[m], 1:length(para_))
    pred_D2 <- bind_cols(pred_D2, as_tibble(tem0))
    all_mod[[mach[m]]] <- tem1
    if(!silent){
      cat(" ... ", round(m/M, 2)*100L,"%", sep = "")
    }
  }
  if(scale_input){
    return(list(predict2 = pred_D2,
                models = all_mod,
                id2 = !id_D1,
                train_data = list(train_input = train_input_scale, 
                                  train_response = train_response,
                                  classes = class_xgb),
                scale_max = maxs,
                scale_min = mins))
  } else{
    return(list(predict2 = pred_D2,
                models = all_mod,
                id2 = !id_D1,
                train_data = list(train_input = train_input_scale, 
                                  train_response = train_response,
                                  classes = class_xgb)))
  }
}

Example.1: In this example, the method is implemented on iris dataset. All types of basic machines are built on the first part of the training data (\(\mathcal{D}_{k}\)), and the accuracy evaluated on the second part of the training data (\(\mathcal{D}_{\ell}\)) used for aggregation) are reported.


df <- iris
basic_machines <- generateMachines(train_input = df[,1:4],
                                   train_response = df$Species,
                                   scale_input = TRUE,
                                   machines = NULL, #c("knn", "tree", "rf", "logit", "svm", "xgb")
                                   basicMachineParam = setBasicParameter(ntree = 10:20 * 25,
                                                                         k = c(2:10),
                                                                         mfinal_boost = 10))

* Building basic machines ...
    ~ Progress: ... 14% ... 29% ... 43% ... 57% ... 71% ... 86% ... 100%
basic_machines$predict2 %>%
  sweep(1, df[basic_machines$id2, "Species"], FUN = "==") %>%
  colMeans %>%
  t %>%
  as_tibble

3 Optimizer : grid search algorithm

This part provides functions to approximate the smoothing parameter \(h>0\) of the aggregation method.

3.1 Function : setGridParameter

This function allows us to set the values of parameters needed to process the grid search algorithm to approximate the smoothing parameter \(h > 0\) of the method.

  • Argument:

    • min_val : the minimum value of parameter grid. By default, min_val = 1e-4
    • max_val : the maximum value of parameter grid. By default, max_val = 0.1
    • n_val : the number of points in the grid. By default, n_val = 300
    • parameters : the vector of paramters in case non-uniform grid is considered. By defaultparameters = NULL
    • print_result : a logical value specifying whether or not to print the observed result. By default, print_result = TRUE
    • figure : a logical value specifying whether or not to plot the graphic of error. By default, figure = TRUE
  • Value:

    This function returns a list of all the parameters given in its arguments.

setGridParameter <- function(min_val = 1e-5, 
                             max_val = 0.5, 
                             n_val = 300, 
                             parameters = NULL,
                             print_result = TRUE,
                             figure = TRUE){
  return(list(min_val = min_val,
              max_val = max_val,
              n_val = n_val,
              parameters = parameters,
              print_result = print_result,
              figure = figure))
}

3.1.1 Function : gridOptimizer

This function performs grid search algorithm in approximating the values of parameters \(h>0\) of the aggregation method.

  • Argument:

    • obj_fun : the objective function for which its minimizer is to be estimated. It should be a univarate function of real positive variables.
    • setParameter : the control of grid search algorithm parameters which should be the function setGridParameter() defined above.
    • naive : a logical value specifying if the naive kernel is used. By default, naive = FALSE.
    • silent : a logical value specifying whether or not all the messages should be silent. By default, silent = FALSE.
    • ker : the name of kernel function.
  • Value:

    This function returns a list of the following objects (except for naive kernel for which NAs are returned):

    • opt_param : the observed value of the minimizer.
    • opt_error : the value of optimal risk.
    • all_risk : the vector of all the errors evaluated at all the values of considered parameters.
    • run.time : the running time of the algorithm.

🧾 Remark.2: For "naive" kernel function (0-1 weight), there is no parameter \(h\) to be tuned. One just searches for the training data with the same predicted classes as the query point, then computes the total weight among them. The majority class among such data points is the predicted class.


gridOptimizer <- function(obj_func,
                          setParameter = setGridParameter(),
                          naive = FALSE,
                          silent = FALSE,
                          ker = NULL){
  t0 <- Sys.time()
  if(!naive){
    if(is.null(setParameter$parameters)){
    param <- seq(setParameter$min_val,
                 setParameter$max_val,
                 length.out = setParameter$n_val)
    } else{
      param <- setParameter$parameters
    }
    risk <- param %>%
      map_dbl(.f = obj_func)
    id_opt <- which.min(risk)
    opt_ep <- param[id_opt]
    opt_risk <- risk[id_opt]
    if(setParameter$print_result & !silent){
      cat("\n* Grid search for",ker,"kernel...\n ~ observed parameter :", opt_ep)
    }
    if(setParameter$figure){
      tibble(x = param, 
             y = risk) %>%
        ggplot(aes(x = x, y = y)) +
        geom_line(color = "skyblue", size = 0.75) +
        geom_point(aes(x = opt_ep, y = opt_risk), color = "red") +
        geom_vline(xintercept = opt_ep, color = "red", linetype = "longdash") +
        labs(title = "Error as function of parameter", 
             x = "Parameter",
             y = "Error") -> p
      print(p)
    }
  } else{
      opt_ep = NA
      opt_risk = NA
      risk = NA
  }
  
  t1 <- Sys.time()
  return(list(
    opt_param = opt_ep,
    opt_error = opt_risk,
    all_risk = risk,
    run.time = difftime(t1, 
                        t0, 
                        units = "secs")[[1]])
  )
}

3.2 \(\kappa\)-cross validation lost function

Constructing a combining classifier in this framework is equivalent to approximating the optimal value of smoothing parameter \(h>0\) introduced in section 1.1, by minimizing some lost function. In this study, we propose \(\kappa\)-fold cross validation misclassification error defined by

\[ \varphi^{\kappa}(h)=\frac{1}{\kappa}\sum_{k=1}^{\kappa}\Big(\frac{1}{|F_k|}\sum_{(x_j,y_j)\in F_k}\mathbb{1}_{\{g_n({\bf c}(x_j))\neq y_j\}}\Big) \]

where

  • for any \(k=1,...,\kappa\), \(F_k\) denotes the \(k\)th validation fold.
  • \(g_n({\bf c}(x_j))\) is the prediction of \(x_j\) of \(F_k\), computed using the data points from the remaining part \({\cal D}_{\ell}-F_k\) by,

\[g_n({\bf c}(x_j))=k^*=\text{arg}\max_{1\leq k\leq N}\sum_{(x_i,y_i)\in \mathcal{D}\setminus F_k}K_h(d_{\cal H}({\bf c}(x_j),{\bf c}(x_i)))\mathbb{1}_{\{y_i=k\}}.\]

3.3 Function: dist_matrix

This function computes Hamming distances between data points of each training folds (\(\mathcal{D}_{\ell}-F_k\)) and the corresponding validation fold \(F_k\) for any \(k=1,\dots,\kappa\).

  • Argument:

    • basicMachines : the basic machine object, which is an output of generateMachines function.
    • n_cv : the number \(\kappa\) of cross-validation folds. By default, n_cv = 5.
    • kernel : the kernel function used for the aggregation, which is an element of {“gaussian”, “epanechnikov”, “biweight”, “triweight”, “triangular”, “naive”}. By default, kernel = "gaussian".
  • Value:

    This functions returns a list of the following objects:

    • dist : a data frame (tibble) containing Hamming distances, i.e., \(d_{\cal H}({\bf c}(x),{\bf c}(y))=\sum_{i=1}^M\mathbb{1}_{\{c_{k,i}(x)\neq c_{k,i}(y)\}}\).
    • id_shuffle : the shuffled indices in cross-validation.
    • n_cv : the number \(\kappa\) of cross-validation folds.
dist_matrix <- function(basicMachines,
                        n_cv = 5,
                        id_shuffle = NULL){
  n <- nrow(basicMachines$predict2)
  n_each_fold <- floor(n/n_cv)
  # shuffled indices
  if(is.null(id_shuffle)){
    shuffle <- 1:(n_cv-1) %>%
    rep(n_each_fold) %>%
    c(., rep(n_cv, n - n_each_fold * (n_cv - 1))) %>%
    sample
  }else{
    shuffle <- id_shuffle
  }
  # the prediction matrix D_l
  df_ <- as.matrix(basicMachines$predict2)
  pair_dist <- function(M, N){
    res_ <- 1:nrow(N) %>%
      map_dfc(.f = (\(id) tibble('{{id}}' := rowSums(sweep(M, 2, N[id,], FUN = "!=")))))
    return(res_)
  }
  L <- 1:n_cv %>%
      map(.f = ~ pair_dist(df_[shuffle != .x,],
                           df_[shuffle == .x,]))
  return(list(dist = L, 
              id_shuffle = shuffle,
              n_cv = n_cv))
}

Example.2: The method dist_matrix is implemented on the obtained basic machines built in Example.1.


dis$id_shuffle
 [1] 3 3 2 1 3 3 2 2 2 2 2 2 1 3 1 2 1 3 3 1 1 2 2 3 3 1 3 1 2 3 1 3 2 2 3 2 3 1 1 2 2 1 3 2 1 1 1 2 3 1 3 1 3 3 3 3 1 3 2 2 1 3 1 2 2 2 1 1 2 3 1 1 3 2 1

Example.3: From the distance matrix, we can compute the error corresponding to Gaussian kernel function, then use the grid search optimizer to approximate the smoothing paramter in this case.


# Gaussian kernel
gaussian_kern <- function(.ep = .05,
                          .dist_matrix,
                          .train_response2,
                          .inv_sigma = sqrt(.5),
                          .alpha = 2){
  kern_fun <- function(x, id, D){
    tem0 <- as.matrix(exp(-(x*D)^(.alpha/2)*.inv_sigma^.alpha))
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                     .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                         INDEX = .train_response2[.dist_matrix$id_shuffle != id],
                                                         FUN = sum)))) %>%
      map_chr(.f = (\(x) names(which.max(x))))
    return(mean(y_hat != .train_response2[.dist_matrix$id_shuffle == id]))
  }
  temp <- map2(.x = 1:.dist_matrix$n_cv, 
               .y = .dist_matrix$dist, 
               .f = ~ kern_fun(x = .ep, 
                               id = .x, 
                               D = .y))
  return(Reduce("+", temp)/.dist_matrix$n_cv)
}

# Kappa cross-validation error
cost_fun <- function(.ep = .1,
                     .dist_matrix,
                     .kernel_func = NULL,
                     .train_response2,
                     .inv_sigma = sqrt(.5)){
  return(.kernel_func(.ep = .ep,
                     .dist_matrix = .dist_matrix,
                     .train_response2 = .train_response2,
                     .inv_sigma = .inv_sigma))
}

# cross_validation
err_cv <- function(x, 
                     .dist_matrix = dis,
                     .kernel_func = gaussian_kern,
                     .train_response2 = df[basic_machines$id2, 5]) {
  res <- cost_fun(.ep = x,
              .dist_matrix = .dist_matrix,
              .kernel_func = .kernel_func,
              .train_response2 = .train_response2)
  return(res)
}

# Optimization
opt_param_grid <- gridOptimizer(obj_fun = err_cv,
                                setParameter = setGridParameter(min_val = 0.01,
                                                                max_val = 0.1,
                                                                n_val = 100,
                                                                figure = TRUE))

* Grid search for kernel...
    ~ observed parameter : 0.03090909

3.4 Fitting parameter

This function gathers the constructed machines and performs grid search algorithm to approximate the smoothing parameter for the aggregation method.

  • Argument:

    • train_design : a matrix or data frame of the training input data or the predicted classes given by some classifiers.
      • If the input data is given, the option build_machine must be TRUE and all the chosen machines will be constructed using a proportion of the training input (splits).
      • If the predicted classes are given, the option build_machine must be FALSE which indicates that no basic machine is constructed, and the paramter is tuned using this matrix of predicted classes directly.
    • train_response : a vector of corresponding classes of the train_design.
    • scale_input : a logical value specifying whether or not to scale the input data before building the basic classifiers. By default, scale_input = FALSE.
    • build_machine: a logical value specifying whether or not the basic machines should be constructed. It should be TRUE if the input data is given to the train_design, else it should be FALSE. By default, build_machine = TRUE.
    • machines : a vector of basic machines to be constructed. It must be a subset of {"knn", "tree", "rf", "logit", "svm", "xgb", "adaboost"}. By default, machines = NULL, and all types of basic machines are built.
    • splits : the proportion of training data used to build the basic machines. By default, splits = .5.
    • n_cv : the number of cross-validation folds used to tune the smoothing parameter.
    • inv_sig, alp : the inverse normalized constant \(\sigma^{-1}>0\) and the exponent \(\alpha >0\) of exponential kernel: \(K(x)=e^{-\|x/\sigma\|^{\alpha}}\) for any \(x\in\mathbb{R}^d\). By default, inv_sigma =\(\sqrt{1/2}\) and alpha = 2 which corresponds to the Gaussian kernel.
    • kernels : the kernel function or vector of kernel functions used for the aggregation.
    • setMachineParam : an option used to set the values of the parameters of the basic machines. setBasicParameter function should be fed to this argument.
    • setGridParam : an option used to set the values of the parameters of the grid search algorithm. The setGridParameter function should be fed to it.
    • silent : a logical value to silent all the messages.
  • Value:

    This function returns a list of the following objects:

    • opt_parameters : the observed optimal parameter.
    • add_parameters : other aditional parameters such as scaling options, parameters of kernel functions and the optimization methods used.
    • basic_machines : the list of basic machine object.
fit_parameter <- function(train_design, 
                          train_response,
                          scale_input = FALSE,
                          build_machine = TRUE,
                          machines = NULL, 
                          splits = 0.5, 
                          n_cv = 5,
                          inv_sigma = sqrt(.5),
                          alp = 2,
                          kernels = "gaussian",
                          setMachineParam = setBasicParameter(),
                          setGridParam = setGridParameter(),
                          silent = FALSE){
  kernels_lookup <- c("gaussian", "epanechnikov", "biweight", "triweight", "triangular", "naive")
  kernel_real <- kernels %>%
    map_chr(.f = ~ match.arg(.x, 
                             kernels_lookup))
  if(build_machine){
     mach2 <- generateMachines(train_input = train_design,
                               train_response = train_response,
                               scale_input = scale_input,
                               machines = machines,
                               splits = splits,
                               basicMachineParam = setMachineParam,
                               silent = silent)
  } else{
    mach2 <- list(predict2 = train_design,
                  models = colnames(train_design),
                  id2 = rep(TRUE, nrow(train_design)),
                  train_data = list(train_response = train_response,
                                    classes = unique(train_response)))                 
  }
  # distance matrix to compute loss function
  n_ker <- length(kernels)
  id_shuf <- NULL
  dist_all <- dist_matrix(basicMachines = mach2,
                          n_cv = n_cv)

  # Kernel functions
  # ================
  # Gaussian kernel
  gaussian_kernel <- function(.ep = .05,
                              .dist_matrix,
                              .train_response2,
                              .inv_sigma = sqrt(.5),
                              .alpha = 2){
    kern_fun <- function(x, id, D){
      tem0 <- as.matrix(exp(-(x*D)^(.alpha/2)*.inv_sigma^.alpha))
      y_hat <- map_dfc(.x = 1:ncol(tem0),
                       .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .train_response2[.dist_matrix$id_shuffle != id],
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
      return(mean(y_hat != .train_response2[.dist_matrix$id_shuffle == id]))
    }
    temp <- map2(.x = 1:.dist_matrix$n_cv, 
                 .y = .dist_matrix$dist, 
                 .f = ~ kern_fun(x = .ep, 
                                 id = .x, 
                                 D = .y))
    return(Reduce("+", temp)/.dist_matrix$n_cv)
  }

  # Epanechnikov
  epanechnikov_kernel <- function(.ep = .05,
                                  .dist_matrix,
                                  .train_response2){
    kern_fun <- function(x, id, D){
      tem0 <- as.matrix(1- x*D)
      tem0[tem0 < 0] = 0
      y_hat <- map_dfc(.x = 1:ncol(tem0),
                       .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .train_response2[.dist_matrix$id_shuffle != id],
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
      return(mean(y_hat != .train_response2[.dist_matrix$id_shuffle == id]))
    }
    temp <- map2(.x = 1:.dist_matrix$n_cv, 
                 .y = .dist_matrix$dist, 
                 .f = ~ kern_fun(x = .ep, 
                                 id = .x, 
                                 D = .y))
    return(Reduce("+", temp)/.dist_matrix$n_cv)
  }

  # Biweight
  biweight_kernel <- function(.ep = .05,
                              .dist_matrix,
                              .train_response2){
  kern_fun <- function(x, id, D){
    tem0 <- as.matrix(1- x*D)
    tem0[tem0 < 0] = 0
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                       .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .train_response2[.dist_matrix$id_shuffle != id],
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
      return(mean(y_hat != .train_response2[.dist_matrix$id_shuffle == id]))
    }
    temp <- map2(.x = 1:.dist_matrix$n_cv, 
                 .y = .dist_matrix$dist, 
                 .f = ~ kern_fun(x = .ep, 
                                 id = .x, 
                                 D = .y))
    return(Reduce("+", temp)/.dist_matrix$n_cv)
  }

  # Triweight
  triweight_kernel <- function(.ep = .05,
                               .dist_matrix,
                               .train_response2){
  kern_fun <- function(x, id, D){
    tem0 <- as.matrix(1- x*D)
    tem0[tem0 < 0] = 0
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                       .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .train_response2[.dist_matrix$id_shuffle != id],
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
      return(mean(y_hat != .train_response2[.dist_matrix$id_shuffle == id]))
    }
    temp <- map2(.x = 1:.dist_matrix$n_cv, 
                 .y = .dist_matrix$dist, 
                 .f = ~ kern_fun(x = .ep, 
                                 id = .x, 
                                 D = .y))
    return(Reduce("+", temp)/.dist_matrix$n_cv)
  }

  # Triangular
  triangular_kernel <- function(.ep = .05,
                                .dist_matrix,
                                .train_response2){
  kern_fun <- function(x, id, D){
    tem0 <- as.matrix(1- x*D)
    tem0[tem0 < 0] <- 0
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                       .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .train_response2[.dist_matrix$id_shuffle != id],
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
      return(mean(y_hat != .train_response2[.dist_matrix$id_shuffle == id]))
    }
    temp <- map2(.x = 1:.dist_matrix$n_cv, 
                 .y = .dist_matrix$dist, 
                 .f = ~ kern_fun(x = .ep, 
                                 id = .x, 
                                 D = .y))
    return(Reduce("+", temp)/.dist_matrix$n_cv)
  }

  # error function
  error_cv <- function(x, 
                       .dist_matrix = NULL,
                       .kernel_func = NULL,
                       .train_response2 = NULL){
    res <- .kernel_func(.ep = x,
                        .dist_matrix = .dist_matrix,
                        .train_response2 = .train_response2)
    return(res/n_cv)
  }

  # list of kernel functions
  list_funs <- list(gaussian = gaussian_kernel,
                    epanechnikov = epanechnikov_kernel,
                    biweight = biweight_kernel,
                    triweight = triweight_kernel,
                    triangular = triangular_kernel,
                    naive = epanechnikov_kernel)

  # error for all kernel functions
  error_func <- kernel_real %>%
    map(.f = ~ (\(x) error_cv(x, 
                              .dist_matrix = dist_all,
                              .kernel_func = list_funs[[.x]],
                              .train_response2 = train_response[mach2$id2])))
  names(error_func) <- kernel_real
  
  # Optimization
  parameters <- map(.x = kernel_real,
                    .f = ~ gridOptimizer(obj_fun = error_func[[.x]],
                                         setParameter = setGridParam,
                                         naive = .x == "naive",
                                         silent = silent,
                                         ker = .x))
  names(parameters) <- kernel_real
  return(list(opt_parameters = parameters,
              add_parameters = list(scale_input = scale_input,
                                    inv_sigma = inv_sigma,
                                    alp = alp),
              basic_machines = mach2))
}

Example.4: We approximate the smoothing parameter of Boston data.


df <- iris
train <- logical(nrow(df))
train[sample(length(train), floor(0.75*nrow(df)))] <- TRUE

param <- fit_parameter(train_design = df[train, 1:4],
                       train_response = df$Species[train],
                       machines = c("knn", "rf", "xgb"),
                       splits = .5,
                       n_cv = 3,
                       kernels = c("gaussian","biweight", "naive", "triangular"),
                       setMachineParam = setBasicParameter(k = 2:5,
                                                           ntree = c(1,3,5)*100,
                                                           nrounds_xgb = c(1,3,5)*100),
                       setGridParam = setGridParameter(min_val = 0.001,
                                                      max_val = 0.3,
                                                      n_val = 100),
                       silent = FALSE)

* Building basic machines ...
    ~ Progress: ... 33% ... 67% ... 100%
* Grid search for gaussian kernel...
    ~ observed parameter : 0.1640909
* Grid search for biweight kernel...
    ~ observed parameter : 0.09764646
* Grid search for triangular kernel...
    ~ observed parameter : 0.09764646

param$opt_parameters %>%
  map_dfc(.f = ~ .x$opt_param) %>%
  print

4 Prediction

The smoothing parameter obtained from the previous section can be used to make the final predictions.

4.1 Kernel functions

Several types of kernel functions used for the aggregation are defined in this section.

  • Argument:

    • .h : the value of the parameter used.
    • .y2 : the vector of response variable of the second part \(\mathcal{D}_{\ell}\) of the training data, which is used for the aggregation.
    • .distance : the distance matrix object obtained from dist_matrix function.
    • .kern : the string specifying the kernel function. By default, .kern = "gaussian".
    • .inv_sig, .alp : the parameters of exponential kernel function.
  • Value:

    This function returns the predicted classes of the aggregation method evaluated with the given parameter \(h>0\).

kernel_pred <- function(.h,
                        .y2, 
                        .distance, 
                        .kern = "gaussian",
                        .inv_sig = sqrt(.5), 
                        .alp = 2){
  dis <- as.matrix(.distance)
  # Kernel functions 
  # ================
  gaussian_kernel <- function(.ep,
                              .inv_sigma = .inv_sig,
                              .alpha = .alp){
    tem0 <- exp(- (.ep*dis)^(.alpha/2)*.inv_sig^.alpha)
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                     .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .y2,
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
    return(as.vector(y_hat))
  }

  # Epanechnikov
  epanechnikov_kernel <- function(.ep){
    tem0 <- 1- .ep*dis
    tem0[tem0 < 0] = 0
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                     .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .y2,
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
    return(as.vector(y_hat))
  }
  # Biweight
  biweight_kernel <- function(.ep){
    tem0 <- 1- .ep*dis
    tem0[tem0 < 0] = 0
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                     .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .y2,
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
    return(as.vector(y_hat))
  }

  # Triweight
  triweight_kernel <- function(.ep){
    tem0 <- 1- .ep*dis
    tem0[tem0 < 0] = 0
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                     .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .y2,
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
    return(as.vector(y_hat))
  }

  # Triangular
  triangular_kernel <- function(.ep){
    tem0 <- 1- .ep*dis
    tem0[tem0 < 0] <- 0
    y_hat <- map_dfc(.x = 1:ncol(tem0),
                     .f = (\(x_) tibble("{x_}" := tapply(tem0[, x_], 
                                                           INDEX = .y2,
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
    return(as.vector(y_hat))
  }
  # Naive
  naive_kernel <- function(.ep = NULL){
      y_hat <- map_dfc(.x = 1:ncol(dis),
                       .f = (\(x_) tibble("{x_}" := tapply(dis[, x_] == 0, 
                                                           INDEX = .y2,
                                                           FUN = sum)))) %>%
        map_chr(.f = (\(x) names(which.max(x))))
    return(as.vector(y_hat))
  }
  # Prediction
  kernel_list <- list(gaussian = gaussian_kernel,
                      epanechnikov = epanechnikov_kernel,
                      biweight = biweight_kernel,
                      triweight = triweight_kernel,
                      triangular = triangular_kernel,
                      naive = naive_kernel)
  res <- tibble(as.vector(kernel_list[[.kern]](.ep = .h)))
  names(res) <- .kern
  return(res)
}

4.2 Functions: predict_agg

This function takes the fit_parameter object (which contains the observed optimal parameter) to predict a new data.

  • Argument:

    • fitted_models : the object obtained from fit_parameter function.
    • new_data : the new testing data to be predicted.
    • test_response : the actual classes of testing data. It is optional. If it is given, the misclassification and accuracy evaluated on the testing data are also computed. By default, test_response = NULL.
    • naive : logical value specifying whether "naive" kernel is used or not.
  • Value:

    This function returns a list of the following objects:

    • fitted_aggregate : the predictions by the aggregation methods.
    • fitted_machine : the predictions given by all the basic machines.
    • mis_error, accuracy : the misclassification error and accuracy computed only if the test_reponse argument is not NULL.
# Prediction
predict_agg <- function(fitted_models,
                        new_data,
                        test_response = NULL,
                        naive = FALSE){
  opt_param <- fitted_models$opt_parameters
  add_param <- fitted_models$add_parameters
  basic_mach <- fitted_models$basic_machines
  kern0 <- names(opt_param)
  new_data_ <- new_data
  # if basic machines are built
  if(is.list(basic_mach$models)){
    mat_input <- as.matrix(basic_mach$train_data$train_input)
    if(add_param$scale_input){
      new_data_ <- scale(new_data, 
                         center = basic_mach$scale_min, 
                         scale = basic_mach$scale_max - basic_mach$scale_min)
    }
    if(is.matrix(new_data_)){
      mat_test <- new_data_
      df_test <- as_tibble(new_data_)
    } else {
      mat_test <- as.matrix(new_data_)
      df_test <- new_data_
    }
    
    # Prediction test by basic machines
    built_models <- basic_mach$models
    pred_test <- function(meth){
      if(meth == "knn"){
        pre <- 1:length(built_models[[meth]]) %>%
          map_dfc(.f = (\(k) tibble('{{k}}' := FNN::knn(train = mat_input[!basic_mach$id2,], 
                                                        test = mat_test, 
                                                        cl = basic_mach$train_data$train_response[!basic_mach$id2],
                                                        k = built_models[[meth]][[k]]))))
      }
      if(meth == "xgb"){
        pre <- 1:length(built_models[[meth]]) %>%
          map_dfc(.f = (\(k) tibble('{{k}}' := as.vector(basic_mach$train_data$classes[predict(built_models[[meth]][[k]], mat_test)]))))
      }
      if(meth == "adaboost"){
        pre <- 1:length(built_models[[meth]]) %>%
          map_dfc(.f = (\(k) tibble('{{k}}' := as.vector(predict.boosting(built_models[[meth]][[k]], as.data.frame(df_test))$class))))
      }
      if(!(meth %in% c("xgb", "knn", "adaboost"))){
        pre <- 1:length(built_models[[meth]]) %>%
          map_dfc(.f = (\(k) tibble('{{k}}' := as.vector(predict(built_models[[meth]][[k]], df_test, type = 'class')))))
      }
      colnames(pre) <- names(built_models[[meth]])
      return(pre)
    }
    pred_test_all <- names(built_models) %>%
      map_dfc(.f = pred_test)
  } else{
    pred_test_all <- new_data_
  }
  pred_test0 <- pred_test_all
  # Prediction train2
  pred_train_all <- basic_mach$predict2
  colnames(pred_test_all) <- colnames(pred_train_all)
  d_train <- dim(pred_train_all)
  d_test <- dim(pred_test_all)
  pred_test_mat <- as.matrix(pred_test_all)
  pred_train_mat <- as.matrix(pred_train_all)
  # Distance matrix
  dists <- 1:d_test[1] %>%
      map_dfc(.f = (\(id) tibble('{{id}}' := rowSums(sweep(pred_train_mat, 2, pred_test_mat[id,], FUN = "!=")))))
  prediction <- 1:length(kern0) %>% 
    map_dfc(.f = ~ kernel_pred(.h = opt_param[[kern0[.x]]]$opt_param,
                               .y2 = basic_mach$train_data$train_response[basic_mach$id2],
                               .distance = dists,
                               .kern = kern0[.x], 
                               .inv_sig = add_param$inv_sigma, 
                               .alp = add_param$alp))
  if(is.null(test_response)){
    return(list(fitted_aggregate = prediction,
           fitted_machine = pred_test_all))
  } else{
    error <- cbind(pred_test_all, prediction) %>%
      dplyr::mutate(y_test = test_response) %>%
      dplyr::summarise_all(.funs = ~ (. != y_test)) %>%
      dplyr::select(-y_test) %>%
      dplyr::summarise_all(.funs = ~ mean(.))
    return(list(fitted_aggregate = prediction,
                fitted_machine = pred_test0,
                mis_error = error,
                accuracy = 1 - error))
  }
}

Example.5 Aggregation on iris dataset.


pred <- predict_agg(param,
            new_data = df[!train, 1:4],
            test_response = df$Species[!train])
pred$mis_error

5 Function : KernelAggClassifier

This function puts together all the functions above and provides the desire result of the kernel-based combined classifiers.

KernelAggClassifier <- function(train_design, 
                           train_response,
                           test_design,
                           test_response = NULL,
                           scale_input = FALSE,
                           build_machine = TRUE,
                           machines = NULL, 
                           splits = 0.5, 
                           n_cv = 5,
                           inv_sigma = sqrt(.5),
                           alp = 2,
                           kernels = "gaussian",
                           setMachineParam = setBasicParameter(),
                           setGridParam = setGridParameter(),
                           silent = FALSE){
  # build machines + tune parameter
  fit_mod <- fit_parameter(train_design = train_design, 
                           train_response = train_response,
                           scale_input = scale_input,
                           build_machine = build_machine,
                           machines = machines, 
                           splits = splits, 
                           n_cv = n_cv,
                           inv_sigma = inv_sigma,
                           alp = alp,
                           kernels = kernels,
                           setMachineParam = setMachineParam,
                           setGridParam = setGridParam,
                           silent = silent)
  # prediction
  pred <- predict_agg(fitted_models = fit_mod,
                      new_data = test_design,
                      test_response = test_response)
  return(list(fitted_aggregate = pred$fitted_aggregate,
              fitted_machine = pred$fitted_machine,
              pred_train2 = fit_mod$basic_machines$predict2,
              opt_parameter = fit_mod$opt_parameters,
              mis_class = pred$mis_error,
              accuracy = pred$accuracy,
              kernels = kernels,
              ind_D2 = fit_mod$basic_machines$id2))
}

Example.6 A complete aggregation is implemented on iris dataset. Four types of basic machines, and four different kernel functions are used.


df1 <- iris
train <- logical(nrow(df1))
train[sample(length(train), floor(0.8*nrow(df1)))] <- TRUE

agg <- KernelAggClassifier(train_design = df1[train, 1:4],
                      train_response = df1$Species[train],
                      test_design = df1[!train, 1:4],
                      test_response = df1$Species[!train],
                      machines = c("knn", "rf", "xgb", "svm"),
                      splits = .5,
                      n_cv = 5,
                      kernels = c("gaussian","naive", "epan", "triang"),
                      setMachineParam = setBasicParameter(k = 2:5,
                                                          ntree = 1:3*100,
                                                          nrounds_xgb = 1:3*100),
                      setGridParam = setGridParameter(n_val = 100),
                      silent = FALSE)

* Building basic machines ...
    ~ Progress: ... 25% ... 50% ... 75% ... 100%
* Grid search for gaussian kernel...
    ~ observed parameter : 0.09596768
* Grid search for epanechnikov kernel...
    ~ observed parameter : 0.04041323
* Grid search for triangular kernel...
    ~ observed parameter : 0.04041323

  agg$accuracy

References



LS0tDQp0aXRsZTogIjxzcGFuIHN0eWxlPSdjb2xvcjogIzFDODFBQTsnPioqS2VybmVsLWJhc2VkIENvbWJpbmVkIENsYXNzaWZpY2F0aW9uIFJ1bGU8L3NwYW4+KioiDQphdXRob3I6ICI8c3BhbiBzdHlsZT0nY29sb3I6ICNENEE1MUM7Jz4qKipTb3RoZWEgSGFzKioqPC9zcGFuPiINCmRhdGU6ICcyMDIyLTA2LTAzJw0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIGNzczogaGlkZU91dHB1dC5jc3MNCiAgICBpbmNsdWRlczoNCiAgICAgIGluX2hlYWRlcjogaGlkZU91dHB1dC5zY3JpcHQNCiAgICBkZl9wcmludDogcGFnZWQNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRvYzogeWVzDQogICAgdG9jX2RlcHRoOiAnMicNCiAgICB0b2NkZXB0aDogMg0KICBodG1sX25vdGVib29rOg0KICAgIGNzczogaGlkZU91dHB1dC5jc3MNCiAgICBpbmNsdWRlczoNCiAgICAgIGluX2hlYWRlcjogaGlkZU91dHB1dC5zY3JpcHQNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRvYzogeWVzDQogICAgdG9jX2RlcHRoOiAyDQogICAgdG9jZGVwdGg6IDINCiAgcGRmX2RvY3VtZW50Og0KICAgIHRvYzogeWVzDQogICAgdG9jX2RlcHRoOiAnMicNCi0tLQ0KDQo8c3R5bGU+DQogIC5idG4gew0KICAgIGJvcmRlci13aWR0aDogMCAwcHggMHB4IDBweDsNCiAgICBmb250LXdlaWdodDogbm9ybWFsOw0KICAgIHRleHQtdHJhbnNmb3JtOiA7DQogIH0NCi5idG4tZGVmYXVsdCB7DQogIGNvbG9yOiAjMmVjYzcxOw0KICAgIGJhY2tncm91bmQtY29sb3I6ICNmZmZmZmY7DQogICAgYm9yZGVyLWNvbG9yOiAjZmZmZmZmOw0KfQ0KPC9zdHlsZT4NCg0KPCEtLSBDb2xvcnMNCmJsdWUgOiAjMUZBQUUzDQp5ZWxsb3cgOiAjRjBBRTE0DQpncmVlbiA6ICM1NEQzMTkgDQpyZWQgOiAjRTYxODBBDQotLT4NCg0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCiMgQ2hlY2sgaWYgcGFja2FnZSAiZm9udGF3ZXNvbWUiIGlzIGFscmVhZHkgaW5zdGFsbGVkIA0KDQpsb29rdXBfcGFja2FnZXMgPC0gaW5zdGFsbGVkLnBhY2thZ2VzKClbLDFdDQppZighKCJmb250YXdlc29tZSIgJWluJSBsb29rdXBfcGFja2FnZXMpKQ0KICBpbnN0YWxsLnBhY2thZ2VzKCJmb250YXdlc29tZSIpDQpgYGANCg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICMxRkFBRTM7Ij4mIzEyODI3MDs8dT4gSG93IHRvIGRvd25sb2FkICYgcnVuIHRoZSBjb2Rlcz88L3U+PC9zcGFuPnstfQ0KPT09DQoNCkFsbCB0aGUgc291cmNlIGNvZGVzIG9mIHRoZSBhZ2dyZWdhdGlvbiBtZXRob2RzIGFyZSBhdmFpbGFibGUgW2hlcmUgPHNwYW4gc3R5bGU9ImNvbG9yOiAjMDk3QkMxIj4gYHIgZm9udGF3ZXNvbWU6OmZhKCJnaXRodWIiKWA8L3NwYW4+XShodHRwczovL2dpdGh1Yi5jb20vaGFzc290aGVhL0FnZ3JlZ2F0aW9uTWV0aG9kcykuIFRvIHJ1biB0aGUgY29kZXMsIHlvdSBjYW4gPHNwYW4gc3R5bGU9ImNvbG9yOiAjMDk3QkMxIj5gY2xvbmVgPC9zcGFuPiB0aGUgcmVwb3NpdG9yeSBkaXJlY3RseSBvciBzaW1wbHkgbG9hZCB0aGUgPHNwYW4gc3R5bGU9ImNvbG9yOiAjMDk3QkMxIj5gUiBzY3JpcHRgPC9zcGFuPiBzb3VyY2UgZmlsZSBmcm9tIHRoZSByZXBvc2l0b3J5IHVzaW5nIFtkZXZ0b29sc10oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL2RldnRvb2xzL2luZGV4Lmh0bWwpIHBhY2thZ2UgaW4gPHNwYW4gc3R5bGU9ImNvbG9yOiAjMDI4N0Q4OyI+ICoqUnN0dWRpbyoqIDwvc3Bhbj4gYXMgZm9sbG93Og0KDQoxLiBJbnN0YWxsIFtkZXZ0b29sc10oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL2RldnRvb2xzL2luZGV4Lmh0bWwpIHBhY2thZ2UgdXNpbmcgY29tbWFuZDogDQoNCiAgICBgaW5zdGFsbC5wYWNrYWdlcygiZGV2dG9vbHMiKWANCg0KMi4gTG9hZGluZyB0aGUgc291cmNlIGNvZGVzIGZyb20gPHNwYW4gc3R5bGU9ImNvbG9yOiAjMDk3QkMxIj5HaXRIdWIgYHIgZm9udGF3ZXNvbWU6OmZhKCJnaXRodWIiKWA8L3NwYW4+IHJlcG9zaXRvcnkgdXNpbmcgYHNvdXJjZV91cmxgIGZ1bmN0aW9uIGJ5OiANCg0KICAgIGBkZXZ0b29sczo6c291cmNlX3VybCgiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2hhc3NvdGhlYS9BZ2dyZWdhdGlvbk1ldGhvZHMvbWFpbi9LZXJuZWxBZ2dDbGFzcy5SIilgDQoNCi0tLQ0KDQo+ICoqJiM5OTk4OyBOb3RlKio6IEFsbCBjb2RlcyBjb250YWluZWQgaW4gdGhpcyBgUm1hcmtkb3duYCBhcmUgYnVpbHQgd2l0aCByZWNlbnQgdmVyc2lvbiBvZiA8c3BhbiBzdHlsZT0iY29sb3I6ICMwOTdCQzEiPmByIGZvbnRhd2Vzb21lOjpmYSgici1wcm9qZWN0IilgPC9zcGFuPiAodmVyc2lvbiAkPiQgNC4xLCBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL2Jpbi93aW5kb3dzL2Jhc2UvKSkgYW5kIDxzcGFuIHN0eWxlPSJjb2xvcjogIzAyODdEODsiPiAqKlJzdHVkaW8qKiA8L3NwYW4+ICh2ZXJzaW9uID4gYDIwMjIuMDIuMis0ODVgLCBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vd3d3LnJzdHVkaW8uY29tL3Byb2R1Y3RzL3JzdHVkaW8vZG93bmxvYWQvI2Rvd25sb2FkKSkuIE5vdGUgYWxzbyB0aGF0IHRoZSBjb2RlIGNodWNrcyBhcmUgPHNwYW4gc3R5bGU9ImNvbG9yOiAjRTYxODBBOyI+aGlkZGVuPC9zcGFuPiBieSBkZWZhdWx0Lg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQiPiAqKlRvIHNlZSB0aGUgY29kZXMsIHlvdSBjYW46KiogPC9zcGFuPg0KDQotIGNsaWNrIG9uIHRoZSB0b3AtcmlnaHQgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNTREMzE5IDsiPkNvZGU8L3NwYW4+IGJ1dHRvbiBvZiB0aGUgcGFnZSwgdGhlbiBjaG9vc2UgKipTaG93IEFsbCBDb2RlKiogdG8gc2hvdyBhbGwgdGhlIGNvZGVzLCBvciANCi0gc2ltcGx5IGNsaWNrIG9uIHRoZSByaWdodC1jb3JuZXIgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNTREMzE5IDsiPkNvZGU8L3NwYW4+IGJ1dHRvbiBhdCBlYWNoIHNlY3Rpb24gdG8gc2hvdyB0aGUgY29kZXMgb2YgdGhhdCBzcGVjaWZpYyBzZWN0aW9uLg0KDQotLS0NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+PHU+IEFnZ3JlZ2F0aW9uIG1ldGhvZCAmIGltcG9ydGFudCBwYWNrYWdlcyA8L3U+PC9zcGFuPg0KPT09DQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogI0YwQUUxNDsiPjx1PiBBZ2dyZWdhdGlvbiBtZXRob2Q8L3U+PC9zcGFuPg0KLS0tDQoNClRoaXMgYFJtYXJrZG93bmAgcHJvdmlkZXMgdGhlIGltcGxlbWVudGF0aW9uIG9mIGEga2VybmVsLWJhc2VkIGNvbWJpbmVkIGNsYXNzaWZpY2F0aW9uIHJ1bGUgYnkgPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+W01vamlyc2hlaWJhbmkgKDIwMjApXShodHRwczovL3d3dy5zY2llbmNlZGlyZWN0LmNvbS9zY2llbmNlL2FydGljbGUvYWJzL3BpaS9TMDE2NzcxNTIwMDAwMDI0OT92aWElM0RpaHViKTwvc3Bhbj4uIExldCAkXG1hdGhjYWx7RH1fbj1ceyh4XzEseV8xKSwuLi4sKHhfbix5X24pXH0kIGJlIGEgdHJhaW5pbmcgZGF0YSBvZiBzaXplICRuJCwgd2hlcmUgdGhlIGlucHV0LW91dHB1dCBjb3VwbGVzICQoeF9pLHlfaSlcaW5cbWF0aGJie1J9XmRcdGltZXNcezEsXGRvdHMsTlx9JCBmb3IgYWxsICRpPTEsLi4uLG4kLCBhbmQgJE4kIGlzIHRoZSBudW1iZXIgb2YgY2xhc3Nlcy4gJFxtYXRoY2Fse0R9X3tufSQgaXMgZmlyc3QgcmFuZG9tbHkgcGFydGl0aW9uZWQgaW50byAkXG1hdGhjYWx7RH1fe2t9JCBhbmQgJFxtYXRoY2Fse0R9X3tcZWxsfSQgb2Ygc2l6ZSAkayQgYW5kICRcZWxsJCByZXNwZWN0aXZlbHkgc3VjaCB0aGF0ICRrK1xlbGw9biQuIFdlIGNvbnN0cnVjdCAkTSQgY2xhc3NpZmllcnMgKG1hY2hpbmVzKSAgJENfMSwuLi4sQ19NJCB1c2luZyBvbmx5ICRcbWF0aGNhbHtEfV97a30kLiBMZXQgJHtcYmYgQ30oeCk9KENfMSh4KSwuLi4sQ19NKHgpKV5UXGluXHsxLC4uLixOXH1eTSQgYmUgdGhlIHZlY3RvciBvZiBwcmVkaWN0ZWQgY2xhc3NlcyBvZiAkeFxpblxtYXRoYmJ7Un1eZCQsIHRoZSBrZXJuZWwtYmFzZWQgY29tYmluaW5nIG1ldGhvZCBldmFsdWF0ZWQgYXQgcG9pbnQgJHgkIGlzIGRlZmluZWQgYnkNCg0KJCQNCmdfbih7XGJmIEN9KHgpKT1rXio9XHRleHR7YXJnfVxtYXhfezFcbGVxIGtcbGVxIE59XHN1bV97aT0xfV57XGVsbH1LX2goZF97XGNhbCBIfSh7XGJmIEN9KHgpLHtcYmYgQ30oeF9pKSkpXG1hdGhiYnsxfV97XHt5X2k9a1x9fS4NCiQkDQpIZXJlLA0KDQotICRLOlxtYXRoYmJ7Un1fK1x0b1xtYXRoYmJ7Un1fKyQgaXMgYSBub24taW5jcmVhc2luZyBrZXJuZWwgZnVuY3Rpb24gd2l0aCAkS19oKHgpPUsoeC9oKSQgZm9yIHNvbWUgc21vb3RoaW5nIHBhcmFtZXRlciAkaD4wJCB0byBiZSB0dW5lZA0KLSAkZF97XGNhbCBIfSQgaXMgdGhlIEhhbW1pbmcgZGlzdGFuY2Ugb3IgdGhlIG51bWJlciBvZiBkaWZmZXJlbnQgY2xhc3NlcyBhbW9uZyAkTSQgY29vcmRpbmF0ZXMuDQoNCkluIHdvcmRzLCB0aGUgcHJlZGljdGVkIGNsYXNzIGlzIHRoZSBjbGFzcyBjb21wcmlzaW5nIGhlYXZpZXN0IHRvdGFsIHdlaWdodC4gRXZlbiB0aG91Z2ggdGhlIGZvcm11bGEgb2YgJGdfbiQgaXMgZGVmaW5lZCB1c2luZyBvbmx5IGRhdGEgcG9pbnRzIGluICRcbWF0aGNhbHtEfV97XGVsbH0kLCBpdCBkb2VzIGRlcGVuZCBvbiB0aGUgd2hvbGUgdHJhaW5pbmcgZGF0YSAkXG1hdGhjYWx7RH1fe259JCBhcyB0aGUgYmFzaWMgbWFjaGluZXMgJChDX2kpX3tpPTF9Xk0kIGFyZSBidWlsdCB1c2luZyAkXG1hdGhjYWx7RH1fe2t9JC4NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+IDx1PiBJbXBvcnRhbnQgcGFja2FnZXM8L3U+PC9zcGFuPg0KLS0tDQoNCldlIHByZXBhcmUgYWxsIHRoZSBuZWNlc3NhcnkgdG9vbHMgZm9yIHRoaXMgYFJtYXJrZG93bmAuIGBwYWNtYW5gIHBhY2thZ2UgYWxsb3dzIHVzIHRvIGxvYWQgKGlmIGV4aXN0cykgb3IgaW5zdGFsbCAoaWYgZG9lcyBub3QgZXhpc3QpIGFueSBhdmFpbGFibGUgcGFja2FnZXMgZnJvbSBbVGhlIENvbXByZWhlbnNpdmUgUiBBcmNoaXZlIE5ldHdvcmsgKENSQU4pXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy8pIG9mIDxzcGFuIHN0eWxlPSJjb2xvcjogIzA5N0JDMSI+YHIgZm9udGF3ZXNvbWU6OmZhKCJyLXByb2plY3QiKWA8L3NwYW4+LiANCg0KDQpgYGB7cn0NCiMgQ2hlY2sgaWYgcGFja2FnZSAicGFjbWFuIiBpcyBhbHJlYWR5IGluc3RhbGxlZCANCg0KbG9va3VwX3BhY2thZ2VzIDwtIGluc3RhbGxlZC5wYWNrYWdlcygpWywxXQ0KaWYoISgicGFjbWFuIiAlaW4lIGxvb2t1cF9wYWNrYWdlcykpDQogIGluc3RhbGwucGFja2FnZXMoInBhY21hbiIpDQoNCg0KIyBUbyBiZSBpbnN0YWxsZWQgb3IgbG9hZGVkDQpwYWNtYW46OnBfbG9hZChtYWdyaXR0cikNCnBhY21hbjo6cF9sb2FkKHRpZHl2ZXJzZSkNCg0KIyMgcGFja2FnZSBmb3IgImdlbmVyYXRlTWFjaGluZXMiDQpwYWNtYW46OnBfbG9hZCh0cmVlKQ0KcGFjbWFuOjpwX2xvYWQobm5ldCkNCnBhY21hbjo6cF9sb2FkKGUxMDcxKQ0KcGFjbWFuOjpwX2xvYWQocmFuZG9tRm9yZXN0KQ0KcGFjbWFuOjpwX2xvYWQoRk5OKQ0KcGFjbWFuOjpwX2xvYWQoeGdib29zdCkNCnBhY21hbjo6cF9sb2FkKGFkYWJhZykNCnBhY21hbjo6cF9sb2FkKGtlcmFzKQ0KcGFjbWFuOjpwX2xvYWQocHJhY21hKQ0KcGFjbWFuOjpwX2xvYWQobGF0ZXgyZXhwKQ0KcGFjbWFuOjpwX2xvYWQocGxvdGx5KQ0Kcm0obG9va3VwX3BhY2thZ2VzKQ0KYGBgDQoNCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+PHU+QmFzaWMgbWFjaGluZSBnZW5lcmF0b3I8L3U+PC9zcGFuPg0KPT09DQoNClRoaXMgc2VjdGlvbiBwcm92aWRlcyBmdW5jdGlvbnMgdG8gZ2VuZXJhdGUgYmFzaWMgbWFjaGluZXMgKGNsYXNzaWZpZXJzKSB0byBiZSBhZ2dyZWdhdGVkLg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQ7Ij48dT5GdW5jdGlvbjwvdT48L3NwYW4+IDogYHNldEJhc2ljUGFyYW1ldGVyYA0KLS0tLQ0KDQpUaGlzIGZ1bmN0aW9uIGFsbG93cyB1cyB0byBzZXQgdGhlIHZhbHVlcyBvZiBzb21lIGtleSBwYXJhbWV0ZXJzIG9mIHRoZSBiYXNpYyBtYWNoaW5lcy4NCg0KLSAqKkFyZ3VtZW50Kio6DQoNCiAgICAtIGBrYCA6IHRoZSBwYXJhbWV0ZXIgJGskIG9mICRrJE5OIChga25uYCkgY2xhc3NpZmllcnMgYW5kIHRoZSBkZWZhdWx0IHZhbHVlIGlzICRrPTEwJC4NCiAgICAtIGBudHJlZWAgOiB0aGUgbnVtYmVyIG9mIHRyZWVzIGluIHJhbmRvbSBmb3Jlc3QgKGByZmApLiBCeSBkZWZhdWx0LCBgbnRyZWUgPSAzMDBgLg0KICAgIC0gYG10cnlgIDogdGhlIG51bWJlciBvZiByYW5kb20gZmVhdHVyZXMgY2hvc2VuIGluIGVhY2ggc3BsaXQgb2YgcmFuZG9tIGZvcmVzdCBwcm9jZWR1cmUuIEJ5IGRlZmF1bHQsIGBtdHJ5ID0gTlVMTGAgYW5kIHRoZSBkZWZhdWx0IHZhbHVlIG9mIGBtdHJ5YCBvZiAqcmFuZG9tRm9yZXN0KiBmdW5jdGlvbiBmcm9tIFtyYW5kb21Gb3Jlc3RdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9yYW5kb21Gb3Jlc3QvaW5kZXguaHRtbCkgbGlicmFyeSBpcyB1c2VkLg0KICAgIC0gYGtlcl9zdm1gIDoga2VybmVsIG9wdGlvbiBpbiBTVk0uIEl0IHNob3VsZCBiZSBhIHN1YnNldCBvZiBgeyJsaW5lYXIiLCAicG9seW5vbWlhbCIsICJyYWRpYWwiLCAic2lnbW9pZCJ9YC4gQnkgZGVmYXVsdCwgYGtlcl9zdm0gPSAicmFkaWFsImAuDQogICAgLSBgZGVnX3N2bWAgOiBkZWdyZWUgb2YgcG9seW5vbWlhbCBrZXJuZWwgaW4gU1ZNLiBCeSBkZWZhdWx0LCBgZGVnX3N2bSA9IDNgLg0KICAgIC0gYGJyZWdfYm9vc3RgIDogQnJlZ21hbiBkaXZlcmdlbmNlIHVzZWQgaW4gYGJvb3N0aW5nYCBvZiBbbWFib29zdF0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL21hYm9vc3QvaW5kZXguaHRtbCkgcGFja2FnZS4gQnkgZGVmYXVsdCwgYGJyZWdfYm9vc3QgPSAiZW50cm9wImAgYW5kIEtMIGRpdmVyZ2VuY2UgaXMgdXNlZCwgcmVzdWx0aW5nIEFkYWJvb3N0LWxpa2UgYWxnb3JpdGhtLg0KICAgIC0gYGl0ZXJfYm9vc3RgIDogbnVtYmVyIG9mIGJvb3N0aW5nIGl0ZXJhdGlvbnMgdG8gcGVyZm9ybS4gRGVmYXVsdCBgaXRlcl9ib29zdCA9IDEwMGAuDQogICAgLSBgZXRhX3hnYmAgOiB0aGUgbGVhcm5pbmcgcmF0ZSAkXGV0YT4wJCBpbiBncmFkaWVudCBzdGVwIG9mICpleHRyZW1lIGdyYWRpZW50IGJvb3N0aW5nKiBtZXRob2QgKGB4Z2JgKSBvZiBbeGdib29zdF0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3hnYm9vc3QvaW5kZXguaHRtbCkgbGlicmFyeS4NCiAgICAtIGBucm91bmRzX3hnYmAgOiB0aGUgcGFyYW1ldGVyIGBucm91bmRzYCBpbmRpY2F0aW5nIHRoZSBtYXggbnVtYmVyIG9mIGJvb3N0aW5nIGl0ZXJhdGlvbnMuIEJ5IGRlZmF1bHQsIGBucm91bmRzX3hnYiA9IDEwMGAuDQogICAgLSBgZWFybHlfc3RvcF94Z2JgIDogdGhlIGVhcmx5IHN0b3BwaW5nIHJvdW5kIGNyaXRlcmlvbiBvZiBgeGdib29zdGAgZnVuY3Rpb24uIEJ5LCBkZWZhdWx0LCBgZWFybHlfc3RvcF94Z2IgPSBOVUxMYCBhbmQgdGhlIGVhcmx5IHN0b3BwaW5nIGZ1bmN0aW9uIGlzIG5vdCB0cmlnZ2VyZWQuDQogICAgLSBgbWF4X2RlcHRoX3hnYmAgOiBtYXhpbXVtIGRlcHRoIG9mIHRyZWVzIGNvbnN0cnVjdGVkIGluIGB4Z2Jvb3N0YC4gDQogICAgLSBgcGFyYW1feGdiYCA6IGxpc3Qgb2YgYWRkaXRpb25hbCBwYXJhbWV0ZXJzIG9mIGB4Z2Jvb3N0YCBjbGFzc2lmaWVyLiBCeSBkZWZhdWx0LCBgcGFyYW1feGdiID0gTlVMTGAuIEZvciBtb3JlIGluZm9ybWF0aW9uLCByZWFkIFtvbmxpbmUgZG9jdW1lbnRhdGlvbl0oaHR0cHM6Ly94Z2Jvb3N0LnJlYWR0aGVkb2NzLmlvL2VuL2xhdGVzdC9wYXJhbWV0ZXIuaHRtbCkuDQoNCi0gKipWYWx1ZSoqOiANCiAgICANCiAgICBUaGlzIGZ1bmN0aW9uIHJldHVybnMgYSAqbGlzdCogb2YgYWxsIHRoZSBwYXJhbWV0ZXJzIGdpdmVuIGluIGl0cyBhcmd1bWVudHMsIHRvIGJlIGZlZCB0byB0aGUgYGJhc2ljTWFjaGluZVBhcmFtYCBhcmd1bWVudCBvZiBmdW5jdGlvbiBgZ2VuZXJhdGVNYWNoaW5lc2AgZGVmaW5lZCBpbiB0aGUgbmV4dCBzZWN0aW9uLg0KDQotLS0NCg0KPiAqKiYjOTk5OTsgUmVtYXJrLjEqKjogDQpga2AsIGBudHJlZWAsIGBpdGVyX2Jvb3N0YCBhbmQgYG5yb3VuZHNfeGdiYCBjYW4gYmUgYSBzaW5nbGUgdmFsdWUgb3IgYSB2ZWN0b3IuIEluIG90aGVyIHdvcmRzLCBlYWNoIHR5cGUgb2YgbW9kZWxzIGNhbiBiZSBjb25zdHJ1Y3RlZCBzZXZlcmFsIHRpbWVzIGFjY29yZGluZyB0byB0aGUgdmFsdWVzIG9mIHRoZSBoeXBlcnBhcmFtZXRlcnMgKGEgc2luZ2xlIHZhbHVlIG9yIHZlY3RvcikuDQoNCi0tLQ0KDQpgYGB7cn0NCnNldEJhc2ljUGFyYW1ldGVyIDwtIGZ1bmN0aW9uKGsgPSAxMCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG50cmVlID0gMzAwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXRyeSA9IE5VTEwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrZXJfc3ZtID0gInJhZGlhbCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZWdfc3ZtID0gMywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1maW5hbF9ib29zdCA9IDUwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9vc3RyYXAgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZXRhX3hnYiA9IDEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnJvdW5kc194Z2IgPSAxMDAsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZWFybHlfc3RvcF94Z2IgPSBOVUxMLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4X2RlcHRoX3hnYiA9IDMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbV94Z2IgPSBOVUxMKXsNCiAgcmV0dXJuKGxpc3QoDQogICAgayA9IGssDQogICAgbnRyZWUgPSBudHJlZSwgDQogICAgbXRyeSA9IG10cnksIA0KICAgIGtlcl9zdm0gPSBrZXJfc3ZtLA0KICAgIGRlZ19zdm0gPSBkZWdfc3ZtLA0KICAgIG1maW5hbF9ib29zdCA9IG1maW5hbF9ib29zdCwNCiAgICBib29zdHJhcCA9IGJvb3N0cmFwLA0KICAgIGV0YV94Z2IgPSBldGFfeGdiLCANCiAgICBucm91bmRzX3hnYiA9IG5yb3VuZHNfeGdiLCANCiAgICBlYXJseV9zdG9wX3hnYiA9IGVhcmx5X3N0b3BfeGdiLA0KICAgIG1heF9kZXB0aF94Z2IgPSBtYXhfZGVwdGhfeGdiLA0KICAgIHBhcmFtX3hnYiA9IHBhcmFtX3hnYikNCiAgKQ0KfQ0KYGBgDQoNCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+PHU+RnVuY3Rpb248L3U+PC9zcGFuPiA6IGBnZW5lcmF0ZU1hY2hpbmVzYA0KLS0tDQoNClRoaXMgZnVuY3Rpb24gZ2VuZXJhdGVzIGFsbCB0aGUgYmFzaWMgbWFjaGluZXMgdG8gYmUgYWdncmVnYXRlZC4gDQoNCi0gKipBcmd1bWVudCoqOg0KDQogICAgLSBgdHJhaW5faW5wdXRgIDogYSBtYXRyaXggb3IgZGF0YSBmcmFtZSBvZiB0aGUgdHJhaW5pbmcgaW5wdXQgZGF0YS4NCiAgICAtIGB0cmFpbl9yZXNwb25zZWAgOiBhIHZlY3RvciBvZiB0cmFpbmluZyByZXNwb25zZSB2YXJpYWJsZSBjb3JyZXNwb25kaW5nIHRvIHRoZSBgdHJhaW5faW5wdXRgLg0KICAgIC0gYHNjYWxlX2lucHV0YCA6IGxvZ2ljYWwgdmFsdWUgc3BlY2lmeWluZyB3aGV0aGVyIHRvIHNjYWxlIHRoZSBpbnB1dCBkYXRhICh0byBiZSBiZXR3ZWVuICQwJCBhbmQgJDEkKSBvciBub3QuIEJ5IGRlZmF1bHQsIGBzY2FsZV9pbnB1dCA9IEZBTFNFYC4NCiAgICAtIGBtYWNoaW5lc2AgOiB0eXBlcyBvZiBiYXNpYyBtYWNoaW5lcyB0byBiZSBjb25zdHJ1Y3RlZC4gSXQgaXMgYSBzdWJzZXQgb2Yge2Aia25uImAsIGAidHJlZSJgLCBgInJmImAsIGAibG9naXQiYCwgYCJzdm0iYCwgYCJ4Z2IiYCwgYCJhZGFib29zdCJgfS4gQnkgZGVmYXVsdCwgYG1hY2hpbmVzID0gTlVMTGAgYW5kIGFsbCB0eXBlcyBvZiB0aGUgYmFzaWMgbWFjaGluZXMgYXJlIGJ1aWx0Lg0KICAgIC0gYHNwbGl0c2AgOiByZWFsIG51bWJlciBiZXR3ZWVuICQwJCBhbmQgJDEkIHNwZWNpZnlpbmcgdGhlIHByb3BvcnRpb24gb2YgdHJhaW5pbmcgZGF0YSB1c2VkIHRvIHRyYWluIHRoZSBiYXNpYyBtYWNoaW5lcyAoJFxtYXRoY2Fse0R9X2skKS4gVGhlIHJlbWFpbmluZyBwcm9wb3J0aW9uIG9mICgkMS0kIGBzcGxpdHNgKSBpcyB1c2VkIGZvciB0aGUgYWdncmVnYXRpb24gKCRcbWF0aGNhbHtEfV97XGVsbH0kKS4gQnkgZGVmYXVsdCwgYHNwbGl0cyA9IDAuNWAuDQogICAgLSBgYmFzaWNNYWNoaW5lUGFyYW1gIDogdGhlIG9wdGlvbiB1c2VkIHRvIHNldHVwIHRoZSB2YWx1ZXMgb2YgcGFyYW1ldGVycyBvZiBlYWNoIG1hY2hpbmVzLiBPbmUgc2hvdWxkIGZlZWQgdGhlIGZ1bmN0aW9uIGBzZXRCYXNpY1BhcmFtZXRlcigpYCBkZWZpbmVkIGFib3ZlIHRvIHRoaXMgYXJndW1lbnQuDQogICAgLSBgc2lsZW50YCA6IGEgbG9naWNhbCB2YWx1ZSBzcGVjaWZ5aW5nIHdoZXRoZXIgb3Igbm90IHByb2dyZXNzaW5nIG1lc3NhZ2VzIHNob3VsZCBiZSBwcmludGVkLiBCZSBkZWZhdWx0LCBgc2lsZW50ID0gRkFMU0VgIGFuZCB0aGUgcHJvZ3Jlc3Mgb2YgdGhlIGFsZ29yaXRobSBpcyBwcmludGVkLg0KICAgIA0KICAgIA0KLSAqKlZhbHVlKio6IA0KDQogICAgVGhpcyBmdW5jdGlvbiByZXR1cm5zIGEgbGlzdCBvZiB0aGUgZm9sbG93aW5nIG9iamVjdHMuDQoNCiAgICAtIGBwcmVkaWN0MmAgOiB0aGUgcHJlZGljdGlvbnMgb2YgdGhlIHJlbWFpbmluZyBwYXJ0ICgkXG1hdGhjYWx7RH1fe1xlbGx9JCkgb2YgdGhlIHRyYWluaW5nIGRhdGEgdXNlZCBmb3IgdGhlIGFnZ3JlZ2F0aW9uLg0KICAgIC0gYG1vZGVsc2AgOiBhbGwgdGhlIGNvbnN0cnVjdGVkIGJhc2ljIG1hY2hpbmVzIChpdCBjb250YWlucyBvbmx5IHRoZSB2YWx1ZXMgb2YgcHJhcGV0ZXIgJGskIGZvciBga25uYCkuDQogICAgLSBgaWQyYCA6IGEgbG9naWNhbCB2ZWN0b3Igb2Ygc2l6ZSBlcXVhbHMgdG8gdGhlIG51bWJlciBvZiBsaW5lcyBvZiB0aGUgdHJhaW5pbmcgZGF0YSBpbmRpY2F0aW5nIHRoZSBsb2NhdGlvbiBvZiB0aGUgcG9pbnRzIHVzZWQgdG8gYnVpbGQgdGhlIGJhc2ljIG1hY2hpbmVzIChgRkFMU0VgKSBhbmQgdGhlIHJlbWFpbmluZyBvbmVzIChgVFJVRWApLg0KICAgIC0gYHRyYWluX2RhdGFgIDogYSBsaXN0IG9mOg0KICAgICAgICAtIGB0cmFpbl9pbnB1dGAgOiB0aGUgdHJhaW5uaWcgaW5wdXQgZGF0YS4NCiAgICAgICAgLSBgdHJhaW5fcmVzcG9uc2VgIDogdGhlIHRyYWluaW5nIHJlc3BvbnNlIHZhcmlhYmxlLg0KICAgICAgICAtIGBjbGFzc2VzYCA6IHRoZSBjbGFzc2VzICh1bmlxdWUpIG9mIHJlc3BvbnNlIHZhcmlhYmxlLg0KICAgIC0gYHNjYWxlX21heGAsIGBzY2FsZV9taW5gIDogaWYgdGhlIGFyZ3VtZW50IGBzY2FsZV9pbnB1dCA9IFRSVUVgLCB0aGUgbWF4aW11biBhbmQgbWluaW11biB2YWx1ZXMgb2YgYWxsIGNvbHVtbnMgYXJlIGNvbnRhaW5lZCBpbiB0aGVzZSB2ZWN0b3JzLiANCg0KDQp0cmFpbl9kYXRhID0gbGlzdCh0cmFpbl9pbnB1dCA9IHRyYWluX2lucHV0X3NjYWxlLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbl9yZXNwb25zZSA9IHRyYWluX3Jlc3BvbnNlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzZXMgPSBjbGFzc194Z2IpDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgDQotLS0NCg0KPiAqKiYjOTk5ODsgTm90ZSoqOiAqWW91IG1heSBuZWVkIHRvIG1vZGlmeSB0aGUgZnVuY3Rpb24gYWNjb3JkaW5nbHkgaWYgeW91IHdhbnQgdG8gYnVpbGQgZGlmZmVyZW50IHR5cGVzIG9mIGJhc2ljIG1hY2hpbmVzKi4NCg0KLS0tDQoNCmBgYHtyfQ0KZ2VuZXJhdGVNYWNoaW5lcyA8LSBmdW5jdGlvbih0cmFpbl9pbnB1dCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluX3Jlc3BvbnNlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYWNoaW5lcyA9IE5VTEwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNwbGl0cyA9IDAuNSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJhc2ljTWFjaGluZVBhcmFtID0gc2V0QmFzaWNQYXJhbWV0ZXIoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2lsZW50ID0gRkFMU0Upew0KICBrIDwtIGJhc2ljTWFjaGluZVBhcmFtJGsgDQogIG50cmVlIDwtIGJhc2ljTWFjaGluZVBhcmFtJG50cmVlIA0KICBtdHJ5IDwtIGJhc2ljTWFjaGluZVBhcmFtJG10cnkNCiAga2VyX3N2bSA8LSBiYXNpY01hY2hpbmVQYXJhbSRrZXJfc3ZtDQogIGRlZ19zdm0gPC0gYmFzaWNNYWNoaW5lUGFyYW0kZGVnX3N2bQ0KICBtZmluYWxfYm9vc3QgPSBiYXNpY01hY2hpbmVQYXJhbSRtZmluYWxfYm9vc3QNCiAgYm9vc3RyYXAgPSBiYXNpY01hY2hpbmVQYXJhbSRib29zdHJhcA0KICBldGFfeGdiIDwtIGJhc2ljTWFjaGluZVBhcmFtJGV0YV94Z2IgDQogIG5yb3VuZHNfeGdiIDwtIGJhc2ljTWFjaGluZVBhcmFtJG5yb3VuZHNfeGdiDQogIGVhcmx5X3N0b3BfeGdiIDwtIGJhc2ljTWFjaGluZVBhcmFtJGVhcmx5X3N0b3BfeGdiDQogIG1heF9kZXB0aF94Z2IgPC0gYmFzaWNNYWNoaW5lUGFyYW0kbWF4X2RlcHRoX3hnYg0KICBwYXJhbV94Z2IgPC0gYmFzaWNNYWNoaW5lUGFyYW0kcGFyYW1feGdiDQogIGNsYXNzX3hnYiA8LSB1bmlxdWUodHJhaW5fcmVzcG9uc2UpDQogIG51bWJlck9mQ2xhc3NlcyA8LSBsZW5ndGgoY2xhc3NfeGdiKQ0KICBpZihpcy5udWxsKHBhcmFtX3hnYikpew0KICAgIHBhcmFtX3hnYiA8LSBsaXN0KCJvYmplY3RpdmUiID0gIm11bHRpOnNvZnRtYXgiLA0KICAgICAgICAgICAgICAgICAgICAgICJldmFsX21ldHJpYyIgPSAibWxvZ2xvc3MiLA0KICAgICAgICAgICAgICAgICAgICAgICJudW1fY2xhc3MiID0gbnVtYmVyT2ZDbGFzc2VzKzEpDQogIH0NCiAgDQogICMgUGFja2FnZXMNCiAgcGFjbWFuOjpwX2xvYWQobm5ldCkNCiAgcGFjbWFuOjpwX2xvYWQoZTEwNzEpDQogIHBhY21hbjo6cF9sb2FkKHRyZWUpDQogIHBhY21hbjo6cF9sb2FkKHJhbmRvbUZvcmVzdCkNCiAgcGFjbWFuOjpwX2xvYWQoRk5OKQ0KICBwYWNtYW46OnBfbG9hZCh4Z2Jvb3N0KQ0KICBwYWNtYW46OnBfbG9hZChtYWJvb3N0KQ0KICANCiAgIyBQcmVwYXJpbmcgZGF0YQ0KICBpbnB1dF9uYW1lcyA8LSBjb2xuYW1lcyh0cmFpbl9pbnB1dCkNCiAgaW5wdXRfc2l6ZSA8LSBkaW0odHJhaW5faW5wdXQpDQogIGRmX2lucHV0IDwtIHRyYWluX2lucHV0X3NjYWxlIDwtIHRyYWluX2lucHV0DQogIGlmKHNjYWxlX2lucHV0KXsNCiAgICBtYXhzIDwtIG1hcF9kYmwoLnggPSBkZl9pbnB1dCwgLmYgPSBtYXgpDQogICAgbWlucyA8LSBtYXBfZGJsKC54ID0gZGZfaW5wdXQsIC5mID0gbWluKQ0KICAgIHRyYWluX2lucHV0X3NjYWxlIDwtIHNjYWxlKHRyYWluX2lucHV0LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjZW50ZXIgPSBtaW5zLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZSA9IG1heHMgLSBtaW5zKQ0KICB9DQogIGlmKGlzLm1hdHJpeCh0cmFpbl9pbnB1dF9zY2FsZSkpew0KICAgIGRmX2lucHV0IDwtIGFzX3RpYmJsZSh0cmFpbl9pbnB1dF9zY2FsZSkNCiAgICBtYXRyaXhfaW5wdXQgPC0gdHJhaW5faW5wdXRfc2NhbGUNCiAgfSBlbHNlew0KICAgIGRmX2lucHV0IDwtIHRyYWluX2lucHV0X3NjYWxlDQogICAgbWF0cml4X2lucHV0IDwtIGFzLm1hdHJpeCh0cmFpbl9pbnB1dF9zY2FsZSkNCiAgfQ0KICANCiAgIyBNYWNoaW5lcw0KICBzdm1fbWFjaGluZSA8LSBmdW5jdGlvbih4LCBwYSA9IE5VTEwpew0KICAgIG1vZCA8LSBzdm0oeCA9IGRmX3RyYWluX3gxLCANCiAgICAgICAgICAgICAgIHkgPSB0cmFpbl95MSwNCiAgICAgICAgICAgICAgIGtlcm5lbCA9IGtlcl9zdm0sDQogICAgICAgICAgICAgICBkZWdyZWUgPSBkZWdfc3ZtLA0KICAgICAgICAgICAgICAgdHlwZSA9ICJDLWNsYXNzaWZpY2F0aW9uIikNCiAgICByZXMgPC0gcHJlZGljdChtb2QsIA0KICAgICAgICAgICAgICAgICAgIG5ld2RhdGEgPSB4KQ0KICAgIHJldHVybihsaXN0KHByZWQgPSByZXMsDQogICAgICAgICAgICAgICAgbW9kZWwgPSBtb2QpKQ0KICB9DQogIHRyZWVfbWFjaGluZSA8LSBmdW5jdGlvbih4LCBwYSA9IE5VTEwpIHsNCiAgICBtb2QgPC0gdHJlZShhcy5mb3JtdWxhKHBhc3RlKCJ0cmFpbl95MX4iLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlKGlucHV0X25hbWVzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlcCA9ICIiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbGxhcHNlID0gIisiKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xsYXBzZSA9ICIiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlcCA9ICIiKSksIA0KICAgICAgICAgICAgICAgIGRhdGEgPSBkZl90cmFpbl94MSkNCiAgICByZXMgPC0gcHJlZGljdChtb2QsIHgsIHR5cGUgPSAnY2xhc3MnKQ0KICAgIHJldHVybihsaXN0KHByZWQgPSByZXMsDQogICAgICAgICAgICAgICAgbW9kZWwgPSBtb2QpKQ0KICB9DQogIGtubl9tYWNoaW5lIDwtIGZ1bmN0aW9uKHgsIGswKSB7DQogICAgbW9kIDwtIGtubih0cmFpbiA9IG1hdHJpeF90cmFpbl94MSwgDQogICAgICAgICAgICAgICAgICAgdGVzdCA9IHgsIA0KICAgICAgICAgICAgICAgICAgIGNsID0gdHJhaW5feTEsIA0KICAgICAgICAgICAgICAgICAgIGsgPSBrMCkNCiAgICByZXR1cm4obGlzdChwcmVkID0gbW9kLA0KICAgICAgICAgICAgICAgIG1vZGVsID0gazApKQ0KICB9DQogIFJGX21hY2hpbmUgPC0gZnVuY3Rpb24oeCwgbnRyZWUwKSB7DQogICAgaWYoaXMubnVsbChtdHJ5KSl7DQogICAgICBtb2QgPC0gcmFuZG9tRm9yZXN0KHggPSBkZl90cmFpbl94MSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIHkgPSB0cmFpbl95MSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIG50cmVlID0gbnRyZWUwKQ0KICAgIH1lbHNlew0KICAgICAgbW9kIDwtIHJhbmRvbUZvcmVzdCh4ID0gZGZfdHJhaW5feDEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICB5ID0gdHJhaW5feTEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBudHJlZSA9IG50cmVlMCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIG10cnkgPSBtdHJ5KQ0KICAgIH0NCiAgICByZXMgPC0gYXMudmVjdG9yKHByZWRpY3QobW9kLCB4KSkNCiAgICByZXR1cm4obGlzdChwcmVkID0gcmVzLA0KICAgICAgICAgICAgICAgIG1vZGVsID0gbW9kKSkNCiAgfQ0KICB4Z2JfbWFjaGluZSA8LSBmdW5jdGlvbih4LCBucm91bmRzX3hnYjApew0KICAgIG1vZCA8LSB4Z2Jvb3N0KGRhdGEgPSBtYXRyaXhfdHJhaW5feDEsDQogICAgICAgICAgICAgICAgICAgbGFiZWwgPSB0cmFpbl95MSwNCiAgICAgICAgICAgICAgICAgICBwYXJhbXMgPSBwYXJhbV94Z2IsDQogICAgICAgICAgICAgICAgICAgZXRhID0gZXRhX3hnYiwNCiAgICAgICAgICAgICAgICAgICBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSBlYXJseV9zdG9wX3hnYiwNCiAgICAgICAgICAgICAgICAgICBtYXhfZGVwdGggPSBtYXhfZGVwdGhfeGdiLA0KICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSAwLA0KICAgICAgICAgICAgICAgICAgIG5yb3VuZHMgPSBucm91bmRzX3hnYjApDQogICAgcmVzIDwtIGNsYXNzX3hnYltwcmVkaWN0KG1vZCwgeCldDQogICAgcmV0dXJuKGxpc3QocHJlZCA9IHJlcywNCiAgICAgICAgICAgICAgICBtb2RlbCA9IG1vZCkpDQogIH0NCiAgYWRhX21hY2hpbmUgPC0gZnVuY3Rpb24oeCwgbWZpbmFsMCl7DQogICAgZGF0YV90ZW0gPC0gY2JpbmQoZGZfdHJhaW5feDEsICJ0YXJnZXQiID0gdHJhaW5feTEpDQogICAgbW9kXyA8LSBib29zdGluZyh0YXJnZXQgfiAuLCANCiAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhX3RlbSwNCiAgICAgICAgICAgICAgICAgICAgIG1maW5hbCA9IG1maW5hbDAsDQogICAgICAgICAgICAgICAgICAgICBib29zID0gYm9vc3RyYXApDQogICAgcmVzIDwtIHByZWRpY3QuYm9vc3RpbmcobW9kXywgDQogICAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YSA9IGFzLmRhdGEuZnJhbWUoeCkpDQogICAgcmV0dXJuKGxpc3QocHJlZCA9IHJlcyRjbGFzcywNCiAgICAgICAgICAgICAgICBtb2RlbCA9IG1vZF8pKQ0KICB9DQogIGxvZ2l0X21hY2hpbmUgPC0gZnVuY3Rpb24oeCwgcGEgPSBOVUxMKXsNCiAgICBtb2QgPC0gbXVsdGlub20oYXMuZm9ybXVsYShwYXN0ZSgidHJhaW5feTF+IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZShpbnB1dF9uYW1lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZXAgPSAiIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xsYXBzZSA9ICIrIiksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29sbGFwc2UgPSAiIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZXAgPSAiIikpLCANCiAgICAgICAgICAgICAgICBkYXRhID0gZGZfdHJhaW5feDEsDQogICAgICAgICAgICAgICAgdHJhY2UgPSBGQUxTRSkNCiAgICByZXMgPC0gcHJlZGljdChtb2QsIA0KICAgICAgICAgICAgICAgICAgIG5ld2RhdGEgPSB4KQ0KICAgIHJldHVybihsaXN0KHByZWQgPSByZXMsDQogICAgICAgICAgICAgICAgbW9kZWwgPSBtb2QpKQ0KICB9DQogICMgQWxsIG1hY2hpbmVzDQogIGFsbF9tYWNoaW5lcyA8LSBsaXN0KGtubiA9IGtubl9tYWNoaW5lLCANCiAgICAgICAgICAgICAgICAgICAgICAgdHJlZSA9IHRyZWVfbWFjaGluZSwgDQogICAgICAgICAgICAgICAgICAgICAgIHJmID0gUkZfbWFjaGluZSwNCiAgICAgICAgICAgICAgICAgICAgICAgbG9naXQgPSBsb2dpdF9tYWNoaW5lLA0KICAgICAgICAgICAgICAgICAgICAgICBzdm0gPSBzdm1fbWFjaGluZSwNCiAgICAgICAgICAgICAgICAgICAgICAgYWRhYm9vc3QgPSBhZGFfbWFjaGluZSwNCiAgICAgICAgICAgICAgICAgICAgICAgeGdiID0geGdiX21hY2hpbmUpDQogICMgQWxsIHBhcmFtZXRlcnMNCiAgYWxsX3BhcmFtZXRlcnMgPC0gbGlzdChrbm4gPSBrLCANCiAgICAgICAgICAgICAgICAgICAgICAgICB0cmVlID0gMSwNCiAgICAgICAgICAgICAgICAgICAgICAgICByZiA9IG50cmVlLA0KICAgICAgICAgICAgICAgICAgICAgICAgIGxvZ2l0ID0gTkEsDQogICAgICAgICAgICAgICAgICAgICAgICAgc3ZtID0gZGVnX3N2bSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBhZGFib29zdCA9IG1maW5hbF9ib29zdCwNCiAgICAgICAgICAgICAgICAgICAgICAgICB4Z2IgPSBucm91bmRzX3hnYikNCiAgbG9va3VwX21hY2hpbmVzIDwtIGMoImtubiIsICJ0cmVlIiwgInJmIiwgImxvZ2l0IiwgInN2bSIsICJ4Z2IiLCAiYWRhYm9vc3QiKQ0KICBpZihpcy5udWxsKG1hY2hpbmVzKSl7DQogICAgbWFjaCA8LSBsb29rdXBfbWFjaGluZXMNCiAgfWVsc2V7DQogICAgbWFjaCA8LSBtYXBfY2hyKC54ID0gbWFjaGluZXMsDQogICAgICAgICAgICAgICAgICAgIC5mID0gfiBtYXRjaC5hcmcoLngsIGxvb2t1cF9tYWNoaW5lcykpDQogIH0NCiAgIyBFeHRyYWN0aW5nIGRhdGENCiAgTSA8LSBsZW5ndGgobWFjaCkNCiAgc2l6ZV9EMSA8LSBmbG9vcihzcGxpdHMqaW5wdXRfc2l6ZVsxXSkNCiAgaWRfRDEgPC0gbG9naWNhbChpbnB1dF9zaXplWzFdKQ0KICBpZF9EMVtzYW1wbGUoaW5wdXRfc2l6ZVsxXSwgc2l6ZV9EMSldIDwtIFRSVUUNCg0KICBkZl90cmFpbl94MSA8LSBkZl9pbnB1dFtpZF9EMSxdDQogIG1hdHJpeF90cmFpbl94MSA8LSBtYXRyaXhfaW5wdXRbaWRfRDEsXQ0KICB0cmFpbl95MSA8LSB0cmFpbl9yZXNwb25zZVtpZF9EMV0NCiAgZGZfdHJhaW5feDIgPC0gZGZfaW5wdXRbIWlkX0QxLF0NCiAgbWF0cml4X3RyYWluX3gyIDwtIG1hdHJpeF9pbnB1dFshaWRfRDEsXQ0KICANCiAgIyBGdW5jdGlvbiB0byBleHRyYWN0IGRmIGFuZCBtb2RlbCBmcm9tICdtYXAnIGZ1bmN0aW9uDQogIGV4dHJfZGYgPC0gZnVuY3Rpb24oeCwgbmFtLCBpZCl7DQogICAgcmV0dXJuKHRpYmJsZSgie25hbX1fe2lkfSIgOj0gYXMudmVjdG9yKHByZWRfbVtbeF1dJHByZWQpKSkNCiAgfQ0KICBleHRyX21vZCA8LSBmdW5jdGlvbih4LCBpZCl7DQogICAgcmV0dXJuKHByZWRfbVtbeF1dJG1vZGVsKQ0KICB9DQogIA0KICBwcmVkX0QyIDwtIGMoKQ0KICBhbGxfbW9kIDwtIGMoKQ0KICBpZighc2lsZW50KXsNCiAgICBjYXQoIlxuKiBCdWlsZGluZyBiYXNpYyBtYWNoaW5lcyAuLi5cbiIpDQogICAgY2F0KCJcdH4gUHJvZ3Jlc3M6IikNCiAgfQ0KICBmb3IobSBpbiAxOk0pew0KICAgIGlmKG1hY2hbbV0gJWluJSBjKCJrbm4iLCAieGdiIikpew0KICAgICAgeDBfdGVzdCA8LSAgbWF0cml4X3RyYWluX3gyDQogICAgfSBlbHNlIHsNCiAgICAgIHgwX3Rlc3QgPC0gZGZfdHJhaW5feDINCiAgICB9DQogICAgaWYoaXMubnVsbChhbGxfcGFyYW1ldGVyc1tbbWFjaFttXV1dKSl7DQogICAgICBwYXJhXyA8LSAxDQogICAgfWVsc2V7DQogICAgICBwYXJhXyA8LSBhbGxfcGFyYW1ldGVyc1tbbWFjaFttXV1dDQogICAgfQ0KICAgIHByZWRfbSA8LSAgbWFwKHBhcmFfLA0KICAgICAgICAgICAgICAgICAgIC5mID0gfiBhbGxfbWFjaGluZXNbW21hY2hbbV1dXSh4MF90ZXN0LCAueCkpDQogICAgdGVtMCA8LSBpbWFwX2RmYygueCA9IDE6bGVuZ3RoKHBhcmFfKSwgDQogICAgICAgICAgICAgICAgICAgICAuZiA9IH4gZXh0cl9kZih4ID0gLngsIG5hbSA9IG1hY2hbbV0sIGlkID0gcGFyYV9bLnhdKSkNCiAgICB0ZW0xIDwtIG1hcCgueCA9IDE6bGVuZ3RoKHBhcmFfKSwgDQogICAgICAgICAgICAgICAgIC5mID0gZXh0cl9tb2QpDQogICAgbmFtZXModGVtMCkgPC0gbmFtZXModGVtMSkgPC0gcGFzdGUwKG1hY2hbbV0sIDE6bGVuZ3RoKHBhcmFfKSkNCiAgICBwcmVkX0QyIDwtIGJpbmRfY29scyhwcmVkX0QyLCBhc190aWJibGUodGVtMCkpDQogICAgYWxsX21vZFtbbWFjaFttXV1dIDwtIHRlbTENCiAgICBpZighc2lsZW50KXsNCiAgICAgIGNhdCgiIC4uLiAiLCByb3VuZChtL00sIDIpKjEwMEwsIiUiLCBzZXAgPSAiIikNCiAgICB9DQogIH0NCiAgaWYoc2NhbGVfaW5wdXQpew0KICAgIHJldHVybihsaXN0KHByZWRpY3QyID0gcHJlZF9EMiwNCiAgICAgICAgICAgICAgICBtb2RlbHMgPSBhbGxfbW9kLA0KICAgICAgICAgICAgICAgIGlkMiA9ICFpZF9EMSwNCiAgICAgICAgICAgICAgICB0cmFpbl9kYXRhID0gbGlzdCh0cmFpbl9pbnB1dCA9IHRyYWluX2lucHV0X3NjYWxlLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbl9yZXNwb25zZSA9IHRyYWluX3Jlc3BvbnNlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzZXMgPSBjbGFzc194Z2IpLA0KICAgICAgICAgICAgICAgIHNjYWxlX21heCA9IG1heHMsDQogICAgICAgICAgICAgICAgc2NhbGVfbWluID0gbWlucykpDQogIH0gZWxzZXsNCiAgICByZXR1cm4obGlzdChwcmVkaWN0MiA9IHByZWRfRDIsDQogICAgICAgICAgICAgICAgbW9kZWxzID0gYWxsX21vZCwNCiAgICAgICAgICAgICAgICBpZDIgPSAhaWRfRDEsDQogICAgICAgICAgICAgICAgdHJhaW5fZGF0YSA9IGxpc3QodHJhaW5faW5wdXQgPSB0cmFpbl9pbnB1dF9zY2FsZSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJhaW5fcmVzcG9uc2UgPSB0cmFpbl9yZXNwb25zZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc2VzID0gY2xhc3NfeGdiKSkpDQogIH0NCn0NCmBgYA0KDQotLS0NCg0KPiAqKkV4YW1wbGUuMSoqOiBJbiB0aGlzIGV4YW1wbGUsIHRoZSBtZXRob2QgaXMgaW1wbGVtZW50ZWQgb24gYGlyaXNgIGRhdGFzZXQuIEFsbCB0eXBlcyBvZiBiYXNpYyBtYWNoaW5lcyBhcmUgYnVpbHQgb24gdGhlIGZpcnN0IHBhcnQgb2YgdGhlIHRyYWluaW5nIGRhdGEgKCRcbWF0aGNhbHtEfV97a30kKSwgYW5kIHRoZSAqYWNjdXJhY3kqIGV2YWx1YXRlZCBvbiB0aGUgc2Vjb25kIHBhcnQgb2YgdGhlIHRyYWluaW5nIGRhdGEgKCRcbWF0aGNhbHtEfV97XGVsbH0kKSB1c2VkIGZvciBhZ2dyZWdhdGlvbikgYXJlIHJlcG9ydGVkLg0KDQotLS0NCg0KYGBge3J9DQpkZiA8LSBpcmlzDQpiYXNpY19tYWNoaW5lcyA8LSBnZW5lcmF0ZU1hY2hpbmVzKHRyYWluX2lucHV0ID0gZGZbLDE6NF0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluX3Jlc3BvbnNlID0gZGYkU3BlY2llcywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfaW5wdXQgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYWNoaW5lcyA9IE5VTEwsICNjKCJrbm4iLCAidHJlZSIsICJyZiIsICJsb2dpdCIsICJzdm0iLCAieGdiIikNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYmFzaWNNYWNoaW5lUGFyYW0gPSBzZXRCYXNpY1BhcmFtZXRlcihudHJlZSA9IDEwOjIwICogMjUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgayA9IGMoMjoxMCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWZpbmFsX2Jvb3N0ID0gMTApKQ0KYmFzaWNfbWFjaGluZXMkcHJlZGljdDIgJT4lDQogIHN3ZWVwKDEsIGRmW2Jhc2ljX21hY2hpbmVzJGlkMiwgIlNwZWNpZXMiXSwgRlVOID0gIj09IikgJT4lDQogIGNvbE1lYW5zICU+JQ0KICB0ICU+JQ0KICBhc190aWJibGUNCmBgYA0KDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogIzFGQUFFMzsiPjx1Pk9wdGltaXplciA6IGdyaWQgc2VhcmNoIGFsZ29yaXRobTwvdT48L3NwYW4+DQo9PT0NCg0KVGhpcyBwYXJ0IHByb3ZpZGVzIGZ1bmN0aW9ucyB0byBhcHByb3hpbWF0ZSB0aGUgc21vb3RoaW5nIHBhcmFtZXRlciAkaD4wJCBvZiB0aGUgYWdncmVnYXRpb24gbWV0aG9kLg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQ7Ij5GdW5jdGlvbjwvc3Bhbj4gOiBgc2V0R3JpZFBhcmFtZXRlcmANCi0tLQ0KDQotICoqQXJndW1lbnQqKjoNCg0KICAgIC0gYG1pbl92YWxgIDogdGhlIG1pbmltdW0gdmFsdWUgb2YgcGFyYW1ldGVyIGdyaWQuDQogICAgLSBgbWF4X3ZhbGAgOiB0aGUgbWF4aW11bSB2YWx1ZSBvZiBwYXJhbWV0ZXIgZ3JpZC4NCiAgICAtIGBuX3ZhbGAgOiB0aGUgbnVtYmVyIG9mIHBvaW50cyBpbiB0aGUgZ3JpZC4NCiAgICAtIGBwYXJhbWV0ZXJzYCA6IHRoZSB2ZWN0b3Igb2YgcGFyYW10ZXJzIGluIGNhc2UgdGhlIG5vbi11bmlmb3JtIGdyaWQgaXMgY29uc2lkZXJlZC4NCiAgICAtIGBwcmludF9yZXN1bHRgIDogYSBsb2dpY2FsIHZhbHVlIHNwZWNpZnlpbmcgd2hldGhlciBvciBub3QgdG8gcHJpbnQgdGhlIG9ic2VydmVkIHJlc3VsdC4NCiAgICAtIGBmaWd1cmVgIDogYSBsb2dpY2FsIHZhbHVlIHNwZWNpZnlpbmcgd2hldGhlciBvciBub3QgdG8gcGxvdCB0aGUgZ3JhcGhpYyBvZiBlcnJvci4NCg0KLSAqKlZhbHVlKio6DQoNCiAgICBUaGlzIGZ1bmN0aW9uIHJldHVybnMgYSAqbGlzdCogb2YgYWxsIHRoZSBwYXJhbWV0ZXJzIGdpdmVuIGluIGl0cyBhcmd1bWVudHMuDQoNCmBgYHtyfQ0Kc2V0R3JpZFBhcmFtZXRlciA8LSBmdW5jdGlvbihtaW5fdmFsID0gMWUtNSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1heF92YWwgPSAwLjUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX3ZhbCA9IDMwMCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFtZXRlcnMgPSBOVUxMLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmludF9yZXN1bHQgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmaWd1cmUgPSBUUlVFKXsNCiAgcmV0dXJuKGxpc3QobWluX3ZhbCA9IG1pbl92YWwsDQogICAgICAgICAgICAgIG1heF92YWwgPSBtYXhfdmFsLA0KICAgICAgICAgICAgICBuX3ZhbCA9IG5fdmFsLA0KICAgICAgICAgICAgICBwYXJhbWV0ZXJzID0gcGFyYW1ldGVycywNCiAgICAgICAgICAgICAgcHJpbnRfcmVzdWx0ID0gcHJpbnRfcmVzdWx0LA0KICAgICAgICAgICAgICBmaWd1cmUgPSBmaWd1cmUpKQ0KfQ0KYGBgDQoNCg0KIyMjIDxzcGFuIHN0eWxlPSJjb2xvcjogI0YwQUUxNDsiPkZ1bmN0aW9uPC9zcGFuPiA6IGBncmlkT3B0aW1pemVyYA0KDQotICoqQXJndW1lbnQqKjoNCiAgICANCiAgICAtIGBvYmpfZnVuYCA6IHRoZSBvYmplY3RpdmUgZnVuY3Rpb24gZm9yIHdoaWNoIGl0cyBtaW5pbWl6ZXIgaXMgdG8gYmUgZXN0aW1hdGVkLiBJdCBzaG91bGQgYmUgYSB1bml2YXJhdGUgZnVuY3Rpb24gb2YgcmVhbCBwb3NpdGl2ZSB2YXJpYWJsZXMuDQogICAgLSBgc2V0UGFyYW1ldGVyYCA6IHRoZSBjb250cm9sIG9mIGdyaWQgc2VhcmNoIGFsZ29yaXRobSBwYXJhbWV0ZXJzIHdoaWNoIHNob3VsZCBiZSB0aGUgZnVuY3Rpb24gYHNldEdyaWRQYXJhbWV0ZXIoKWAgZGVmaW5lZCBhYm92ZS4NCiAgICAtIGBuYWl2ZWAgOiBhIGxvZ2ljYWwgdmFsdWUgc3BlY2lmeWluZyBpZiB0aGUgYG5haXZlYCBrZXJuZWwgaXMgdXNlZC4gQnkgZGVmYXVsdCwgYG5haXZlID0gRkFMU0VgLg0KICAgIC0gYHNpbGVudGAgOiBhIGxvZ2ljYWwgdmFsdWUgc3BlY2lmeWluZyB3aGV0aGVyIG9yIG5vdCBhbGwgdGhlIG1lc3NhZ2VzIHNob3VsZCBiZSBzaWxlbnQuIEJ5IGRlZmF1bHQsIGBzaWxlbnQgPSBGQUxTRWAuDQogICAgLSBga2VyYCA6IHRoZSBuYW1lIG9mIGtlcm5lbCBmdW5jdGlvbi4NCg0KLSAqKlZhbHVlKio6DQoNCiAgICBUaGlzIGZ1bmN0aW9uIHJldHVybnMgYSBsaXN0IG9mIHRoZSBmb2xsb3dpbmcgb2JqZWN0cyAoZXhjZXB0IGZvciBgbmFpdmVgIGtlcm5lbCBmb3Igd2hpY2ggYE5BYHMgYXJlIHJldHVybmVkKToNCg0KICAgIC0gYG9wdF9wYXJhbWAgOiB0aGUgb2JzZXJ2ZWQgdmFsdWUgb2YgdGhlIG1pbmltaXplci4NCiAgICAtIGBvcHRfZXJyb3JgIDogdGhlIHZhbHVlIG9mIG9wdGltYWwgcmlzay4NCiAgICAtIGBhbGxfcmlza2AgOiB0aGUgdmVjdG9yIG9mIGFsbCB0aGUgZXJyb3JzIGV2YWx1YXRlZCBhdCBhbGwgdGhlIHZhbHVlcyBvZiBjb25zaWRlcmVkIHBhcmFtZXRlcnMuDQogICAgLSBgcnVuLnRpbWVgIDogdGhlIHJ1bm5pbmcgdGltZSBvZiB0aGUgYWxnb3JpdGhtLiANCiAgICANCi0tLSANCg0KPiAqKiYjOTk5OTsgUmVtYXJrLjEqKjogDQpGb3IgYCJuYWl2ZSJgIGtlcm5lbCBmdW5jdGlvbiAoMC0xIHdlaWdodCksIHRoZXJlIGlzIG5vIHBhcmFtZXRlciAkaCQgdG8gYmUgdHVuZWQuIE9uZSBqdXN0IHNlYXJjaGVzIGZvciB0aGUgdHJhaW5pbmcgZGF0YSB3aXRoIHRoZSBzYW1lIHByZWRpY3RlZCBjbGFzc2VzIGFzIHRoZSBxdWVyeSBwb2ludCwgdGhlbiBjb21wdXRlcyB0aGUgdG90YWwgd2VpZ2h0IGFtb25nIHRoZW0uIFRoZSBtYWpvcml0eSBjbGFzcyBhbW9uZyBzdWNoIGRhdGEgcG9pbnRzIGlzIHRoZSBwcmVkaWN0ZWQgY2xhc3MuDQoNCi0tLQ0KDQoNCmBgYHtyfQ0KZ3JpZE9wdGltaXplciA8LSBmdW5jdGlvbihvYmpfZnVuYywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2V0UGFyYW1ldGVyID0gc2V0R3JpZFBhcmFtZXRlcigpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBuYWl2ZSA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzaWxlbnQgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAga2VyID0gTlVMTCl7DQogIHQwIDwtIFN5cy50aW1lKCkNCiAgaWYoIW5haXZlKXsNCiAgICBpZihpcy5udWxsKHNldFBhcmFtZXRlciRwYXJhbWV0ZXJzKSl7DQogICAgcGFyYW0gPC0gc2VxKHNldFBhcmFtZXRlciRtaW5fdmFsLA0KICAgICAgICAgICAgICAgICBzZXRQYXJhbWV0ZXIkbWF4X3ZhbCwNCiAgICAgICAgICAgICAgICAgbGVuZ3RoLm91dCA9IHNldFBhcmFtZXRlciRuX3ZhbCkNCiAgICB9IGVsc2V7DQogICAgICBwYXJhbSA8LSBzZXRQYXJhbWV0ZXIkcGFyYW1ldGVycw0KICAgIH0NCiAgICByaXNrIDwtIHBhcmFtICU+JQ0KICAgICAgbWFwX2RibCguZiA9IG9ial9mdW5jKQ0KICAgIGlkX29wdCA8LSB3aGljaC5taW4ocmlzaykNCiAgICBvcHRfZXAgPC0gcGFyYW1baWRfb3B0XQ0KICAgIG9wdF9yaXNrIDwtIHJpc2tbaWRfb3B0XQ0KICAgIGlmKHNldFBhcmFtZXRlciRwcmludF9yZXN1bHQgJiAhc2lsZW50KXsNCiAgICAgIGNhdCgiXG4qIEdyaWQgc2VhcmNoIGZvciIsa2VyLCJrZXJuZWwuLi5cblx0fiBvYnNlcnZlZCBwYXJhbWV0ZXIgOiIsIG9wdF9lcCkNCiAgICB9DQogICAgaWYoc2V0UGFyYW1ldGVyJGZpZ3VyZSl7DQogICAgICB0aWJibGUoeCA9IHBhcmFtLCANCiAgICAgICAgICAgICB5ID0gcmlzaykgJT4lDQogICAgICAgIGdncGxvdChhZXMoeCA9IHgsIHkgPSB5KSkgKw0KICAgICAgICBnZW9tX2xpbmUoY29sb3IgPSAic2t5Ymx1ZSIsIHNpemUgPSAwLjc1KSArDQogICAgICAgIGdlb21fcG9pbnQoYWVzKHggPSBvcHRfZXAsIHkgPSBvcHRfcmlzayksIGNvbG9yID0gInJlZCIpICsNCiAgICAgICAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gb3B0X2VwLCBjb2xvciA9ICJyZWQiLCBsaW5ldHlwZSA9ICJsb25nZGFzaCIpICsNCiAgICAgICAgbGFicyh0aXRsZSA9ICJFcnJvciBhcyBmdW5jdGlvbiBvZiBwYXJhbWV0ZXIiLCANCiAgICAgICAgICAgICB4ID0gIlBhcmFtZXRlciIsDQogICAgICAgICAgICAgeSA9ICJFcnJvciIpIC0+IHANCiAgICAgIHByaW50KHApDQogICAgfQ0KICB9IGVsc2V7DQogICAgICBvcHRfZXAgPSBOQQ0KICAgICAgb3B0X3Jpc2sgPSBOQQ0KICAgICAgcmlzayA9IE5BDQogIH0NCiAgDQogIHQxIDwtIFN5cy50aW1lKCkNCiAgcmV0dXJuKGxpc3QoDQogICAgb3B0X3BhcmFtID0gb3B0X2VwLA0KICAgIG9wdF9lcnJvciA9IG9wdF9yaXNrLA0KICAgIGFsbF9yaXNrID0gcmlzaywNCiAgICBydW4udGltZSA9IGRpZmZ0aW1lKHQxLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHQwLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHVuaXRzID0gInNlY3MiKVtbMV1dKQ0KICApDQp9DQpgYGANCg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQ7Ij48dT4kXGthcHBhJC1jcm9zcyB2YWxpZGF0aW9uIGxvc3QgZnVuY3Rpb248L3U+PC9zcGFuPg0KLS0tDQoNCkNvbnN0cnVjdGluZyBhIGNvbWJpbmluZyBjbGFzc2lmaWVyIGluIHRoaXMgZnJhbWV3b3JrIGlzIGVxdWl2YWxlbnQgdG8gYXBwcm94aW1hdGluZyB0aGUgb3B0aW1hbCB2YWx1ZSBvZiBzbW9vdGhpbmcgcGFyYW1ldGVyICRoPjAkIGludHJvZHVjZWQgaW4gc2VjdGlvbiAxLjEsIGJ5IG1pbmltaXppbmcgc29tZSBsb3N0IGZ1bmN0aW9uLiBJbiB0aGlzIHN0dWR5LCB3ZSBwcm9wb3NlICRca2FwcGEkLWZvbGQgY3Jvc3MgdmFsaWRhdGlvbiBtaXNjbGFzc2lmaWNhdGlvbiBlcnJvciBkZWZpbmVkIGJ5DQoNCiQkDQpcdmFycGhpXntca2FwcGF9KGgpPVxmcmFjezF9e1xrYXBwYX1cc3VtX3trPTF9Xntca2FwcGF9XEJpZyhcZnJhY3sxfXt8Rl9rfH1cc3VtX3soeF9qLHlfailcaW4gRl9rfVxtYXRoYmJ7MX1fe1x7Z19uKHtcYmYgQ30oeF9qKSlcbmVxIHlfalx9fVxCaWcpDQokJA0KDQp3aGVyZQ0KDQotIGZvciBhbnkgJGs9MSwuLi4sXGthcHBhJCwgJEZfayQgZGVub3RlcyB0aGUgJGskdGggdmFsaWRhdGlvbiBmb2xkLg0KLSAkZ19uKHtcYmYgQ30oeF9qKSkkIGlzIHRoZSBwcmVkaWN0aW9uIG9mICR4X2okIG9mICRGX2skLCBjb21wdXRlZCB1c2luZyB0aGUgZGF0YSBwb2ludHMgZnJvbSB0aGUgcmVtYWluaW5nIHBhcnQgJHtcY2FsIER9X3tcZWxsfS1GX2skIGJ5LA0KDQokJGdfbih7XGJmIEN9KHhfaikpPWteKj1cdGV4dHthcmd9XG1heF97MVxsZXEga1xsZXEgTn1cc3VtX3soeF9pLHlfaSlcaW4gXG1hdGhjYWx7RH1cc2V0bWludXMgRl9rfUtfaChkX3tcY2FsIEh9KHtcYmYgQ30oeF9qKSx7XGJmIEN9KHhfaSkpKVxtYXRoYmJ7MX1fe1x7eV9pPWtcfX0uJCQNCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+PHU+RnVuY3Rpb248L3U+PC9zcGFuPjogYGRpc3RfbWF0cml4YA0KLS0tDQoNClRoaXMgZnVuY3Rpb24gY29tcHV0ZXMgSGFtbWluZyBkaXN0YW5jZXMgYmV0d2VlbiBkYXRhIHBvaW50cyBvZiBlYWNoIHRyYWluaW5nIGZvbGRzICgkXG1hdGhjYWx7RH1fe1xlbGx9LUZfayQpIGFuZCB0aGUgY29ycmVzcG9uZGluZyB2YWxpZGF0aW9uIGZvbGQgJEZfayQgZm9yIGFueSAkaz0xLFxkb3RzLFxrYXBwYSQuDQoNCi0gKipBcmd1bWVudCoqOg0KICAgIA0KICAgIC0gYGJhc2ljTWFjaGluZXNgIDogdGhlIGJhc2ljIG1hY2hpbmUgb2JqZWN0LCB3aGljaCBpcyBhbiBvdXRwdXQgb2YgYGdlbmVyYXRlTWFjaGluZXNgIGZ1bmN0aW9uLg0KICAgIC0gYG5fY3ZgIDogdGhlIG51bWJlciAkXGthcHBhJCBvZiBjcm9zcy12YWxpZGF0aW9uIGZvbGRzLiBCeSBkZWZhdWx0LCBgbl9jdiA9IDVgLg0KICAgIC0gYGtlcm5lbGAgOiB0aGUga2VybmVsIGZ1bmN0aW9uIHVzZWQgZm9yIHRoZSBhZ2dyZWdhdGlvbiwgd2hpY2ggaXMgYW4gZWxlbWVudCBvZiB7ImdhdXNzaWFuIiwgImVwYW5lY2huaWtvdiIsICJiaXdlaWdodCIsICJ0cml3ZWlnaHQiLCAidHJpYW5ndWxhciIsICJuYWl2ZSJ9LiBCeSBkZWZhdWx0LCBga2VybmVsID0gImdhdXNzaWFuImAuDQogICAgDQotICoqVmFsdWUqKjoNCiAgICANCiAgICBUaGlzIGZ1bmN0aW9ucyByZXR1cm5zIGEgbGlzdCBvZiB0aGUgZm9sbG93aW5nIG9iamVjdHM6DQogICAgDQogICAgLSBgZGlzdGAgOiBhIGRhdGEgZnJhbWUgKGB0aWJibGVgKSBjb250YWluaW5nIEhhbW1pbmcgZGlzdGFuY2VzLg0KICAgIC0gYGlkX3NodWZmbGVgIDogdGhlIHNodWZmbGVkIGluZGljZXMgaW4gY3Jvc3MtdmFsaWRhdGlvbi4NCiAgICAtIGBuX2N2YCA6IHRoZSBudW1iZXIgJFxrYXBwYSQgb2YgY3Jvc3MtdmFsaWRhdGlvbiBmb2xkcy4NCiAgICANCmBgYHtyfQ0KZGlzdF9tYXRyaXggPC0gZnVuY3Rpb24oYmFzaWNNYWNoaW5lcywNCiAgICAgICAgICAgICAgICAgICAgICAgIG5fY3YgPSA1LA0KICAgICAgICAgICAgICAgICAgICAgICAgaWRfc2h1ZmZsZSA9IE5VTEwpew0KICBuIDwtIG5yb3coYmFzaWNNYWNoaW5lcyRwcmVkaWN0MikNCiAgbl9lYWNoX2ZvbGQgPC0gZmxvb3Iobi9uX2N2KQ0KICAjIHNodWZmbGVkIGluZGljZXMNCiAgaWYoaXMubnVsbChpZF9zaHVmZmxlKSl7DQogICAgc2h1ZmZsZSA8LSAxOihuX2N2LTEpICU+JQ0KICAgIHJlcChuX2VhY2hfZm9sZCkgJT4lDQogICAgYyguLCByZXAobl9jdiwgbiAtIG5fZWFjaF9mb2xkICogKG5fY3YgLSAxKSkpICU+JQ0KICAgIHNhbXBsZQ0KICB9ZWxzZXsNCiAgICBzaHVmZmxlIDwtIGlkX3NodWZmbGUNCiAgfQ0KICAjIHRoZSBwcmVkaWN0aW9uIG1hdHJpeCBEX2wNCiAgZGZfIDwtIGFzLm1hdHJpeChiYXNpY01hY2hpbmVzJHByZWRpY3QyKQ0KICBwYWlyX2Rpc3QgPC0gZnVuY3Rpb24oTSwgTil7DQogICAgcmVzXyA8LSAxOm5yb3coTikgJT4lDQogICAgICBtYXBfZGZjKC5mID0gKFwoaWQpIHRpYmJsZSgne3tpZH19JyA6PSByb3dTdW1zKHN3ZWVwKE0sIDIsIE5baWQsXSwgRlVOID0gIiE9IikpKSkpDQogICAgcmV0dXJuKHJlc18pDQogIH0NCiAgTCA8LSAxOm5fY3YgJT4lDQogICAgICBtYXAoLmYgPSB+IHBhaXJfZGlzdChkZl9bc2h1ZmZsZSAhPSAueCxdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZGZfW3NodWZmbGUgPT0gLngsXSkpDQogIHJldHVybihsaXN0KGRpc3QgPSBMLCANCiAgICAgICAgICAgICAgaWRfc2h1ZmZsZSA9IHNodWZmbGUsDQogICAgICAgICAgICAgIG5fY3YgPSBuX2N2KSkNCn0NCmBgYA0KDQotLS0NCg0KPiAqKkV4YW1wbGUuMioqOiBUaGUgbWV0aG9kIGBkaXN0X21hdHJpeGAgaXMgaW1wbGVtZW50ZWQgb24gdGhlIG9idGFpbmVkIGJhc2ljIG1hY2hpbmVzIGJ1aWx0IGluICpFeGFtcGxlLjEqLg0KDQotLS0NCg0KYGBge3J9DQpkaXMgPC0gZGlzdF9tYXRyaXgoYmFzaWNNYWNoaW5lcyA9IGJhc2ljX21hY2hpbmVzLA0KICAgICAgICAgICAgbl9jdiA9IDMpDQpkaXMkaWRfc2h1ZmZsZQ0KYGBgDQoNCi0tLQ0KDQo+ICoqRXhhbXBsZS4zKio6IEZyb20gdGhlIGRpc3RhbmNlIG1hdHJpeCwgd2UgY2FuIGNvbXB1dGUgdGhlIGVycm9yIGNvcnJlc3BvbmRpbmcgdG8gR2F1c3NpYW4ga2VybmVsIGZ1bmN0aW9uLCB0aGVuIHVzZSB0aGUgZ3JpZCBzZWFyY2ggb3B0aW1pemVyIHRvIGFwcHJveGltYXRlIHRoZSBzbW9vdGhpbmcgcGFyYW10ZXIgaW4gdGhpcyBjYXNlLg0KDQotLS0NCg0KDQpgYGB7cn0NCiMgR2F1c3NpYW4ga2VybmVsDQpnYXVzc2lhbl9rZXJuIDwtIGZ1bmN0aW9uKC5lcCA9IC4wNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgLmRpc3RfbWF0cml4LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAudHJhaW5fcmVzcG9uc2UyLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAuaW52X3NpZ21hID0gc3FydCguNSksDQogICAgICAgICAgICAgICAgICAgICAgICAgIC5hbHBoYSA9IDIpew0KICBrZXJuX2Z1biA8LSBmdW5jdGlvbih4LCBpZCwgRCl7DQogICAgdGVtMCA8LSBhcy5tYXRyaXgoZXhwKC0oeCpEKV4oLmFscGhhLzIpKi5pbnZfc2lnbWFeLmFscGhhKSkNCiAgICB5X2hhdCA8LSBtYXBfZGZjKC54ID0gMTpuY29sKHRlbTApLA0KICAgICAgICAgICAgICAgICAgICAgLmYgPSAoXCh4XykgdGliYmxlKCJ7eF99IiA6PSB0YXBwbHkodGVtMFssIHhfXSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBJTkRFWCA9IC50cmFpbl9yZXNwb25zZTJbLmRpc3RfbWF0cml4JGlkX3NodWZmbGUgIT0gaWRdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOID0gc3VtKSkpKSAlPiUNCiAgICAgIG1hcF9jaHIoLmYgPSAoXCh4KSBuYW1lcyh3aGljaC5tYXgoeCkpKSkNCiAgICByZXR1cm4obWVhbih5X2hhdCAhPSAudHJhaW5fcmVzcG9uc2UyWy5kaXN0X21hdHJpeCRpZF9zaHVmZmxlID09IGlkXSkpDQogIH0NCiAgdGVtcCA8LSBtYXAyKC54ID0gMTouZGlzdF9tYXRyaXgkbl9jdiwgDQogICAgICAgICAgICAgICAueSA9IC5kaXN0X21hdHJpeCRkaXN0LCANCiAgICAgICAgICAgICAgIC5mID0gfiBrZXJuX2Z1bih4ID0gLmVwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZCA9IC54LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBEID0gLnkpKQ0KICByZXR1cm4oUmVkdWNlKCIrIiwgdGVtcCkvLmRpc3RfbWF0cml4JG5fY3YpDQp9DQoNCiMgS2FwcGEgY3Jvc3MtdmFsaWRhdGlvbiBlcnJvcg0KY29zdF9mdW4gPC0gZnVuY3Rpb24oLmVwID0gLjEsDQogICAgICAgICAgICAgICAgICAgICAuZGlzdF9tYXRyaXgsDQogICAgICAgICAgICAgICAgICAgICAua2VybmVsX2Z1bmMgPSBOVUxMLA0KICAgICAgICAgICAgICAgICAgICAgLnRyYWluX3Jlc3BvbnNlMiwNCiAgICAgICAgICAgICAgICAgICAgIC5pbnZfc2lnbWEgPSBzcXJ0KC41KSl7DQogIHJldHVybigua2VybmVsX2Z1bmMoLmVwID0gLmVwLA0KICAgICAgICAgICAgICAgICAgICAgLmRpc3RfbWF0cml4ID0gLmRpc3RfbWF0cml4LA0KICAgICAgICAgICAgICAgICAgICAgLnRyYWluX3Jlc3BvbnNlMiA9IC50cmFpbl9yZXNwb25zZTIsDQogICAgICAgICAgICAgICAgICAgICAuaW52X3NpZ21hID0gLmludl9zaWdtYSkpDQp9DQoNCiMgY3Jvc3NfdmFsaWRhdGlvbg0KZXJyX2N2IDwtIGZ1bmN0aW9uKHgsIA0KICAgICAgICAgICAgICAgICAgICAgLmRpc3RfbWF0cml4ID0gZGlzLA0KICAgICAgICAgICAgICAgICAgICAgLmtlcm5lbF9mdW5jID0gZ2F1c3NpYW5fa2VybiwNCiAgICAgICAgICAgICAgICAgICAgIC50cmFpbl9yZXNwb25zZTIgPSBkZltiYXNpY19tYWNoaW5lcyRpZDIsIDVdKSB7DQogIHJlcyA8LSBjb3N0X2Z1biguZXAgPSB4LA0KICAgICAgICAgICAgICAuZGlzdF9tYXRyaXggPSAuZGlzdF9tYXRyaXgsDQogICAgICAgICAgICAgIC5rZXJuZWxfZnVuYyA9IC5rZXJuZWxfZnVuYywNCiAgICAgICAgICAgICAgLnRyYWluX3Jlc3BvbnNlMiA9IC50cmFpbl9yZXNwb25zZTIpDQogIHJldHVybihyZXMpDQp9DQoNCiMgT3B0aW1pemF0aW9uDQpvcHRfcGFyYW1fZ3JpZCA8LSBncmlkT3B0aW1pemVyKG9ial9mdW4gPSBlcnJfY3YsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNldFBhcmFtZXRlciA9IHNldEdyaWRQYXJhbWV0ZXIobWluX3ZhbCA9IDAuMDEsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4X3ZhbCA9IDAuMSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX3ZhbCA9IDEwMCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmaWd1cmUgPSBUUlVFKSkNCg0KYGBgDQoNCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+PHU+Rml0dGluZyBwYXJhbWV0ZXI8L3U+PC9zcGFuPg0KLS0tDQoNClRoaXMgZnVuY3Rpb24gZ2F0aGVycyB0aGUgY29uc3RydWN0ZWQgbWFjaGluZXMgYW5kIHBlcmZvcm0gZ3JpZCBzZWFyY2ggYWxnb3JpdGhtIHRvIGFwcHJveGltYXRlIHRoZSBzbW9vdGhpbmcgcGFyYW1ldGVyIGZvciB0aGUgYWdncmVnYXRpb24uDQoNCi0gKipBcmd1bWVudCoqOg0KICAgIA0KICAgIC0gYHRyYWluX2Rlc2lnbmAgOiBhIG1hdHJpeCBvciBkYXRhIGZyYW1lIG9mIHRoZSB0cmFpbmluZyAqaW5wdXQgZGF0YSogb3IgdGhlICpwcmVkaWN0ZWQgY2xhc3NlcyogZ2l2ZW4gYnkgc29tZSBjbGFzc2lmaWVycy4NCiAgICAgICAgLSBJZiB0aGUgKmlucHV0KiBkYXRhIGlzIGdpdmVuLCB0aGUgb3B0aW9uIGBidWlsZF9tYWNoaW5lYCBtdXN0IGJlIGBUUlVFYCBhbmQgYWxsIHRoZSBjaG9zZW4gbWFjaGluZXMgd2lsbCBiZSBjb25zdHJ1Y3RlZCB1c2luZyBhIHByb3BvcnRpb24gb2YgdGhlIHRyYWluaW5nIGlucHV0IChgc3BsaXRzYCkuDQogICAgICAgIC0gSWYgdGhlICpwcmVkaWN0ZWQgY2xhc3NlcyogYXJlIGdpdmVuLCB0aGUgb3B0aW9uIGBidWlsZF9tYWNoaW5lYCBtdXN0IGJlIGBGQUxTRWAgd2hpY2ggaW5kaWNhdGVzIHRoYXQgbm8gYmFzaWMgbWFjaGluZSBpcyBjb25zdHJ1Y3RlZCwgYW5kIHRoZSBwYXJhbXRlciBpcyB0dW5lZCB1c2luZyB0aGlzIG1hdHJpeCBvZiBwcmVkaWN0ZWQgY2xhc3NlcyBkaXJlY3RseS4NCiAgICAtIGB0cmFpbl9yZXNwb25zZWAgOiBhIHZlY3RvciBvZiBjb3JyZXNwb25kaW5nIGNsYXNzZXMgb2YgdGhlIGB0cmFpbl9kZXNpZ25gLg0KICAgIC0gYHNjYWxlX2lucHV0YCA6IGEgbG9naWNhbCB2YWx1ZSBzcGVjaWZ5aW5nIHdoZXRoZXIgb3Igbm90IHRvIHNjYWxlIHRoZSBpbnB1dCBkYXRhIGJlZm9yZSBidWlsZGluZyB0aGUgYmFzaWMgY2xhc3NpZmllcnMuIEJ5IGRlZmF1bHQsIGBzY2FsZV9pbnB1dCA9IEZBTFNFYC4NCiAgICAtIGBidWlsZF9tYWNoaW5lYDogYSBsb2dpY2FsIHZhbHVlIHNwZWNpZnlpbmcgd2hldGhlciBvciBub3QgdGhlIGJhc2ljIG1hY2hpbmVzIHNob3VsZCBiZSBjb25zdHJ1Y3RlZC4gSXQgc2hvdWxkIGJlIGBUUlVFYCBpZiB0aGUgaW5wdXQgZGF0YSBpcyBnaXZlbiB0byB0aGUgYHRyYWluX2Rlc2lnbmAsIGVsc2UgaXQgc2hvdWxkIGJlIGBGQUxTRWAuIEJ5IGRlZmF1bHQsIGBidWlsZF9tYWNoaW5lID0gVFJVRWAuDQogICAgLSBgbWFjaGluZXNgIDogYSB2ZWN0b3Igb2YgYmFzaWMgbWFjaGluZXMgdG8gYmUgY29uc3RydWN0ZWQuIEl0IG11c3QgYmUgYSBzdWJzZXQgb2Yge2Aia25uImAsIGAidHJlZSJgLCBgInJmImAsIGAibG9naXQiYCwgYCJzdm0iYCwgYCJ4Z2IiYCwgYCJhZGFib29zdCJgfS4gQnkgZGVmYXVsdCwgYG1hY2hpbmVzID0gTlVMTGAsIGFuZCBhbGwgdHlwZXMgb2YgYmFzaWMgbWFjaGluZXMgYXJlIGJ1aWx0Lg0KICAgIC0gYHNwbGl0c2AgOiB0aGUgcHJvcG9ydGlvbiBvZiB0cmFpbmluZyBkYXRhIHVzZWQgdG8gYnVpbGQgdGhlIGJhc2ljIG1hY2hpbmVzLiBCeSBkZWZhdWx0LCBgc3BsaXRzID0gLjVgLg0KICAgIC0gYG5fY3ZgIDogdGhlIG51bWJlciBvZiBjcm9zcy12YWxpZGF0aW9uIGZvbGRzIHVzZWQgdG8gdHVuZSB0aGUgc21vb3RoaW5nIHBhcmFtZXRlci4NCiAgICAtIGBpbnZfc2lnLCBhbHBgIDogdGhlIGludmVyc2Ugbm9ybWFsaXplZCBjb25zdGFudCAkXHNpZ21hXnstMX0+MCQgYW5kIHRoZSBleHBvbmVudCAkXGFscD4wJCBvZiBleHBvbmVudGlhbCBrZXJuZWw6ICRLKHgpPWVeey1cfHgvXHNpZ21hXHxee1xhbHBoYX19JCBmb3IgYW55ICR4XGluXG1hdGhiYntSfV5kJC4gQnkgZGVmYXVsdCwgYGludl9zaWdtYSA9IGAkXHNxcnR7MS8yfSQgYW5kIGBhbHBoYSA9IDJgIHdoaWNoIGNvcnJlc3BvbmRzIHRvIHRoZSBHYXVzc2lhbiBrZXJuZWwuDQogICAgLSBga2VybmVsc2AgOiB0aGUga2VybmVsIGZ1bmN0aW9uIG9yIHZlY3RvciBvZiBrZXJuZWwgZnVuY3Rpb25zIHVzZWQgZm9yIHRoZSBhZ2dyZWdhdGlvbi4NCiAgICAtIGBzZXRNYWNoaW5lUGFyYW1gIDogYW4gb3B0aW9uIHVzZWQgdG8gc2V0IHRoZSB2YWx1ZXMgb2YgdGhlIHBhcmFtZXRlcnMgb2YgdGhlIGJhc2ljIG1hY2hpbmVzLiBgc2V0QmFzaWNQYXJhbWV0ZXJgIGZ1bmN0aW9uIHNob3VsZCBiZSBmZWQgdG8gdGhpcyBhcmd1bWVudC4NCiAgICAtIGBzZXRHcmlkUGFyYW1gIDogYW4gb3B0aW9uIHVzZWQgdG8gc2V0IHRoZSB2YWx1ZXMgb2YgdGhlIHBhcmFtZXRlcnMgb2YgdGhlIGdyaWQgc2VhcmNoIGFsZ29yaXRobS4gVGhlIGBzZXRHcmlkUGFyYW1ldGVyYCBmdW5jdGlvbiBzaG91bGQgYmUgZmVkIHRvIGl0Lg0KICAgIC0gYHNpbGVudGAgOiBhIGxvZ2ljYWwgdmFsdWUgdG8gc2lsZW50IGFsbCB0aGUgbWVzc2FnZXMuDQogICAgDQotICoqVmFsdWUqKjoNCg0KICAgIFRoaXMgZnVuY3Rpb24gcmV0dXJucyBhIGxpc3Qgb2YgdGhlIGZvbGxvd2luZyBvYmplY3RzOg0KDQogICAgLSBgb3B0X3BhcmFtZXRlcnNgIDogdGhlIG9ic2VydmVkIG9wdGltYWwgcGFyYW1ldGVyLg0KICAgIC0gYGFkZF9wYXJhbWV0ZXJzYCA6IG90aGVyIGFkaXRpb25hbCBwYXJhbWV0ZXJzIHN1Y2ggYXMgc2NhbGluZyBvcHRpb25zLCBwYXJhbWV0ZXJzIG9mIGtlcm5lbCBmdW5jdGlvbnMgYW5kIHRoZSBvcHRpbWl6YXRpb24gbWV0aG9kcyB1c2VkLg0KICAgIC0gYGJhc2ljX21hY2hpbmVzYCA6IHRoZSBsaXN0IG9mIGJhc2ljIG1hY2hpbmUgb2JqZWN0Lg0KDQpgYGB7cn0NCmZpdF9wYXJhbWV0ZXIgPC0gZnVuY3Rpb24odHJhaW5fZGVzaWduLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgdHJhaW5fcmVzcG9uc2UsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlX2lucHV0ID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGJ1aWxkX21hY2hpbmUgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBtYWNoaW5lcyA9IE5VTEwsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzcGxpdHMgPSAwLjUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBuX2N2ID0gNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgaW52X3NpZ21hID0gc3FydCguNSksDQogICAgICAgICAgICAgICAgICAgICAgICAgIGFscCA9IDIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGtlcm5lbHMgPSAiZ2F1c3NpYW4iLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzZXRNYWNoaW5lUGFyYW0gPSBzZXRCYXNpY1BhcmFtZXRlcigpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzZXRHcmlkUGFyYW0gPSBzZXRHcmlkUGFyYW1ldGVyKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgIHNpbGVudCA9IEZBTFNFKXsNCiAga2VybmVsc19sb29rdXAgPC0gYygiZ2F1c3NpYW4iLCAiZXBhbmVjaG5pa292IiwgImJpd2VpZ2h0IiwgInRyaXdlaWdodCIsICJ0cmlhbmd1bGFyIiwgIm5haXZlIikNCiAga2VybmVsX3JlYWwgPC0ga2VybmVscyAlPiUNCiAgICBtYXBfY2hyKC5mID0gfiBtYXRjaC5hcmcoLngsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrZXJuZWxzX2xvb2t1cCkpDQogIGlmKGJ1aWxkX21hY2hpbmUpew0KICAgICBtYWNoMiA8LSBnZW5lcmF0ZU1hY2hpbmVzKHRyYWluX2lucHV0ID0gdHJhaW5fZGVzaWduLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluX3Jlc3BvbnNlID0gdHJhaW5fcmVzcG9uc2UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfaW5wdXQgPSBzY2FsZV9pbnB1dCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYWNoaW5lcyA9IG1hY2hpbmVzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNwbGl0cyA9IHNwbGl0cywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBiYXNpY01hY2hpbmVQYXJhbSA9IHNldE1hY2hpbmVQYXJhbSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaWxlbnQgPSBzaWxlbnQpDQogIH0gZWxzZXsNCiAgICBtYWNoMiA8LSBsaXN0KHByZWRpY3QyID0gdHJhaW5fZGVzaWduLA0KICAgICAgICAgICAgICAgICAgbW9kZWxzID0gY29sbmFtZXModHJhaW5fZGVzaWduKSwNCiAgICAgICAgICAgICAgICAgIGlkMiA9IHJlcChUUlVFLCBucm93KHRyYWluX2Rlc2lnbikpLA0KICAgICAgICAgICAgICAgICAgdHJhaW5fZGF0YSA9IGxpc3QodHJhaW5fcmVzcG9uc2UgPSB0cmFpbl9yZXNwb25zZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzZXMgPSB1bmlxdWUodHJhaW5fcmVzcG9uc2UpKSkgICAgICAgICAgICAgICAgIA0KICB9DQogICMgZGlzdGFuY2UgbWF0cml4IHRvIGNvbXB1dGUgbG9zcyBmdW5jdGlvbg0KICBuX2tlciA8LSBsZW5ndGgoa2VybmVscykNCiAgaWRfc2h1ZiA8LSBOVUxMDQogIGRpc3RfYWxsIDwtIGRpc3RfbWF0cml4KGJhc2ljTWFjaGluZXMgPSBtYWNoMiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgbl9jdiA9IG5fY3YpDQoNCiAgIyBLZXJuZWwgZnVuY3Rpb25zDQogICMgPT09PT09PT09PT09PT09PQ0KICAjIEdhdXNzaWFuIGtlcm5lbA0KICBnYXVzc2lhbl9rZXJuZWwgPC0gZnVuY3Rpb24oLmVwID0gLjA1LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmRpc3RfbWF0cml4LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLnRyYWluX3Jlc3BvbnNlMiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5pbnZfc2lnbWEgPSBzcXJ0KC41KSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5hbHBoYSA9IDIpew0KICAgIGtlcm5fZnVuIDwtIGZ1bmN0aW9uKHgsIGlkLCBEKXsNCiAgICAgIHRlbTAgPC0gYXMubWF0cml4KGV4cCgtKHgqRCleKC5hbHBoYS8yKSouaW52X3NpZ21hXi5hbHBoYSkpDQogICAgICB5X2hhdCA8LSBtYXBfZGZjKC54ID0gMTpuY29sKHRlbTApLA0KICAgICAgICAgICAgICAgICAgICAgICAuZiA9IChcKHhfKSB0aWJibGUoInt4X30iIDo9IHRhcHBseSh0ZW0wWywgeF9dLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgSU5ERVggPSAudHJhaW5fcmVzcG9uc2UyWy5kaXN0X21hdHJpeCRpZF9zaHVmZmxlICE9IGlkXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOID0gc3VtKSkpKSAlPiUNCiAgICAgICAgbWFwX2NociguZiA9IChcKHgpIG5hbWVzKHdoaWNoLm1heCh4KSkpKQ0KICAgICAgcmV0dXJuKG1lYW4oeV9oYXQgIT0gLnRyYWluX3Jlc3BvbnNlMlsuZGlzdF9tYXRyaXgkaWRfc2h1ZmZsZSA9PSBpZF0pKQ0KICAgIH0NCiAgICB0ZW1wIDwtIG1hcDIoLnggPSAxOi5kaXN0X21hdHJpeCRuX2N2LCANCiAgICAgICAgICAgICAgICAgLnkgPSAuZGlzdF9tYXRyaXgkZGlzdCwgDQogICAgICAgICAgICAgICAgIC5mID0gfiBrZXJuX2Z1bih4ID0gLmVwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGlkID0gLngsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRCA9IC55KSkNCiAgICByZXR1cm4oUmVkdWNlKCIrIiwgdGVtcCkvLmRpc3RfbWF0cml4JG5fY3YpDQogIH0NCg0KICAjIEVwYW5lY2huaWtvdg0KICBlcGFuZWNobmlrb3Zfa2VybmVsIDwtIGZ1bmN0aW9uKC5lcCA9IC4wNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuZGlzdF9tYXRyaXgsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLnRyYWluX3Jlc3BvbnNlMil7DQogICAga2Vybl9mdW4gPC0gZnVuY3Rpb24oeCwgaWQsIEQpew0KICAgICAgdGVtMCA8LSBhcy5tYXRyaXgoMS0geCpEKQ0KICAgICAgdGVtMFt0ZW0wIDwgMF0gPSAwDQogICAgICB5X2hhdCA8LSBtYXBfZGZjKC54ID0gMTpuY29sKHRlbTApLA0KICAgICAgICAgICAgICAgICAgICAgICAuZiA9IChcKHhfKSB0aWJibGUoInt4X30iIDo9IHRhcHBseSh0ZW0wWywgeF9dLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgSU5ERVggPSAudHJhaW5fcmVzcG9uc2UyWy5kaXN0X21hdHJpeCRpZF9zaHVmZmxlICE9IGlkXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOID0gc3VtKSkpKSAlPiUNCiAgICAgICAgbWFwX2NociguZiA9IChcKHgpIG5hbWVzKHdoaWNoLm1heCh4KSkpKQ0KICAgICAgcmV0dXJuKG1lYW4oeV9oYXQgIT0gLnRyYWluX3Jlc3BvbnNlMlsuZGlzdF9tYXRyaXgkaWRfc2h1ZmZsZSA9PSBpZF0pKQ0KICAgIH0NCiAgICB0ZW1wIDwtIG1hcDIoLnggPSAxOi5kaXN0X21hdHJpeCRuX2N2LCANCiAgICAgICAgICAgICAgICAgLnkgPSAuZGlzdF9tYXRyaXgkZGlzdCwgDQogICAgICAgICAgICAgICAgIC5mID0gfiBrZXJuX2Z1bih4ID0gLmVwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGlkID0gLngsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRCA9IC55KSkNCiAgICByZXR1cm4oUmVkdWNlKCIrIiwgdGVtcCkvLmRpc3RfbWF0cml4JG5fY3YpDQogIH0NCg0KICAjIEJpd2VpZ2h0DQogIGJpd2VpZ2h0X2tlcm5lbCA8LSBmdW5jdGlvbiguZXAgPSAuMDUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuZGlzdF9tYXRyaXgsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAudHJhaW5fcmVzcG9uc2UyKXsNCiAga2Vybl9mdW4gPC0gZnVuY3Rpb24oeCwgaWQsIEQpew0KICAgIHRlbTAgPC0gYXMubWF0cml4KDEtIHgqRCkNCiAgICB0ZW0wW3RlbTAgPCAwXSA9IDANCiAgICB5X2hhdCA8LSBtYXBfZGZjKC54ID0gMTpuY29sKHRlbTApLA0KICAgICAgICAgICAgICAgICAgICAgICAuZiA9IChcKHhfKSB0aWJibGUoInt4X30iIDo9IHRhcHBseSh0ZW0wWywgeF9dLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgSU5ERVggPSAudHJhaW5fcmVzcG9uc2UyWy5kaXN0X21hdHJpeCRpZF9zaHVmZmxlICE9IGlkXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOID0gc3VtKSkpKSAlPiUNCiAgICAgICAgbWFwX2NociguZiA9IChcKHgpIG5hbWVzKHdoaWNoLm1heCh4KSkpKQ0KICAgICAgcmV0dXJuKG1lYW4oeV9oYXQgIT0gLnRyYWluX3Jlc3BvbnNlMlsuZGlzdF9tYXRyaXgkaWRfc2h1ZmZsZSA9PSBpZF0pKQ0KICAgIH0NCiAgICB0ZW1wIDwtIG1hcDIoLnggPSAxOi5kaXN0X21hdHJpeCRuX2N2LCANCiAgICAgICAgICAgICAgICAgLnkgPSAuZGlzdF9tYXRyaXgkZGlzdCwgDQogICAgICAgICAgICAgICAgIC5mID0gfiBrZXJuX2Z1bih4ID0gLmVwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGlkID0gLngsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRCA9IC55KSkNCiAgICByZXR1cm4oUmVkdWNlKCIrIiwgdGVtcCkvLmRpc3RfbWF0cml4JG5fY3YpDQogIH0NCg0KICAjIFRyaXdlaWdodA0KICB0cml3ZWlnaHRfa2VybmVsIDwtIGZ1bmN0aW9uKC5lcCA9IC4wNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuZGlzdF9tYXRyaXgsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLnRyYWluX3Jlc3BvbnNlMil7DQogIGtlcm5fZnVuIDwtIGZ1bmN0aW9uKHgsIGlkLCBEKXsNCiAgICB0ZW0wIDwtIGFzLm1hdHJpeCgxLSB4KkQpDQogICAgdGVtMFt0ZW0wIDwgMF0gPSAwDQogICAgeV9oYXQgPC0gbWFwX2RmYygueCA9IDE6bmNvbCh0ZW0wKSwNCiAgICAgICAgICAgICAgICAgICAgICAgLmYgPSAoXCh4XykgdGliYmxlKCJ7eF99IiA6PSB0YXBwbHkodGVtMFssIHhfXSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIElOREVYID0gLnRyYWluX3Jlc3BvbnNlMlsuZGlzdF9tYXRyaXgkaWRfc2h1ZmZsZSAhPSBpZF0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTiA9IHN1bSkpKSkgJT4lDQogICAgICAgIG1hcF9jaHIoLmYgPSAoXCh4KSBuYW1lcyh3aGljaC5tYXgoeCkpKSkNCiAgICAgIHJldHVybihtZWFuKHlfaGF0ICE9IC50cmFpbl9yZXNwb25zZTJbLmRpc3RfbWF0cml4JGlkX3NodWZmbGUgPT0gaWRdKSkNCiAgICB9DQogICAgdGVtcCA8LSBtYXAyKC54ID0gMTouZGlzdF9tYXRyaXgkbl9jdiwgDQogICAgICAgICAgICAgICAgIC55ID0gLmRpc3RfbWF0cml4JGRpc3QsIA0KICAgICAgICAgICAgICAgICAuZiA9IH4ga2Vybl9mdW4oeCA9IC5lcCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZCA9IC54LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEQgPSAueSkpDQogICAgcmV0dXJuKFJlZHVjZSgiKyIsIHRlbXApLy5kaXN0X21hdHJpeCRuX2N2KQ0KICB9DQoNCiAgIyBUcmlhbmd1bGFyDQogIHRyaWFuZ3VsYXJfa2VybmVsIDwtIGZ1bmN0aW9uKC5lcCA9IC4wNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmRpc3RfbWF0cml4LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAudHJhaW5fcmVzcG9uc2UyKXsNCiAga2Vybl9mdW4gPC0gZnVuY3Rpb24oeCwgaWQsIEQpew0KICAgIHRlbTAgPC0gYXMubWF0cml4KDEtIHgqRCkNCiAgICB0ZW0wW3RlbTAgPCAwXSA8LSAwDQogICAgeV9oYXQgPC0gbWFwX2RmYygueCA9IDE6bmNvbCh0ZW0wKSwNCiAgICAgICAgICAgICAgICAgICAgICAgLmYgPSAoXCh4XykgdGliYmxlKCJ7eF99IiA6PSB0YXBwbHkodGVtMFssIHhfXSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIElOREVYID0gLnRyYWluX3Jlc3BvbnNlMlsuZGlzdF9tYXRyaXgkaWRfc2h1ZmZsZSAhPSBpZF0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTiA9IHN1bSkpKSkgJT4lDQogICAgICAgIG1hcF9jaHIoLmYgPSAoXCh4KSBuYW1lcyh3aGljaC5tYXgoeCkpKSkNCiAgICAgIHJldHVybihtZWFuKHlfaGF0ICE9IC50cmFpbl9yZXNwb25zZTJbLmRpc3RfbWF0cml4JGlkX3NodWZmbGUgPT0gaWRdKSkNCiAgICB9DQogICAgdGVtcCA8LSBtYXAyKC54ID0gMTouZGlzdF9tYXRyaXgkbl9jdiwgDQogICAgICAgICAgICAgICAgIC55ID0gLmRpc3RfbWF0cml4JGRpc3QsIA0KICAgICAgICAgICAgICAgICAuZiA9IH4ga2Vybl9mdW4oeCA9IC5lcCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZCA9IC54LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEQgPSAueSkpDQogICAgcmV0dXJuKFJlZHVjZSgiKyIsIHRlbXApLy5kaXN0X21hdHJpeCRuX2N2KQ0KICB9DQoNCiAgIyBlcnJvciBmdW5jdGlvbg0KICBlcnJvcl9jdiA8LSBmdW5jdGlvbih4LCANCiAgICAgICAgICAgICAgICAgICAgICAgLmRpc3RfbWF0cml4ID0gTlVMTCwNCiAgICAgICAgICAgICAgICAgICAgICAgLmtlcm5lbF9mdW5jID0gTlVMTCwNCiAgICAgICAgICAgICAgICAgICAgICAgLnRyYWluX3Jlc3BvbnNlMiA9IE5VTEwpew0KICAgIHJlcyA8LSAua2VybmVsX2Z1bmMoLmVwID0geCwNCiAgICAgICAgICAgICAgICAgICAgICAgIC5kaXN0X21hdHJpeCA9IC5kaXN0X21hdHJpeCwNCiAgICAgICAgICAgICAgICAgICAgICAgIC50cmFpbl9yZXNwb25zZTIgPSAudHJhaW5fcmVzcG9uc2UyKQ0KICAgIHJldHVybihyZXMvbl9jdikNCiAgfQ0KDQogICMgbGlzdCBvZiBrZXJuZWwgZnVuY3Rpb25zDQogIGxpc3RfZnVucyA8LSBsaXN0KGdhdXNzaWFuID0gZ2F1c3NpYW5fa2VybmVsLA0KICAgICAgICAgICAgICAgICAgICBlcGFuZWNobmlrb3YgPSBlcGFuZWNobmlrb3Zfa2VybmVsLA0KICAgICAgICAgICAgICAgICAgICBiaXdlaWdodCA9IGJpd2VpZ2h0X2tlcm5lbCwNCiAgICAgICAgICAgICAgICAgICAgdHJpd2VpZ2h0ID0gdHJpd2VpZ2h0X2tlcm5lbCwNCiAgICAgICAgICAgICAgICAgICAgdHJpYW5ndWxhciA9IHRyaWFuZ3VsYXJfa2VybmVsLA0KICAgICAgICAgICAgICAgICAgICBuYWl2ZSA9IGVwYW5lY2huaWtvdl9rZXJuZWwpDQoNCiAgIyBlcnJvciBmb3IgYWxsIGtlcm5lbCBmdW5jdGlvbnMNCiAgZXJyb3JfZnVuYyA8LSBrZXJuZWxfcmVhbCAlPiUNCiAgICBtYXAoLmYgPSB+IChcKHgpIGVycm9yX2N2KHgsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmRpc3RfbWF0cml4ID0gZGlzdF9hbGwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAua2VybmVsX2Z1bmMgPSBsaXN0X2Z1bnNbWy54XV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAudHJhaW5fcmVzcG9uc2UyID0gdHJhaW5fcmVzcG9uc2VbbWFjaDIkaWQyXSkpKQ0KICBuYW1lcyhlcnJvcl9mdW5jKSA8LSBrZXJuZWxfcmVhbA0KICANCiAgIyBPcHRpbWl6YXRpb24NCiAgcGFyYW1ldGVycyA8LSBtYXAoLnggPSBrZXJuZWxfcmVhbCwNCiAgICAgICAgICAgICAgICAgICAgLmYgPSB+IGdyaWRPcHRpbWl6ZXIob2JqX2Z1biA9IGVycm9yX2Z1bmNbWy54XV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNldFBhcmFtZXRlciA9IHNldEdyaWRQYXJhbSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmFpdmUgPSAueCA9PSAibmFpdmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaWxlbnQgPSBzaWxlbnQsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGtlciA9IC54KSkNCiAgbmFtZXMocGFyYW1ldGVycykgPC0ga2VybmVsX3JlYWwNCiAgcmV0dXJuKGxpc3Qob3B0X3BhcmFtZXRlcnMgPSBwYXJhbWV0ZXJzLA0KICAgICAgICAgICAgICBhZGRfcGFyYW1ldGVycyA9IGxpc3Qoc2NhbGVfaW5wdXQgPSBzY2FsZV9pbnB1dCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGludl9zaWdtYSA9IGludl9zaWdtYSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFscCA9IGFscCksDQogICAgICAgICAgICAgIGJhc2ljX21hY2hpbmVzID0gbWFjaDIpKQ0KfQ0KYGBgDQoNCi0tLQ0KDQo+ICoqRXhhbXBsZS40Kio6IFdlIGFwcHJveGltYXRlIHRoZSBzbW9vdGhpbmcgcGFyYW1ldGVyIG9mIGBCb3N0b25gIGRhdGEuDQoNCi0tLQ0KDQpgYGB7cn0NCmRmIDwtIGlyaXMNCnRyYWluIDwtIGxvZ2ljYWwobnJvdyhkZikpDQp0cmFpbltzYW1wbGUobGVuZ3RoKHRyYWluKSwgZmxvb3IoMC43NSpucm93KGRmKSkpXSA8LSBUUlVFDQoNCnBhcmFtIDwtIGZpdF9wYXJhbWV0ZXIodHJhaW5fZGVzaWduID0gZGZbdHJhaW4sIDE6NF0sDQogICAgICAgICAgICAgICAgICAgICAgIHRyYWluX3Jlc3BvbnNlID0gZGYkU3BlY2llc1t0cmFpbl0sDQogICAgICAgICAgICAgICAgICAgICAgIG1hY2hpbmVzID0gYygia25uIiwgInJmIiwgInhnYiIpLA0KICAgICAgICAgICAgICAgICAgICAgICBzcGxpdHMgPSAuNSwNCiAgICAgICAgICAgICAgICAgICAgICAgbl9jdiA9IDMsDQogICAgICAgICAgICAgICAgICAgICAgIGtlcm5lbHMgPSBjKCJnYXVzc2lhbiIsImJpd2VpZ2h0IiwgIm5haXZlIiwgInRyaWFuZ3VsYXIiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgc2V0TWFjaGluZVBhcmFtID0gc2V0QmFzaWNQYXJhbWV0ZXIoayA9IDI6NSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnRyZWUgPSBjKDEsMyw1KSoxMDAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5yb3VuZHNfeGdiID0gYygxLDMsNSkqMTAwKSwNCiAgICAgICAgICAgICAgICAgICAgICAgc2V0R3JpZFBhcmFtID0gc2V0R3JpZFBhcmFtZXRlcihtaW5fdmFsID0gMC4wMDEsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhfdmFsID0gMC4zLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbl92YWwgPSAxMDApLA0KICAgICAgICAgICAgICAgICAgICAgICBzaWxlbnQgPSBGQUxTRSkNCnBhcmFtJG9wdF9wYXJhbWV0ZXJzICU+JQ0KICBtYXBfZGZjKC5mID0gfiAueCRvcHRfcGFyYW0pICU+JQ0KICBwcmludA0KYGBgDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogIzFGQUFFMzsiPjx1PlByZWRpY3Rpb248L3U+PC9zcGFuPg0KPT09DQoNClRoZSBzbW9vdGhpbmcgcGFyYW1ldGVyIG9idGFpbmVkIGZyb20gdGhlIHByZXZpb3VzIHNlY3Rpb24gY2FuIGJlIHVzZWQgdG8gbWFrZSB0aGUgZmluYWwgcHJlZGljdGVkIGNsYXNzZXMuIA0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQ7Ij48dT5LZXJuZWwgZnVuY3Rpb25zPC91Pjwvc3Bhbj4NCi0tLQ0KDQpTZXZlcmFsIHR5cGVzIG9mIGtlcm5lbCBmdW5jdGlvbnMgdXNlZCBmb3IgdGhlIGFnZ3JlZ2F0aW9uIGFyZSBkZWZpbmVkIGluIHRoaXMgc2VjdGlvbi4NCg0KLSAqKkFyZ3VtZW50Kio6DQogICAgDQogICAgLSBgLmhgIDogdGhlIHZhbHVlIG9mIHRoZSBwYXJhbWV0ZXIgdXNlZC4NCiAgICAtIGAueTJgIDogdGhlIHZlY3RvciBvZiByZXNwb25zZSB2YXJpYWJsZSBvZiB0aGUgc2Vjb25kIHBhcnQgJFxtYXRoY2Fse0R9X3tcZWxsfSQgb2YgdGhlIHRyYWluaW5nIGRhdGEsIHdoaWNoIGlzIHVzZWQgZm9yIHRoZSBhZ2dyZWdhdGlvbi4NCiAgICAtIGAuZGlzdGFuY2VgIDogdGhlIGRpc3RhbmNlIG1hdHJpeCBvYmplY3Qgb2J0YWluZWQgZnJvbSBgZGlzdF9tYXRyaXhgIGZ1bmN0aW9uLg0KICAgIC0gYC5rZXJuYCA6IHRoZSBzdHJpbmcgc3BlY2lmeWluZyB0aGUga2VybmVsIGZ1bmN0aW9uLiBCeSBkZWZhdWx0LCBgLmtlcm4gPSAiZ2F1c3NpYW4iYC4NCiAgICAtIGAuaW52X3NpZywgLmFscGAgOiB0aGUgcGFyYW1ldGVycyBvZiBleHBvbmVudGlhbCBrZXJuZWwgZnVuY3Rpb24uDQogICAgDQotICoqVmFsdWUqKjoNCg0KICAgIFRoaXMgZnVuY3Rpb24gcmV0dXJucyB0aGUgcHJlZGljdGVkIGNsYXNzZXMgb2YgdGhlIGFnZ3JlZ2F0aW9uIG1ldGhvZCBldmFsdWF0ZWQgd2l0aCB0aGUgZ2l2ZW4gcGFyYW1ldGVyICRoJC4NCg0KDQpgYGB7cn0NCmtlcm5lbF9wcmVkIDwtIGZ1bmN0aW9uKC5oLA0KICAgICAgICAgICAgICAgICAgICAgICAgLnkyLCANCiAgICAgICAgICAgICAgICAgICAgICAgIC5kaXN0YW5jZSwgDQogICAgICAgICAgICAgICAgICAgICAgICAua2VybiA9ICJnYXVzc2lhbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAuaW52X3NpZyA9IHNxcnQoLjUpLCANCiAgICAgICAgICAgICAgICAgICAgICAgIC5hbHAgPSAyKXsNCiAgZGlzIDwtIGFzLm1hdHJpeCguZGlzdGFuY2UpDQogICMgS2VybmVsIGZ1bmN0aW9ucyANCiAgIyA9PT09PT09PT09PT09PT09DQogIGdhdXNzaWFuX2tlcm5lbCA8LSBmdW5jdGlvbiguZXAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuaW52X3NpZ21hID0gLmludl9zaWcsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuYWxwaGEgPSAuYWxwKXsNCiAgICB0ZW0wIDwtIGV4cCgtICguZXAqZGlzKV4oLmFscGhhLzIpKi5pbnZfc2lnXi5hbHBoYSkNCiAgICB5X2hhdCA8LSBtYXBfZGZjKC54ID0gMTpuY29sKHRlbTApLA0KICAgICAgICAgICAgICAgICAgICAgLmYgPSAoXCh4XykgdGliYmxlKCJ7eF99IiA6PSB0YXBwbHkodGVtMFssIHhfXSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIElOREVYID0gLnkyLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBGVU4gPSBzdW0pKSkpICU+JQ0KICAgICAgICBtYXBfY2hyKC5mID0gKFwoeCkgbmFtZXMod2hpY2gubWF4KHgpKSkpDQogICAgcmV0dXJuKGFzLnZlY3Rvcih5X2hhdCkpDQogIH0NCg0KICAjIEVwYW5lY2huaWtvdg0KICBlcGFuZWNobmlrb3Zfa2VybmVsIDwtIGZ1bmN0aW9uKC5lcCl7DQogICAgdGVtMCA8LSAxLSAuZXAqZGlzDQogICAgdGVtMFt0ZW0wIDwgMF0gPSAwDQogICAgeV9oYXQgPC0gbWFwX2RmYygueCA9IDE6bmNvbCh0ZW0wKSwNCiAgICAgICAgICAgICAgICAgICAgIC5mID0gKFwoeF8pIHRpYmJsZSgie3hffSIgOj0gdGFwcGx5KHRlbTBbLCB4X10sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBJTkRFWCA9IC55MiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOID0gc3VtKSkpKSAlPiUNCiAgICAgICAgbWFwX2NociguZiA9IChcKHgpIG5hbWVzKHdoaWNoLm1heCh4KSkpKQ0KICAgIHJldHVybihhcy52ZWN0b3IoeV9oYXQpKQ0KICB9DQogICMgQml3ZWlnaHQNCiAgYml3ZWlnaHRfa2VybmVsIDwtIGZ1bmN0aW9uKC5lcCl7DQogICAgdGVtMCA8LSAxLSAuZXAqZGlzDQogICAgdGVtMFt0ZW0wIDwgMF0gPSAwDQogICAgeV9oYXQgPC0gbWFwX2RmYygueCA9IDE6bmNvbCh0ZW0wKSwNCiAgICAgICAgICAgICAgICAgICAgIC5mID0gKFwoeF8pIHRpYmJsZSgie3hffSIgOj0gdGFwcGx5KHRlbTBbLCB4X10sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBJTkRFWCA9IC55MiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgRlVOID0gc3VtKSkpKSAlPiUNCiAgICAgICAgbWFwX2NociguZiA9IChcKHgpIG5hbWVzKHdoaWNoLm1heCh4KSkpKQ0KICAgIHJldHVybihhcy52ZWN0b3IoeV9oYXQpKQ0KICB9DQoNCiAgIyBUcml3ZWlnaHQNCiAgdHJpd2VpZ2h0X2tlcm5lbCA8LSBmdW5jdGlvbiguZXApew0KICAgIHRlbTAgPC0gMS0gLmVwKmRpcw0KICAgIHRlbTBbdGVtMCA8IDBdID0gMA0KICAgIHlfaGF0IDwtIG1hcF9kZmMoLnggPSAxOm5jb2wodGVtMCksDQogICAgICAgICAgICAgICAgICAgICAuZiA9IChcKHhfKSB0aWJibGUoInt4X30iIDo9IHRhcHBseSh0ZW0wWywgeF9dLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgSU5ERVggPSAueTIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTiA9IHN1bSkpKSkgJT4lDQogICAgICAgIG1hcF9jaHIoLmYgPSAoXCh4KSBuYW1lcyh3aGljaC5tYXgoeCkpKSkNCiAgICByZXR1cm4oYXMudmVjdG9yKHlfaGF0KSkNCiAgfQ0KDQogICMgVHJpYW5ndWxhcg0KICB0cmlhbmd1bGFyX2tlcm5lbCA8LSBmdW5jdGlvbiguZXApew0KICAgIHRlbTAgPC0gMS0gLmVwKmRpcw0KICAgIHRlbTBbdGVtMCA8IDBdIDwtIDANCiAgICB5X2hhdCA8LSBtYXBfZGZjKC54ID0gMTpuY29sKHRlbTApLA0KICAgICAgICAgICAgICAgICAgICAgLmYgPSAoXCh4XykgdGliYmxlKCJ7eF99IiA6PSB0YXBwbHkodGVtMFssIHhfXSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIElOREVYID0gLnkyLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBGVU4gPSBzdW0pKSkpICU+JQ0KICAgICAgICBtYXBfY2hyKC5mID0gKFwoeCkgbmFtZXMod2hpY2gubWF4KHgpKSkpDQogICAgcmV0dXJuKGFzLnZlY3Rvcih5X2hhdCkpDQogIH0NCiAgIyBOYWl2ZQ0KICBuYWl2ZV9rZXJuZWwgPC0gZnVuY3Rpb24oLmVwID0gTlVMTCl7DQogICAgICB5X2hhdCA8LSBtYXBfZGZjKC54ID0gMTpuY29sKGRpcyksDQogICAgICAgICAgICAgICAgICAgICAgIC5mID0gKFwoeF8pIHRpYmJsZSgie3hffSIgOj0gdGFwcGx5KGRpc1ssIHhfXSA9PSAwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgSU5ERVggPSAueTIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTiA9IHN1bSkpKSkgJT4lDQogICAgICAgIG1hcF9jaHIoLmYgPSAoXCh4KSBuYW1lcyh3aGljaC5tYXgoeCkpKSkNCiAgICByZXR1cm4oYXMudmVjdG9yKHlfaGF0KSkNCiAgfQ0KICAjIFByZWRpY3Rpb24NCiAga2VybmVsX2xpc3QgPC0gbGlzdChnYXVzc2lhbiA9IGdhdXNzaWFuX2tlcm5lbCwNCiAgICAgICAgICAgICAgICAgICAgICBlcGFuZWNobmlrb3YgPSBlcGFuZWNobmlrb3Zfa2VybmVsLA0KICAgICAgICAgICAgICAgICAgICAgIGJpd2VpZ2h0ID0gYml3ZWlnaHRfa2VybmVsLA0KICAgICAgICAgICAgICAgICAgICAgIHRyaXdlaWdodCA9IHRyaXdlaWdodF9rZXJuZWwsDQogICAgICAgICAgICAgICAgICAgICAgdHJpYW5ndWxhciA9IHRyaWFuZ3VsYXJfa2VybmVsLA0KICAgICAgICAgICAgICAgICAgICAgIG5haXZlID0gbmFpdmVfa2VybmVsKQ0KICByZXMgPC0gdGliYmxlKGFzLnZlY3RvcihrZXJuZWxfbGlzdFtbLmtlcm5dXSguZXAgPSAuaCkpKQ0KICBuYW1lcyhyZXMpIDwtIC5rZXJuDQogIHJldHVybihyZXMpDQp9DQpgYGANCg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQ7Ij48dT5GdW5jdGlvbnM8L3U+PC9zcGFuPjogYHByZWRpY3RfYWdnYA0KLS0tDQoNCi0gKipBcmd1bWVudCoqOg0KICAgIA0KICAgIC0gYGZpdHRlZF9tb2RlbHNgIDogdGhlIG9iamVjdCBvYnRhaW5lZCBmcm9tIGBmaXRfcGFyYW1ldGVyYCBmdW5jdGlvbi4NCiAgICAtIGBuZXdfZGF0YWAgOiB0aGUgbmV3IHRlc3RpbmcgZGF0YSB0byBiZSBwcmVkaWN0ZWQuDQogICAgLSBgdGVzdF9yZXNwb25zZWAgOiB0aGUgYWN0dWFsIGNsYXNzZXMgb2YgdGVzdGluZyBkYXRhLiBJdCBpcyBvcHRpb25hbC4gSWYgaXQgaXMgZ2l2ZW4sIHRoZSBtaXNjbGFzc2lmaWNhdGlvbiBhbmQgYWNjdXJhY3kgZXZhbHVhdGVkIG9uIHRoZSB0ZXN0aW5nIGRhdGEgYXJlIGFsc28gY29tcHV0ZWQuIEJ5IGRlZmF1bHQsIGB0ZXN0X3Jlc3BvbnNlID0gTlVMTGAuDQogICAgLSBgbmFpdmVgIDogbG9naWNhbCB2YWx1ZSBzcGVjaWZ5aW5nIHdoZXRoZXIgYCJuYWl2ZSJgIGtlcm5lbCBpcyB1c2VkIG9yIG5vdC4NCiAgICANCi0gKipWYWx1ZSoqOg0KDQogICAgVGhpcyBmdW5jdGlvbiByZXR1cm5zIGEgKmxpc3QqIG9mIHRoZSBmb2xsb3dpbmcgb2JqZWN0czoNCiAgICANCiAgICAtIGBmaXR0ZWRfYWdncmVnYXRlYCA6IHRoZSBwcmVkaWN0aW9ucyBieSB0aGUgYWdncmVnYXRpb24gbWV0aG9kcy4NCiAgICAtIGBmaXR0ZWRfbWFjaGluZWAgOiB0aGUgcHJlZGljdGlvbnMgZ2l2ZW4gYnkgYWxsIHRoZSBiYXNpYyBtYWNoaW5lcy4NCiAgICAtIGBtaXNfZXJyb3JgLCBgYWNjdXJhY3lgIDogdGhlIG1pc2NsYXNzaWZpY2F0aW9uIGVycm9yIGFuZCBhY2N1cmFjeSBjb21wdXRlZCBvbmx5IGlmIHRoZSBgdGVzdF9yZXBvbnNlYCBhcmd1bWVudCBpcyBub3QgYE5VTExgLg0KDQoNCmBgYHtyfQ0KIyBQcmVkaWN0aW9uDQpwcmVkaWN0X2FnZyA8LSBmdW5jdGlvbihmaXR0ZWRfbW9kZWxzLA0KICAgICAgICAgICAgICAgICAgICAgICAgbmV3X2RhdGEsDQogICAgICAgICAgICAgICAgICAgICAgICB0ZXN0X3Jlc3BvbnNlID0gTlVMTCwNCiAgICAgICAgICAgICAgICAgICAgICAgIG5haXZlID0gRkFMU0Upew0KICBvcHRfcGFyYW0gPC0gZml0dGVkX21vZGVscyRvcHRfcGFyYW1ldGVycw0KICBhZGRfcGFyYW0gPC0gZml0dGVkX21vZGVscyRhZGRfcGFyYW1ldGVycw0KICBiYXNpY19tYWNoIDwtIGZpdHRlZF9tb2RlbHMkYmFzaWNfbWFjaGluZXMNCiAga2VybjAgPC0gbmFtZXMob3B0X3BhcmFtKQ0KICBuZXdfZGF0YV8gPC0gbmV3X2RhdGENCiAgIyBpZiBiYXNpYyBtYWNoaW5lcyBhcmUgYnVpbHQNCiAgaWYoaXMubGlzdChiYXNpY19tYWNoJG1vZGVscykpew0KICAgIG1hdF9pbnB1dCA8LSBhcy5tYXRyaXgoYmFzaWNfbWFjaCR0cmFpbl9kYXRhJHRyYWluX2lucHV0KQ0KICAgIGlmKGFkZF9wYXJhbSRzY2FsZV9pbnB1dCl7DQogICAgICBuZXdfZGF0YV8gPC0gc2NhbGUobmV3X2RhdGEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgIGNlbnRlciA9IGJhc2ljX21hY2gkc2NhbGVfbWluLCANCiAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZSA9IGJhc2ljX21hY2gkc2NhbGVfbWF4IC0gYmFzaWNfbWFjaCRzY2FsZV9taW4pDQogICAgfQ0KICAgIGlmKGlzLm1hdHJpeChuZXdfZGF0YV8pKXsNCiAgICAgIG1hdF90ZXN0IDwtIG5ld19kYXRhXw0KICAgICAgZGZfdGVzdCA8LSBhc190aWJibGUobmV3X2RhdGFfKQ0KICAgIH0gZWxzZSB7DQogICAgICBtYXRfdGVzdCA8LSBhcy5tYXRyaXgobmV3X2RhdGFfKQ0KICAgICAgZGZfdGVzdCA8LSBuZXdfZGF0YV8NCiAgICB9DQogICAgDQogICAgIyBQcmVkaWN0aW9uIHRlc3QgYnkgYmFzaWMgbWFjaGluZXMNCiAgICBidWlsdF9tb2RlbHMgPC0gYmFzaWNfbWFjaCRtb2RlbHMNCiAgICBwcmVkX3Rlc3QgPC0gZnVuY3Rpb24obWV0aCl7DQogICAgICBpZihtZXRoID09ICJrbm4iKXsNCiAgICAgICAgcHJlIDwtIDE6bGVuZ3RoKGJ1aWx0X21vZGVsc1tbbWV0aF1dKSAlPiUNCiAgICAgICAgICBtYXBfZGZjKC5mID0gKFwoaykgdGliYmxlKCd7e2t9fScgOj0gRk5OOjprbm4odHJhaW4gPSBtYXRfaW5wdXRbIWJhc2ljX21hY2gkaWQyLF0sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXN0ID0gbWF0X3Rlc3QsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbCA9IGJhc2ljX21hY2gkdHJhaW5fZGF0YSR0cmFpbl9yZXNwb25zZVshYmFzaWNfbWFjaCRpZDJdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrID0gYnVpbHRfbW9kZWxzW1ttZXRoXV1bW2tdXSkpKSkNCiAgICAgIH0NCiAgICAgIGlmKG1ldGggPT0gInhnYiIpew0KICAgICAgICBwcmUgPC0gMTpsZW5ndGgoYnVpbHRfbW9kZWxzW1ttZXRoXV0pICU+JQ0KICAgICAgICAgIG1hcF9kZmMoLmYgPSAoXChrKSB0aWJibGUoJ3t7a319JyA6PSBhcy52ZWN0b3IoYmFzaWNfbWFjaCR0cmFpbl9kYXRhJGNsYXNzZXNbcHJlZGljdChidWlsdF9tb2RlbHNbW21ldGhdXVtba11dLCBtYXRfdGVzdCldKSkpKQ0KICAgICAgfQ0KICAgICAgaWYobWV0aCA9PSAiYWRhYm9vc3QiKXsNCiAgICAgICAgcHJlIDwtIDE6bGVuZ3RoKGJ1aWx0X21vZGVsc1tbbWV0aF1dKSAlPiUNCiAgICAgICAgICBtYXBfZGZjKC5mID0gKFwoaykgdGliYmxlKCd7e2t9fScgOj0gYXMudmVjdG9yKHByZWRpY3QuYm9vc3RpbmcoYnVpbHRfbW9kZWxzW1ttZXRoXV1bW2tdXSwgYXMuZGF0YS5mcmFtZShkZl90ZXN0KSkkY2xhc3MpKSkpDQogICAgICB9DQogICAgICBpZighKG1ldGggJWluJSBjKCJ4Z2IiLCAia25uIiwgImFkYWJvb3N0IikpKXsNCiAgICAgICAgcHJlIDwtIDE6bGVuZ3RoKGJ1aWx0X21vZGVsc1tbbWV0aF1dKSAlPiUNCiAgICAgICAgICBtYXBfZGZjKC5mID0gKFwoaykgdGliYmxlKCd7e2t9fScgOj0gYXMudmVjdG9yKHByZWRpY3QoYnVpbHRfbW9kZWxzW1ttZXRoXV1bW2tdXSwgZGZfdGVzdCwgdHlwZSA9ICdjbGFzcycpKSkpKQ0KICAgICAgfQ0KICAgICAgY29sbmFtZXMocHJlKSA8LSBuYW1lcyhidWlsdF9tb2RlbHNbW21ldGhdXSkNCiAgICAgIHJldHVybihwcmUpDQogICAgfQ0KICAgIHByZWRfdGVzdF9hbGwgPC0gbmFtZXMoYnVpbHRfbW9kZWxzKSAlPiUNCiAgICAgIG1hcF9kZmMoLmYgPSBwcmVkX3Rlc3QpDQogIH0gZWxzZXsNCiAgICBwcmVkX3Rlc3RfYWxsIDwtIG5ld19kYXRhXw0KICB9DQogIHByZWRfdGVzdDAgPC0gcHJlZF90ZXN0X2FsbA0KICAjIFByZWRpY3Rpb24gdHJhaW4yDQogIHByZWRfdHJhaW5fYWxsIDwtIGJhc2ljX21hY2gkcHJlZGljdDINCiAgY29sbmFtZXMocHJlZF90ZXN0X2FsbCkgPC0gY29sbmFtZXMocHJlZF90cmFpbl9hbGwpDQogIGRfdHJhaW4gPC0gZGltKHByZWRfdHJhaW5fYWxsKQ0KICBkX3Rlc3QgPC0gZGltKHByZWRfdGVzdF9hbGwpDQogIHByZWRfdGVzdF9tYXQgPC0gYXMubWF0cml4KHByZWRfdGVzdF9hbGwpDQogIHByZWRfdHJhaW5fbWF0IDwtIGFzLm1hdHJpeChwcmVkX3RyYWluX2FsbCkNCiAgIyBEaXN0YW5jZSBtYXRyaXgNCiAgZGlzdHMgPC0gMTpkX3Rlc3RbMV0gJT4lDQogICAgICBtYXBfZGZjKC5mID0gKFwoaWQpIHRpYmJsZSgne3tpZH19JyA6PSByb3dTdW1zKHN3ZWVwKHByZWRfdHJhaW5fbWF0LCAyLCBwcmVkX3Rlc3RfbWF0W2lkLF0sIEZVTiA9ICIhPSIpKSkpKQ0KICBwcmVkaWN0aW9uIDwtIDE6bGVuZ3RoKGtlcm4wKSAlPiUgDQogICAgbWFwX2RmYyguZiA9IH4ga2VybmVsX3ByZWQoLmggPSBvcHRfcGFyYW1bW2tlcm4wWy54XV1dJG9wdF9wYXJhbSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAueTIgPSBiYXNpY19tYWNoJHRyYWluX2RhdGEkdHJhaW5fcmVzcG9uc2VbYmFzaWNfbWFjaCRpZDJdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5kaXN0YW5jZSA9IGRpc3RzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5rZXJuID0ga2VybjBbLnhdLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuaW52X3NpZyA9IGFkZF9wYXJhbSRpbnZfc2lnbWEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5hbHAgPSBhZGRfcGFyYW0kYWxwKSkNCiAgaWYoaXMubnVsbCh0ZXN0X3Jlc3BvbnNlKSl7DQogICAgcmV0dXJuKGxpc3QoZml0dGVkX2FnZ3JlZ2F0ZSA9IHByZWRpY3Rpb24sDQogICAgICAgICAgIGZpdHRlZF9tYWNoaW5lID0gcHJlZF90ZXN0X2FsbCkpDQogIH0gZWxzZXsNCiAgICBlcnJvciA8LSBjYmluZChwcmVkX3Rlc3RfYWxsLCBwcmVkaWN0aW9uKSAlPiUNCiAgICAgIGRwbHlyOjptdXRhdGUoeV90ZXN0ID0gdGVzdF9yZXNwb25zZSkgJT4lDQogICAgICBkcGx5cjo6c3VtbWFyaXNlX2FsbCguZnVucyA9IH4gKC4gIT0geV90ZXN0KSkgJT4lDQogICAgICBkcGx5cjo6c2VsZWN0KC15X3Rlc3QpICU+JQ0KICAgICAgZHBseXI6OnN1bW1hcmlzZV9hbGwoLmZ1bnMgPSB+IG1lYW4oLikpDQogICAgcmV0dXJuKGxpc3QoZml0dGVkX2FnZ3JlZ2F0ZSA9IHByZWRpY3Rpb24sDQogICAgICAgICAgICAgICAgZml0dGVkX21hY2hpbmUgPSBwcmVkX3Rlc3QwLA0KICAgICAgICAgICAgICAgIG1pc19lcnJvciA9IGVycm9yLA0KICAgICAgICAgICAgICAgIGFjY3VyYWN5ID0gMSAtIGVycm9yKSkNCiAgfQ0KfQ0KYGBgDQoNCi0tLQ0KDQo+ICoqRXhhbXBsZS41KiogQWdncmVnYXRpb24gb24gYGlyaXNgIGRhdGFzZXQuDQoNCi0tLQ0KDQpgYGB7cn0NCnByZWQgPC0gcHJlZGljdF9hZ2cocGFyYW0sDQogICAgICAgICAgICBuZXdfZGF0YSA9IGRmWyF0cmFpbiwgMTo0XSwNCiAgICAgICAgICAgIHRlc3RfcmVzcG9uc2UgPSBkZiRTcGVjaWVzWyF0cmFpbl0pDQpwcmVkJG1pc19lcnJvcg0KYGBgDQoNCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+PHU+RnVuY3Rpb248L3U+PC9zcGFuPiA6IGBrZXJuZWxBZ2dDbGFzc2ANCj09PQ0KDQpUaGlzIGZ1bmN0aW9uIHB1dHMgdG9nZXRoZXIgYWxsIHRoZSBmdW5jdGlvbnMgYWJvdmUgYW5kIHByb3ZpZGVzIHRoZSBkZXNpcmUgcmVzdWx0IG9mIHRoZSBrZXJuZWwtYmFzZWQgY29tYmluZWQgY2xhc3NpZmllcnMuDQoNCi0gKipBcmd1bWVudCoqOg0KICAgIA0KICAgIC0gYHRyYWluX2Rlc2lnbmAgOiBhIG1hdHJpeCBvciBkYXRhIGZyYW1lIG9mIHRoZSB0cmFpbmluZyAqaW5wdXQgZGF0YSogb3IgdGhlICpwcmVkaWN0ZWQgY2xhc3NlcyogZ2l2ZW4gYnkgc29tZSBjbGFzc2lmaWVycy4NCiAgICAgICAgLSBJZiB0aGUgKmlucHV0KiBkYXRhIGlzIGdpdmVuLCB0aGUgb3B0aW9uIGBidWlsZF9tYWNoaW5lYCBtdXN0IGJlIGBUUlVFYCBhbmQgYWxsIHRoZSBjaG9zZW4gbWFjaGluZXMgd2lsbCBiZSBjb25zdHJ1Y3RlZCB1c2luZyBhIHByb3BvcnRpb24gb2YgdGhlIHRyYWluaW5nIGlucHV0IChgc3BsaXRzYCkuDQogICAgICAgIC0gSWYgdGhlICpwcmVkaWN0ZWQgY2xhc3NlcyogYXJlIGdpdmVuLCB0aGUgb3B0aW9uIGBidWlsZF9tYWNoaW5lYCBtdXN0IGJlIGBGQUxTRWAgd2hpY2ggaW5kaWNhdGVzIHRoYXQgbm8gYmFzaWMgbWFjaGluZSBpcyBjb25zdHJ1Y3RlZCwgYW5kIHRoZSBwYXJhbXRlciBpcyB0dW5lZCB1c2luZyB0aGlzIG1hdHJpeCBvZiBwcmVkaWN0ZWQgY2xhc3NlcyBkaXJlY3RseS4NCiAgICAtIGB0cmFpbl9yZXNwb25zZWAgOiBhIHZlY3RvciBvZiBjb3JyZXNwb25kaW5nIGNsYXNzZXMgb2YgdGhlIGB0cmFpbl9kZXNpZ25gLg0KICAgIC0gYHRlc3RfcmVzcG9uc2VgIDogdGhlIGFjdHVhbCBjbGFzc2VzIG9mIHRlc3RpbmcgZGF0YS4gSXQgaXMgb3B0aW9uYWwuIElmIGl0IGlzIGdpdmVuLCB0aGUgbWlzY2xhc3NpZmljYXRpb24gYW5kIGFjY3VyYWN5IGV2YWx1YXRlZCBvbiB0aGUgdGVzdGluZyBkYXRhIGFyZSBhbHNvIGNvbXB1dGVkLiBCeSBkZWZhdWx0LCBgdGVzdF9yZXNwb25zZSA9IE5VTExgLg0KICAgIC0gYHNjYWxlX2lucHV0YCA6IGEgbG9naWNhbCB2YWx1ZSBzcGVjaWZ5aW5nIHdoZXRoZXIgb3Igbm90IHRvIHNjYWxlIHRoZSBpbnB1dCBkYXRhIGJlZm9yZSBidWlsZGluZyB0aGUgYmFzaWMgY2xhc3NpZmllcnMuIEJ5IGRlZmF1bHQsIGBzY2FsZV9pbnB1dCA9IEZBTFNFYC4NCiAgICAtIGBidWlsZF9tYWNoaW5lYDogYSBsb2dpY2FsIHZhbHVlIHNwZWNpZnlpbmcgd2hldGhlciBvciBub3QgdGhlIGJhc2ljIG1hY2hpbmVzIHNob3VsZCBiZSBjb25zdHJ1Y3RlZC4gSXQgc2hvdWxkIGJlIGBUUlVFYCBpZiB0aGUgaW5wdXQgZGF0YSBpcyBnaXZlbiB0byB0aGUgYHRyYWluX2Rlc2lnbmAsIGVsc2UgaXQgc2hvdWxkIGJlIGBGQUxTRWAuIEJ5IGRlZmF1bHQsIGBidWlsZF9tYWNoaW5lID0gVFJVRWAuDQogICAgLSBgbWFjaGluZXNgIDogYSB2ZWN0b3Igb2YgYmFzaWMgbWFjaGluZXMgdG8gYmUgY29uc3RydWN0ZWQuIEl0IG11c3QgYmUgYSBzdWJzZXQgb2Yge2Aia25uImAsIGAidHJlZSJgLCBgInJmImAsIGAibG9naXQiYCwgYCJzdm0iYCwgYCJ4Z2IiYCwgYCJhZGFib29zdCJgfS4gQnkgZGVmYXVsdCwgYG1hY2hpbmVzID0gTlVMTGAsIGFuZCBhbGwgdHlwZXMgb2YgYmFzaWMgbWFjaGluZXMgYXJlIGJ1aWx0Lg0KICAgIC0gYHNwbGl0c2AgOiB0aGUgcHJvcG9ydGlvbiBvZiB0cmFpbmluZyBkYXRhIHVzZWQgdG8gYnVpbGQgdGhlIGJhc2ljIG1hY2hpbmVzLiBCeSBkZWZhdWx0LCBgc3BsaXRzID0gLjVgLg0KICAgIC0gYG5fY3ZgIDogdGhlIG51bWJlciBvZiBjcm9zcy12YWxpZGF0aW9uIGZvbGRzIHVzZWQgdG8gdHVuZSB0aGUgc21vb3RoaW5nIHBhcmFtZXRlci4NCiAgICAtIGBpbnZfc2lnLCBhbHBgIDogdGhlIGludmVyc2Ugbm9ybWFsaXplZCBjb25zdGFudCAkXHNpZ21hXnstMX0+MCQgYW5kIHRoZSBleHBvbmVudCAkXGFscGhhPjAkIG9mIGV4cG9uZW50aWFsIGtlcm5lbDogJEsoeCk9ZV57LVx8eC9cc2lnbWFcfF57XGFscGhhfX0kIGZvciBhbnkgJHhcaW5cbWF0aGJie1J9XmQkLiBCeSBkZWZhdWx0LCBgaW52X3NpZ21hID0gYCRcc3FydHsxLzJ9JCBhbmQgYGFscGhhID0gMmAgd2hpY2ggY29ycmVzcG9uZHMgdG8gdGhlIEdhdXNzaWFuIGtlcm5lbC4NCiAgICAtIGBrZXJuZWxzYCA6IHRoZSBrZXJuZWwgZnVuY3Rpb24gb3IgdmVjdG9yIG9mIGtlcm5lbCBmdW5jdGlvbnMgdXNlZCBmb3IgdGhlIGFnZ3JlZ2F0aW9uLg0KICAgIC0gYHNldE1hY2hpbmVQYXJhbWAgOiBhbiBvcHRpb24gdXNlZCB0byBzZXQgdGhlIHZhbHVlcyBvZiB0aGUgcGFyYW1ldGVycyBvZiB0aGUgYmFzaWMgbWFjaGluZXMuIGBzZXRCYXNpY1BhcmFtZXRlcmAgZnVuY3Rpb24gc2hvdWxkIGJlIGZlZCB0byB0aGlzIGFyZ3VtZW50Lg0KICAgIC0gYHNldEdyaWRQYXJhbWAgOiBhbiBvcHRpb24gdXNlZCB0byBzZXQgdGhlIHZhbHVlcyBvZiB0aGUgcGFyYW1ldGVycyBvZiB0aGUgZ3JpZCBzZWFyY2ggYWxnb3JpdGhtLiBUaGUgYHNldEdyaWRQYXJhbWV0ZXJgIGZ1bmN0aW9uIHNob3VsZCBiZSBmZWQgdG8gaXQuDQogICAgLSBgc2lsZW50YCA6IGEgbG9naWNhbCB2YWx1ZSB0byBzaWxlbnQgYWxsIHRoZSBtZXNzYWdlcy4NCiAgICANCi0gKipWYWx1ZSoqOg0KDQogICAgVGhpcyBmdW5jdGlvbiByZXR1cm4gYSAqbGlzdCogb2YgdGhlIGZvbGxvd2luZyBvYmplY3RzOg0KICAgIA0KICAgIC0gYGZpdHRlZF9hZ2dyZWdhdGVgIDogdGhlIHByZWRpY3Rpb25zIG9mIHRoZSB0ZXN0aW5nIGRhdGEgZ2l2ZW4gYnkgdGhlIGFnZ3JlZ2F0aW9uIG1ldGhvZHMgY29ycmVwc29uZGluZyB0byBhbGwgdGhlIGtlcm5lbCBmdW5jdGlvbnMuDQogICAgLSBgZml0dGVkX21hY2hpbmVgIDogdGhlIHByZWRpY3Rpb25zIG9mIHRoZSB0ZXN0aW5nIGRhdGEgZ2l2ZW4gYnkgdGhlIGJhc2ljIG1hY2hpbmVzLg0KICAgIC0gYHByZWRfdHJhaW4yYCA6IHRoZSBwcmVkaWN0aW9ucyBvZiB0aGUgc2Vjb25kIHBhcnQgb2YgdGhlIHRyYWluaW5nIGRhdGEgJFxtYXRoY2Fse0R9X3tcZWxsfSQgYnkgYWxsIHRoZSBiYXNpYyBtYWNoaW5lcy4NCiAgICAtIGBvcHRfcGFyYW1ldGVyYCA6IHRoZSBvYnNlcnZlZCBvcHRpbWFsIHNtb290aGluZyBwYXJhbWV0ZXJzLg0KICAgIC0gYG1zZWAgOiB0aGUgbWVhbiBzcXVhcmUgZXJyb3IgZXZhbHVhdGVkIG9uIHRoZSB0ZXN0aW5nIGRhdGEgaWYgdGhlIHRlc3RpbmcgcmVzcG9uc2UgdmFyaWFibGUgaXMgZ2l2ZW4uDQogICAgLSBga2VybmVsc2AgOiB0aGUga2VybmVsIGZ1bmN0aW9ucyB1c2VkLg0KICAgIA0KDQpgYGB7cn0NCmtlcm5lbEFnZ0NsYXNzIDwtIGZ1bmN0aW9uKHRyYWluX2Rlc2lnbiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbl9yZXNwb25zZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3RfZGVzaWduLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdF9yZXNwb25zZSA9IE5VTEwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgYnVpbGRfbWFjaGluZSA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBtYWNoaW5lcyA9IE5VTEwsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgc3BsaXRzID0gMC41LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fY3YgPSA1LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgaW52X3NpZ21hID0gc3FydCguNSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBhbHAgPSAyLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAga2VybmVscyA9ICJnYXVzc2lhbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzZXRNYWNoaW5lUGFyYW0gPSBzZXRCYXNpY1BhcmFtZXRlcigpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgc2V0R3JpZFBhcmFtID0gc2V0R3JpZFBhcmFtZXRlcigpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgc2lsZW50ID0gRkFMU0Upew0KICAjIGJ1aWxkIG1hY2hpbmVzICsgdHVuZSBwYXJhbWV0ZXINCiAgZml0X21vZCA8LSBmaXRfcGFyYW1ldGVyKHRyYWluX2Rlc2lnbiA9IHRyYWluX2Rlc2lnbiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbl9yZXNwb25zZSA9IHRyYWluX3Jlc3BvbnNlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfaW5wdXQgPSBzY2FsZV9pbnB1dCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ1aWxkX21hY2hpbmUgPSBidWlsZF9tYWNoaW5lLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgbWFjaGluZXMgPSBtYWNoaW5lcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzcGxpdHMgPSBzcGxpdHMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgbl9jdiA9IG5fY3YsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBpbnZfc2lnbWEgPSBpbnZfc2lnbWEsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBhbHAgPSBhbHAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBrZXJuZWxzID0ga2VybmVscywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNldE1hY2hpbmVQYXJhbSA9IHNldE1hY2hpbmVQYXJhbSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNldEdyaWRQYXJhbSA9IHNldEdyaWRQYXJhbSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNpbGVudCA9IHNpbGVudCkNCiAgIyBwcmVkaWN0aW9uDQogIHByZWQgPC0gcHJlZGljdF9hZ2coZml0dGVkX21vZGVscyA9IGZpdF9tb2QsDQogICAgICAgICAgICAgICAgICAgICAgbmV3X2RhdGEgPSB0ZXN0X2Rlc2lnbiwNCiAgICAgICAgICAgICAgICAgICAgICB0ZXN0X3Jlc3BvbnNlID0gdGVzdF9yZXNwb25zZSkNCiAgcmV0dXJuKGxpc3QoZml0dGVkX2FnZ3JlZ2F0ZSA9IHByZWQkZml0dGVkX2FnZ3JlZ2F0ZSwNCiAgICAgICAgICAgICAgZml0dGVkX21hY2hpbmUgPSBwcmVkJGZpdHRlZF9tYWNoaW5lLA0KICAgICAgICAgICAgICBwcmVkX3RyYWluMiA9IGZpdF9tb2QkYmFzaWNfbWFjaGluZXMkcHJlZGljdDIsDQogICAgICAgICAgICAgIG9wdF9wYXJhbWV0ZXIgPSBmaXRfbW9kJG9wdF9wYXJhbWV0ZXJzLA0KICAgICAgICAgICAgICBtaXNfY2xhc3MgPSBwcmVkJG1pc19lcnJvciwNCiAgICAgICAgICAgICAgYWNjdXJhY3kgPSBwcmVkJGFjY3VyYWN5LA0KICAgICAgICAgICAgICBrZXJuZWxzID0ga2VybmVscywNCiAgICAgICAgICAgICAgaW5kX0QyID0gZml0X21vZCRiYXNpY19tYWNoaW5lcyRpZDIpKQ0KfQ0KYGBgDQoNCi0tLQ0KDQo+KipFeGFtcGxlLjYqKiBBIGNvbXBsZXRlIGFnZ3JlZ2F0aW9uIGlzIGltcGxlbWVudGVkIG9uIGBpcmlzYCBkYXRhc2V0LiBGb3VyIHR5cGVzIG9mIGJhc2ljIG1hY2hpbmVzLCBhbmQgZm91ciBkaWZmZXJlbnQga2VybmVsIGZ1bmN0aW9ucyBhcmUgdXNlZC4gDQoNCi0tLQ0KDQpgYGB7cn0NCmRmMSA8LSBpcmlzDQp0cmFpbiA8LSBsb2dpY2FsKG5yb3coZGYxKSkNCnRyYWluW3NhbXBsZShsZW5ndGgodHJhaW4pLCBmbG9vcigwLjgqbnJvdyhkZjEpKSldIDwtIFRSVUUNCg0KYWdnIDwtIGtlcm5lbEFnZ0NsYXNzKHRyYWluX2Rlc2lnbiA9IGRmMVt0cmFpbiwgMTo0XSwNCiAgICAgICAgICAgICAgICAgICAgICB0cmFpbl9yZXNwb25zZSA9IGRmMSRTcGVjaWVzW3RyYWluXSwNCiAgICAgICAgICAgICAgICAgICAgICB0ZXN0X2Rlc2lnbiA9IGRmMVshdHJhaW4sIDE6NF0sDQogICAgICAgICAgICAgICAgICAgICAgdGVzdF9yZXNwb25zZSA9IGRmMSRTcGVjaWVzWyF0cmFpbl0sDQogICAgICAgICAgICAgICAgICAgICAgbWFjaGluZXMgPSBjKCJrbm4iLCAicmYiLCAieGdiIiwgInN2bSIpLA0KICAgICAgICAgICAgICAgICAgICAgIHNwbGl0cyA9IC41LA0KICAgICAgICAgICAgICAgICAgICAgIG5fY3YgPSA1LA0KICAgICAgICAgICAgICAgICAgICAgIGtlcm5lbHMgPSBjKCJnYXVzc2lhbiIsIm5haXZlIiwgImVwYW4iLCAidHJpYW5nIiksDQogICAgICAgICAgICAgICAgICAgICAgc2V0TWFjaGluZVBhcmFtID0gc2V0QmFzaWNQYXJhbWV0ZXIoayA9IDI6NSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBudHJlZSA9IDE6MyoxMDAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbnJvdW5kc194Z2IgPSAxOjMqMTAwKSwNCiAgICAgICAgICAgICAgICAgICAgICBzZXRHcmlkUGFyYW0gPSBzZXRHcmlkUGFyYW1ldGVyKG5fdmFsID0gMTAwKSwNCiAgICAgICAgICAgICAgICAgICAgICBzaWxlbnQgPSBGQUxTRSkNCiAgYWdnJGFjY3VyYWN5DQpgYGANCg0KDQotLS0NCg0KPiA8c3BhbiBzdHlsZT0iY29sb3I6ICMxRkFBRTM7Ij4mIzEyODIxNDsgUmVhZCBhbHNvIFtLZXJuZWxBZ2dSZWddKGh0dHBzOi8vaGFzc290aGVhLmdpdGh1Yi5pby9maWxlcy9LZXJuZWxBZ2dSZWcvS2VybmVsQWdnUmVnLmh0bWwpIGFuZCBbTWl4Q29icmFdKGh0dHBzOi8vaGFzc290aGVhLmdpdGh1Yi5pby9maWxlcy9LZXJuZWxBZ2dSZWcvTWl4Q29icmFSZWcuaHRtbCk8L3NwYW4+IG1ldGhvZC4NCg0KLS0tDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogIzFGQUFFMzsiPlJlZmVyZW5jZXM8L3NwYW4+ey19DQo9PT0NCg0KLS0tDQoNCi0gW01vamlyc2hlaWJhbmkgKDE5OTkpXShodHRwczovL3d3dy50YW5kZm9ubGluZS5jb20vZG9pL2Ficy8xMC4xMDgwLzAxNjIxNDU5LjE5OTkuMTA0NzQxNTQ/am91cm5hbENvZGU9dWFzYTIwKSBmb3IgY2xhc3NpZmljYXRpb24NCi0gW01vamlyc2hlaWJhbmkgKDIwMDApXShodHRwczovL3d3dy5zY2llbmNlZGlyZWN0LmNvbS9zY2llbmNlL2FydGljbGUvcGlpL1MwMTY3NzE1MjAwMDAwMjQ5KSBmb3IgY2xhc3NpZmljYXRpb24NCi0gW01vamlyc2hlaWJhbmkgYW5kIEtvbmcgKDIwMTYpXShodHRwczovL3d3dy5zY2llbmNlZGlyZWN0LmNvbS9zY2llbmNlL2FydGljbGUvcGlpL1MwMTY3NzE1MjE2MzAxMzA0KSBmb3IgY2xhc3NpZmljYXRpb24NCi0gW0JpYXUgZXQgYWwuICgyMDE2KV0oaHR0cHM6Ly93d3cuc2NpZW5jZWRpcmVjdC5jb20vc2NpZW5jZS9hcnRpY2xlL3BpaS9TMDA0NzI1OVgxNTAwMDk1MCkgZm9yIHJlZ3Jlc3Npb24NCi0gW0hhcyAoMjAyMSldKGh0dHBzOi8vaGFsLmFyY2hpdmVzLW91dmVydGVzLmZyL2hhbC0wMjg4NDMzM3Y1KSBmb3IgcmVncmVzc2lvbiBmb3IgcmVncmVzc2lvbg0KLSBbRmlzY2hlciBhbmQgTW91Z2VvdCAoMjAxOSldKGh0dHBzOi8vd3d3LnNjaWVuY2VkaXJlY3QuY29tL3NjaWVuY2UvYXJ0aWNsZS9waWkvUzAzNzgzNzU4MTgzMDIzNDkpIGZvciBib3RoDQotIC4uLg0KLSBbZHBseXIgdmlkZW9zXShodHRwczovL3d3dy55b3V0dWJlLmNvbS9oYXNodGFnL2RwbHlyKSBgciBmb250YXdlc29tZTo6ZmEoInZpZGVvIilgDQotIFtnZ3Bsb3QyIHZpZGVvIHR1dG9yaWFsXShodHRwczovL3d3dy55b3V0dWJlLmNvbS9oYXNodGFnL2dncGxvdDIpIGByIGZvbnRhd2Vzb21lOjpmYSgidmlkZW8iKWANCi0gW1IgZm9yIGRhdGEgc2NpZW5jZV0oaHR0cHM6Ly9yNGRzLmhhZC5jby5uei8pDQoNCi0tLQ0K

📖 Read also GradientCOBRARegressor and MixCOBRARegressor methods.