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")
pander::pander(table(datasetframe_train[,Outcome]),caption="Training")
pander::pander(table(datasetframe_test[,Outcome]),caption="Testing")
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:
The name will be unaltered if their maximum correlation to other features was lower than the threshold.
The name will have the “Ba_” prefix is the feature was correlated but used as unaltered basis
The name will have the “De_” prefix is the feature was original correlated and its correlation to “Ba_” features has been removed.
Furthermore, the returned data frame will have the following attributes:
Transformation matrix: “GDSTM”
Features:
“fsocre”
“varincluded”
“topFeatures”
Unaltered Basis:
“baseFeatures”
- “correlatedToBase”
“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 |
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
PCA : van
GDSTM : van
Raw : saab

PCA : saab
GDSTM : saab
Raw : bus

PCA : bus
GDSTM : bus
Raw : opel

PCA : opel
GDSTM : opel

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
AtThr:van

AtThr:saab AtThr:bus AtThr:opel
RLM_Pearson:van

RLM_Pearson:saab RLM_Pearson:bus RLM_Pearson:opel
LM_Spearman:van

LM_Spearman:saab LM_Spearman:bus LM_Spearman:opel
RLM_Spearman:van

RLM_Spearman:saab RLM_Spearman:bus RLM_Spearman:opel
Sup_Default:van

Sup_Default:saab Sup_Default:bus Sup_Default:opel
Sup_AtThr:van

Sup_AtThr:saab Sup_AtThr:bus Sup_AtThr:opel
Sup_RLM_Pearson:van

Sup_RLM_Pearson:saab Sup_RLM_Pearson:bus Sup_RLM_Pearson:opel
Sup_LM_Spearman:van

Sup_LM_Spearman:saab Sup_LM_Spearman:bus Sup_LM_Spearman:opel
Sup_RLM_Spearman:van

Sup_RLM_Spearman:saab Sup_RLM_Spearman:bus Sup_RLM_Spearman:opel
Sup_KS_RLM_Spearman:van

Sup_KS_RLM_Spearman:saab Sup_KS_RLM_Spearman:bus Sup_KS_RLM_Spearman:opel

Final Plot Comparing the ROC AUC of all Options
par(mfrow=c(1,1))
rownames(AllRocAUC) <- c("Raw","PCA",unlist(decortype))
pander::pander(AllRocAUC)
| 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=