Session 17. Generalized Linear Models II.Multinomial Logistic Regression for classification problems. ROC analysis for classification problems. Maximum Likelihood Estimation (MLE) revisited.

Feedback should be send to goran.milovanovic@datakolektiv.com. These notebooks accompany the Intro to Data Science: Non-Technical Background course 2020/21.


What do we want to do today?

We continue our exploration of Generalized Linear Models (GLMs) by generalizing the Binomial Logistic Regression even further to solve for classification in multiple categories. As we introduce the resulting model of Multinomial Logistic Regression we also revisit the theory of the Maximum Likelihood Estimate (MLE). Then we introduce the Receiver-Operating Characteristic (ROC) Analysis for classification problems as an important tool in model selection: the topic to be explored in detail in our following sessions.

0. Prerequisits

Setup: install {nnet} for Multinomial Logistic Regression.

install.packages('nnet')
install.packages('ggrepel')
dataDir <- paste0(getwd(), "/_data/")
library(tidyverse)
library(data.table)
library(nnet)
library(car)
library(ggrepel)

1. MLE revisited

2. Multinomial Logistic Regression

The Multinomial Logistic Regression model is a powerful classification tool. Consider a problem where some outcome variable can result in more than two discrete outcomes. For example, a customer visiting a webshop can end up their visit (a) buying nothing, (b) buying some Product A, or (c) Product B, or (d) Product C, etc. If we have some information about a particular customer’s journey through the website (e.g. how much time did they spend on some particular pages, did they visit the webshop before or not, or any other information that customers might have chose to disclose on their sign-up…), we can use it as a set of predictors of customer behavior resulting in any of the (a), (b), (c), (d). We do that by means of a simple extension of the Binomial Logistic Regression that is used to solve for dichotomies: enters the Multinomial Logistic Regression model.

2.1 The Model

First, similar to what happens in dummy coding, given a set of \(K\) possible outcomes we choose one of them as a baseline. Thus all results of the Multionomial Logistic Regression will be interpreted as effects relative to that baseline outcome category, for example: for a unit increase in predictor \(X_1\) what is the change in odds to switch from (a) bying nothing to (b) buying Product A. We are already familiar with this logic, right?

So, consider a set of \(K-1\) independent Binary Logistic Models with only one predictor \(X\) where the baseline is now referred to as \(K\):

\[log\frac{P(Y_i = 1)}{P(Y_i = K)} = \beta_1X_i\]

\[log\frac{P(Y_i = 2)}{P(Y_i = K)} = \beta_2X_i\]

\[log\frac{P(Y_i = K-1)}{P(Y_i = K)} = \beta_{K-1}X_i\] N.B. The intercept \(\beta_0\) is not included for reasons of simplicity.

Obviously, we are introducing a new regression coefficient \(\beta_k\) for each possible value of the outcome \(k = 1, 2,.., K-1\). The log-odds are on the LHS while the linear model remains on the RHS.

Now we exponentiate the equations to arrive at the expressions for odds:

\[\frac{P(Y_i = 1)}{P(Y_i = K)} = e^{\beta_1X_i}\]

\[\frac{P(Y_i = 2)}{P(Y_i = K)} = e^{\beta_2X_i}\]

\[\frac{P(Y_i = K-1)}{P(Y_i = K)} = e^{\beta_{K-1}X_i}\]

And solve for \(P(Y_i = 1), P(Y_i = 2),.. P(Y_i = K-1)\):

\[P(Y_i = 1) = P(Y_i = K)e^{\beta_1X_i}\]

\[P(Y_i = 2) = P(Y_i = K)e^{\beta_2X_i}\]

\[P(Y_i = K-1) = P(Y_i = K)e^{\beta_{K-1}X_i}\]

From the fact that all probabilities \(P(Y_i = 1), P(Y_i = 2), .., P(Y_i = K-1)\) must sum to one it can be shown that

\[P(Y_i = K) = \frac{1}{1+\sum_{k=1}^{K-1}e^{\beta_KX_i}}\]

and then it is easy to derive the expressions for all \(K-1\) probabilities of the outcome resulting in a particular class:

\[P(Y_i = 1) = \frac{e^{\beta_1X_i}}{1+\sum_{k=1}^{K-1}e^{\beta_KX_i}}\]

\[P(Y_i = 2) = \frac{e^{\beta_2X_i}}{1+\sum_{k=1}^{K-1}e^{\beta_KX_i}}\]

\[P(Y_i = K-1) = \frac{e^{\beta_{K-1}X_i}}{1+\sum_{k=1}^{K-1}e^{\beta_KX_i}}\]

2.2 Multinomial Logistic Regression in R

Let’s begin with a simple example: classify iris$Species.

head(iris)

Now check if iris$Species is a factor:

str(iris)
'data.frame':   150 obs. of  5 variables:
 $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
 $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
 $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
 $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
 $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...

Let’s pick versicolor as our baseline:

iris$Species <- relevel(iris$Species,
                        ref = 'versicolor')

Finally, we will use multinom() from {nnet} to estimate the Multinomial Logistic Regression model:

mlr_model <- nnet::multinom(Species ~ .,
                             data = iris)
# weights:  18 (10 variable)
initial  value 164.791843 
iter  10 value 8.858726
iter  20 value 6.109855
iter  30 value 5.978051
iter  40 value 5.960110
iter  50 value 5.949916
iter  60 value 5.949625
final  value 5.949587 
converged
summary(mlr_model)
Call:
nnet::multinom(formula = Species ~ ., data = iris)

Coefficients:
          (Intercept) Sepal.Length Sepal.Width Petal.Length Petal.Width
setosa     -0.4917771     2.205180    6.313709   -10.370916   -5.135036
virginica -42.3514649    -2.471389   -6.655958     9.391305   18.207938

Std. Errors:
          (Intercept) Sepal.Length Sepal.Width Petal.Length Petal.Width
setosa       42.21659   100.974966  162.889191   100.537988   48.061685
virginica    25.52521     2.394038    4.461874     4.703818    9.688497

Residual Deviance: 11.89917 
AIC: 31.89917 

The interpretation of the model results is similar to the Binomial Logistic Regression case. We will discuss the findings on iris$Species in the Session.

Grab the model coefficients:

coeffs <- as.data.frame(
  summary(mlr_model)$coeff
  )
print(coeffs)

And do not forget the exp(coefficient) thing to get back to the odds scale…

exp(coeffs)

What are the most influential predictors in categorizing setosa and virginica vs versicolor?

which.max(exp(coeffs)[1, ])
Sepal.Width 
          3 
which.max(exp(coeffs)[2, ])
Petal.Width 
          5 

And we have learned that we can tell setosa from versicolor by Sepal.Width and virginica vs versicolor by Petal.Width! N.B. As ever, stop before jumping to conclusions… It is a bit more complicated than that, I assure you.

2.3 Model Accuracy and the ROC analysis

Q: How well does the model perform overall? We will first derive the model predictions from the mlr_model model object in R:

predictions <- predict(mlr_model,
                       newdata = iris,
                       "probs")
predictions <- apply(predictions, 1, which.max)
table(predictions)
predictions
 1  2  3 
50 50 50 

Store predictions in iris and than compute the model accuracy:

classes <- levels(iris$Species)
predictions <- classes[predictions]
iris$Prediction <- predictions
head(iris)
accuracy <- round(
  sum(iris$Species == iris$Prediction)/dim(iris)[1]*100,
  2)
print(paste0("The overal model accuracy is ", accuracy, "%."))
[1] "The overal model accuracy is 98.67%."

Predicted probabilities can also be obtained easily from fitted():

fittedProbs <- fitted(mlr_model)
head(fittedProbs)
    versicolor    setosa    virginica
1 3.047579e-08 1.0000000 6.185179e-35
2 1.113008e-06 0.9999989 1.032478e-31
3 1.734736e-07 0.9999998 2.724517e-33
4 3.236004e-06 0.9999968 8.282840e-31
5 2.020802e-08 1.0000000 2.698905e-35
6 7.889966e-08 0.9999999 3.398755e-33

Of course:

table(rowSums(fittedProbs))

  1 
150 

Important. Accuracy in itself does not tell the full story of how a given model performs. Moreover, from accuracy alone one can be led to completely incorrect and misleading conclusions about model performance. We need to introduce a set of more granular measures of the quality of a categorization. Consider the following four cases:

  • The true empirical observation of the outcome is class \(C\), and the model also predicts \(C\); we call this a Hit or a True Positive (TP).
  • The true empirical observation of the outcome is class \(C\), but the model predicts some other class; we call this a Miss or a False Negative (FN) (and this is also what is known as Type II Error in statistics).
  • The true empirical observation of the outcome is not class \(C\), but the model predicts \(C\); we call this a False Alarm (FA) or a False Positive (FP) (and this is also what is known as Type I Error in statistics).
  • The true empirical observation of the outcome is not class \(C\), and the model predicts some other class; we call this a Correct Rejection (CR) or a True Negative (TN) (and this is also what is known as **Type I Errors in statistics).

This is the Receiver-Operating Characteristic (ROC) Analysis of model performance. Compute the True Positive Rate (TPR), False Negative Rate (FNR), False Positive Rate (FPR), and True Negative Rate (TNR) for all classes:

mlr_model_ROC <- lapply(unique(iris$Species), function(x) {
  tpr <- sum(iris$Species == x & iris$Prediction == x)
  tpr <- tpr/sum(iris$Species == x)
  fnr <- sum(iris$Species == x & iris$Prediction != x)
  fnr <- fnr/sum(iris$Species == x)
  fpr <- sum(iris$Species != x & iris$Prediction == x)
  fpr <- fpr/sum(iris$Species != x)
  tnr <- sum(iris$Species != x & iris$Prediction != x)
  tnr <- tnr/sum(iris$Species != x)
  roc <- c(tpr, fnr, fpr, tnr)
  names(roc) <- c('tpr', 'fnr', 'fpr', 'tnr')
  return(roc)
})
mlr_model_ROC <- reduce(mlr_model_ROC, rbind)
rownames(mlr_model_ROC) <- levels(iris$Species)
print(mlr_model_ROC)
            tpr  fnr  fpr  tnr
versicolor 1.00 0.00 0.00 1.00
setosa     0.98 0.02 0.01 0.99
virginica  0.98 0.02 0.01 0.99

A good model does not commit to classification errors in the following sense: it maintains a high True Positive (Hit) Rate and a low False Positive (False Alarm, Type I Error) Rate across the classes.

2.3.1 ROC analysis for the Binomial Logistic Regression

We need to reconsider briefly the Binomial Logistic Regression example from Session 16 now.

dataSet <- read.csv("https://stats.idre.ucla.edu/stat/data/binary.csv")
head(dataSet)
dataSet$rank <- factor(dataSet$rank)
mylogit <- glm(admit ~ gre + gpa + rank,
               data = dataSet,
               family = "binomial")
modelsummary <- summary(mylogit)
print(modelsummary)

Call:
glm(formula = admit ~ gre + gpa + rank, family = "binomial", 
    data = dataSet)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-1.6268  -0.8662  -0.6388   1.1490   2.0790  

Coefficients:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept) -3.989979   1.139951  -3.500 0.000465 ***
gre          0.002264   0.001094   2.070 0.038465 *  
gpa          0.804038   0.331819   2.423 0.015388 *  
rank2       -0.675443   0.316490  -2.134 0.032829 *  
rank3       -1.340204   0.345306  -3.881 0.000104 ***
rank4       -1.551464   0.417832  -3.713 0.000205 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 499.98  on 399  degrees of freedom
Residual deviance: 458.52  on 394  degrees of freedom
AIC: 470.52

Number of Fisher Scoring iterations: 4

Ok. Now we will do the following:

  • derive the model predictions and place them in our dataset;
  • write out a function that predicts the class of the outcome for a given decision trashold (remember that we have used \(.5\) as default?); and finally,
  • perform an ROC analysis for each value of the decision boundary that we chose.
dataSet$predictions <- fitted(mylogit)
head(dataSet)

Now the function. We will write out the predictClass() function that takes two arguments: decBoundary - the decision boundary, and decData - a data.frame with two columns representing the observed (observed) class (either 1 or 0) and the predicted (predicted) probability of being 1.The predictClass() function will automatically perform the ROC analysis following the class prediction.

predictClass <- function(decBoundary, decData) {
  
  # - predict class
  predClass <- ifelse(decData$predicted >= decBoundary, 1, 0)
  
  # - ROC analysis
  tpr <- sum(decData$observed == 1 & predClass == 1)
  tpr <- tpr/sum(decData$observed == 1)
  fnr <- sum(decData$observed == 1 & predClass == 0)
  fnr <- fnr/sum(decData$observed == 1)
  fpr <- sum(decData$observed == 0 & predClass == 1)
  fpr <- fpr/sum(decData$observed == 0)
  tnr <- sum(decData$observed == 0 & predClass == 0)
  tnr <- tnr/sum(decData$observed == 0)
  roc <- c(decBoundary, tpr, fnr, fpr, tnr)
  names(roc) <- c('decBoundary', 'tpr', 'fnr', 'fpr', 'tnr')
  return(roc)
}

Now we will inspect the ROC analysis for this Binomial Logistic Regression model across a range of decision boundaries:

decBoundaries <- seq(.001, .999, by = .001)
ROC_dataset <- data.frame(observed = dataSet$admit, 
                          predicted = dataSet$predictions)
ROC_results <- lapply(decBoundaries, function(x) {
  as.data.frame(t(predictClass(x, ROC_dataset)))
})
ROC_results <- rbindlist(ROC_results)
head(ROC_results)

We now plot the False Positive Rate (fpr, False Alarm) vs the True Positive Rate (tpr, Hit):

ROC_results$diff <- ROC_results$tpr - ROC_results$fpr
ROC_results$label <- ""
ROC_results$label[which.max(ROC_results$diff)] <- "Here!"
ggplot(data = ROC_results, 
       aes(x = fpr, 
           y = tpr, 
           label = label)) +
  geom_path(color = "red") + geom_abline(intercept = 0, slope = 1) +
  geom_text_repel(arrow = arrow(length = unit(0.06, "inches"),
                                ends = "last", 
                                type = "closed"), 
                  min.segment.length = unit(0, 'lines'),
                  nudge_y = .1) + 
  ggtitle("ROC analysis for the Binomial Regression Model") +
  xlab("Specificity (False Alarm Rate)") + ylab("Sensitivity (Hit Rate)") + 
  theme_bw() + 
  theme(plot.title = element_text(hjust = .5))

The value of the decision boundary (sometimes called the cut-off) is where the model achieves the highest possible value for the Hit Rate given the lowest possible value for the False Alarm Rate.

This analysis can be generalized to multiclass classification problems in different ways, but that definitely goes beyond the scope of an introductory course…

2.4 Tests for model coefficients and multicollinearity diganostics

Recall from Binomial Logistic Regression that the Wald \(Z\)-test for a regression coefficient is obtained by dividing the estimate with the respective standard error:

summary(mlr_model)$coefficients
          (Intercept) Sepal.Length Sepal.Width Petal.Length Petal.Width
setosa     -0.4917771     2.205180    6.313709   -10.370916   -5.135036
virginica -42.3514649    -2.471389   -6.655958     9.391305   18.207938
summary(mlr_model)$standard.errors
          (Intercept) Sepal.Length Sepal.Width Petal.Length Petal.Width
setosa       42.21659   100.974966  162.889191   100.537988   48.061685
virginica    25.52521     2.394038    4.461874     4.703818    9.688497

Then we can obtain the \(Z\)-tests in the following way:

z <- summary(mlr_model)$coefficients/summary(mlr_model)$standard.errors
print(z)
          (Intercept) Sepal.Length Sepal.Width Petal.Length Petal.Width
setosa    -0.01164891   0.02183888  0.03876076   -0.1031542  -0.1068426
virginica -1.65920119  -1.03230969 -1.49174051    1.9965279   1.8793358

And because the \(Z\)-test follows a standard normal distribution (see this discussion), the respective probabilities of the Type I Error (i.e. statistical significance) are:

p <- (1 - pnorm(abs(z), 0, 1)) * 2
print(p)
          (Intercept) Sepal.Length Sepal.Width Petal.Length Petal.Width
setosa     0.99070573    0.9825765   0.9690811   0.91784059  0.91491384
virginica  0.09707526    0.3019271   0.1357672   0.04587649  0.06019866

However, using \(Z\)-tests in Multinomial Logistic Regression is not recommended. Instead, we can use MASS:dropterm() to perform the Likelihood-Ratio Tests for the model coefficients:

lrt <- MASS::dropterm(object = mlr_model,
                      scope = Species ~ Sepal.Length + Sepal.Width + Petal.Length + Petal.Width,
                      test = 'Chisq',
                      trace = F, # - to print additional information or not
                      k = 2, # - for Akaike Information Criterion (AIC)
                      ) 
# weights:  15 (8 variable)
initial  value 164.791843 
iter  10 value 19.997212
iter  20 value 6.933728
iter  30 value 6.659047
iter  40 value 6.646751
iter  50 value 6.641402
iter  60 value 6.637619
final  value 6.633941 
converged
# weights:  15 (8 variable)
initial  value 164.791843 
iter  10 value 12.235903
iter  20 value 7.983247
iter  30 value 7.750879
iter  40 value 7.746346
final  value 7.746251 
converged
# weights:  15 (8 variable)
initial  value 164.791843 
iter  10 value 41.301109
iter  20 value 16.748468
iter  30 value 14.595863
iter  40 value 12.950927
final  value 12.950889 
converged
# weights:  15 (8 variable)
initial  value 164.791843 
iter  10 value 15.825451
iter  20 value 12.188082
iter  30 value 11.888231
final  value 11.885927 
converged
as.data.frame(lrt)

The dropterm() function will fit all models that differ from the full model by successively dropping a single predictor and perform a comparison between the performance of the full model and the model with a dropped predictor. The results of this procedure suggest that we could drop both Sepal.Length and Sepal.Width from the model; let’s try it out:

data(iris)
iris$Species <- relevel(iris$Species,
                        ref = 'versicolor')
mlr_model_drop <- multinom(Species ~ Petal.Length + Petal.Width,
                           data = iris)
# weights:  12 (6 variable)
initial  value 164.791843 
iter  10 value 13.836346
iter  20 value 10.411183
iter  30 value 10.285364
iter  40 value 10.284340
iter  50 value 10.284134
iter  60 value 10.283943
final  value 10.283879 
converged
summary(mlr_model_drop)
Call:
multinom(formula = Species ~ Petal.Length + Petal.Width, data = iris)

Coefficients:
          (Intercept) Petal.Length Petal.Width
setosa       27.34100    -8.761018    -7.64127
virginica   -45.27185     5.754243    10.44731

Std. Errors:
          (Intercept) Petal.Length Petal.Width
setosa       99.26516    80.219938  157.926079
virginica    13.61140     2.305799    3.755773

Residual Deviance: 20.56776 
AIC: 32.56776 

The \(AIC\) of the model with all predictors included was 31.89917 and following the removal of Sepal.Length and Sepal.Width it increased only a bit. Let’s perform additional checks:

z <- summary(mlr_model_drop)$coefficients/summary(mlr_model_drop)$standard.errors
print(z)
          (Intercept) Petal.Length Petal.Width
setosa       0.275434   -0.1092125  -0.0483851
virginica   -3.326025    2.4955534   2.7816679
p <- (1 - pnorm(abs(z), 0, 1)) * 2
print(p)
           (Intercept) Petal.Length Petal.Width
setosa    0.7829828275   0.91303397 0.961409330
virginica 0.0008809408   0.01257608 0.005408034
predictions <- predict(mlr_model_drop,
                       newdata = iris,
                       "probs")
predictions <- apply(predictions, 1, which.max)
classes <- levels(iris$Species)
predictions <- classes[predictions]
iris$Prediction <- predictions
head(iris)
mlr_model_ROC <- lapply(unique(iris$Species), function(x) {
  tpr <- sum(iris$Species == x & iris$Prediction == x)
  tpr <- tpr/sum(iris$Species == x)
  fnr <- sum(iris$Species == x & iris$Prediction != x)
  fnr <- fnr/sum(iris$Species == x)
  fpr <- sum(iris$Species != x & iris$Prediction == x)
  fpr <- fpr/sum(iris$Species != x)
  tnr <- sum(iris$Species != x & iris$Prediction != x)
  tnr <- tnr/sum(iris$Species != x)
  roc <- c(tpr, fnr, fpr, tnr)
  names(roc) <- c('tpr', 'fnr', 'fpr', 'tnr')
  return(roc)
})
mlr_model_ROC <- reduce(mlr_model_ROC, rbind)
rownames(mlr_model_ROC) <- levels(iris$Species)
print(mlr_model_ROC)
            tpr  fnr  fpr  tnr
versicolor 1.00 0.00 0.00 1.00
setosa     0.94 0.06 0.03 0.97
virginica  0.94 0.06 0.03 0.97
accuracy <- round(
  sum(iris$Species == iris$Prediction)/dim(iris)[1]*100,
  2)
print(paste0("The overal model accuracy is ", accuracy, "%."))
[1] "The overal model accuracy is 96%."

Recall how the accuracy used to be above 98% with the full model. Would you trade a 2% drop in model accuracy against the cost of collecting two additional predictors?

Now, as of the multicollinearity in Multinomial Logistic Regression: run lm() as if the categorical outcome was a continuous variable and use vif() from {car} to assess the Variance Inflation Factor:

data(iris)
iris$Species <- relevel(iris$Species,
                        ref = 'versicolor')
iris$Species <- sapply(iris$Species, function(x) {
  if (x == 'versicolor') {
    x <- 0
  } else if (x == 'setosa') {
    x <- 1
  } else {
    x <- 2
  }
})
mlr_model_vif <- lm(Species ~ .,
                    data = iris)
vif(mlr_model_vif)
Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
    7.072722     2.100872    31.261498    16.090175 

And now one could perhaps see what the real problem with our Multinomial Logistic Regression models of the iris dataset is?

cor(select(iris, -Species))
             Sepal.Length Sepal.Width Petal.Length Petal.Width
Sepal.Length    1.0000000  -0.1175698    0.8717538   0.8179411
Sepal.Width    -0.1175698   1.0000000   -0.4284401  -0.3661259
Petal.Length    0.8717538  -0.4284401    1.0000000   0.9628654
Petal.Width     0.8179411  -0.3661259    0.9628654   1.0000000

R Markdown


Goran S. Milovanović

DataKolektiv, 2020/21

contact:


License: GPLv3 This Notebook is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This Notebook is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this Notebook. If not, see http://www.gnu.org/licenses/.


LS0tDQp0aXRsZTogSW50cm8gdG8gRGF0YSBTY2llbmNlIChOb24tVGVjaG5pY2FsIEJhY2tncm91bmQsIFIpIC0gU2Vzc2lvbjE3DQphdXRob3I6DQotIG5hbWU6IEdvcmFuIFMuIE1pbG92YW5vdmnEhywgUGhEDQogIGFmZmlsaWF0aW9uOiBEYXRhS29sZWt0aXYsIENoaWVmIFNjaWVudGlzdCAmIE93bmVyOyBEYXRhIFNjaWVudGlzdCBmb3IgV2lraWRhdGEsIFdNREUNCmFic3RyYWN0OiANCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICBjb2RlX2ZvbGRpbmc6IHNob3cNCiAgICB0aGVtZTogc3BhY2VsYWINCiAgICB0b2M6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogICAgdG9jX2RlcHRoOiA1DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6IDUNCi0tLQ0KDQohW10oLi4vX2ltZy9ES19Mb2dvXzEwMC5wbmcpDQoNCioqKg0KIyBTZXNzaW9uIDE3LiBHZW5lcmFsaXplZCBMaW5lYXIgTW9kZWxzIElJLk11bHRpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24gZm9yIGNsYXNzaWZpY2F0aW9uIHByb2JsZW1zLiBST0MgYW5hbHlzaXMgZm9yIGNsYXNzaWZpY2F0aW9uIHByb2JsZW1zLiBNYXhpbXVtIExpa2VsaWhvb2QgRXN0aW1hdGlvbiAoTUxFKSByZXZpc2l0ZWQuDQoNCioqRmVlZGJhY2sqKiBzaG91bGQgYmUgc2VuZCB0byBgZ29yYW4ubWlsb3Zhbm92aWNAZGF0YWtvbGVrdGl2LmNvbWAuIA0KVGhlc2Ugbm90ZWJvb2tzIGFjY29tcGFueSB0aGUgSW50cm8gdG8gRGF0YSBTY2llbmNlOiBOb24tVGVjaG5pY2FsIEJhY2tncm91bmQgY291cnNlIDIwMjAvMjEuDQoNCioqKg0KDQojIyMgV2hhdCBkbyB3ZSB3YW50IHRvIGRvIHRvZGF5Pw0KDQpXZSBjb250aW51ZSBvdXIgZXhwbG9yYXRpb24gb2YgR2VuZXJhbGl6ZWQgTGluZWFyIE1vZGVscyAoR0xNcykgYnkgZ2VuZXJhbGl6aW5nIHRoZSBCaW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uIGV2ZW4gZnVydGhlciB0byBzb2x2ZSBmb3IgY2xhc3NpZmljYXRpb24gaW4gbXVsdGlwbGUgY2F0ZWdvcmllcy4gQXMgd2UgaW50cm9kdWNlIHRoZSByZXN1bHRpbmcgbW9kZWwgb2YgKk11bHRpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24qIHdlIGFsc28gcmV2aXNpdCB0aGUgdGhlb3J5IG9mIHRoZSBNYXhpbXVtIExpa2VsaWhvb2QgRXN0aW1hdGUgKE1MRSkuIFRoZW4gd2UgaW50cm9kdWNlIHRoZSAqKlJlY2VpdmVyLU9wZXJhdGluZyBDaGFyYWN0ZXJpc3RpYyAoUk9DKSBBbmFseXNpcyoqIGZvciBjbGFzc2lmaWNhdGlvbiBwcm9ibGVtcyBhcyBhbiBpbXBvcnRhbnQgdG9vbCBpbiAqbW9kZWwgc2VsZWN0aW9uKjogdGhlIHRvcGljIHRvIGJlIGV4cGxvcmVkIGluIGRldGFpbCBpbiBvdXIgZm9sbG93aW5nIHNlc3Npb25zLiANCg0KIyMjIDAuIFByZXJlcXVpc2l0cw0KDQpTZXR1cDogaW5zdGFsbCB7bm5ldH0gZm9yIE11bHRpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24uDQoNCmBgYHtyIGVjaG8gPSBULCBldmFsID0gRn0NCmluc3RhbGwucGFja2FnZXMoJ25uZXQnKQ0KaW5zdGFsbC5wYWNrYWdlcygnZ2dyZXBlbCcpDQpgYGANCg0KYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0NCmRhdGFEaXIgPC0gcGFzdGUwKGdldHdkKCksICIvX2RhdGEvIikNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShkYXRhLnRhYmxlKQ0KbGlicmFyeShubmV0KQ0KbGlicmFyeShjYXIpDQpsaWJyYXJ5KGdncmVwZWwpDQpgYGANCg0KDQojIyMgMS4gTUxFIHJldmlzaXRlZA0KDQohW10oLi4vX2ltZy9TMTdfMDJfTGlrZWxpaG9vZEZ1bmN0aW9uLmpwZWcpDQoNCiMjIyAyLiBNdWx0aW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uDQoNClRoZSBNdWx0aW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uIG1vZGVsIGlzIGEgcG93ZXJmdWwgY2xhc3NpZmljYXRpb24gdG9vbC4gQ29uc2lkZXIgYSBwcm9ibGVtIHdoZXJlIHNvbWUgb3V0Y29tZSB2YXJpYWJsZSBjYW4gcmVzdWx0IGluIG1vcmUgdGhhbiB0d28gZGlzY3JldGUgb3V0Y29tZXMuIEZvciBleGFtcGxlLCBhIGN1c3RvbWVyIHZpc2l0aW5nIGEgd2Vic2hvcCBjYW4gZW5kIHVwIHRoZWlyIHZpc2l0IChhKSBidXlpbmcgbm90aGluZywgKGIpIGJ1eWluZyBzb21lIFByb2R1Y3QgQSwgb3IgKGMpIFByb2R1Y3QgQiwgb3IgKGQpIFByb2R1Y3QgQywgZXRjLiBJZiB3ZSBoYXZlIHNvbWUgaW5mb3JtYXRpb24gYWJvdXQgYSBwYXJ0aWN1bGFyIGN1c3RvbWVyJ3Mgam91cm5leSB0aHJvdWdoIHRoZSB3ZWJzaXRlIChlLmcuIGhvdyBtdWNoIHRpbWUgZGlkIHRoZXkgc3BlbmQgb24gc29tZSBwYXJ0aWN1bGFyIHBhZ2VzLCBkaWQgdGhleSB2aXNpdCB0aGUgd2Vic2hvcCBiZWZvcmUgb3Igbm90LCBvciBhbnkgb3RoZXIgaW5mb3JtYXRpb24gdGhhdCBjdXN0b21lcnMgbWlnaHQgaGF2ZSBjaG9zZSB0byBkaXNjbG9zZSBvbiB0aGVpciBzaWduLXVwLi4uKSwgd2UgY2FuIHVzZSBpdCBhcyBhIHNldCBvZiBwcmVkaWN0b3JzIG9mIGN1c3RvbWVyIGJlaGF2aW9yIHJlc3VsdGluZyBpbiBhbnkgb2YgdGhlIChhKSwgKGIpLCAoYyksIChkKS4gV2UgZG8gdGhhdCBieSBtZWFucyBvZiBhIHNpbXBsZSBleHRlbnNpb24gb2YgdGhlIEJpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24gdGhhdCBpcyB1c2VkIHRvIHNvbHZlIGZvciBkaWNob3RvbWllczogZW50ZXJzIHRoZSBNdWx0aW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uIG1vZGVsLg0KDQojIyMjIDIuMSBUaGUgTW9kZWwNCg0KRmlyc3QsIHNpbWlsYXIgdG8gd2hhdCBoYXBwZW5zIGluICpkdW1teSBjb2RpbmcqLCBnaXZlbiBhIHNldCBvZiAkSyQgcG9zc2libGUgb3V0Y29tZXMgd2UgY2hvb3NlIG9uZSBvZiB0aGVtIGFzIGEgKmJhc2VsaW5lKi4gVGh1cyBhbGwgcmVzdWx0cyBvZiB0aGUgTXVsdGlvbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24gd2lsbCBiZSBpbnRlcnByZXRlZCBhcyBlZmZlY3RzIHJlbGF0aXZlIHRvIHRoYXQgYmFzZWxpbmUgb3V0Y29tZSBjYXRlZ29yeSwgZm9yIGV4YW1wbGU6IGZvciBhIHVuaXQgaW5jcmVhc2UgaW4gcHJlZGljdG9yICRYXzEkIHdoYXQgaXMgdGhlIGNoYW5nZSBpbiBvZGRzIHRvIHN3aXRjaCBmcm9tIChhKSBieWluZyBub3RoaW5nIHRvIChiKSBidXlpbmcgUHJvZHVjdCBBLiBXZSBhcmUgYWxyZWFkeSBmYW1pbGlhciB3aXRoIHRoaXMgbG9naWMsIHJpZ2h0Pw0KDQpTbywgY29uc2lkZXIgYSBzZXQgb2YgJEstMSQgaW5kZXBlbmRlbnQgQmluYXJ5IExvZ2lzdGljIE1vZGVscyB3aXRoIG9ubHkgb25lIHByZWRpY3RvciAkWCQgd2hlcmUgdGhlIGJhc2VsaW5lIGlzIG5vdyByZWZlcnJlZCB0byBhcyAkSyQ6DQoNCiQkbG9nXGZyYWN7UChZX2kgPSAxKX17UChZX2kgPSBLKX0gPSBcYmV0YV8xWF9pJCQNCg0KJCRsb2dcZnJhY3tQKFlfaSA9IDIpfXtQKFlfaSA9IEspfSA9IFxiZXRhXzJYX2kkJA0KDQokJGxvZ1xmcmFje1AoWV9pID0gSy0xKX17UChZX2kgPSBLKX0gPSBcYmV0YV97Sy0xfVhfaSQkDQoqKk4uQi4qKiBUaGUgaW50ZXJjZXB0ICRcYmV0YV8wJCBpcyBub3QgaW5jbHVkZWQgZm9yIHJlYXNvbnMgb2Ygc2ltcGxpY2l0eS4NCg0KT2J2aW91c2x5LCB3ZSBhcmUgaW50cm9kdWNpbmcgYSBuZXcgcmVncmVzc2lvbiBjb2VmZmljaWVudCAkXGJldGFfayQgZm9yIGVhY2ggcG9zc2libGUgdmFsdWUgb2YgdGhlIG91dGNvbWUgJGsgPSAxLCAyLC4uLCBLLTEkLiBUaGUgKmxvZy1vZGRzKiBhcmUgb24gdGhlIExIUyB3aGlsZSB0aGUgbGluZWFyIG1vZGVsIHJlbWFpbnMgb24gdGhlIFJIUy4NCg0KTm93IHdlIGV4cG9uZW50aWF0ZSB0aGUgZXF1YXRpb25zIHRvIGFycml2ZSBhdCB0aGUgZXhwcmVzc2lvbnMgZm9yICpvZGRzKjoNCg0KJCRcZnJhY3tQKFlfaSA9IDEpfXtQKFlfaSA9IEspfSA9IGVee1xiZXRhXzFYX2l9JCQNCg0KJCRcZnJhY3tQKFlfaSA9IDIpfXtQKFlfaSA9IEspfSA9IGVee1xiZXRhXzJYX2l9JCQNCg0KJCRcZnJhY3tQKFlfaSA9IEstMSl9e1AoWV9pID0gSyl9ID0gZV57XGJldGFfe0stMX1YX2l9JCQNCg0KQW5kIHNvbHZlIGZvciAkUChZX2kgPSAxKSwgUChZX2kgPSAyKSwuLiBQKFlfaSA9IEstMSkkOg0KDQokJFAoWV9pID0gMSkgPSBQKFlfaSA9IEspZV57XGJldGFfMVhfaX0kJA0KDQokJFAoWV9pID0gMikgPSBQKFlfaSA9IEspZV57XGJldGFfMlhfaX0kJA0KDQokJFAoWV9pID0gSy0xKSA9IFAoWV9pID0gSyllXntcYmV0YV97Sy0xfVhfaX0kJA0KDQpGcm9tIHRoZSBmYWN0IHRoYXQgYWxsIHByb2JhYmlsaXRpZXMgJFAoWV9pID0gMSksIFAoWV9pID0gMiksIC4uLCBQKFlfaSA9IEstMSkkIG11c3Qgc3VtIHRvIG9uZSBpdCBjYW4gYmUgc2hvd24gdGhhdA0KDQokJFAoWV9pID0gSykgPSBcZnJhY3sxfXsxK1xzdW1fe2s9MX1ee0stMX1lXntcYmV0YV9LWF9pfX0kJA0KDQphbmQgdGhlbiBpdCBpcyBlYXN5IHRvIGRlcml2ZSB0aGUgZXhwcmVzc2lvbnMgZm9yIGFsbCAkSy0xJCBwcm9iYWJpbGl0aWVzIG9mIHRoZSBvdXRjb21lIHJlc3VsdGluZyBpbiBhIHBhcnRpY3VsYXIgY2xhc3M6DQoNCiQkUChZX2kgPSAxKSA9IFxmcmFje2Vee1xiZXRhXzFYX2l9fXsxK1xzdW1fe2s9MX1ee0stMX1lXntcYmV0YV9LWF9pfX0kJA0KDQokJFAoWV9pID0gMikgPSBcZnJhY3tlXntcYmV0YV8yWF9pfX17MStcc3VtX3trPTF9XntLLTF9ZV57XGJldGFfS1hfaX19JCQNCg0KJCRQKFlfaSA9IEstMSkgPSBcZnJhY3tlXntcYmV0YV97Sy0xfVhfaX19ezErXHN1bV97az0xfV57Sy0xfWVee1xiZXRhX0tYX2l9fSQkDQoNCiMjIyMgMi4yIE11bHRpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24gaW4gUg0KDQpMZXQncyBiZWdpbiB3aXRoIGEgc2ltcGxlIGV4YW1wbGU6IGNsYXNzaWZ5IGBpcmlzJFNwZWNpZXNgLg0KDQpgYGB7ciBlY2hvID0gVH0NCmhlYWQoaXJpcykNCmBgYA0KDQpOb3cgY2hlY2sgaWYgYGlyaXMkU3BlY2llc2AgaXMgYSAqZmFjdG9yKjoNCg0KYGBge3IgZWNobyA9IFR9DQpzdHIoaXJpcykNCmBgYA0KTGV0J3MgcGljayBgdmVyc2ljb2xvcmAgYXMgb3VyICpiYXNlbGluZSo6IA0KDQpgYGB7ciBlY2hvID0gVH0NCmlyaXMkU3BlY2llcyA8LSByZWxldmVsKGlyaXMkU3BlY2llcywNCiAgICAgICAgICAgICAgICAgICAgICAgIHJlZiA9ICd2ZXJzaWNvbG9yJykNCmBgYA0KDQpGaW5hbGx5LCB3ZSB3aWxsIHVzZSBgbXVsdGlub20oKWAgZnJvbSBge25uZXR9YCB0byBlc3RpbWF0ZSB0aGUgTXVsdGlub21pYWwgTG9naXN0aWMgUmVncmVzc2lvbiBtb2RlbDoNCg0KYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGfQ0KbWxyX21vZGVsIDwtIG5uZXQ6Om11bHRpbm9tKFNwZWNpZXMgfiAuLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gaXJpcykNCnN1bW1hcnkobWxyX21vZGVsKQ0KYGBgDQoNClRoZSBpbnRlcnByZXRhdGlvbiBvZiB0aGUgbW9kZWwgcmVzdWx0cyBpcyBzaW1pbGFyIHRvIHRoZSBCaW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uIGNhc2UuIFdlIHdpbGwgZGlzY3VzcyB0aGUgZmluZGluZ3Mgb24gYGlyaXMkU3BlY2llc2AgaW4gdGhlIFNlc3Npb24uDQoNCkdyYWIgdGhlIG1vZGVsIGNvZWZmaWNpZW50czoNCg0KYGBge3IgZWNobyA9IFR9DQpjb2VmZnMgPC0gYXMuZGF0YS5mcmFtZSgNCiAgc3VtbWFyeShtbHJfbW9kZWwpJGNvZWZmDQogICkNCnByaW50KGNvZWZmcykNCmBgYA0KDQpBbmQgZG8gbm90IGZvcmdldCB0aGUgYGV4cChjb2VmZmljaWVudClgIHRoaW5nIHRvIGdldCBiYWNrIHRvIHRoZSBvZGRzIHNjYWxlLi4uDQoNCmBgYHtyIGVjaG8gPSBUfQ0KZXhwKGNvZWZmcykNCmBgYA0KDQpXaGF0IGFyZSB0aGUgbW9zdCBpbmZsdWVudGlhbCBwcmVkaWN0b3JzIGluIGNhdGVnb3JpemluZyBgc2V0b3NhYCBhbmQgYHZpcmdpbmljYWAgdnMgYHZlcnNpY29sb3JgPw0KDQpgYGB7ciBlY2hvID0gVH0NCndoaWNoLm1heChleHAoY29lZmZzKVsxLCBdKQ0KYGBgDQpgYGB7ciBlY2hvID0gVH0NCndoaWNoLm1heChleHAoY29lZmZzKVsyLCBdKQ0KYGBgDQoNCkFuZCB3ZSBoYXZlIGxlYXJuZWQgdGhhdCB3ZSBjYW4gdGVsbCBgc2V0b3NhYCBmcm9tIGB2ZXJzaWNvbG9yYCBieSBgU2VwYWwuV2lkdGhgIGFuZCBgdmlyZ2luaWNhYCB2cyBgdmVyc2ljb2xvcmAgYnkgYFBldGFsLldpZHRoYCEgKipOLkIuKiogQXMgZXZlciwgc3RvcCBiZWZvcmUganVtcGluZyB0byBjb25jbHVzaW9ucy4uLiBJdCBpcyBhIGJpdCBtb3JlIGNvbXBsaWNhdGVkIHRoYW4gdGhhdCwgSSBhc3N1cmUgeW91Lg0KDQoNCiMjIyMgMi4zIE1vZGVsIEFjY3VyYWN5IGFuZCB0aGUgUk9DIGFuYWx5c2lzDQoNCioqUToqKiBIb3cgd2VsbCBkb2VzIHRoZSBtb2RlbCBwZXJmb3JtIG92ZXJhbGw/IFdlIHdpbGwgZmlyc3QgZGVyaXZlIHRoZSBtb2RlbCBwcmVkaWN0aW9ucyBmcm9tIHRoZSBgbWxyX21vZGVsYCBtb2RlbCBvYmplY3QgaW4gUjoNCg0KYGBge3IgZWNobyA9IFR9DQpwcmVkaWN0aW9ucyA8LSBwcmVkaWN0KG1scl9tb2RlbCwNCiAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YSA9IGlyaXMsDQogICAgICAgICAgICAgICAgICAgICAgICJwcm9icyIpDQpwcmVkaWN0aW9ucyA8LSBhcHBseShwcmVkaWN0aW9ucywgMSwgd2hpY2gubWF4KQ0KdGFibGUocHJlZGljdGlvbnMpDQpgYGANCg0KU3RvcmUgcHJlZGljdGlvbnMgaW4gYGlyaXNgIGFuZCB0aGFuIGNvbXB1dGUgdGhlIG1vZGVsIGFjY3VyYWN5Og0KDQpgYGB7ciBlY2hvID0gVH0NCmNsYXNzZXMgPC0gbGV2ZWxzKGlyaXMkU3BlY2llcykNCnByZWRpY3Rpb25zIDwtIGNsYXNzZXNbcHJlZGljdGlvbnNdDQppcmlzJFByZWRpY3Rpb24gPC0gcHJlZGljdGlvbnMNCmhlYWQoaXJpcykNCmBgYA0KDQpgYGB7ciBlY2hvID0gVH0NCmFjY3VyYWN5IDwtIHJvdW5kKA0KICBzdW0oaXJpcyRTcGVjaWVzID09IGlyaXMkUHJlZGljdGlvbikvZGltKGlyaXMpWzFdKjEwMCwNCiAgMikNCnByaW50KHBhc3RlMCgiVGhlIG92ZXJhbCBtb2RlbCBhY2N1cmFjeSBpcyAiLCBhY2N1cmFjeSwgIiUuIikpDQpgYGANCg0KUHJlZGljdGVkIHByb2JhYmlsaXRpZXMgY2FuIGFsc28gYmUgb2J0YWluZWQgZWFzaWx5IGZyb20gYGZpdHRlZCgpYDoNCg0KYGBge3IgZWNobyA9IFR9DQpmaXR0ZWRQcm9icyA8LSBmaXR0ZWQobWxyX21vZGVsKQ0KaGVhZChmaXR0ZWRQcm9icykNCmBgYA0KT2YgY291cnNlOg0KDQpgYGB7ciBlY2hvID0gVH0NCnRhYmxlKHJvd1N1bXMoZml0dGVkUHJvYnMpKQ0KYGBgDQoNCioqSW1wb3J0YW50LioqICpBY2N1cmFjeSBpbiBpdHNlbGYgZG9lcyBub3QgdGVsbCB0aGUgZnVsbCBzdG9yeSBvZiBob3cgYSBnaXZlbiBtb2RlbCBwZXJmb3JtcyouIE1vcmVvdmVyLCBmcm9tIGFjY3VyYWN5IGFsb25lIG9uZSBjYW4gYmUgbGVkIHRvICpjb21wbGV0ZWx5IGluY29ycmVjdCBhbmQgbWlzbGVhZGluZyBjb25jbHVzaW9ucyBhYm91dCBtb2RlbCBwZXJmb3JtYW5jZSouIFdlIG5lZWQgdG8gaW50cm9kdWNlIGEgc2V0IG9mIG1vcmUgZ3JhbnVsYXIgbWVhc3VyZXMgb2YgdGhlIHF1YWxpdHkgb2YgYSBjYXRlZ29yaXphdGlvbi4gQ29uc2lkZXIgdGhlIGZvbGxvd2luZyBmb3VyIGNhc2VzOg0KDQotIFRoZSB0cnVlIGVtcGlyaWNhbCBvYnNlcnZhdGlvbiBvZiB0aGUgb3V0Y29tZSBpcyBjbGFzcyAkQyQsIGFuZCB0aGUgbW9kZWwgYWxzbyBwcmVkaWN0cyAkQyQ7IHdlIGNhbGwgdGhpcyBhICoqSGl0Kiogb3IgYSAqKlRydWUgUG9zaXRpdmUgKFRQKSoqLg0KLSBUaGUgdHJ1ZSBlbXBpcmljYWwgb2JzZXJ2YXRpb24gb2YgdGhlIG91dGNvbWUgaXMgY2xhc3MgJEMkLCBidXQgdGhlIG1vZGVsIHByZWRpY3RzIHNvbWUgb3RoZXIgY2xhc3M7IHdlIGNhbGwgdGhpcyBhICoqTWlzcyoqIG9yIGEgKipGYWxzZSBOZWdhdGl2ZSAoRk4pKiogKGFuZCB0aGlzIGlzIGFsc28gd2hhdCBpcyBrbm93biBhcyAqKlR5cGUgSUkgRXJyb3IqKiBpbiBzdGF0aXN0aWNzKS4NCi0gVGhlIHRydWUgZW1waXJpY2FsIG9ic2VydmF0aW9uIG9mIHRoZSBvdXRjb21lIGlzICpub3QqIGNsYXNzICRDJCwgYnV0IHRoZSBtb2RlbCBwcmVkaWN0cyAkQyQ7IHdlIGNhbGwgdGhpcyBhICoqRmFsc2UgQWxhcm0gKEZBKSoqIG9yIGEgKipGYWxzZSBQb3NpdGl2ZSAoRlApKiogKGFuZCB0aGlzIGlzIGFsc28gd2hhdCBpcyBrbm93biBhcyAqKlR5cGUgSSBFcnJvcioqIGluIHN0YXRpc3RpY3MpLg0KLSBUaGUgdHJ1ZSBlbXBpcmljYWwgb2JzZXJ2YXRpb24gb2YgdGhlIG91dGNvbWUgaXMgKm5vdCogY2xhc3MgJEMkLCBhbmQgdGhlIG1vZGVsIHByZWRpY3RzIHNvbWUgb3RoZXIgY2xhc3M7IHdlIGNhbGwgdGhpcyBhICoqQ29ycmVjdCBSZWplY3Rpb24gKENSKSoqIG9yIGEgKipUcnVlIE5lZ2F0aXZlIChUTikqKiAoYW5kIHRoaXMgaXMgYWxzbyB3aGF0IGlzIGtub3duIGFzICoqVHlwZSBJIEVycm9yKnMqIGluIHN0YXRpc3RpY3MpLg0KDQohW10oLi4vX2ltZy9TMTdfMDFfUk9DLmpwZWcpe3dpZHRoPTcwJX0NCg0KVGhpcyBpcyB0aGUgKipSZWNlaXZlci1PcGVyYXRpbmcgQ2hhcmFjdGVyaXN0aWMgKFJPQykgQW5hbHlzaXMqKiBvZiBtb2RlbCBwZXJmb3JtYW5jZS4gQ29tcHV0ZSB0aGUgVHJ1ZSBQb3NpdGl2ZSBSYXRlIChUUFIpLCBGYWxzZSBOZWdhdGl2ZSBSYXRlIChGTlIpLCBGYWxzZSBQb3NpdGl2ZSBSYXRlIChGUFIpLCBhbmQgVHJ1ZSBOZWdhdGl2ZSBSYXRlIChUTlIpIGZvciBhbGwgY2xhc3NlczoNCg0KYGBge3IgZWNobyA9IFR9DQptbHJfbW9kZWxfUk9DIDwtIGxhcHBseSh1bmlxdWUoaXJpcyRTcGVjaWVzKSwgZnVuY3Rpb24oeCkgew0KICB0cHIgPC0gc3VtKGlyaXMkU3BlY2llcyA9PSB4ICYgaXJpcyRQcmVkaWN0aW9uID09IHgpDQogIHRwciA8LSB0cHIvc3VtKGlyaXMkU3BlY2llcyA9PSB4KQ0KICBmbnIgPC0gc3VtKGlyaXMkU3BlY2llcyA9PSB4ICYgaXJpcyRQcmVkaWN0aW9uICE9IHgpDQogIGZuciA8LSBmbnIvc3VtKGlyaXMkU3BlY2llcyA9PSB4KQ0KICBmcHIgPC0gc3VtKGlyaXMkU3BlY2llcyAhPSB4ICYgaXJpcyRQcmVkaWN0aW9uID09IHgpDQogIGZwciA8LSBmcHIvc3VtKGlyaXMkU3BlY2llcyAhPSB4KQ0KICB0bnIgPC0gc3VtKGlyaXMkU3BlY2llcyAhPSB4ICYgaXJpcyRQcmVkaWN0aW9uICE9IHgpDQogIHRuciA8LSB0bnIvc3VtKGlyaXMkU3BlY2llcyAhPSB4KQ0KICByb2MgPC0gYyh0cHIsIGZuciwgZnByLCB0bnIpDQogIG5hbWVzKHJvYykgPC0gYygndHByJywgJ2ZucicsICdmcHInLCAndG5yJykNCiAgcmV0dXJuKHJvYykNCn0pDQptbHJfbW9kZWxfUk9DIDwtIHJlZHVjZShtbHJfbW9kZWxfUk9DLCByYmluZCkNCnJvd25hbWVzKG1scl9tb2RlbF9ST0MpIDwtIGxldmVscyhpcmlzJFNwZWNpZXMpDQpwcmludChtbHJfbW9kZWxfUk9DKQ0KYGBgDQpBIGdvb2QgbW9kZWwgZG9lcyBub3QgY29tbWl0IHRvIGNsYXNzaWZpY2F0aW9uIGVycm9ycyBpbiB0aGUgZm9sbG93aW5nIHNlbnNlOiBpdCBtYWludGFpbnMgYSBoaWdoIFRydWUgUG9zaXRpdmUgKEhpdCkgUmF0ZSBhbmQgYSBsb3cgRmFsc2UgUG9zaXRpdmUgKEZhbHNlIEFsYXJtLCBUeXBlIEkgRXJyb3IpIFJhdGUgYWNyb3NzIHRoZSBjbGFzc2VzLg0KDQojIyMjIyAyLjMuMSBST0MgYW5hbHlzaXMgZm9yIHRoZSBCaW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uDQoNCldlIG5lZWQgdG8gcmVjb25zaWRlciBicmllZmx5IHRoZSBCaW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uIGV4YW1wbGUgZnJvbSBTZXNzaW9uIDE2IG5vdy4NCg0KYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGfQ0KZGF0YVNldCA8LSByZWFkLmNzdigiaHR0cHM6Ly9zdGF0cy5pZHJlLnVjbGEuZWR1L3N0YXQvZGF0YS9iaW5hcnkuY3N2IikNCmhlYWQoZGF0YVNldCkNCmBgYA0KDQpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEZ9DQpkYXRhU2V0JHJhbmsgPC0gZmFjdG9yKGRhdGFTZXQkcmFuaykNCm15bG9naXQgPC0gZ2xtKGFkbWl0IH4gZ3JlICsgZ3BhICsgcmFuaywNCiAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhU2V0LA0KICAgICAgICAgICAgICAgZmFtaWx5ID0gImJpbm9taWFsIikNCm1vZGVsc3VtbWFyeSA8LSBzdW1tYXJ5KG15bG9naXQpDQpwcmludChtb2RlbHN1bW1hcnkpDQpgYGANCg0KT2suIE5vdyB3ZSB3aWxsIGRvIHRoZSBmb2xsb3dpbmc6IA0KDQotIGRlcml2ZSB0aGUgbW9kZWwgcHJlZGljdGlvbnMgYW5kIHBsYWNlIHRoZW0gaW4gb3VyIGRhdGFzZXQ7DQotIHdyaXRlIG91dCBhIGZ1bmN0aW9uIHRoYXQgcHJlZGljdHMgdGhlIGNsYXNzIG9mIHRoZSBvdXRjb21lIGZvciBhIGdpdmVuIGRlY2lzaW9uIHRyYXNob2xkIChyZW1lbWJlciB0aGF0IHdlIGhhdmUgdXNlZCAkLjUkIGFzIGRlZmF1bHQ/KTsgYW5kIGZpbmFsbHksDQotIHBlcmZvcm0gYW4gUk9DIGFuYWx5c2lzIGZvciBlYWNoIHZhbHVlIG9mIHRoZSBkZWNpc2lvbiBib3VuZGFyeSB0aGF0IHdlIGNob3NlLg0KDQpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEZ9DQpkYXRhU2V0JHByZWRpY3Rpb25zIDwtIGZpdHRlZChteWxvZ2l0KQ0KaGVhZChkYXRhU2V0KQ0KYGBgDQoNCk5vdyB0aGUgZnVuY3Rpb24uIFdlIHdpbGwgd3JpdGUgb3V0IHRoZSBgcHJlZGljdENsYXNzKClgIGZ1bmN0aW9uIHRoYXQgdGFrZXMgdHdvIGFyZ3VtZW50czogYGRlY0JvdW5kYXJ5YCAtIHRoZSBkZWNpc2lvbiBib3VuZGFyeSwgYW5kIGBkZWNEYXRhYCAtIGEgZGF0YS5mcmFtZSB3aXRoIHR3byBjb2x1bW5zIHJlcHJlc2VudGluZyB0aGUgb2JzZXJ2ZWQgKGBvYnNlcnZlZGApIGNsYXNzIChlaXRoZXIgYDFgIG9yIGAwYCkgYW5kIHRoZSBwcmVkaWN0ZWQgKGBwcmVkaWN0ZWRgKSBwcm9iYWJpbGl0eSBvZiBiZWluZyBgMWAuVGhlIGBwcmVkaWN0Q2xhc3MoKWAgZnVuY3Rpb24gd2lsbCBhdXRvbWF0aWNhbGx5IHBlcmZvcm0gdGhlIFJPQyBhbmFseXNpcyBmb2xsb3dpbmcgdGhlIGNsYXNzIHByZWRpY3Rpb24uDQoNCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRn0NCnByZWRpY3RDbGFzcyA8LSBmdW5jdGlvbihkZWNCb3VuZGFyeSwgZGVjRGF0YSkgew0KICANCiAgIyAtIHByZWRpY3QgY2xhc3MNCiAgcHJlZENsYXNzIDwtIGlmZWxzZShkZWNEYXRhJHByZWRpY3RlZCA+PSBkZWNCb3VuZGFyeSwgMSwgMCkNCiAgDQogICMgLSBST0MgYW5hbHlzaXMNCiAgdHByIDwtIHN1bShkZWNEYXRhJG9ic2VydmVkID09IDEgJiBwcmVkQ2xhc3MgPT0gMSkNCiAgdHByIDwtIHRwci9zdW0oZGVjRGF0YSRvYnNlcnZlZCA9PSAxKQ0KICBmbnIgPC0gc3VtKGRlY0RhdGEkb2JzZXJ2ZWQgPT0gMSAmIHByZWRDbGFzcyA9PSAwKQ0KICBmbnIgPC0gZm5yL3N1bShkZWNEYXRhJG9ic2VydmVkID09IDEpDQogIGZwciA8LSBzdW0oZGVjRGF0YSRvYnNlcnZlZCA9PSAwICYgcHJlZENsYXNzID09IDEpDQogIGZwciA8LSBmcHIvc3VtKGRlY0RhdGEkb2JzZXJ2ZWQgPT0gMCkNCiAgdG5yIDwtIHN1bShkZWNEYXRhJG9ic2VydmVkID09IDAgJiBwcmVkQ2xhc3MgPT0gMCkNCiAgdG5yIDwtIHRuci9zdW0oZGVjRGF0YSRvYnNlcnZlZCA9PSAwKQ0KICByb2MgPC0gYyhkZWNCb3VuZGFyeSwgdHByLCBmbnIsIGZwciwgdG5yKQ0KICBuYW1lcyhyb2MpIDwtIGMoJ2RlY0JvdW5kYXJ5JywgJ3RwcicsICdmbnInLCAnZnByJywgJ3RucicpDQogIHJldHVybihyb2MpDQp9DQpgYGANCg0KTm93IHdlIHdpbGwgaW5zcGVjdCB0aGUgUk9DIGFuYWx5c2lzIGZvciB0aGlzIEJpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24gbW9kZWwgYWNyb3NzIGEgcmFuZ2Ugb2YgZGVjaXNpb24gYm91bmRhcmllczoNCg0KYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGfQ0KZGVjQm91bmRhcmllcyA8LSBzZXEoLjAwMSwgLjk5OSwgYnkgPSAuMDAxKQ0KUk9DX2RhdGFzZXQgPC0gZGF0YS5mcmFtZShvYnNlcnZlZCA9IGRhdGFTZXQkYWRtaXQsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkaWN0ZWQgPSBkYXRhU2V0JHByZWRpY3Rpb25zKQ0KUk9DX3Jlc3VsdHMgPC0gbGFwcGx5KGRlY0JvdW5kYXJpZXMsIGZ1bmN0aW9uKHgpIHsNCiAgYXMuZGF0YS5mcmFtZSh0KHByZWRpY3RDbGFzcyh4LCBST0NfZGF0YXNldCkpKQ0KfSkNClJPQ19yZXN1bHRzIDwtIHJiaW5kbGlzdChST0NfcmVzdWx0cykNCmhlYWQoUk9DX3Jlc3VsdHMpDQpgYGANCg0KV2Ugbm93IHBsb3QgdGhlIEZhbHNlIFBvc2l0aXZlIFJhdGUgKGBmcHJgLCBGYWxzZSBBbGFybSkgdnMgdGhlIFRydWUgUG9zaXRpdmUgUmF0ZSAoYHRwcmAsIEhpdCk6DQoNCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgZmlnLndpZHRoID0gNi41LCBmaWcuaGVpZ2h0ID0gNn0NClJPQ19yZXN1bHRzJGRpZmYgPC0gUk9DX3Jlc3VsdHMkdHByIC0gUk9DX3Jlc3VsdHMkZnByDQpST0NfcmVzdWx0cyRsYWJlbCA8LSAiIg0KUk9DX3Jlc3VsdHMkbGFiZWxbd2hpY2gubWF4KFJPQ19yZXN1bHRzJGRpZmYpXSA8LSAiSGVyZSEiDQpnZ3Bsb3QoZGF0YSA9IFJPQ19yZXN1bHRzLCANCiAgICAgICBhZXMoeCA9IGZwciwgDQogICAgICAgICAgIHkgPSB0cHIsIA0KICAgICAgICAgICBsYWJlbCA9IGxhYmVsKSkgKw0KICBnZW9tX3BhdGgoY29sb3IgPSAicmVkIikgKyBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEpICsNCiAgZ2VvbV90ZXh0X3JlcGVsKGFycm93ID0gYXJyb3cobGVuZ3RoID0gdW5pdCgwLjA2LCAiaW5jaGVzIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVuZHMgPSAibGFzdCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gImNsb3NlZCIpLCANCiAgICAgICAgICAgICAgICAgIG1pbi5zZWdtZW50Lmxlbmd0aCA9IHVuaXQoMCwgJ2xpbmVzJyksDQogICAgICAgICAgICAgICAgICBudWRnZV95ID0gLjEpICsgDQogIGdndGl0bGUoIlJPQyBhbmFseXNpcyBmb3IgdGhlIEJpbm9taWFsIFJlZ3Jlc3Npb24gTW9kZWwiKSArDQogIHhsYWIoIlNwZWNpZmljaXR5IChGYWxzZSBBbGFybSBSYXRlKSIpICsgeWxhYigiU2Vuc2l0aXZpdHkgKEhpdCBSYXRlKSIpICsgDQogIHRoZW1lX2J3KCkgKyANCiAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IC41KSkNCmBgYA0KDQpUaGUgdmFsdWUgb2YgdGhlIGRlY2lzaW9uIGJvdW5kYXJ5IChzb21ldGltZXMgY2FsbGVkIHRoZSAqKmN1dC1vZmYqKikgaXMgd2hlcmUgdGhlIG1vZGVsIGFjaGlldmVzIHRoZSBoaWdoZXN0IHBvc3NpYmxlIHZhbHVlIGZvciB0aGUgSGl0IFJhdGUgZ2l2ZW4gdGhlIGxvd2VzdCBwb3NzaWJsZSB2YWx1ZSBmb3IgdGhlIEZhbHNlIEFsYXJtIFJhdGUuDQoNClRoaXMgYW5hbHlzaXMgY2FuIGJlIGdlbmVyYWxpemVkIHRvIG11bHRpY2xhc3MgY2xhc3NpZmljYXRpb24gcHJvYmxlbXMgaW4gZGlmZmVyZW50IHdheXMsIGJ1dCB0aGF0IGRlZmluaXRlbHkgZ29lcyBiZXlvbmQgdGhlIHNjb3BlIG9mIGFuIGludHJvZHVjdG9yeSBjb3Vyc2UuLi4NCg0KIyMjIyAyLjQgVGVzdHMgZm9yIG1vZGVsIGNvZWZmaWNpZW50cyBhbmQgbXVsdGljb2xsaW5lYXJpdHkgZGlnYW5vc3RpY3MNCg0KUmVjYWxsIGZyb20gQmlub21pYWwgTG9naXN0aWMgUmVncmVzc2lvbiB0aGF0IHRoZSBXYWxkICRaJC10ZXN0IGZvciBhIHJlZ3Jlc3Npb24gY29lZmZpY2llbnQgaXMgb2J0YWluZWQgYnkgZGl2aWRpbmcgdGhlIGVzdGltYXRlIHdpdGggdGhlIHJlc3BlY3RpdmUgc3RhbmRhcmQgZXJyb3I6DQoNCmBgYHtyIGVjaG8gPSBUfQ0Kc3VtbWFyeShtbHJfbW9kZWwpJGNvZWZmaWNpZW50cw0KYGBgDQpgYGB7ciBlY2hvID0gVH0NCnN1bW1hcnkobWxyX21vZGVsKSRzdGFuZGFyZC5lcnJvcnMNCmBgYA0KVGhlbiB3ZSBjYW4gb2J0YWluIHRoZSAkWiQtdGVzdHMgaW4gdGhlIGZvbGxvd2luZyB3YXk6DQoNCmBgYHtyIGVjaG8gPSBUfQ0KeiA8LSBzdW1tYXJ5KG1scl9tb2RlbCkkY29lZmZpY2llbnRzL3N1bW1hcnkobWxyX21vZGVsKSRzdGFuZGFyZC5lcnJvcnMNCnByaW50KHopDQpgYGANCkFuZCBiZWNhdXNlIHRoZSAkWiQtdGVzdCBmb2xsb3dzIGEgc3RhbmRhcmQgbm9ybWFsIGRpc3RyaWJ1dGlvbiAoc2VlIFt0aGlzIGRpc2N1c3Npb25dKGh0dHBzOi8vc3RhdHMuc3RhY2tleGNoYW5nZS5jb20vcXVlc3Rpb25zLzYwMDc0L3dhbGQtdGVzdC1mb3ItbG9naXN0aWMtcmVncmVzc2lvbikpLCB0aGUgcmVzcGVjdGl2ZSBwcm9iYWJpbGl0aWVzIG9mIHRoZSBUeXBlIEkgRXJyb3IgKGkuZS4gc3RhdGlzdGljYWwgc2lnbmlmaWNhbmNlKSBhcmU6DQoNCmBgYHtyIGVjaG8gPSBUfQ0KcCA8LSAoMSAtIHBub3JtKGFicyh6KSwgMCwgMSkpICogMg0KcHJpbnQocCkNCmBgYA0KDQpIb3dldmVyLCB1c2luZyAkWiQtdGVzdHMgaW4gTXVsdGlub21pYWwgTG9naXN0aWMgUmVncmVzc2lvbiAqaXMgbm90IHJlY29tbWVuZGVkKi4gSW5zdGVhZCwgd2UgY2FuIHVzZSBgTUFTUzpkcm9wdGVybSgpYCB0byBwZXJmb3JtIHRoZSAqTGlrZWxpaG9vZC1SYXRpbyBUZXN0cyogZm9yIHRoZSBtb2RlbCBjb2VmZmljaWVudHM6DQoNCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRn0NCmxydCA8LSBNQVNTOjpkcm9wdGVybShvYmplY3QgPSBtbHJfbW9kZWwsDQogICAgICAgICAgICAgICAgICAgICAgc2NvcGUgPSBTcGVjaWVzIH4gU2VwYWwuTGVuZ3RoICsgU2VwYWwuV2lkdGggKyBQZXRhbC5MZW5ndGggKyBQZXRhbC5XaWR0aCwNCiAgICAgICAgICAgICAgICAgICAgICB0ZXN0ID0gJ0NoaXNxJywNCiAgICAgICAgICAgICAgICAgICAgICB0cmFjZSA9IEYsICMgLSB0byBwcmludCBhZGRpdGlvbmFsIGluZm9ybWF0aW9uIG9yIG5vdA0KICAgICAgICAgICAgICAgICAgICAgIGsgPSAyLCAjIC0gZm9yIEFrYWlrZSBJbmZvcm1hdGlvbiBDcml0ZXJpb24gKEFJQykNCiAgICAgICAgICAgICAgICAgICAgICApIA0KYXMuZGF0YS5mcmFtZShscnQpDQpgYGANCg0KVGhlIGBkcm9wdGVybSgpYCBmdW5jdGlvbiB3aWxsIGZpdCBhbGwgbW9kZWxzIHRoYXQgZGlmZmVyIGZyb20gdGhlIGZ1bGwgbW9kZWwgYnkgc3VjY2Vzc2l2ZWx5IGRyb3BwaW5nIGEgc2luZ2xlIHByZWRpY3RvciBhbmQgcGVyZm9ybSBhIGNvbXBhcmlzb24gYmV0d2VlbiB0aGUgcGVyZm9ybWFuY2Ugb2YgdGhlIGZ1bGwgbW9kZWwgYW5kIHRoZSBtb2RlbCB3aXRoIGEgZHJvcHBlZCBwcmVkaWN0b3IuIFRoZSByZXN1bHRzIG9mIHRoaXMgcHJvY2VkdXJlIHN1Z2dlc3QgdGhhdCB3ZSBjb3VsZCBkcm9wIGJvdGggYFNlcGFsLkxlbmd0aGAgYW5kIGBTZXBhbC5XaWR0aGAgZnJvbSB0aGUgbW9kZWw7IGxldCdzIHRyeSBpdCBvdXQ6DQoNCmBgYHtyIGVjaG8gPSBUfQ0KZGF0YShpcmlzKQ0KaXJpcyRTcGVjaWVzIDwtIHJlbGV2ZWwoaXJpcyRTcGVjaWVzLA0KICAgICAgICAgICAgICAgICAgICAgICAgcmVmID0gJ3ZlcnNpY29sb3InKQ0KbWxyX21vZGVsX2Ryb3AgPC0gbXVsdGlub20oU3BlY2llcyB+IFBldGFsLkxlbmd0aCArIFBldGFsLldpZHRoLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IGlyaXMpDQpzdW1tYXJ5KG1scl9tb2RlbF9kcm9wKQ0KYGBgDQoNClRoZSAkQUlDJCBvZiB0aGUgbW9kZWwgd2l0aCBhbGwgcHJlZGljdG9ycyBpbmNsdWRlZCB3YXMgYDMxLjg5OTE3YCBhbmQgZm9sbG93aW5nIHRoZSByZW1vdmFsIG9mIGBTZXBhbC5MZW5ndGhgIGFuZCBgU2VwYWwuV2lkdGhgIGl0IGluY3JlYXNlZCBvbmx5IGEgYml0LiBMZXQncyBwZXJmb3JtIGFkZGl0aW9uYWwgY2hlY2tzOg0KDQpgYGB7ciBlY2hvID0gVH0NCnogPC0gc3VtbWFyeShtbHJfbW9kZWxfZHJvcCkkY29lZmZpY2llbnRzL3N1bW1hcnkobWxyX21vZGVsX2Ryb3ApJHN0YW5kYXJkLmVycm9ycw0KcHJpbnQoeikNCmBgYA0KDQpgYGB7ciBlY2hvID0gVH0NCnAgPC0gKDEgLSBwbm9ybShhYnMoeiksIDAsIDEpKSAqIDINCnByaW50KHApDQpgYGANCmBgYHtyIGVjaG8gPSBUfQ0KcHJlZGljdGlvbnMgPC0gcHJlZGljdChtbHJfbW9kZWxfZHJvcCwNCiAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YSA9IGlyaXMsDQogICAgICAgICAgICAgICAgICAgICAgICJwcm9icyIpDQpwcmVkaWN0aW9ucyA8LSBhcHBseShwcmVkaWN0aW9ucywgMSwgd2hpY2gubWF4KQ0KY2xhc3NlcyA8LSBsZXZlbHMoaXJpcyRTcGVjaWVzKQ0KcHJlZGljdGlvbnMgPC0gY2xhc3Nlc1twcmVkaWN0aW9uc10NCmlyaXMkUHJlZGljdGlvbiA8LSBwcmVkaWN0aW9ucw0KaGVhZChpcmlzKQ0KYGBgDQoNCmBgYHtyIGVjaG8gPSBUfQ0KbWxyX21vZGVsX1JPQyA8LSBsYXBwbHkodW5pcXVlKGlyaXMkU3BlY2llcyksIGZ1bmN0aW9uKHgpIHsNCiAgdHByIDwtIHN1bShpcmlzJFNwZWNpZXMgPT0geCAmIGlyaXMkUHJlZGljdGlvbiA9PSB4KQ0KICB0cHIgPC0gdHByL3N1bShpcmlzJFNwZWNpZXMgPT0geCkNCiAgZm5yIDwtIHN1bShpcmlzJFNwZWNpZXMgPT0geCAmIGlyaXMkUHJlZGljdGlvbiAhPSB4KQ0KICBmbnIgPC0gZm5yL3N1bShpcmlzJFNwZWNpZXMgPT0geCkNCiAgZnByIDwtIHN1bShpcmlzJFNwZWNpZXMgIT0geCAmIGlyaXMkUHJlZGljdGlvbiA9PSB4KQ0KICBmcHIgPC0gZnByL3N1bShpcmlzJFNwZWNpZXMgIT0geCkNCiAgdG5yIDwtIHN1bShpcmlzJFNwZWNpZXMgIT0geCAmIGlyaXMkUHJlZGljdGlvbiAhPSB4KQ0KICB0bnIgPC0gdG5yL3N1bShpcmlzJFNwZWNpZXMgIT0geCkNCiAgcm9jIDwtIGModHByLCBmbnIsIGZwciwgdG5yKQ0KICBuYW1lcyhyb2MpIDwtIGMoJ3RwcicsICdmbnInLCAnZnByJywgJ3RucicpDQogIHJldHVybihyb2MpDQp9KQ0KbWxyX21vZGVsX1JPQyA8LSByZWR1Y2UobWxyX21vZGVsX1JPQywgcmJpbmQpDQpyb3duYW1lcyhtbHJfbW9kZWxfUk9DKSA8LSBsZXZlbHMoaXJpcyRTcGVjaWVzKQ0KcHJpbnQobWxyX21vZGVsX1JPQykNCmBgYA0KYGBge3IgZWNobyA9IFR9DQphY2N1cmFjeSA8LSByb3VuZCgNCiAgc3VtKGlyaXMkU3BlY2llcyA9PSBpcmlzJFByZWRpY3Rpb24pL2RpbShpcmlzKVsxXSoxMDAsDQogIDIpDQpwcmludChwYXN0ZTAoIlRoZSBvdmVyYWwgbW9kZWwgYWNjdXJhY3kgaXMgIiwgYWNjdXJhY3ksICIlLiIpKQ0KYGBgDQoNClJlY2FsbCBob3cgdGhlIGFjY3VyYWN5IHVzZWQgdG8gYmUgYWJvdmUgOTglIHdpdGggdGhlIGZ1bGwgbW9kZWwuIFdvdWxkIHlvdSB0cmFkZSBhIDIlIGRyb3AgaW4gbW9kZWwgYWNjdXJhY3kgYWdhaW5zdCB0aGUgY29zdCBvZiBjb2xsZWN0aW5nIHR3byBhZGRpdGlvbmFsIHByZWRpY3RvcnM/DQoNCk5vdywgYXMgb2YgdGhlIG11bHRpY29sbGluZWFyaXR5IGluIE11bHRpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb246IHJ1biBgbG0oKWAgYXMgaWYgdGhlIGNhdGVnb3JpY2FsIG91dGNvbWUgd2FzIGEgY29udGludW91cyB2YXJpYWJsZSBhbmQgdXNlIGB2aWYoKWAgZnJvbSBge2Nhcn1gIHRvIGFzc2VzcyB0aGUgVmFyaWFuY2UgSW5mbGF0aW9uIEZhY3RvcjoNCg0KYGBge3IgZWNobyA9IFR9DQpkYXRhKGlyaXMpDQppcmlzJFNwZWNpZXMgPC0gcmVsZXZlbChpcmlzJFNwZWNpZXMsDQogICAgICAgICAgICAgICAgICAgICAgICByZWYgPSAndmVyc2ljb2xvcicpDQppcmlzJFNwZWNpZXMgPC0gc2FwcGx5KGlyaXMkU3BlY2llcywgZnVuY3Rpb24oeCkgew0KICBpZiAoeCA9PSAndmVyc2ljb2xvcicpIHsNCiAgICB4IDwtIDANCiAgfSBlbHNlIGlmICh4ID09ICdzZXRvc2EnKSB7DQogICAgeCA8LSAxDQogIH0gZWxzZSB7DQogICAgeCA8LSAyDQogIH0NCn0pDQptbHJfbW9kZWxfdmlmIDwtIGxtKFNwZWNpZXMgfiAuLA0KICAgICAgICAgICAgICAgICAgICBkYXRhID0gaXJpcykNCnZpZihtbHJfbW9kZWxfdmlmKQ0KYGBgDQpBbmQgbm93IG9uZSBjb3VsZCBwZXJoYXBzIHNlZSB3aGF0IHRoZSByZWFsIHByb2JsZW0gd2l0aCBvdXIgTXVsdGlub21pYWwgTG9naXN0aWMgUmVncmVzc2lvbiBtb2RlbHMgb2YgdGhlIGBpcmlzYCBkYXRhc2V0IGlzPyANCg0KYGBge3IgZWNobyA9IFR9DQpjb3Ioc2VsZWN0KGlyaXMsIC1TcGVjaWVzKSkNCmBgYA0KDQoqKioNCg0KIyMjIEZ1cnRoZXIgUmVhZGluZ3MNCg0KKyBbTXVsdGlub21pYWwgTG9naXN0aWMgUmVncmVzc2lvbiBmcm9tIEVuZ2xpc2ggV2lraXBlZGlhIC0gQW4gZXhjZWxsZW50IGludHJvZHVjdG9yeSByZWFkIV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvTXVsdGlub21pYWxfbG9naXN0aWNfcmVncmVzc2lvbikNCg0KKyBbQ29tcGFuaW9uIHRvIEJFUiA2NDI6IEFkdmFuY2VkIFJlZ3Jlc3Npb24gTWV0aG9kcywgQ2hhcHRlciAxMSBNdWx0aW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uLCBDaGVuZyBIdWEsIERyLiBZb3VuLUplbmcgQ2hvaSwgUWluZ3pob3UgU2hpLCAyMDIxLTA0LTI5XShodHRwczovL2Jvb2tkb3duLm9yZy9jaHVhL2JlcjY0Ml9hZHZhbmNlZF9yZWdyZXNzaW9uL211bHRpbm9taWFsLWxvZ2lzdGljLXJlZ3Jlc3Npb24uaHRtbCkNCg0KKyBbe25uZXR9IHBhY2thZ2UgZG9jdW1lbnRhdGlvbl0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL25uZXQvbm5ldC5wZGYpDQoNCisgW3ttbmxvZ2l0fSBwYWNrYWdlIHZpZ25ldHRlXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvbW5sb2dpdC92aWduZXR0ZXMvbW5sb2dpdC5wZGYpIC0gW3ttbmxvZ2l0fV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL21ubG9naXQvKSBoYXMgbWFueSBhZHZhbnRhZ2VzIGluIEdMTXMgaW4gY29tcGFyaXNvbiB0byB7bm5ldH0gd2hpY2ggaXMgdHlwaWNhbGx5IHVzZWQgZm9yIGVkdWNhdGlvbmFsIHB1cnBvc2VzDQoNCg0KIyMjIFIgTWFya2Rvd24NCg0KKyBbUiBNYXJrZG93bl0oaHR0cHM6Ly9ybWFya2Rvd24ucnN0dWRpby5jb20vKSBpcyB3aGF0IEkgaGF2ZSB1c2VkIHRvIHByb2R1Y2UgdGhpcyBiZWF1dGlmdWwgTm90ZWJvb2suIFdlIHdpbGwgbGVhcm4gbW9yZSBhYm91dCBpdCBuZWFyIHRoZSBlbmQgb2YgdGhlIGNvdXJzZSwgYnV0IGlmIHlvdSBhbHJlYWR5IGZlZWwgcmVhZHkgdG8gZGl2ZSBkZWVwLCBoZXJlJ3MgYSBib29rOiBbUiBNYXJrZG93bjogVGhlIERlZmluaXRpdmUgR3VpZGUsIFlpaHVpIFhpZSwgSi4gSi4gQWxsYWlyZSwgR2FycmV0dCBHcm9sZW11bmRzLl0oaHR0cHM6Ly9ib29rZG93bi5vcmcveWlodWkvcm1hcmtkb3duLykgDQoNCioqKg0KR29yYW4gUy4gTWlsb3Zhbm92acSHDQoNCkRhdGFLb2xla3RpdiwgMjAyMC8yMQ0KDQpjb250YWN0OiBnb3Jhbi5taWxvdmFub3ZpY0BkYXRha29sZWt0aXYuY29tDQoNCiFbXSguLi9faW1nL0RLX0xvZ29fMTAwLnBuZykNCg0KKioqDQpMaWNlbnNlOiBbR1BMdjNdKGh0dHA6Ly93d3cuZ251Lm9yZy9saWNlbnNlcy9ncGwtMy4wLnR4dCkNClRoaXMgTm90ZWJvb2sgaXMgZnJlZSBzb2Z0d2FyZTogeW91IGNhbiByZWRpc3RyaWJ1dGUgaXQgYW5kL29yIG1vZGlmeSBpdCB1bmRlciB0aGUgdGVybXMgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFzIHB1Ymxpc2hlZCBieSB0aGUgRnJlZSBTb2Z0d2FyZSBGb3VuZGF0aW9uLCBlaXRoZXIgdmVyc2lvbiAzIG9mIHRoZSBMaWNlbnNlLCBvciAoYXQgeW91ciBvcHRpb24pIGFueSBsYXRlciB2ZXJzaW9uLg0KVGhpcyBOb3RlYm9vayBpcyBkaXN0cmlidXRlZCBpbiB0aGUgaG9wZSB0aGF0IGl0IHdpbGwgYmUgdXNlZnVsLCBidXQgV0lUSE9VVCBBTlkgV0FSUkFOVFk7IHdpdGhvdXQgZXZlbiB0aGUgaW1wbGllZCB3YXJyYW50eSBvZiBNRVJDSEFOVEFCSUxJVFkgb3IgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UuICBTZWUgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGZvciBtb3JlIGRldGFpbHMuDQpZb3Ugc2hvdWxkIGhhdmUgcmVjZWl2ZWQgYSBjb3B5IG9mIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBhbG9uZyB3aXRoIHRoaXMgTm90ZWJvb2suIElmIG5vdCwgc2VlIDxodHRwOi8vd3d3LmdudS5vcmcvbGljZW5zZXMvPi4NCg0KKioqDQoNCg==