FCA-Based Decorrelation on Vehicle

This document describes the use of the FRESA.CAD::GDSTMDecorrelation() function that runs the feature correlation analysis (FCA) algorithm.

This demo uses FRESA.CAD and mlbench R packages:

knitr::opts_chunk$set(collapse = TRUE, warning = FALSE, message = FALSE,comment = "#>")

library("FRESA.CAD")
Loading required package: Rcpp
Loading required package: stringr
Loading required package: miscTools
Loading required package: Hmisc
Loading required package: lattice
Loading required package: survival
Loading required package: Formula
Loading required package: ggplot2
Registered S3 methods overwritten by 'htmltools':
  method               from         
  print.html           tools:rstudio
  print.shiny.tag      tools:rstudio
  print.shiny.tag.list tools:rstudio
Registered S3 method overwritten by 'htmlwidgets':
  method           from         
  print.htmlwidget tools:rstudio
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table     

Attaching package: ‘Hmisc’

The following objects are masked from ‘package:base’:

    format.pval, units

Loading required package: pROC
Type 'citation("pROC")' for a citation.

Attaching package: ‘pROC’

The following objects are masked from ‘package:stats’:

    cov, smooth, var
library(mlbench)

op <- par(no.readonly = TRUE)

I’ll load the vehicle data set

data("Vehicle", package = "mlbench")
print(table(Vehicle$Class))

 bus opel saab  van 
 218  212  217  199 

Setting some variables for downstream analysis

studyName = "Vehicle"
datasetframe <- Vehicle
Outcome <- "Class"

# The fractions of samples to be use in the training set
trainFraction = 0.5

# The correlation threshold 
correlationThreshold = 0.6

Setting the Training and Testing sets


tb <- table(datasetframe[,Outcome])
classNames <- unique(datasetframe[,Outcome])

allrowClass <- datasetframe[,Outcome]
names(allrowClass) <- rownames(datasetframe)

trainsize <- trainFraction*min(tb);
trainSamples <- NULL;
for (theClass in classNames)
{
  classSample <- allrowClass[allrowClass == theClass]
  trainSamples <- c(trainSamples,names(classSample[sample(length(classSample),trainsize)]))
}


datasetframe_train <- datasetframe[trainSamples,]
testSamples <- !(rownames(datasetframe) %in% trainSamples)
datasetframe_test <- datasetframe[testSamples,]

outcomes <- datasetframe_train[,Outcome]

pander::pander(table(datasetframe[,Outcome]),caption="All")
All
bus opel saab van
218 212 217 199
pander::pander(table(datasetframe_train[,Outcome]),caption="Training")
Training
bus opel saab van
99 99 99 99
pander::pander(table(datasetframe_test[,Outcome]),caption="Testing")
Testing
bus opel saab van
119 113 118 100

FCA with Default Parameters

The default parameters will compute the transformation matrix with a maximum correlation goal of 0.8 using fast matrix multiplication with Pearson correlation and linear models estimation.

Default Parameters: thr=0.80,method=“fast”,type=“LM”



datasetframeDecor<-list();
decortype <- list();
system.time(datasetframeDecor[[1]] <- GDSTMDecorrelation(datasetframe_train))

user system elapsed 0.06 0.00 0.06

decortype[[1]] <- "Default"

Testing Set Decorrelation

To decorrelate the testing set use the predictDecorrelate() function

decor_test <- predictDecorrelate(datasetframeDecor[[1]],datasetframe_test)

Heat Maps of the Correlation Matrices

Here are the heat maps of the correlation matrices before and after decorrelation on the testing set



featnames <- attr(datasetframeDecor[[1]],"varincluded")
cormat <- cor(datasetframe_test[,featnames],method="pearson")
gplots::heatmap.2(abs(cormat),
                  trace = "none",
                  scale = "none",
                  mar = c(10,10),
                  col=rev(heat.colors(5)),
                  main = paste("Raw Correlation:",studyName),
                  cexRow = 0.75,
                  cexCol = 0.75,
                  key.title=NA,
                  key.xlab="Pearson Correlation",
                  xlab="Feature", ylab="Feature")



featnames <- colnames(attr(datasetframeDecor[[1]],"GDSTM"))
cormat <- cor(decor_test[,featnames],method="pearson")
gplots::heatmap.2(abs(cormat),
                  trace = "none",
                  scale = "none",
                  mar = c(10,10),
                  col=rev(heat.colors(5)),
                  main = paste("After decorrelation:",studyName),
                  cexRow = 0.75,
                  cexCol = 0.75,
                  key.title=NA,
                  key.xlab="Pearson Correlation",
                  xlab="Transformed Feature", ylab="Transformed Feature")

NA
NA

FCA with Options

The following code are examples of running GDSTMDecorrelation() function with options:



# Change the maximum correlation goal

decortype[[2]] <- "AtThr"
system.time(datasetframeDecor[[2]] <- GDSTMDecorrelation(
  datasetframe_train,
  thr = correlationThreshold
  ))

user system elapsed 0.04 0.02 0.05

# Change the maximum correlation goal, and set to Robust Liner Models
decortype[[3]] <- "RLM_Pearson"
system.time(datasetframeDecor[[3]] <- GDSTMDecorrelation(
  datasetframe_train,
  type="RLM",
  method="pearson"))

user system elapsed 0.18 0.03 0.21

# Change the maximum correlation goal, and change to Spearman correlation
decortype[[4]] <- "LM_Spearman"
system.time(datasetframeDecor[[4]] <- GDSTMDecorrelation(
  datasetframe_train,
  type="LM",
  method="spearman"))

user system elapsed 0.05 0.01 0.06

# Change the maximum correlation goal, and set Spearman correlation with robust liner model
decortype[[5]] <- "RLM_Spearman"
system.time(datasetframeDecor[[5]] <- GDSTMDecorrelation(
  datasetframe_train,
  type="RLM",
  method="spearman"))

user system elapsed 0.11 0.02 0.12

# The following are for supervised basis learning

# Set the target class for association learning
decortype[[6]] <- "Sup_Default"
system.time(datasetframeDecor[[6]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome))

user system elapsed 0.03 0.00 0.03

# Change the maximum correlation goal
decortype[[7]] <- "Sup_AtThr"
system.time(datasetframeDecor[[7]] <- GDSTMDecorrelation(
  datasetframe_train,
  thr = correlationThreshold,
  Outcome=Outcome))

user system elapsed 0.03 0.00 0.03

# Change the maximum correlation goal, and set to Robust Liner Models
decortype[[8]] <- "Sup_RLM_Pearson"
system.time(datasetframeDecor[[8]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  type="RLM",
  method="pearson"))

user system elapsed 0.13 0.02 0.13

# Change the maximum correlation goal, and change to Spearman correlation
decortype[[9]] <- "Sup_LM_Spearman"
system.time(datasetframeDecor[[9]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  type="LM",
  method="spearman"))

user system elapsed 0.05 0.00 0.05

# Change the maximum correlation goal, and set to Spearman correlation with robust liner model
decortype[[10]] <- "Sup_RLM_Spearman"
system.time(datasetframeDecor[[10]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  type="RLM",
  method="spearman"))

user system elapsed 0.14 0.02 0.16

# With user defined supervised basis 
decortype[[11]] <- "Sup_KS_RLM_Spearman"
baseKS <- names(univariate_KS(datasetframe_train,
                        Outcome=Outcome,
                        pvalue=0.20,
                        limit=0,
                        thr=correlationThreshold))

system.time(datasetframeDecor[[11]] <- GDSTMDecorrelation(
  datasetframe_train,
  Outcome=Outcome,
  baseFeatures=baseKS,
  type="RLM",
  method="spearman"))

user system elapsed 0.14 0.00 0.14

The GDSTMDecorrelation function returns a data frame with the following column names:

The output features after transformation will be named after the original names and:

Furthermore, the returned data frame will have the following attributes:

  1. Transformation matrix: “GDSTM

  2. Features:

    1. fsocre

    2. varincluded

    3. topFeatures

  3. Unaltered Basis:

    1. baseFeatures

      1. correlatedToBase
    2. AbaseFeatures

GDSTM

The “GDSTM attribute stores the spatial transformation matrix. The matrix only includes continuous features that had some correlation greater than the threshold



## The Spatial Transformation Matrix:
GDSTM <- attr(datasetframeDecor[[1]],"GDSTM")

## The heatmap of the matrix
gplots::heatmap.2(1*(abs(GDSTM) > 0),
                  trace = "none",
                  mar = c(10,10),
                  col=rev(heat.colors(2)),
                  main = paste("GDSTM Matrix:",studyName),
                  cexRow = 0.7,
                  cexCol = 0.7,
                  breaks = c(0,0.5,1),
                  key.title=NA,
                  key.xlab="|beta| > 0",
                  xlab="GDSTM Feature", ylab="Input Feature")


pander::pander(t(colnames(GDSTM)),caption="New names of decorrelated matrix")
New names of decorrelated matrix (continued below)
De_Comp De_Circ De_D.Circ Ba_Scat.Ra De_Elong De_Pr.Axis.Rect
Table continues below
De_Max.L.Rect De_Sc.Var.Maxis De_Sc.Var.maxis De_Ra.Gyr Ba_Kurt.Maxis
De_Holl.Ra

The Coefficients per each Decorrelated Feature

The following code extracts the dependence formula coefficients of each new feature to the original features.


namecol <- colnames(GDSTM);

for (i in 1:ncol(GDSTM))
{
  associ <- abs(GDSTM[,i])>0;
  if (sum(associ) > 1)
  {
    pander::pander(t(GDSTM[associ,i]),caption=namecol[i])
  }
}

----------------
 Comp   Scat.Ra 
------ ---------
  1     -0.2026 
----------------

Table: De_Comp


----------------
 Circ   Scat.Ra 
------ ---------
  1     -0.1595 
----------------

Table: De_Circ


------------------
 D.Circ   Scat.Ra 
-------- ---------
   1      -0.4369 
------------------

Table: De_D.Circ


-----------------
 Scat.Ra   Elong 
--------- -------
 0.2307      1   
-----------------

Table: De_Elong


-------------------------
 Scat.Ra    Pr.Axis.Rect 
---------- --------------
 -0.07737        1       
-------------------------

Table: De_Pr.Axis.Rect


------------------------
 Scat.Ra   Sc.Var.Maxis 
--------- --------------
 -0.8859        1       
------------------------

Table: De_Sc.Var.Maxis


------------------------
 Scat.Ra   Sc.Var.maxis 
--------- --------------
  -5.27         1       
------------------------

Table: De_Sc.Var.maxis


---------------------
 Max.L.Rect   Ra.Gyr 
------------ --------
   -1.989       1    
---------------------

Table: De_Ra.Gyr


----------------------
 Kurt.Maxis   Holl.Ra 
------------ ---------
   -1.071        1    
----------------------

Table: De_Holl.Ra

The Feature Index Score

The FCA analysis of the data features are stored in three attributes: “varincluded”, “topFeatures”, and “fscore”.

  • “varincluded” returns the list of continuous features that were decorrelated

  • “topFeatures” returns the features that at some point were used as independent variables inside the linear models.

  • “fscore” : returns a named vector with the total feature score, \(F_j\), of the analyzed features.

\[ F_j=∑_{n}∑_{i∈B^{n}_{j}}|ρ_{i,j}|^2(|ρ_{i,j}|>ρ_{th}),~ \forall j \in Ind \]

\[ F_j=F_j-∑_{n}|ρ_{B^n_j,j}|^2),~ \forall j \in Dep \]

where \(B^n_j\) is the set of features statistically associated with feature j at iteration n, \(ρ_{i,j}\) is the correlation between features i,j, and \(ρ_{th}\) is the correlation goal, \({Ind, Dep}\) are the set of independent and dependent features respectively. In other words, the “fscore” indicates the degree of total association of”independent” features to dependent features.


pander::pander(t(attr(datasetframeDecor[[1]],"varincluded")),caption="Correlated Features")
Correlated Features (continued below)
Comp Circ D.Circ Scat.Ra Elong Pr.Axis.Rect Max.L.Rect
Sc.Var.Maxis Sc.Var.maxis Ra.Gyr Kurt.Maxis Holl.Ra
pander::pander(t(attr(datasetframeDecor[[1]],"topFeatures")),caption="Independent Features")
Independent Features
Scat.Ra Kurt.Maxis Max.L.Rect
fscore <- attr(datasetframeDecor[[1]],"fscore") 
fscore <- fscore[order(-fscore)];
barplot(fscore,las=2,cex.names = 0.6)

The FCA algorithm will return for unsupervised basis learning the following attribute: “AbaseFeatures


pander::pander(t(attr(datasetframeDecor[[1]],"AbaseFeatures")),caption="Set of unaltered features")
Set of unaltered features
Scat.Ra Kurt.Maxis Pr.Axis.Ra Max.L.Ra Skew.Maxis

The total set of unaltered features is:


atbase <- attr(datasetframeDecor[[1]],"AbaseFeatures")
featnames <- colnames(datasetframe_train)
included <- attr(datasetframeDecor[[1]],"varincluded")

notinvarincluded <-  featnames[!(featnames %in% included)]
pander::pander(t(c(atbase,notinvarincluded)),caption="Set of unaltered features")
Set of unaltered features (continued below)
Scat.Ra Kurt.Maxis Pr.Axis.Ra Max.L.Ra Skew.Maxis Rad.Ra
Pr.Axis.Ra Max.L.Ra Skew.Maxis Skew.maxis Kurt.maxis Class

For supervised basis learning the FCA algorithm will return the following attributes:

  • baseFeatures” and “correlatedToBase

The “baseFeatures” is the set of features that the FRESA.CAD::univariate_correlation() univariate filter function used to get the features associated with the outcome.

# With Supervised Basis

pander::pander(t(attr(datasetframeDecor[[6]],"baseFeatures")),caption="Set of unaltered features")
Set of unaltered features
Holl.Ra D.Circ Comp Max.L.Ra
if (length(attr(datasetframeDecor[[6]],"correlatedToBase"))>0)
{
pander::pander(t(attr(datasetframeDecor[[6]],"correlatedToBase")),caption="Set of features associated with base")
}
Set of features associated with base
Kurt.Maxis
atbaseSup <- attr(datasetframeDecor[[6]],"baseFeatures")
included <- attr(datasetframeDecor[[6]],"varincluded")

notinvarincludedSup <-  featnames[!(featnames %in% included)]
pander::pander(t(c(atbaseSup,notinvarincludedSup)),caption="Set of unaltered features: Supervised")
Set of unaltered features: Supervised (continued below)
Holl.Ra D.Circ Comp Max.L.Ra Rad.Ra Pr.Axis.Ra Max.L.Ra
Skew.Maxis Skew.maxis Kurt.maxis Class
pander::pander(t(c(atbase,notinvarincluded)),caption="Set of unaltered features: Unsupervised")
Set of unaltered features: Unsupervised (continued below)
Scat.Ra Kurt.Maxis Pr.Axis.Ra Max.L.Ra Skew.Maxis Rad.Ra
Pr.Axis.Ra Max.L.Ra Skew.Maxis Skew.maxis Kurt.maxis Class
fscore <- attr(datasetframeDecor[[6]],"fscore") 
fscore <- fscore[order(-fscore)];
barplot(fscore,las=2,cex.names = 0.6,main="Feature Score: Supervised")

Machine Learning and GDSTM

Train a simple NB model on the raw data set

mNBRaw <- filteredFit(paste(Outcome,"~."),
                   datasetframe_train,
                   fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_KS,
                     filtermethod.control=list(pvalue=0.05),
                     Scale="OrderLogit",
                     pca=FALSE
                   )

# With PCA
mNBPCA <- filteredFit(paste(Outcome,"~."),
                   datasetframe_train,
                   fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_KS,
                     filtermethod.control=list(pvalue=0.05),
                     Scale="OrderLogit",
                     pca=TRUE,
                     normalize=FALSE
                   )

Training using the decorrelated data

mNBDecor <- filteredFit(paste(Outcome,"~."),
                   datasetframeDecor[[1]],
                    fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_KS,
                     filtermethod.control=list(pvalue=0.05),
                     Scale="OrderLogit",
                     pca=FALSE
                   )

Selected Raw Features


vnames <- as.data.frame(cbind(mNBRaw$selectedfeatures,mNBRaw$selectedfeatures))
dta <- datasetframe_train;
dta <- FRESAScale(dta,method="OrderLogit")$scaledData
dta$Class <- as.numeric(dta$Class)
hm <- heatMaps(variableList=vnames,
               data=dta,
               Outcome=Outcome,
               hCluster="col",
               srtCol=45,
               xlab="Raw Features",
               ylab="Samples"
               )

Selected decorrelated Features


vnames <- as.data.frame(cbind(mNBDecor$selectedfeatures,mNBDecor$selectedfeatures))
dta <- datasetframeDecor[[1]];
dta <- FRESAScale(dta,method="OrderLogit")$scaledData
dta$Class <- as.numeric(dta$Class)
hm <- heatMaps(variableList=vnames,
               data=dta,
               Outcome=Outcome,
               hCluster="col",
               srtCol=35,
               xlab="Decorrelated Features",
               ylab="Samples"
               )

To make predictions we need to transform the testing set. This is done using the FRESA.CAD::predictDecorrelate() function


# Transform the testing set
decor_test <- predictDecorrelate(datasetframeDecor[[1]],datasetframe_test)

Once we have the transformed testing dataset we can make a side by side comparison of predictions


# Predict the raw testing set
prRAW <- attr(predict(mNBRaw,datasetframe_test),"prob")

# Predict with PCA
prPCA <- attr(predict(mNBPCA,datasetframe_test),"prob")

# Predict the transformed dataset
prDecor <- attr(predict(mNBDecor,decor_test),"prob")


par(mfrow=c(1,3))
meanROCAUC <- numeric(3);
meanPCAROCAUC <- numeric(3);
for (theClass in classNames)
{
  classoutcomes <- 1*(datasetframe_test[,Outcome] == theClass)
  psRaw <- predictionStats_binary(cbind(classoutcomes,prRAW[,theClass]),
                                paste("Raw :",theClass),cex=0.75)
  pander::pander(psRaw$aucs)
  psPCA <- predictionStats_binary(cbind(classoutcomes,prPCA[,theClass]),
                                paste("PCA :",theClass),cex=0.75)
  pander::pander(psPCA$aucs)
  psDecor <- predictionStats_binary(cbind(classoutcomes,prDecor[,theClass]),
                                paste("GDSTM :",theClass),cex=0.75)
  pander::pander(psDecor$aucs)
  meanROCAUC <- meanROCAUC + psRaw$aucs;
  meanPCAROCAUC <- meanPCAROCAUC + psPCA$aucs;
}

Raw : van

est lower upper
0.9666 0.9523 0.9809

PCA : van

est lower upper
0.9706 0.9563 0.9849

GDSTM : van

est lower upper
0.981 0.9715 0.9905

Raw : saab

est lower upper
0.8147 0.7731 0.8562

PCA : saab

est lower upper
0.8417 0.8029 0.8804

GDSTM : saab

est lower upper
0.8481 0.8126 0.8836

Raw : bus

est lower upper
0.9827 0.9734 0.9919

PCA : bus

est lower upper
0.9928 0.9882 0.9975

GDSTM : bus

est lower upper
0.9942 0.9897 0.9987

Raw : opel

est lower upper
0.7927 0.7475 0.8379

PCA : opel

est lower upper
0.8552 0.8157 0.8946

GDSTM : opel

est lower upper
0.8339 0.7954 0.8724

meanROCAUC <- meanROCAUC/length(classNames)
AllRocAUC <- meanROCAUC;
meanPCAROCAUC <- meanPCAROCAUC/length(classNames)
AllRocAUC <- rbind(AllRocAUC,meanPCAROCAUC);

Training and Prediction on all Decorrelations Sets

par(mfrow=c(2,2))

for (i in c(1:length(datasetframeDecor)))
{
  mNBDecor <- filteredFit(paste(Outcome,"~."),
                   datasetframeDecor[[i]],
                    fitmethod=NAIVE_BAYES,
                     filtermethod=univariate_KS,
                     filtermethod.control=list(pvalue=0.05),
                     Scale="OrderLogit",
                     pca=FALSE
                   )

  decor_test <- predictDecorrelate(datasetframeDecor[[i]],datasetframe_test)
  prDecor <- attr(predict(mNBDecor,decor_test),"prob")
  meanROCAUC <- numeric(3);
  for (theClass in classNames)
  {
    classoutcomes <- 1*(datasetframe_test[,Outcome] == theClass)
    psDecor <- predictionStats_binary(cbind(classoutcomes,prDecor[,theClass]),
                                  paste(decortype[[i]],theClass,sep=":"),cex=0.75)
    meanROCAUC <- meanROCAUC + psDecor$aucs;
  }
  meanROCAUC <- meanROCAUC/length(classNames)
  pander::pander(meanROCAUC)
  AllRocAUC <- rbind(AllRocAUC,meanROCAUC)

}

Default:van Default:saab Default:bus Default:opel

est lower upper
0.9143 0.8923 0.9363

AtThr:van

AtThr:saab AtThr:bus AtThr:opel

est lower upper
0.9178 0.8933 0.9417

RLM_Pearson:van

RLM_Pearson:saab RLM_Pearson:bus RLM_Pearson:opel

est lower upper
0.9121 0.8899 0.9343

LM_Spearman:van

LM_Spearman:saab LM_Spearman:bus LM_Spearman:opel

est lower upper
0.9136 0.8919 0.9352

RLM_Spearman:van

RLM_Spearman:saab RLM_Spearman:bus RLM_Spearman:opel

est lower upper
0.912 0.8901 0.9339

Sup_Default:van

Sup_Default:saab Sup_Default:bus Sup_Default:opel

est lower upper
0.9115 0.8889 0.934

Sup_AtThr:van

Sup_AtThr:saab Sup_AtThr:bus Sup_AtThr:opel

est lower upper
0.9178 0.8933 0.9417

Sup_RLM_Pearson:van

Sup_RLM_Pearson:saab Sup_RLM_Pearson:bus Sup_RLM_Pearson:opel

est lower upper
0.9104 0.8879 0.9329

Sup_LM_Spearman:van

Sup_LM_Spearman:saab Sup_LM_Spearman:bus Sup_LM_Spearman:opel

est lower upper
0.914 0.8918 0.9363

Sup_RLM_Spearman:van

Sup_RLM_Spearman:saab Sup_RLM_Spearman:bus Sup_RLM_Spearman:opel

est lower upper
0.9147 0.8933 0.936

Sup_KS_RLM_Spearman:van

Sup_KS_RLM_Spearman:saab Sup_KS_RLM_Spearman:bus Sup_KS_RLM_Spearman:opel

est lower upper
0.9163 0.8949 0.9377

Final Plot Comparing the ROC AUC of all Options

par(mfrow=c(1,1))

rownames(AllRocAUC) <- c("Raw","PCA",unlist(decortype))
pander::pander(AllRocAUC)
  est lower upper
Raw 0.8892 0.8616 0.9167
PCA 0.9151 0.8908 0.9394
Default 0.9143 0.8923 0.9363
AtThr 0.9178 0.8933 0.9417
RLM_Pearson 0.9121 0.8899 0.9343
LM_Spearman 0.9136 0.8919 0.9352
RLM_Spearman 0.912 0.8901 0.9339
Sup_Default 0.9115 0.8889 0.934
Sup_AtThr 0.9178 0.8933 0.9417
Sup_RLM_Pearson 0.9104 0.8879 0.9329
Sup_LM_Spearman 0.914 0.8918 0.9363
Sup_RLM_Spearman 0.9147 0.8933 0.936
Sup_KS_RLM_Spearman 0.9163 0.8949 0.9377
bpROCAUC <- barPlotCiError(as.matrix(AllRocAUC),
                          metricname = "ROCAUC",
                          thesets = "ROC AUC",
                          themethod = rownames(AllRocAUC),
                          main = "ROC AUC",
                          offsets = c(0.5,1),
                          scoreDirection = ">",
                          ho=0.5,
                          args.legend = list(bg = "white",x="bottomright",inset=c(0.0,0),cex=0.75),
                          col = terrain.colors(nrow(AllRocAUC))
                          )

LS0tDQp0aXRsZTogIkZDQSBhbmQgdGhlIEdEU1RNIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KIyMgRkNBLUJhc2VkIERlY29ycmVsYXRpb24gb24gVmVoaWNsZQ0KDQpUaGlzIGRvY3VtZW50IGRlc2NyaWJlcyB0aGUgdXNlIG9mIHRoZSAqKkZSRVNBLkNBRDo6R0RTVE1EZWNvcnJlbGF0aW9uKCkqKiBmdW5jdGlvbiB0aGF0IHJ1bnMgdGhlIGZlYXR1cmUgY29ycmVsYXRpb24gYW5hbHlzaXMgKCoqRkNBKiopIGFsZ29yaXRobS4NCg0KVGhpcyBkZW1vIHVzZXMgRlJFU0EuQ0FEIGFuZCBtbGJlbmNoIFIgcGFja2FnZXM6DQoNCmBgYHtyIGZ1bmN0aW9ucyxlY2hvID0gVFJVRSB9DQprbml0cjo6b3B0c19jaHVuayRzZXQoY29sbGFwc2UgPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSxjb21tZW50ID0gIiM+IikNCg0KbGlicmFyeSgiRlJFU0EuQ0FEIikNCmxpYnJhcnkobWxiZW5jaCkNCg0Kb3AgPC0gcGFyKG5vLnJlYWRvbmx5ID0gVFJVRSkNCg0KYGBgDQoNCkknbGwgbG9hZCB0aGUgdmVoaWNsZSBkYXRhIHNldA0KDQpgYGB7cn0NCmRhdGEoIlZlaGljbGUiLCBwYWNrYWdlID0gIm1sYmVuY2giKQ0KcHJpbnQodGFibGUoVmVoaWNsZSRDbGFzcykpDQpgYGANCg0KU2V0dGluZyBzb21lIHZhcmlhYmxlcyBmb3IgZG93bnN0cmVhbSBhbmFseXNpcw0KDQpgYGB7cn0NCnN0dWR5TmFtZSA9ICJWZWhpY2xlIg0KZGF0YXNldGZyYW1lIDwtIFZlaGljbGUNCk91dGNvbWUgPC0gIkNsYXNzIg0KDQojIFRoZSBmcmFjdGlvbnMgb2Ygc2FtcGxlcyB0byBiZSB1c2UgaW4gdGhlIHRyYWluaW5nIHNldA0KdHJhaW5GcmFjdGlvbiA9IDAuNQ0KDQojIFRoZSBjb3JyZWxhdGlvbiB0aHJlc2hvbGQgDQpjb3JyZWxhdGlvblRocmVzaG9sZCA9IDAuNg0KDQpgYGANCg0KU2V0dGluZyB0aGUgVHJhaW5pbmcgYW5kIFRlc3Rpbmcgc2V0cw0KDQpgYGB7ciwgcmVzdWx0cyA9ICJhc2lzIiwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KdGIgPC0gdGFibGUoZGF0YXNldGZyYW1lWyxPdXRjb21lXSkNCmNsYXNzTmFtZXMgPC0gdW5pcXVlKGRhdGFzZXRmcmFtZVssT3V0Y29tZV0pDQoNCmFsbHJvd0NsYXNzIDwtIGRhdGFzZXRmcmFtZVssT3V0Y29tZV0NCm5hbWVzKGFsbHJvd0NsYXNzKSA8LSByb3duYW1lcyhkYXRhc2V0ZnJhbWUpDQoNCnRyYWluc2l6ZSA8LSB0cmFpbkZyYWN0aW9uKm1pbih0Yik7DQp0cmFpblNhbXBsZXMgPC0gTlVMTDsNCmZvciAodGhlQ2xhc3MgaW4gY2xhc3NOYW1lcykNCnsNCiAgY2xhc3NTYW1wbGUgPC0gYWxscm93Q2xhc3NbYWxscm93Q2xhc3MgPT0gdGhlQ2xhc3NdDQogIHRyYWluU2FtcGxlcyA8LSBjKHRyYWluU2FtcGxlcyxuYW1lcyhjbGFzc1NhbXBsZVtzYW1wbGUobGVuZ3RoKGNsYXNzU2FtcGxlKSx0cmFpbnNpemUpXSkpDQp9DQoNCg0KZGF0YXNldGZyYW1lX3RyYWluIDwtIGRhdGFzZXRmcmFtZVt0cmFpblNhbXBsZXMsXQ0KdGVzdFNhbXBsZXMgPC0gIShyb3duYW1lcyhkYXRhc2V0ZnJhbWUpICVpbiUgdHJhaW5TYW1wbGVzKQ0KZGF0YXNldGZyYW1lX3Rlc3QgPC0gZGF0YXNldGZyYW1lW3Rlc3RTYW1wbGVzLF0NCg0Kb3V0Y29tZXMgPC0gZGF0YXNldGZyYW1lX3RyYWluWyxPdXRjb21lXQ0KDQpwYW5kZXI6OnBhbmRlcih0YWJsZShkYXRhc2V0ZnJhbWVbLE91dGNvbWVdKSxjYXB0aW9uPSJBbGwiKQ0KcGFuZGVyOjpwYW5kZXIodGFibGUoZGF0YXNldGZyYW1lX3RyYWluWyxPdXRjb21lXSksY2FwdGlvbj0iVHJhaW5pbmciKQ0KcGFuZGVyOjpwYW5kZXIodGFibGUoZGF0YXNldGZyYW1lX3Rlc3RbLE91dGNvbWVdKSxjYXB0aW9uPSJUZXN0aW5nIikNCg0KDQpgYGANCg0KIyMgRkNBIHdpdGggRGVmYXVsdCBQYXJhbWV0ZXJzDQoNClRoZSBkZWZhdWx0IHBhcmFtZXRlcnMgd2lsbCBjb21wdXRlIHRoZSB0cmFuc2Zvcm1hdGlvbiBtYXRyaXggd2l0aCBhIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCBvZiAwLjggdXNpbmcgZmFzdCBtYXRyaXggbXVsdGlwbGljYXRpb24gd2l0aCBQZWFyc29uIGNvcnJlbGF0aW9uIGFuZCBsaW5lYXIgbW9kZWxzIGVzdGltYXRpb24uDQoNCkRlZmF1bHQgUGFyYW1ldGVyczogdGhyPTAuODAsbWV0aG9kPSJmYXN0Iix0eXBlPSJMTSINCg0KYGBge3IsIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KDQpkYXRhc2V0ZnJhbWVEZWNvcjwtbGlzdCgpOw0KZGVjb3J0eXBlIDwtIGxpc3QoKTsNCnN5c3RlbS50aW1lKGRhdGFzZXRmcmFtZURlY29yW1sxXV0gPC0gR0RTVE1EZWNvcnJlbGF0aW9uKGRhdGFzZXRmcmFtZV90cmFpbikpDQpkZWNvcnR5cGVbWzFdXSA8LSAiRGVmYXVsdCINCg0KDQpgYGANCg0KIyMjIFRlc3RpbmcgU2V0IERlY29ycmVsYXRpb24NCg0KVG8gZGVjb3JyZWxhdGUgdGhlIHRlc3Rpbmcgc2V0IHVzZSB0aGUgKipwcmVkaWN0RGVjb3JyZWxhdGUoKSoqIGZ1bmN0aW9uDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCmRlY29yX3Rlc3QgPC0gcHJlZGljdERlY29ycmVsYXRlKGRhdGFzZXRmcmFtZURlY29yW1sxXV0sZGF0YXNldGZyYW1lX3Rlc3QpDQoNCmBgYA0KDQojIyBIZWF0IE1hcHMgb2YgdGhlIENvcnJlbGF0aW9uIE1hdHJpY2VzDQoNCkhlcmUgYXJlIHRoZSBoZWF0IG1hcHMgb2YgdGhlIGNvcnJlbGF0aW9uIG1hdHJpY2VzIGJlZm9yZSBhbmQgYWZ0ZXIgZGVjb3JyZWxhdGlvbiBvbiB0aGUgdGVzdGluZyBzZXQNCg0KYGBge3IgcmVzdWx0cyA9ICJhc2lzIiwgd2FybmluZyA9IEZBTFNFLCBkcGk9NjAwLCBmaWcuaGVpZ2h0PSA2LjAsIGZpZy53aWR0aD0gOC4wfQ0KDQoNCmZlYXRuYW1lcyA8LSBhdHRyKGRhdGFzZXRmcmFtZURlY29yW1sxXV0sInZhcmluY2x1ZGVkIikNCmNvcm1hdCA8LSBjb3IoZGF0YXNldGZyYW1lX3Rlc3RbLGZlYXRuYW1lc10sbWV0aG9kPSJwZWFyc29uIikNCmdwbG90czo6aGVhdG1hcC4yKGFicyhjb3JtYXQpLA0KICAgICAgICAgICAgICAgICAgdHJhY2UgPSAibm9uZSIsDQogICAgICAgICAgICAgICAgICBzY2FsZSA9ICJub25lIiwNCiAgICAgICAgICAgICAgICAgIG1hciA9IGMoMTAsMTApLA0KICAgICAgICAgICAgICAgICAgY29sPXJldihoZWF0LmNvbG9ycyg1KSksDQogICAgICAgICAgICAgICAgICBtYWluID0gcGFzdGUoIlJhdyBDb3JyZWxhdGlvbjoiLHN0dWR5TmFtZSksDQogICAgICAgICAgICAgICAgICBjZXhSb3cgPSAwLjc1LA0KICAgICAgICAgICAgICAgICAgY2V4Q29sID0gMC43NSwNCiAgICAgICAgICAgICAgICAgIGtleS50aXRsZT1OQSwNCiAgICAgICAgICAgICAgICAgIGtleS54bGFiPSJQZWFyc29uIENvcnJlbGF0aW9uIiwNCiAgICAgICAgICAgICAgICAgIHhsYWI9IkZlYXR1cmUiLCB5bGFiPSJGZWF0dXJlIikNCg0KDQpmZWF0bmFtZXMgPC0gY29sbmFtZXMoYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLCJHRFNUTSIpKQ0KY29ybWF0IDwtIGNvcihkZWNvcl90ZXN0WyxmZWF0bmFtZXNdLG1ldGhvZD0icGVhcnNvbiIpDQpncGxvdHM6OmhlYXRtYXAuMihhYnMoY29ybWF0KSwNCiAgICAgICAgICAgICAgICAgIHRyYWNlID0gIm5vbmUiLA0KICAgICAgICAgICAgICAgICAgc2NhbGUgPSAibm9uZSIsDQogICAgICAgICAgICAgICAgICBtYXIgPSBjKDEwLDEwKSwNCiAgICAgICAgICAgICAgICAgIGNvbD1yZXYoaGVhdC5jb2xvcnMoNSkpLA0KICAgICAgICAgICAgICAgICAgbWFpbiA9IHBhc3RlKCJBZnRlciBkZWNvcnJlbGF0aW9uOiIsc3R1ZHlOYW1lKSwNCiAgICAgICAgICAgICAgICAgIGNleFJvdyA9IDAuNzUsDQogICAgICAgICAgICAgICAgICBjZXhDb2wgPSAwLjc1LA0KICAgICAgICAgICAgICAgICAga2V5LnRpdGxlPU5BLA0KICAgICAgICAgICAgICAgICAga2V5LnhsYWI9IlBlYXJzb24gQ29ycmVsYXRpb24iLA0KICAgICAgICAgICAgICAgICAgeGxhYj0iVHJhbnNmb3JtZWQgRmVhdHVyZSIsIHlsYWI9IlRyYW5zZm9ybWVkIEZlYXR1cmUiKQ0KDQoNCmBgYA0KDQojIyBGQ0Egd2l0aCBPcHRpb25zDQoNClRoZSBmb2xsb3dpbmcgY29kZSBhcmUgZXhhbXBsZXMgb2YgcnVubmluZyAqKkdEU1RNRGVjb3JyZWxhdGlvbigpKiogZnVuY3Rpb24gd2l0aCBvcHRpb25zOg0KDQpgYGB7ciwgcmVzdWx0cyA9ICJhc2lzIiwgd2FybmluZyA9IEZBTFNFLCBkcGk9NjAwLCBmaWcuaGVpZ2h0PSA2LjAsIGZpZy53aWR0aD0gOC4wfQ0KDQoNCiMgQ2hhbmdlIHRoZSBtYXhpbXVtIGNvcnJlbGF0aW9uIGdvYWwNCg0KZGVjb3J0eXBlW1syXV0gPC0gIkF0VGhyIg0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzJdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgdGhyID0gY29ycmVsYXRpb25UaHJlc2hvbGQNCiAgKSkNCg0KIyBDaGFuZ2UgdGhlIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCwgYW5kIHNldCB0byBSb2J1c3QgTGluZXIgTW9kZWxzDQpkZWNvcnR5cGVbWzNdXSA8LSAiUkxNX1BlYXJzb24iDQpzeXN0ZW0udGltZShkYXRhc2V0ZnJhbWVEZWNvcltbM11dIDwtIEdEU1RNRGVjb3JyZWxhdGlvbigNCiAgZGF0YXNldGZyYW1lX3RyYWluLA0KICB0eXBlPSJSTE0iLA0KICBtZXRob2Q9InBlYXJzb24iKSkNCg0KDQojIENoYW5nZSB0aGUgbWF4aW11bSBjb3JyZWxhdGlvbiBnb2FsLCBhbmQgY2hhbmdlIHRvIFNwZWFybWFuIGNvcnJlbGF0aW9uDQpkZWNvcnR5cGVbWzRdXSA8LSAiTE1fU3BlYXJtYW4iDQpzeXN0ZW0udGltZShkYXRhc2V0ZnJhbWVEZWNvcltbNF1dIDwtIEdEU1RNRGVjb3JyZWxhdGlvbigNCiAgZGF0YXNldGZyYW1lX3RyYWluLA0KICB0eXBlPSJMTSIsDQogIG1ldGhvZD0ic3BlYXJtYW4iKSkNCg0KIyBDaGFuZ2UgdGhlIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCwgYW5kIHNldCBTcGVhcm1hbiBjb3JyZWxhdGlvbiB3aXRoIHJvYnVzdCBsaW5lciBtb2RlbA0KZGVjb3J0eXBlW1s1XV0gPC0gIlJMTV9TcGVhcm1hbiINCnN5c3RlbS50aW1lKGRhdGFzZXRmcmFtZURlY29yW1s1XV0gPC0gR0RTVE1EZWNvcnJlbGF0aW9uKA0KICBkYXRhc2V0ZnJhbWVfdHJhaW4sDQogIHR5cGU9IlJMTSIsDQogIG1ldGhvZD0ic3BlYXJtYW4iKSkNCg0KDQojIFRoZSBmb2xsb3dpbmcgYXJlIGZvciBzdXBlcnZpc2VkIGJhc2lzIGxlYXJuaW5nDQoNCiMgU2V0IHRoZSB0YXJnZXQgY2xhc3MgZm9yIGFzc29jaWF0aW9uIGxlYXJuaW5nDQpkZWNvcnR5cGVbWzZdXSA8LSAiU3VwX0RlZmF1bHQiDQpzeXN0ZW0udGltZShkYXRhc2V0ZnJhbWVEZWNvcltbNl1dIDwtIEdEU1RNRGVjb3JyZWxhdGlvbigNCiAgZGF0YXNldGZyYW1lX3RyYWluLA0KICBPdXRjb21lPU91dGNvbWUpKQ0KDQoNCiMgQ2hhbmdlIHRoZSBtYXhpbXVtIGNvcnJlbGF0aW9uIGdvYWwNCmRlY29ydHlwZVtbN11dIDwtICJTdXBfQXRUaHIiDQpzeXN0ZW0udGltZShkYXRhc2V0ZnJhbWVEZWNvcltbN11dIDwtIEdEU1RNRGVjb3JyZWxhdGlvbigNCiAgZGF0YXNldGZyYW1lX3RyYWluLA0KICB0aHIgPSBjb3JyZWxhdGlvblRocmVzaG9sZCwNCiAgT3V0Y29tZT1PdXRjb21lKSkNCg0KIyBDaGFuZ2UgdGhlIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCwgYW5kIHNldCB0byBSb2J1c3QgTGluZXIgTW9kZWxzDQpkZWNvcnR5cGVbWzhdXSA8LSAiU3VwX1JMTV9QZWFyc29uIg0Kc3lzdGVtLnRpbWUoZGF0YXNldGZyYW1lRGVjb3JbWzhdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgT3V0Y29tZT1PdXRjb21lLA0KICB0eXBlPSJSTE0iLA0KICBtZXRob2Q9InBlYXJzb24iKSkNCg0KIyBDaGFuZ2UgdGhlIG1heGltdW0gY29ycmVsYXRpb24gZ29hbCwgYW5kIGNoYW5nZSB0byBTcGVhcm1hbiBjb3JyZWxhdGlvbg0KZGVjb3J0eXBlW1s5XV0gPC0gIlN1cF9MTV9TcGVhcm1hbiINCnN5c3RlbS50aW1lKGRhdGFzZXRmcmFtZURlY29yW1s5XV0gPC0gR0RTVE1EZWNvcnJlbGF0aW9uKA0KICBkYXRhc2V0ZnJhbWVfdHJhaW4sDQogIE91dGNvbWU9T3V0Y29tZSwNCiAgdHlwZT0iTE0iLA0KICBtZXRob2Q9InNwZWFybWFuIikpDQoNCiMgQ2hhbmdlIHRoZSBtYXhpbXVtIGNvcnJlbGF0aW9uIGdvYWwsIGFuZCBzZXQgdG8gU3BlYXJtYW4gY29ycmVsYXRpb24gd2l0aCByb2J1c3QgbGluZXIgbW9kZWwNCmRlY29ydHlwZVtbMTBdXSA8LSAiU3VwX1JMTV9TcGVhcm1hbiINCnN5c3RlbS50aW1lKGRhdGFzZXRmcmFtZURlY29yW1sxMF1dIDwtIEdEU1RNRGVjb3JyZWxhdGlvbigNCiAgZGF0YXNldGZyYW1lX3RyYWluLA0KICBPdXRjb21lPU91dGNvbWUsDQogIHR5cGU9IlJMTSIsDQogIG1ldGhvZD0ic3BlYXJtYW4iKSkNCg0KDQojIFdpdGggdXNlciBkZWZpbmVkIHN1cGVydmlzZWQgYmFzaXMgDQpkZWNvcnR5cGVbWzExXV0gPC0gIlN1cF9LU19STE1fU3BlYXJtYW4iDQpiYXNlS1MgPC0gbmFtZXModW5pdmFyaWF0ZV9LUyhkYXRhc2V0ZnJhbWVfdHJhaW4sDQogICAgICAgICAgICAgICAgICAgICAgICBPdXRjb21lPU91dGNvbWUsDQogICAgICAgICAgICAgICAgICAgICAgICBwdmFsdWU9MC4yMCwNCiAgICAgICAgICAgICAgICAgICAgICAgIGxpbWl0PTAsDQogICAgICAgICAgICAgICAgICAgICAgICB0aHI9Y29ycmVsYXRpb25UaHJlc2hvbGQpKQ0KDQpzeXN0ZW0udGltZShkYXRhc2V0ZnJhbWVEZWNvcltbMTFdXSA8LSBHRFNUTURlY29ycmVsYXRpb24oDQogIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgT3V0Y29tZT1PdXRjb21lLA0KICBiYXNlRmVhdHVyZXM9YmFzZUtTLA0KICB0eXBlPSJSTE0iLA0KICBtZXRob2Q9InNwZWFybWFuIikpDQoNCg0KYGBgDQoNClRoZSAqKkdEU1RNRGVjb3JyZWxhdGlvbioqIGZ1bmN0aW9uIHJldHVybnMgYSBkYXRhIGZyYW1lIHdpdGggdGhlIGZvbGxvd2luZyBjb2x1bW4gbmFtZXM6DQoNClRoZSBvdXRwdXQgZmVhdHVyZXMgYWZ0ZXIgdHJhbnNmb3JtYXRpb24gd2lsbCBiZSBuYW1lZCBhZnRlciB0aGUgb3JpZ2luYWwgbmFtZXMgYW5kOg0KDQotICAgVGhlIG5hbWUgd2lsbCBiZSB1bmFsdGVyZWQgaWYgdGhlaXIgbWF4aW11bSBjb3JyZWxhdGlvbiB0byBvdGhlciBmZWF0dXJlcyB3YXMgbG93ZXIgdGhhbiB0aGUgdGhyZXNob2xkLg0KDQotICAgVGhlIG5hbWUgd2lsbCBoYXZlIHRoZSAiQmFcXyIgcHJlZml4IGlzIHRoZSBmZWF0dXJlIHdhcyBjb3JyZWxhdGVkIGJ1dCB1c2VkIGFzIHVuYWx0ZXJlZCBiYXNpcw0KDQotICAgVGhlIG5hbWUgd2lsbCBoYXZlIHRoZSAiRGVcXyIgcHJlZml4IGlzIHRoZSBmZWF0dXJlIHdhcyBvcmlnaW5hbCBjb3JyZWxhdGVkIGFuZCBpdHMgY29ycmVsYXRpb24gdG8gIkJhXF8iIGZlYXR1cmVzIGhhcyBiZWVuIHJlbW92ZWQuDQoNCkZ1cnRoZXJtb3JlLCB0aGUgcmV0dXJuZWQgZGF0YSBmcmFtZSB3aWxsIGhhdmUgdGhlIGZvbGxvd2luZyBhdHRyaWJ1dGVzOg0KDQoxKSAgVHJhbnNmb3JtYXRpb24gbWF0cml4OiAiKkdEU1RNKiINCg0KMikgIEZlYXR1cmVzOg0KDQogICAgMS4gICIqZnNvY3JlKiINCg0KICAgIDIuICAiKnZhcmluY2x1ZGVkKiINCg0KICAgIDMuICAiKnRvcEZlYXR1cmVzKiINCg0KMykgIFVuYWx0ZXJlZCBCYXNpczoNCg0KICAgIDEuICAiKmJhc2VGZWF0dXJlcyoiDQoNCiAgICAgICAgMS4gICIqY29ycmVsYXRlZFRvQmFzZSoiDQoNCiAgICAyLiAgIipBYmFzZUZlYXR1cmVzKiINCg0KIyMjIEdEU1RNDQoNClRoZSAiKkdEU1RNKioqIioqIGF0dHJpYnV0ZSBzdG9yZXMgdGhlIHNwYXRpYWwgdHJhbnNmb3JtYXRpb24gbWF0cml4LiBUaGUgbWF0cml4IG9ubHkgaW5jbHVkZXMgY29udGludW91cyBmZWF0dXJlcyB0aGF0IGhhZCBzb21lIGNvcnJlbGF0aW9uIGdyZWF0ZXIgdGhhbiB0aGUgdGhyZXNob2xkDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KDQojIyBUaGUgU3BhdGlhbCBUcmFuc2Zvcm1hdGlvbiBNYXRyaXg6DQpHRFNUTSA8LSBhdHRyKGRhdGFzZXRmcmFtZURlY29yW1sxXV0sIkdEU1RNIikNCg0KIyMgVGhlIGhlYXRtYXAgb2YgdGhlIG1hdHJpeA0KZ3Bsb3RzOjpoZWF0bWFwLjIoMSooYWJzKEdEU1RNKSA+IDApLA0KICAgICAgICAgICAgICAgICAgdHJhY2UgPSAibm9uZSIsDQogICAgICAgICAgICAgICAgICBtYXIgPSBjKDEwLDEwKSwNCiAgICAgICAgICAgICAgICAgIGNvbD1yZXYoaGVhdC5jb2xvcnMoMikpLA0KICAgICAgICAgICAgICAgICAgbWFpbiA9IHBhc3RlKCJHRFNUTSBNYXRyaXg6IixzdHVkeU5hbWUpLA0KICAgICAgICAgICAgICAgICAgY2V4Um93ID0gMC43LA0KICAgICAgICAgICAgICAgICAgY2V4Q29sID0gMC43LA0KICAgICAgICAgICAgICAgICAgYnJlYWtzID0gYygwLDAuNSwxKSwNCiAgICAgICAgICAgICAgICAgIGtleS50aXRsZT1OQSwNCiAgICAgICAgICAgICAgICAgIGtleS54bGFiPSJ8YmV0YXwgPiAwIiwNCiAgICAgICAgICAgICAgICAgIHhsYWI9IkdEU1RNIEZlYXR1cmUiLCB5bGFiPSJJbnB1dCBGZWF0dXJlIikNCg0KcGFuZGVyOjpwYW5kZXIodChjb2xuYW1lcyhHRFNUTSkpLGNhcHRpb249Ik5ldyBuYW1lcyBvZiBkZWNvcnJlbGF0ZWQgbWF0cml4IikNCg0KDQpgYGANCg0KIyMjIFRoZSBDb2VmZmljaWVudHMgcGVyIGVhY2ggRGVjb3JyZWxhdGVkIEZlYXR1cmUNCg0KVGhlIGZvbGxvd2luZyBjb2RlIGV4dHJhY3RzIHRoZSBkZXBlbmRlbmNlIGZvcm11bGEgY29lZmZpY2llbnRzIG9mIGVhY2ggbmV3IGZlYXR1cmUgdG8gdGhlIG9yaWdpbmFsIGZlYXR1cmVzLg0KDQpgYGB7cn0NCg0KbmFtZWNvbCA8LSBjb2xuYW1lcyhHRFNUTSk7DQoNCmZvciAoaSBpbiAxOm5jb2woR0RTVE0pKQ0Kew0KICBhc3NvY2kgPC0gYWJzKEdEU1RNWyxpXSk+MDsNCiAgaWYgKHN1bShhc3NvY2kpID4gMSkNCiAgew0KICAgIHBhbmRlcjo6cGFuZGVyKHQoR0RTVE1bYXNzb2NpLGldKSxjYXB0aW9uPW5hbWVjb2xbaV0pDQogIH0NCn0NCg0KDQpgYGANCg0KIyMjIFRoZSBGZWF0dXJlIEluZGV4IFNjb3JlDQoNClRoZSAqKkZDQSoqIGFuYWx5c2lzIG9mIHRoZSBkYXRhIGZlYXR1cmVzIGFyZSBzdG9yZWQgaW4gdGhyZWUgYXR0cmlidXRlczogIip2YXJpbmNsdWRlZCoiLCAiKnRvcEZlYXR1cmVzKiIsIGFuZCAiKmZzY29yZSoiLg0KDQotICAgInZhcmluY2x1ZGVkIiByZXR1cm5zIHRoZSBsaXN0IG9mIGNvbnRpbnVvdXMgZmVhdHVyZXMgdGhhdCB3ZXJlIGRlY29ycmVsYXRlZA0KDQotICAgInRvcEZlYXR1cmVzIiByZXR1cm5zIHRoZSBmZWF0dXJlcyB0aGF0IGF0IHNvbWUgcG9pbnQgd2VyZSB1c2VkIGFzIGluZGVwZW5kZW50IHZhcmlhYmxlcyBpbnNpZGUgdGhlIGxpbmVhciBtb2RlbHMuDQoNCi0gICAiZnNjb3JlIiA6IHJldHVybnMgYSBuYW1lZCB2ZWN0b3Igd2l0aCB0aGUgdG90YWwgZmVhdHVyZSBzY29yZSwgJEZfaiQsIG9mIHRoZSBhbmFseXplZCBmZWF0dXJlcy4NCg0KJCQNCkZfaj3iiJFfe2594oiRX3tp4oiIQl57bn1fe2p9fXzPgV97aSxqfXxeMih8z4Ffe2ksan18Ps+BX3t0aH0pLH4gXGZvcmFsbCBqIFxpbiBJbmQNCiQkDQoNCiQkDQpGX2o9Rl9qLeKIkV97bn18z4Ffe0Jebl9qLGp9fF4yKSx+IFxmb3JhbGwgaiBcaW4gRGVwDQokJA0KDQp3aGVyZSAkQl5uX2okIGlzIHRoZSBzZXQgb2YgZmVhdHVyZXMgc3RhdGlzdGljYWxseSBhc3NvY2lhdGVkIHdpdGggZmVhdHVyZSAqaiogYXQgaXRlcmF0aW9uICpuLCogJM+BX3tpLGp9JCBpcyB0aGUgY29ycmVsYXRpb24gYmV0d2VlbiBmZWF0dXJlcyAqaSxqKiwgYW5kICTPgV97dGh9JCBpcyB0aGUgY29ycmVsYXRpb24gZ29hbCwgJHtJbmQsIERlcH0kIGFyZSB0aGUgc2V0IG9mIGluZGVwZW5kZW50IGFuZCBkZXBlbmRlbnQgZmVhdHVyZXMgcmVzcGVjdGl2ZWx5LiBJbiBvdGhlciB3b3JkcywgdGhlICIqZnNjb3JlIiogaW5kaWNhdGVzIHRoZSBkZWdyZWUgb2YgdG90YWwgYXNzb2NpYXRpb24gb2YgImluZGVwZW5kZW50IiBmZWF0dXJlcyB0byBkZXBlbmRlbnQgZmVhdHVyZXMuDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KcGFuZGVyOjpwYW5kZXIodChhdHRyKGRhdGFzZXRmcmFtZURlY29yW1sxXV0sInZhcmluY2x1ZGVkIikpLGNhcHRpb249IkNvcnJlbGF0ZWQgRmVhdHVyZXMiKQ0KDQpwYW5kZXI6OnBhbmRlcih0KGF0dHIoZGF0YXNldGZyYW1lRGVjb3JbWzFdXSwidG9wRmVhdHVyZXMiKSksY2FwdGlvbj0iSW5kZXBlbmRlbnQgRmVhdHVyZXMiKQ0KDQpmc2NvcmUgPC0gYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLCJmc2NvcmUiKSANCmZzY29yZSA8LSBmc2NvcmVbb3JkZXIoLWZzY29yZSldOw0KYmFycGxvdChmc2NvcmUsbGFzPTIsY2V4Lm5hbWVzID0gMC42KQ0KDQpgYGANCg0KVGhlIEZDQSBhbGdvcml0aG0gd2lsbCByZXR1cm4gZm9yIHVuc3VwZXJ2aXNlZCBiYXNpcyBsZWFybmluZyB0aGUgZm9sbG93aW5nIGF0dHJpYnV0ZTogIipBYmFzZUZlYXR1cmVzKiINCg0KYGBge3IgcmVzdWx0cyA9ICJhc2lzIiwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KcGFuZGVyOjpwYW5kZXIodChhdHRyKGRhdGFzZXRmcmFtZURlY29yW1sxXV0sIkFiYXNlRmVhdHVyZXMiKSksY2FwdGlvbj0iU2V0IG9mIHVuYWx0ZXJlZCBmZWF0dXJlcyIpDQoNCg0KYGBgDQoNClRoZSB0b3RhbCBzZXQgb2YgdW5hbHRlcmVkIGZlYXR1cmVzIGlzOg0KDQpgYGB7ciByZXN1bHRzID0gImFzaXMiLCB3YXJuaW5nID0gRkFMU0UsIGRwaT02MDAsIGZpZy5oZWlnaHQ9IDYuMCwgZmlnLndpZHRoPSA4LjB9DQoNCmF0YmFzZSA8LSBhdHRyKGRhdGFzZXRmcmFtZURlY29yW1sxXV0sIkFiYXNlRmVhdHVyZXMiKQ0KZmVhdG5hbWVzIDwtIGNvbG5hbWVzKGRhdGFzZXRmcmFtZV90cmFpbikNCmluY2x1ZGVkIDwtIGF0dHIoZGF0YXNldGZyYW1lRGVjb3JbWzFdXSwidmFyaW5jbHVkZWQiKQ0KDQpub3RpbnZhcmluY2x1ZGVkIDwtICBmZWF0bmFtZXNbIShmZWF0bmFtZXMgJWluJSBpbmNsdWRlZCldDQpwYW5kZXI6OnBhbmRlcih0KGMoYXRiYXNlLG5vdGludmFyaW5jbHVkZWQpKSxjYXB0aW9uPSJTZXQgb2YgdW5hbHRlcmVkIGZlYXR1cmVzIikNCg0KYGBgDQoNCkZvciBzdXBlcnZpc2VkIGJhc2lzIGxlYXJuaW5nIHRoZSBGQ0EgYWxnb3JpdGhtIHdpbGwgcmV0dXJuIHRoZSBmb2xsb3dpbmcgYXR0cmlidXRlczoNCg0KLSAgICIqYmFzZUZlYXR1cmVzKiIgYW5kICIqY29ycmVsYXRlZFRvQmFzZSoiDQoNClRoZSAiKmJhc2VGZWF0dXJlcyoiIGlzIHRoZSBzZXQgb2YgZmVhdHVyZXMgdGhhdCB0aGUgKipGUkVTQS5DQUQ6OnVuaXZhcmlhdGVfY29ycmVsYXRpb24oKSoqIHVuaXZhcmlhdGUgZmlsdGVyIGZ1bmN0aW9uIHVzZWQgdG8gZ2V0IHRoZSBmZWF0dXJlcyBhc3NvY2lhdGVkIHdpdGggdGhlIG91dGNvbWUuDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCiMgV2l0aCBTdXBlcnZpc2VkIEJhc2lzDQoNCnBhbmRlcjo6cGFuZGVyKHQoYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbNl1dLCJiYXNlRmVhdHVyZXMiKSksY2FwdGlvbj0iU2V0IG9mIHVuYWx0ZXJlZCBmZWF0dXJlcyIpDQoNCmlmIChsZW5ndGgoYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbNl1dLCJjb3JyZWxhdGVkVG9CYXNlIikpPjApDQp7DQpwYW5kZXI6OnBhbmRlcih0KGF0dHIoZGF0YXNldGZyYW1lRGVjb3JbWzZdXSwiY29ycmVsYXRlZFRvQmFzZSIpKSxjYXB0aW9uPSJTZXQgb2YgZmVhdHVyZXMgYXNzb2NpYXRlZCB3aXRoIGJhc2UiKQ0KfQ0KDQphdGJhc2VTdXAgPC0gYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbNl1dLCJiYXNlRmVhdHVyZXMiKQ0KaW5jbHVkZWQgPC0gYXR0cihkYXRhc2V0ZnJhbWVEZWNvcltbNl1dLCJ2YXJpbmNsdWRlZCIpDQoNCm5vdGludmFyaW5jbHVkZWRTdXAgPC0gIGZlYXRuYW1lc1shKGZlYXRuYW1lcyAlaW4lIGluY2x1ZGVkKV0NCnBhbmRlcjo6cGFuZGVyKHQoYyhhdGJhc2VTdXAsbm90aW52YXJpbmNsdWRlZFN1cCkpLGNhcHRpb249IlNldCBvZiB1bmFsdGVyZWQgZmVhdHVyZXM6IFN1cGVydmlzZWQiKQ0KDQpwYW5kZXI6OnBhbmRlcih0KGMoYXRiYXNlLG5vdGludmFyaW5jbHVkZWQpKSxjYXB0aW9uPSJTZXQgb2YgdW5hbHRlcmVkIGZlYXR1cmVzOiBVbnN1cGVydmlzZWQiKQ0KDQoNCmZzY29yZSA8LSBhdHRyKGRhdGFzZXRmcmFtZURlY29yW1s2XV0sImZzY29yZSIpIA0KZnNjb3JlIDwtIGZzY29yZVtvcmRlcigtZnNjb3JlKV07DQpiYXJwbG90KGZzY29yZSxsYXM9MixjZXgubmFtZXMgPSAwLjYsbWFpbj0iRmVhdHVyZSBTY29yZTogU3VwZXJ2aXNlZCIpDQoNCmBgYA0KDQojIyBNYWNoaW5lIExlYXJuaW5nIGFuZCBHRFNUTQ0KDQpUcmFpbiBhIHNpbXBsZSBOQiBtb2RlbCBvbiB0aGUgcmF3IGRhdGEgc2V0DQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCm1OQlJhdyA8LSBmaWx0ZXJlZEZpdChwYXN0ZShPdXRjb21lLCJ+LiIpLA0KICAgICAgICAgICAgICAgICAgIGRhdGFzZXRmcmFtZV90cmFpbiwNCiAgICAgICAgICAgICAgICAgICBmaXRtZXRob2Q9TkFJVkVfQkFZRVMsDQogICAgICAgICAgICAgICAgICAgICBmaWx0ZXJtZXRob2Q9dW5pdmFyaWF0ZV9LUywNCiAgICAgICAgICAgICAgICAgICAgIGZpbHRlcm1ldGhvZC5jb250cm9sPWxpc3QocHZhbHVlPTAuMDUpLA0KICAgICAgICAgICAgICAgICAgICAgU2NhbGU9Ik9yZGVyTG9naXQiLA0KICAgICAgICAgICAgICAgICAgICAgcGNhPUZBTFNFDQogICAgICAgICAgICAgICAgICAgKQ0KDQojIFdpdGggUENBDQptTkJQQ0EgPC0gZmlsdGVyZWRGaXQocGFzdGUoT3V0Y29tZSwifi4iKSwNCiAgICAgICAgICAgICAgICAgICBkYXRhc2V0ZnJhbWVfdHJhaW4sDQogICAgICAgICAgICAgICAgICAgZml0bWV0aG9kPU5BSVZFX0JBWUVTLA0KICAgICAgICAgICAgICAgICAgICAgZmlsdGVybWV0aG9kPXVuaXZhcmlhdGVfS1MsDQogICAgICAgICAgICAgICAgICAgICBmaWx0ZXJtZXRob2QuY29udHJvbD1saXN0KHB2YWx1ZT0wLjA1KSwNCiAgICAgICAgICAgICAgICAgICAgIFNjYWxlPSJPcmRlckxvZ2l0IiwNCiAgICAgICAgICAgICAgICAgICAgIHBjYT1UUlVFLA0KICAgICAgICAgICAgICAgICAgICAgbm9ybWFsaXplPUZBTFNFDQogICAgICAgICAgICAgICAgICAgKQ0KDQpgYGANCg0KVHJhaW5pbmcgdXNpbmcgdGhlIGRlY29ycmVsYXRlZCBkYXRhDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCm1OQkRlY29yIDwtIGZpbHRlcmVkRml0KHBhc3RlKE91dGNvbWUsIn4uIiksDQogICAgICAgICAgICAgICAgICAgZGF0YXNldGZyYW1lRGVjb3JbWzFdXSwNCiAgICAgICAgICAgICAgICAgICAgZml0bWV0aG9kPU5BSVZFX0JBWUVTLA0KICAgICAgICAgICAgICAgICAgICAgZmlsdGVybWV0aG9kPXVuaXZhcmlhdGVfS1MsDQogICAgICAgICAgICAgICAgICAgICBmaWx0ZXJtZXRob2QuY29udHJvbD1saXN0KHB2YWx1ZT0wLjA1KSwNCiAgICAgICAgICAgICAgICAgICAgIFNjYWxlPSJPcmRlckxvZ2l0IiwNCiAgICAgICAgICAgICAgICAgICAgIHBjYT1GQUxTRQ0KICAgICAgICAgICAgICAgICAgICkNCg0KDQoNCg0KYGBgDQoNClNlbGVjdGVkIFJhdyBGZWF0dXJlcw0KDQpgYGB7ciByZXN1bHRzID0gImFzaXMiLCB3YXJuaW5nID0gRkFMU0UsIGRwaT02MDAsIGZpZy5oZWlnaHQ9IDYuMCwgZmlnLndpZHRoPSA4LjB9DQoNCnZuYW1lcyA8LSBhcy5kYXRhLmZyYW1lKGNiaW5kKG1OQlJhdyRzZWxlY3RlZGZlYXR1cmVzLG1OQlJhdyRzZWxlY3RlZGZlYXR1cmVzKSkNCmR0YSA8LSBkYXRhc2V0ZnJhbWVfdHJhaW47DQpkdGEgPC0gRlJFU0FTY2FsZShkdGEsbWV0aG9kPSJPcmRlckxvZ2l0Iikkc2NhbGVkRGF0YQ0KZHRhJENsYXNzIDwtIGFzLm51bWVyaWMoZHRhJENsYXNzKQ0KaG0gPC0gaGVhdE1hcHModmFyaWFibGVMaXN0PXZuYW1lcywNCiAgICAgICAgICAgICAgIGRhdGE9ZHRhLA0KICAgICAgICAgICAgICAgT3V0Y29tZT1PdXRjb21lLA0KICAgICAgICAgICAgICAgaENsdXN0ZXI9ImNvbCIsDQogICAgICAgICAgICAgICBzcnRDb2w9NDUsDQogICAgICAgICAgICAgICB4bGFiPSJSYXcgRmVhdHVyZXMiLA0KICAgICAgICAgICAgICAgeWxhYj0iU2FtcGxlcyINCiAgICAgICAgICAgICAgICkNCg0KYGBgDQoNClNlbGVjdGVkIGRlY29ycmVsYXRlZCBGZWF0dXJlcw0KDQpgYGB7ciByZXN1bHRzID0gImFzaXMiLCBkcGk9NjAwLCBmaWcuaGVpZ2h0PSA2LjAsIGZpZy53aWR0aD0gOC4wfQ0KDQp2bmFtZXMgPC0gYXMuZGF0YS5mcmFtZShjYmluZChtTkJEZWNvciRzZWxlY3RlZGZlYXR1cmVzLG1OQkRlY29yJHNlbGVjdGVkZmVhdHVyZXMpKQ0KZHRhIDwtIGRhdGFzZXRmcmFtZURlY29yW1sxXV07DQpkdGEgPC0gRlJFU0FTY2FsZShkdGEsbWV0aG9kPSJPcmRlckxvZ2l0Iikkc2NhbGVkRGF0YQ0KZHRhJENsYXNzIDwtIGFzLm51bWVyaWMoZHRhJENsYXNzKQ0KaG0gPC0gaGVhdE1hcHModmFyaWFibGVMaXN0PXZuYW1lcywNCiAgICAgICAgICAgICAgIGRhdGE9ZHRhLA0KICAgICAgICAgICAgICAgT3V0Y29tZT1PdXRjb21lLA0KICAgICAgICAgICAgICAgaENsdXN0ZXI9ImNvbCIsDQogICAgICAgICAgICAgICBzcnRDb2w9MzUsDQogICAgICAgICAgICAgICB4bGFiPSJEZWNvcnJlbGF0ZWQgRmVhdHVyZXMiLA0KICAgICAgICAgICAgICAgeWxhYj0iU2FtcGxlcyINCiAgICAgICAgICAgICAgICkNCg0KYGBgDQoNClRvIG1ha2UgcHJlZGljdGlvbnMgd2UgbmVlZCB0byB0cmFuc2Zvcm0gdGhlIHRlc3Rpbmcgc2V0LiBUaGlzIGlzIGRvbmUgdXNpbmcgdGhlICoqRlJFU0EuQ0FEOjpwcmVkaWN0RGVjb3JyZWxhdGUoKSoqIGZ1bmN0aW9uDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCg0KIyBUcmFuc2Zvcm0gdGhlIHRlc3Rpbmcgc2V0DQpkZWNvcl90ZXN0IDwtIHByZWRpY3REZWNvcnJlbGF0ZShkYXRhc2V0ZnJhbWVEZWNvcltbMV1dLGRhdGFzZXRmcmFtZV90ZXN0KQ0KDQpgYGANCg0KT25jZSB3ZSBoYXZlIHRoZSB0cmFuc2Zvcm1lZCB0ZXN0aW5nIGRhdGFzZXQgd2UgY2FuIG1ha2UgYSBzaWRlIGJ5IHNpZGUgY29tcGFyaXNvbiBvZiBwcmVkaWN0aW9ucw0KDQpgYGB7ciByZXN1bHRzID0gImFzaXMiLCB3YXJuaW5nID0gRkFMU0UsIGRwaT02MDAsIGZpZy5oZWlnaHQ9IDQuMCwgZmlnLndpZHRoPSAxMi4wfQ0KDQojIFByZWRpY3QgdGhlIHJhdyB0ZXN0aW5nIHNldA0KcHJSQVcgPC0gYXR0cihwcmVkaWN0KG1OQlJhdyxkYXRhc2V0ZnJhbWVfdGVzdCksInByb2IiKQ0KDQojIFByZWRpY3Qgd2l0aCBQQ0ENCnByUENBIDwtIGF0dHIocHJlZGljdChtTkJQQ0EsZGF0YXNldGZyYW1lX3Rlc3QpLCJwcm9iIikNCg0KIyBQcmVkaWN0IHRoZSB0cmFuc2Zvcm1lZCBkYXRhc2V0DQpwckRlY29yIDwtIGF0dHIocHJlZGljdChtTkJEZWNvcixkZWNvcl90ZXN0KSwicHJvYiIpDQoNCg0KcGFyKG1mcm93PWMoMSwzKSkNCm1lYW5ST0NBVUMgPC0gbnVtZXJpYygzKTsNCm1lYW5QQ0FST0NBVUMgPC0gbnVtZXJpYygzKTsNCmZvciAodGhlQ2xhc3MgaW4gY2xhc3NOYW1lcykNCnsNCiAgY2xhc3NvdXRjb21lcyA8LSAxKihkYXRhc2V0ZnJhbWVfdGVzdFssT3V0Y29tZV0gPT0gdGhlQ2xhc3MpDQogIHBzUmF3IDwtIHByZWRpY3Rpb25TdGF0c19iaW5hcnkoY2JpbmQoY2xhc3NvdXRjb21lcyxwclJBV1ssdGhlQ2xhc3NdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUoIlJhdyA6Iix0aGVDbGFzcyksY2V4PTAuNzUpDQogIHBhbmRlcjo6cGFuZGVyKHBzUmF3JGF1Y3MpDQogIHBzUENBIDwtIHByZWRpY3Rpb25TdGF0c19iaW5hcnkoY2JpbmQoY2xhc3NvdXRjb21lcyxwclBDQVssdGhlQ2xhc3NdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUoIlBDQSA6Iix0aGVDbGFzcyksY2V4PTAuNzUpDQogIHBhbmRlcjo6cGFuZGVyKHBzUENBJGF1Y3MpDQogIHBzRGVjb3IgPC0gcHJlZGljdGlvblN0YXRzX2JpbmFyeShjYmluZChjbGFzc291dGNvbWVzLHByRGVjb3JbLHRoZUNsYXNzXSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlKCJHRFNUTSA6Iix0aGVDbGFzcyksY2V4PTAuNzUpDQogIHBhbmRlcjo6cGFuZGVyKHBzRGVjb3IkYXVjcykNCiAgbWVhblJPQ0FVQyA8LSBtZWFuUk9DQVVDICsgcHNSYXckYXVjczsNCiAgbWVhblBDQVJPQ0FVQyA8LSBtZWFuUENBUk9DQVVDICsgcHNQQ0EkYXVjczsNCn0NCm1lYW5ST0NBVUMgPC0gbWVhblJPQ0FVQy9sZW5ndGgoY2xhc3NOYW1lcykNCkFsbFJvY0FVQyA8LSBtZWFuUk9DQVVDOw0KbWVhblBDQVJPQ0FVQyA8LSBtZWFuUENBUk9DQVVDL2xlbmd0aChjbGFzc05hbWVzKQ0KQWxsUm9jQVVDIDwtIHJiaW5kKEFsbFJvY0FVQyxtZWFuUENBUk9DQVVDKTsNCg0KYGBgDQoNCiMjIFRyYWluaW5nIGFuZCBQcmVkaWN0aW9uIG9uIGFsbCBEZWNvcnJlbGF0aW9ucyBTZXRzDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCnBhcihtZnJvdz1jKDIsMikpDQoNCmZvciAoaSBpbiBjKDE6bGVuZ3RoKGRhdGFzZXRmcmFtZURlY29yKSkpDQp7DQogIG1OQkRlY29yIDwtIGZpbHRlcmVkRml0KHBhc3RlKE91dGNvbWUsIn4uIiksDQogICAgICAgICAgICAgICAgICAgZGF0YXNldGZyYW1lRGVjb3JbW2ldXSwNCiAgICAgICAgICAgICAgICAgICAgZml0bWV0aG9kPU5BSVZFX0JBWUVTLA0KICAgICAgICAgICAgICAgICAgICAgZmlsdGVybWV0aG9kPXVuaXZhcmlhdGVfS1MsDQogICAgICAgICAgICAgICAgICAgICBmaWx0ZXJtZXRob2QuY29udHJvbD1saXN0KHB2YWx1ZT0wLjA1KSwNCiAgICAgICAgICAgICAgICAgICAgIFNjYWxlPSJPcmRlckxvZ2l0IiwNCiAgICAgICAgICAgICAgICAgICAgIHBjYT1GQUxTRQ0KICAgICAgICAgICAgICAgICAgICkNCg0KICBkZWNvcl90ZXN0IDwtIHByZWRpY3REZWNvcnJlbGF0ZShkYXRhc2V0ZnJhbWVEZWNvcltbaV1dLGRhdGFzZXRmcmFtZV90ZXN0KQ0KICBwckRlY29yIDwtIGF0dHIocHJlZGljdChtTkJEZWNvcixkZWNvcl90ZXN0KSwicHJvYiIpDQogIG1lYW5ST0NBVUMgPC0gbnVtZXJpYygzKTsNCiAgZm9yICh0aGVDbGFzcyBpbiBjbGFzc05hbWVzKQ0KICB7DQogICAgY2xhc3NvdXRjb21lcyA8LSAxKihkYXRhc2V0ZnJhbWVfdGVzdFssT3V0Y29tZV0gPT0gdGhlQ2xhc3MpDQogICAgcHNEZWNvciA8LSBwcmVkaWN0aW9uU3RhdHNfYmluYXJ5KGNiaW5kKGNsYXNzb3V0Y29tZXMscHJEZWNvclssdGhlQ2xhc3NdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZShkZWNvcnR5cGVbW2ldXSx0aGVDbGFzcyxzZXA9IjoiKSxjZXg9MC43NSkNCiAgICBtZWFuUk9DQVVDIDwtIG1lYW5ST0NBVUMgKyBwc0RlY29yJGF1Y3M7DQogIH0NCiAgbWVhblJPQ0FVQyA8LSBtZWFuUk9DQVVDL2xlbmd0aChjbGFzc05hbWVzKQ0KICBwYW5kZXI6OnBhbmRlcihtZWFuUk9DQVVDKQ0KICBBbGxSb2NBVUMgPC0gcmJpbmQoQWxsUm9jQVVDLG1lYW5ST0NBVUMpDQoNCn0NCg0KYGBgDQoNCiMjIEZpbmFsIFBsb3QgQ29tcGFyaW5nIHRoZSBST0MgQVVDIG9mIGFsbCBPcHRpb25zDQoNCmBgYHtyIHJlc3VsdHMgPSAiYXNpcyIsIHdhcm5pbmcgPSBGQUxTRSwgZHBpPTYwMCwgZmlnLmhlaWdodD0gNi4wLCBmaWcud2lkdGg9IDguMH0NCnBhcihtZnJvdz1jKDEsMSkpDQoNCnJvd25hbWVzKEFsbFJvY0FVQykgPC0gYygiUmF3IiwiUENBIix1bmxpc3QoZGVjb3J0eXBlKSkNCnBhbmRlcjo6cGFuZGVyKEFsbFJvY0FVQykNCmJwUk9DQVVDIDwtIGJhclBsb3RDaUVycm9yKGFzLm1hdHJpeChBbGxSb2NBVUMpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRyaWNuYW1lID0gIlJPQ0FVQyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRoZXNldHMgPSAiUk9DIEFVQyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRoZW1ldGhvZCA9IHJvd25hbWVzKEFsbFJvY0FVQyksDQogICAgICAgICAgICAgICAgICAgICAgICAgIG1haW4gPSAiUk9DIEFVQyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIG9mZnNldHMgPSBjKDAuNSwxKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2NvcmVEaXJlY3Rpb24gPSAiPiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIGhvPTAuNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgYXJncy5sZWdlbmQgPSBsaXN0KGJnID0gIndoaXRlIix4PSJib3R0b21yaWdodCIsaW5zZXQ9YygwLjAsMCksY2V4PTAuNzUpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBjb2wgPSB0ZXJyYWluLmNvbG9ycyhucm93KEFsbFJvY0FVQykpDQogICAgICAgICAgICAgICAgICAgICAgICAgICkNCg0KYGBgDQo=