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 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.05 0.02 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.11 0.06 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.12 0.03 0.16

# 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.03 0.00 0.03

# 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.13 0.00 0.13

# 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.02 0.00 0.02

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

user system elapsed 0.04 0.03 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.11 0.00 0.11

# 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.13 0.02 0.13

# 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_Circ De_D.Circ Ba_Scat.Ra De_Elong De_Pr.Axis.Rect De_Max.L.Rect
Table continues below
De_Sc.Var.Maxis De_Sc.Var.maxis De_Ra.Gyr De_Skew.Maxis De_Kurt.Maxis
Ba_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])
  }
}

----------------
 Circ   Scat.Ra 
------ ---------
  1     -0.1593 
----------------

Table: De_Circ


------------------
 D.Circ   Scat.Ra 
-------- ---------
   1      -0.4402 
------------------

Table: De_D.Circ


-----------------
 Scat.Ra   Elong 
--------- -------
 0.2311      1   
-----------------

Table: De_Elong


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

Table: De_Pr.Axis.Rect


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

Table: De_Sc.Var.Maxis


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

Table: De_Sc.Var.maxis


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

Table: De_Ra.Gyr


----------------------
 Skew.Maxis   Holl.Ra 
------------ ---------
     1        0.8278  
----------------------

Table: De_Skew.Maxis


----------------------
 Kurt.Maxis   Holl.Ra 
------------ ---------
     1        -0.7503 
----------------------

Table: De_Kurt.Maxis

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)
Circ D.Circ Scat.Ra Elong Pr.Axis.Rect Max.L.Rect Sc.Var.Maxis
Sc.Var.maxis Ra.Gyr Skew.Maxis Kurt.Maxis Holl.Ra
pander::pander(t(attr(datasetframeDecor[[1]],"topFeatures")),caption="Independent Features")
Independent Features
Scat.Ra Holl.Ra 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 Holl.Ra Comp

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 Holl.Ra Comp Comp Rad.Ra Pr.Axis.Ra Max.L.Ra 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
Skew.Maxis D.Circ Comp
if (length(attr(datasetframeDecor[[6]],"correlatedToBase"))>0)
{
pander::pander(t(attr(datasetframeDecor[[6]],"correlatedToBase")),caption="Set of features associated with base")
}

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)
Skew.Maxis D.Circ Comp Comp Rad.Ra Pr.Axis.Ra Max.L.Ra
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 Holl.Ra Comp Comp Rad.Ra Pr.Axis.Ra Max.L.Ra 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.9101 0.8771 0.9431

PCA : van

est lower upper
0.9267 0.8859 0.9676

GDSTM : van

est lower upper
0.9661 0.9521 0.9801

Raw : saab

est lower upper
0.75 0.6991 0.8009

PCA : saab

est lower upper
0.8697 0.8358 0.9036

GDSTM : saab

est lower upper
0.8415 0.8042 0.8789

Raw : bus

est lower upper
0.964 0.9494 0.9786

PCA : bus

est lower upper
0.9688 0.9484 0.9892

GDSTM : bus

est lower upper
0.9925 0.9866 0.9984

Raw : opel

est lower upper
0.7492 0.699 0.7994

PCA : opel

est lower upper
0.8555 0.8163 0.8946

GDSTM : opel

est lower upper
0.8438 0.8034 0.8841

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.911 0.8866 0.9354

AtThr:van

AtThr:saab AtThr:bus AtThr:opel

est lower upper
0.9154 0.893 0.9377

RLM_Pearson:van

RLM_Pearson:saab RLM_Pearson:bus RLM_Pearson:opel

est lower upper
0.9047 0.8775 0.9318

LM_Spearman:van

LM_Spearman:saab LM_Spearman:bus LM_Spearman:opel

est lower upper
0.9078 0.8823 0.9332

RLM_Spearman:van

RLM_Spearman:saab RLM_Spearman:bus RLM_Spearman:opel

est lower upper
0.9015 0.8723 0.9308

Sup_Default:van

Sup_Default:saab Sup_Default:bus Sup_Default:opel

est lower upper
0.911 0.8866 0.9354

Sup_AtThr:van

Sup_AtThr:saab Sup_AtThr:bus Sup_AtThr:opel

est lower upper
0.9201 0.8966 0.9437

Sup_RLM_Pearson:van

Sup_RLM_Pearson:saab Sup_RLM_Pearson:bus Sup_RLM_Pearson:opel

est lower upper
0.9047 0.8775 0.9318

Sup_LM_Spearman:van

Sup_LM_Spearman:saab Sup_LM_Spearman:bus Sup_LM_Spearman:opel

est lower upper
0.8995 0.8694 0.9297

Sup_RLM_Spearman:van

Sup_RLM_Spearman:saab Sup_RLM_Spearman:bus Sup_RLM_Spearman:opel

est lower upper
0.9054 0.8768 0.9339

Sup_KS_RLM_Spearman:van

Sup_KS_RLM_Spearman:saab Sup_KS_RLM_Spearman:bus Sup_KS_RLM_Spearman:opel

est lower upper
0.9053 0.8767 0.9339

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.8433 0.8062 0.8805
PCA 0.9052 0.8716 0.9388
Default 0.911 0.8866 0.9354
AtThr 0.9154 0.893 0.9377
RLM_Pearson 0.9047 0.8775 0.9318
LM_Spearman 0.9078 0.8823 0.9332
RLM_Spearman 0.9015 0.8723 0.9308
Sup_Default 0.911 0.8866 0.9354
Sup_AtThr 0.9201 0.8966 0.9437
Sup_RLM_Pearson 0.9047 0.8775 0.9318
Sup_LM_Spearman 0.8995 0.8694 0.9297
Sup_RLM_Spearman 0.9054 0.8768 0.9339
Sup_KS_RLM_Spearman 0.9053 0.8767 0.9339
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=