Intro

The purpose of this analysis is to find a correlation between the boolean models fitness to a steady state activity profile and their performance in terms of the number of True Positive (TP) synergies predicted and/or the overall MCC score (Matthews Correlation Coefficient score). We want to show that a closer fitness to the steady state suggests more predictive models, corroborating thus our proof of concept of using an ensemble-based approach where models are trained towards a specific steady state signaling pattern for drug combination predictions.

The boolean model datasets we will use are in total \(9\): one for each cell line of interest (8 cell lines) where the models were fitted to a specific steady state in each case and one for the so-called random models which were generated randomly in the sense that were fitted only to a proliferation state (simulations were done using the DrugLogics software modules Gitsbe and Drabme).

Each boolean model dataset constitues of:

  • The model predictions file which has for each model the prediction for each drug combination tested (0 = no synergy predicted, 1 = synergy predicted, NA = couldn’t find stable states in either the drug combination inhibited model or in any of the two single-drug inhibited models)
  • The models stable state (one per model). A fitness score for each model can easily be calculated then by matching the model’s stable state (which is something inherent in the boolean’s model structure, a unique fixpoint attractor) with the steady state of interest, node per node. A higher fitness score would mean a better match of a model’s stable state to the cell line derived steady state (a perfect match would result in a fitness of 1).
  • The models link operators which is a representation of the boolean equations of each model. Each boolean equation is in the form: **Target *= (Activator OR Activator OR…) AND NOT (Inhibitor OR Inhibitor OR…)** and the difference between the models can be found in the link operator (1 = ‘OR NOT’, 0 = ‘AND NOT’, or absent) which has been mutated (changed) through the genetic algorithm in Gitsbe. Note that the equations that do not have link operators are the same for every model and are thus discarded.
  • The observed synergies file which lists the drug combinations that were observed as synergistic for each cell line.
  • The steady state file which lists the network nodes (protein, gene, complexes names, etc) and their activity value (0 or 1, representing an inhibited or active node respectively). This input is provided per cell line and not for the random models since they are just trained to a profileration state.

Input

Loading libraries:

library(DT)
library(ggpubr)
library(emba)
library(usefun)
library(nnet)
library(pscl)
library(ComplexHeatmap)
library(circlize)
library(dplyr)
library(tibble)
library(Ckmeans.1d.dp)
library(RColorBrewer)

First we load the cell-specific input data:

# Cell Lines
cell.lines = c("A498", "AGS", "DU145", "colo205", "SW620", "SF295", "UACC62", "MDA-MB-468")

cell.line.dirs = sapply(cell.lines, function(cell.line) {
  paste0(getwd(), "/", cell.line)
})

# Model predictions
model.predictions.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/model_predictions")
})

model.predictions.per.cell.line = lapply(model.predictions.files, 
  function(file) {
    get_model_predictions(file)
  }
)

# Observed synergies
observed.synergies.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/observed_synergies")
})

observed.synergies.per.cell.line = lapply(observed.synergies.files, 
  function(file) {
    get_observed_synergies(file)
  }
)

# Models Stable State (1 per model)
models.stable.state.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/models_stable_state")
})

models.stable.state.per.cell.line = lapply(models.stable.state.files,
  function(file) {
    as.matrix(read.table(file, check.names = FALSE))
  }
)

# Models Link Operators
models.link.operator.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/models_link_operator")
})

models.link.operators.per.cell.line = lapply(models.link.operator.files,
  function(file) {
    as.matrix(read.table(file, check.names = FALSE))
  }
)

# the node names used in our analysis
node.names = colnames(models.stable.state.per.cell.line[[1]])

# Steady States
steady.state.files = sapply(cell.line.dirs, function(cell.line.dir) {
  paste0(cell.line.dir, "/steady_state")
})

steady.state.per.cell.line = lapply(steady.state.files,
  function(file) {
    ss.df = read.table(file, sep = "\t", stringsAsFactors = FALSE)
    steady.state = ss.df[,2]
    names(steady.state) = ss.df[,1]
    
    # change value to NA for nodes for which there was no activity found (dash)
    steady.state[steady.state == "-"] = NA
    
    # keep only the nodes that are included in the analysis
    steady.state = prune_and_reorder_vector(steady.state, node.names)
    
    # check
    stopifnot(names(steady.state) %in% node.names)
    
    # return an integer vector since the activity values are binarized (0,1)
    return(sapply(steady.state, as.integer))
  }
)

The random model input data:

random.dir = paste0(getwd(), "/random")
random.model.predictions = get_model_predictions(paste0(random.dir, "/model_predictions"))

random.models.stable.state = as.matrix(
  read.table(file = paste0(random.dir, "/models_stable_state"), check.names = FALSE)
)

random.models.link.operator =
  as.matrix(read.table(file = paste0(random.dir, "/models_link_operator"), check.names = FALSE))

Model Analysis

In order to find the number of true positive predicted synergies, MCC scores and fitness scores for each of the models in each of the 9 datasets, we use functions from the emba R package.

Cell-specific

We find the MCC, TP and fitness values for each model per cell line (note that each model’s stable state in a specific cell line is matched against the steady state from that cell line):

models.tp.per.cell.line = list()
models.mcc.per.cell.line = list()
models.fitness.per.cell.line = list()

for (cell.line in cell.lines) {
  model.predictions = model.predictions.per.cell.line[[cell.line]]
  observed.synergies = observed.synergies.per.cell.line[[cell.line]]
  number.of.drug.comb.tested = ncol(model.predictions.per.cell.line[[cell.line]])
  
  # Split model.predictions to positive (observed) and negative (non-observed) results
  observed.model.predictions =
    get_observed_model_predictions(model.predictions, observed.synergies)
  unobserved.model.predictions =
    get_unobserved_model_predictions(model.predictions, observed.synergies)
  
  # Count the predictions of the observed synergies per model (TP)
  models.tp.per.cell.line[[cell.line]] = calculate_models_synergies_tp(observed.model.predictions)
  
  # Calculate Matthews Correlation Coefficient (MCC) for every model
  models.mcc.per.cell.line[[cell.line]] = 
    calculate_models_mcc(observed.model.predictions,
                         unobserved.model.predictions,
                         number.of.drug.comb.tested)
  
  # Fitness per model contrasted to steady state from cell line
  steady.state = steady.state.per.cell.line[[cell.line]]
  models.stable.state = models.stable.state.per.cell.line[[cell.line]]
  models.fitness.per.cell.line[[cell.line]] = apply(
    models.stable.state[,names(steady.state)], 1, get_percentage_of_matches, steady.state
  )
}

Random Models

Finding the MCC, TP and fitness values for each model from the above result (note that each model’s stable state in a specific cell line is matched against the steady state from that cell line and that the random models’ stable state data does not change per cell line, i.e. same random.models.stable.state object):

random.models.mcc.per.cell.line = list()
random.models.tp.per.cell.line = list()
random.models.fitness.per.cell.line = list()

for (cell.line in cell.lines) {
  observed.synergies = observed.synergies.per.cell.line[[cell.line]]
  number.of.drug.comb.tested = ncol(random.model.predictions)
  
  # Split model.predictions to positive (observed) and negative (non-observed) results
  observed.model.predictions =
    get_observed_model_predictions(random.model.predictions, observed.synergies)
  unobserved.model.predictions =
    get_unobserved_model_predictions(random.model.predictions, observed.synergies)
  
   # Count the predictions of the observed synergies per model (TP)
  random.models.tp.per.cell.line[[cell.line]] = 
    calculate_models_synergies_tp(observed.model.predictions)
  
  # Calculate Matthews Correlation Coefficient (MCC) for every model
  random.models.mcc.per.cell.line[[cell.line]] = 
    calculate_models_mcc(observed.model.predictions,
                         unobserved.model.predictions,
                         number.of.drug.comb.tested)
  
  # Fitness per model contrasted to steady state from cell line 
  steady.state = steady.state.per.cell.line[[cell.line]]
  random.models.fitness.per.cell.line[[cell.line]] = apply(
    random.models.stable.state[,names(steady.state)], 1, 
    get_percentage_of_matches, steady.state
  )
}

Choose best dataset

We now want to find the best dataset & cell line for our subsequent analysis - that is to show the performance vs fitness correlation. The argument here is that we want to choose a boolean dataset that has a large enough fitness value range along with a large TP and/or MCC value range, since with smaller value ranges it would be harder to distinguish the difference of the estimated distributions of the fitness scores belonging to different performance classes (i.e. models’ fitnesses that belong to different classification groups with the metric being either the number of TPs or the MCC score).

The next two tables will help us determine exactly which cell line and dataset to use:

cell.specific.model.data = matrix(data = NA, nrow = length(cell.lines), ncol = 11)
rownames(cell.specific.model.data) = cell.lines
colnames(cell.specific.model.data) = c("fitness range", "Min fitness", 
  "Max fitness", "Mean fitness", "Median fitness", "MCC range", "Min MCC", 
  "Max MCC", "Mean MCC", "Median MCC", "Max TPR")

for (cell.line in cell.lines) {
  models.fitness = models.fitness.per.cell.line[[cell.line]]
  models.mcc = models.mcc.per.cell.line[[cell.line]]
  models.tp = models.tp.per.cell.line[[cell.line]]
  
  fit.summary = unclass(summary(models.fitness))
  mcc.summary = unclass(summary(models.mcc))
  max.tpr = max(models.tp) / length(observed.synergies.per.cell.line[[cell.line]])
  
  cell.specific.model.data[cell.line, "fitness range"] = fit.summary["Max."] - fit.summary["Min."]
  cell.specific.model.data[cell.line, "Min fitness"] = fit.summary["Min."]
  cell.specific.model.data[cell.line, "Max fitness"] = fit.summary["Max."]
  cell.specific.model.data[cell.line, "Mean fitness"] = fit.summary["Mean"]
  cell.specific.model.data[cell.line, "Median fitness"] = fit.summary["Median"]
  
  cell.specific.model.data[cell.line, "MCC range"] = mcc.summary["Max."] - mcc.summary["Min."]
  cell.specific.model.data[cell.line, "Min MCC"] = mcc.summary["Min."]
  cell.specific.model.data[cell.line, "Max MCC"] = mcc.summary["Max."]
  cell.specific.model.data[cell.line, "Mean MCC"] = mcc.summary["Mean"]
  cell.specific.model.data[cell.line, "Median MCC"] = mcc.summary["Median"]
  
  cell.specific.model.data[cell.line, "Max TPR"] = max.tpr
}

# color columns
fit.breaks = quantile(cell.specific.model.data[,"fitness range"], probs = seq(.05, .95, .05), na.rm = TRUE)
fit.colors = round(seq(255, 40, length.out = length(fit.breaks) + 1), 0) %>%
  {paste0("rgb(255,", ., ",", ., ")")} # red
mcc.breaks = quantile(cell.specific.model.data[,"MCC range"], probs = seq(.05, .95, .05), na.rm = TRUE)
mcc.colors = round(seq(255, 40, length.out = length(mcc.breaks) + 1), 0) %>%
  {paste0("rgb(", ., ",255,", ., ")")} # green

caption.title = "Table 1: Fitness, MCC scores and TP (True positives) for the Cell-specific model predictions across 8 Cell Lines"
datatable(data = cell.specific.model.data, 
          options = list(dom = "t"), # just show the table
          caption = htmltools::tags$caption(caption.title, style="color:#dd4814; font-size: 18px")) %>% 
  formatRound(1:11, digits = 3) %>%
  formatStyle(columns = c("fitness range"), backgroundColor = styleInterval(fit.breaks, fit.colors)) %>%
  formatStyle(columns = c("MCC range"), backgroundColor = styleInterval(mcc.breaks, mcc.colors))
random.model.data = matrix(data = NA, nrow = length(cell.lines), ncol = 11)
rownames(random.model.data) = cell.lines
colnames(random.model.data) = c("fitness range", "Min fitness", 
  "Max fitness", "Mean fitness", "Median fitness", "MCC range", "Min MCC", 
  "Max MCC", "Mean MCC", "Median MCC", "Max TPR")

for (cell.line in cell.lines) {
  models.fitness = random.models.fitness.per.cell.line[[cell.line]]
  models.mcc = random.models.mcc.per.cell.line[[cell.line]]
  models.tp = random.models.tp.per.cell.line[[cell.line]]
  
  fit.summary = unclass(summary(models.fitness))
  mcc.summary = unclass(summary(models.mcc))
  max.tpr = max(models.tp) / length(observed.synergies.per.cell.line[[cell.line]])
  
  random.model.data[cell.line, "fitness range"] = fit.summary["Max."] - fit.summary["Min."]
  random.model.data[cell.line, "Min fitness"] = fit.summary["Min."]
  random.model.data[cell.line, "Max fitness"] = fit.summary["Max."]
  random.model.data[cell.line, "Mean fitness"] = fit.summary["Mean"]
  random.model.data[cell.line, "Median fitness"] = fit.summary["Median"]
  
  random.model.data[cell.line, "MCC range"] = mcc.summary["Max."] - mcc.summary["Min."]
  random.model.data[cell.line, "Min MCC"] = mcc.summary["Min."]
  random.model.data[cell.line, "Max MCC"] = mcc.summary["Max."]
  random.model.data[cell.line, "Mean MCC"] = mcc.summary["Mean"]
  random.model.data[cell.line, "Median MCC"] = mcc.summary["Median"]
  
  random.model.data[cell.line, "Max TPR"] = max.tpr
}

# color columns
fit.breaks = quantile(random.model.data[,"fitness range"], probs = seq(.05, .95, .05), na.rm = TRUE)
fit.colors = round(seq(255, 40, length.out = length(fit.breaks) + 1), 0) %>%
  {paste0("rgb(255,", ., ",", ., ")")} # red
mcc.breaks = quantile(random.model.data[,"MCC range"], probs = seq(.05, .95, .05), na.rm = TRUE)
mcc.colors = round(seq(255, 40, length.out = length(mcc.breaks) + 1), 0) %>%
  {paste0("rgb(", ., ",255,", ., ")")} # green

caption.title = "Table 2: Fitness, MCC scores and TP (True positives) for the random model predictions across 8 Cell Lines"
datatable(data = random.model.data, 
          options = list(dom = "t"), # just show the table
          caption = htmltools::tags$caption(caption.title, style="color:#dd4814; font-size: 18px")) %>% 
  formatRound(1:11, digits = 3) %>%
  formatStyle(columns = c("fitness range"), backgroundColor = styleInterval(fit.breaks, fit.colors)) %>%
  formatStyle(columns = c("MCC range"), backgroundColor = styleInterval(mcc.breaks, mcc.colors))

The below box plots compare the MCC values and fitness scores across all cell lines between the cell-specific models (trained to steady state) and the random ones (trained to proliferation):

num.of.models = nrow(random.models.stable.state)
data.list = list()

for (cell.line in cell.lines) {
  cell.line.vec = as.data.frame(rep(cell.line, num.of.models), stringsAsFactors = FALSE)
  models.mcc.cell.specific = remove_rownames(as.data.frame(models.mcc.per.cell.line[[cell.line]]))
  models.mcc.random        = remove_rownames(as.data.frame(random.models.mcc.per.cell.line[[cell.line]]))
  
  models.fitness.cell.specific = remove_rownames(as.data.frame(models.fitness.per.cell.line[[cell.line]]))
  models.fitness.random        = remove_rownames(as.data.frame(random.models.fitness.per.cell.line[[cell.line]]))
  
  data.list[[cell.line]] = bind_cols(cell.line.vec, models.mcc.cell.specific, 
    models.mcc.random, models.fitness.cell.specific, models.fitness.random)
}

data = bind_rows(data.list)
colnames(data) = c("cell.line", "MCC cell-specific", "MCC random", 
                   "fitness cell-specific", "fitness random")
# Note the cell-specific models have NaN MCC values
ggboxplot(data, x = "cell.line", y = c("MCC cell-specific", "MCC random"),
          palette = brewer.pal(3, "Set1"), merge = "asis",
          xlab = "Cell Lines", ylab = "MCC values", add = "point", add.params = list(size = 0.5))

ggboxplot(data, x = "cell.line", y = c("fitness cell-specific", "fitness random"),
          palette = brewer.pal(3, "Set1"), merge = "asis",
          xlab = "Cell Lines", ylab = "Fitness values", add = "point", add.params = list(size = 0.5))

So, from the two tables and the two box plots above we conclude that:

  • In general, the random models offer a larger range of fitness values when their stable states are matched against the steady state of each particular cell line. This is something we expected since these models weren’t fitted to a specific cell-line steady state (meaning that they weren’t chosen from the Gitsbe module as the 3 best from each simulation that match that steady state as best as possible) but rather to a more generic state of proliferation. Thus, they represent a set of models with larger variation in terms of structure (boolean model equations) compared to the cell-specific generated ones.
  • The cell-specific models have always a larger maximum fitness and median value compared to the random ones for each respective cell line.
  • In most of the cell lines, the cell-specific models show an equal or less performance (smaller MCC values) than the random ones

All in all, we will use the random models data, contrasted to the steady state of the AGS cell line (see Table 2). This dataset has the largest MCC range and TPR value (third largest fitness range value as well) combined in both tables above.

best.cell.line = "AGS"

fit = random.models.fitness.per.cell.line[[best.cell.line]]
tp  = random.models.tp.per.cell.line[[best.cell.line]]
mcc = random.models.mcc.per.cell.line[[best.cell.line]]

Statistical Analysis

Data preprocessing

Firstly, we filter the data by finding the unique models - those that have strictly different boolean equations. Then, we take a random sample out of these (while stabilizing the seed number for reproducibility purposes):

# For reproducibility
set.seed(10)
sample.size = 1000

# find the unique models
unique.models = rownames(unique(random.models.link.operator))

# remove models with NaN MCC scores
unique.models = unique.models[!unique.models %in% names(which(is.na(mcc)))]

# take a sample
unique.models.sample = sample(unique.models, size = sample.size)

fit.unique = fit[names(fit) %in% unique.models.sample]
tp.unique  = tp[names(tp) %in% unique.models.sample]
mcc.unique = mcc[names(mcc) %in% unique.models.sample]

df = as.data.frame(cbind(fit.unique, mcc.unique, tp.unique))

Note that the fitness and MCC score are continuous variables while the number of true positives (TP) is discrete.

Correlation Plots

Then, we check if the our data is normally distributed (using the Shapiro-Wilk test for normality and the Q-Q plots):

ggqqplot(data = df, x = "fit.unique", ylab = "Fitness")

ggqqplot(data = df, x = "mcc.unique", ylab = "MCC")

shapiro.test(x = fit.unique)

    Shapiro-Wilk normality test

data:  fit.unique
W = 0.98571, p-value = 2.606e-08
shapiro.test(x = mcc.unique)

    Shapiro-Wilk normality test

data:  mcc.unique
W = 0.97356, p-value = 1.599e-12

From the above results we observe that both the fitness and MCC scores are surely not normally distributed (with statistical significance). Thus, we will use non-parametric correlation scores, namely the Spearman and Kendall rank-based correlation tests, to check the correlation between the two continuous variables (models fitness values and their corresponding MCC score):

ggscatter(df, x = "mcc.unique", y = "fit.unique", 
          title = "Fitness vs Performance (MCC) - Spearman Correlation",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "spearman", label.x.npc = 0.7, label.y.npc = 0.1),
          xlab = "MCC scores", ylab = "Fitness Values")

ggscatter(df, x = "mcc.unique", y = "fit.unique", 
          title = "Fitness vs Performance (MCC) - Kendall Correlation",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall", label.x.npc = 0.7, label.y.npc = 0.1),
          xlab = "MCC scores", ylab = "Fitness Values")

From the correlation plots above, we observe a weak/small positive correlation between performance and fitness to the steady state.

To assess the correlation between the number of TP predictions of the models (discrete variable) and the models fitness (continuous variable), we construct a predictor of the categorical variable from the continuous variable: if the resulting classifier has a high degree of fit we can conclude the two variables share a relationship and are indeed correlated. Since there are more than 2 TP classes (values) in the dataset, we will use Multinomial Logistic Regression and fit log-linear models via neural networks:

model.classifier = multinom(data = df, formula = tp.unique ~ fit.unique)
# weights:  15 (8 variable)
initial  value 1609.437912 
iter  10 value 1542.742099
final  value 1540.769166 
converged
pseudo.r2.measures = pR2(model.classifier)
fitting null model for pseudo-r2
# weights:  10 (4 variable)
initial  value 1609.437912 
final  value 1546.531448 
converged
pseudo.r2.measures["McFadden"]
   McFadden 
0.003725939 

To measure the goodness-of-fit for our model, there are several measures proposed for logistic regression. We emphasize on the McFaddens’s pseudo-\(R^2\) measure for our classifier, for which a value of \(0.2-0.4\) would indicate an excellent fit [source]. Since we found less, we can also assume that there is only a weak/small positive correlation between the number of TPs and the fitness score of the models.

Next, we will next proceed with a more elaborate analysis, where the models will be split to different performance classes (TP or MCC score-derived) and the statistical correlation between the individual groups will be tested (with regards to their fitness scores).

TP-class vs fitness

First, some box and density plots to see practically what and where is the difference between the models fitness values belonging to different TP-classes:

# Box plots
ggboxplot(df, x = "tp.unique", y = "fit.unique", color = "tp.unique",
          palette = usefun:::colors.100[1:length(unique(tp.unique))],
          xlab = "True Positives (TP)", ylab = "Fitness values")

# Density Plots
densities = list()
for (tp.num in sort(unique(tp.unique))) {
  x = df %>%
    filter(tp.unique == tp.num) %>%
    select_at(.vars = c("fit.unique"))
  den = density(x$fit.unique)
  densities[[paste0(tp.num, " (", den$n, ")")]] = den
}

make_multiple_density_plot(densities, legend.title = "TP classes (#models)",
        title = "Density Estimation", x.axis.label = "Fitness score")

As we can see from the above plots, there doesn’t seem to be any significant difference between the fitness values of the different TP classes. Mathematically, we can show that by using the Kruskal-Wallis Rank Sum Test (notice the p-value - it is larger than \(0.001\)):

# Hypothesis testing: Are the location parameters of the distribution of x the same in each group?
kruskal.test(x = df[,"fit.unique"], g = df[, "tp.unique"])

    Kruskal-Wallis rank sum test

data:  df[, "fit.unique"] and df[, "tp.unique"]
Kruskal-Wallis chi-squared = 14.828, df = 4, p-value = 0.005072

To demonstrate that almost all pair of groups are the same, we perform pairwise Wilcoxon rank sum tests and we draw a heatmap of the each test’s p-value (note that all except one are not statistically significant):

res = pairwise.wilcox.test(x = df[,"fit.unique"], g = df[, "tp.unique"], p.adjust.method = "BH")
p.value.mat = res$p.value
col_fun = colorRamp2(breaks = c(0, 0.05, 0.5, 1), c("green", "white", "orange", "red"))
ht = Heatmap(matrix = p.value.mat, cluster_rows = FALSE, cluster_columns = FALSE,
  na_col = "white", name = "p-value", col = col_fun, column_names_rot = 0,
  row_title = "TP", row_title_rot = 0, row_names_side = "left",
  column_title = "P-values (Pairwise Wilcoxon tests) - TP-classified fitnesses", column_title_gp = gpar(fontsize = 20),
  cell_fun = function(j, i, x, y, width, height, fill) {
    if (!is.na(p.value.mat[i,j]))
        grid.text(sprintf("%.6f", p.value.mat[i, j]), x, y, gp = gpar(fontsize = 16))
})
draw(ht)

Note that the number of true positive predictions is an one-dimensional metric and by excluding the TN, FP and FNs we have a less-informant (and arguably incorrect) picture of the models performance classification. That’s why we will proceed with the more balanced MCC score for classifing the models fitnesses to different performance groups.

MCC-class vs fitness

Firstly, we perform a univariate k-means clustering to split the models MCC values to different classes and plot the data histogram. The number of MCC classes to split the data can be arbitrarily chosen and so we chose the same as the number of TP classes:

num.of.mcc.classes = 5
mcc.class.ids = 1:num.of.mcc.classes

# find the clusters
res = Ckmeans.1d.dp(x = df[,"mcc.unique"], k = num.of.mcc.classes)
mcc.class.id = res$cluster
df = cbind(df, mcc.class.id)

plot_mcc_classes_hist(df[,"mcc.unique"], df[,"mcc.class.id"],
                      num.of.mcc.classes, mcc.class.ids)

Then we show some box and density plots to see practically what and where is the difference between the models fitness values belonging to different MCC-classes:

# Box plots
ggboxplot(df, x = "mcc.class.id", y = "fit.unique", color = "mcc.class.id",
          palette = usefun:::colors.100[1:num.of.mcc.classes],
          xlab = "MCC class", ylab = "Fitness values")

# Density Plots
densities = list()
for (id in mcc.class.ids) {
  x = df %>%
    filter(mcc.class.id == id) %>%
    select_at(.vars = c("fit.unique"))
  den = density(x$fit.unique)
  densities[[paste0(id, " (", res$size[id], ")")]] = den
}

make_multiple_density_plot(densities, legend.title = "MCC classes (#models)",
        title = "Density Estimation", x.axis.label = "Fitness score")

Again we observe that there doesn’t seem to be any significant difference between the fitness values of the different MCC classes. The next statistical tests and heatmap show the same:

# Hypothesis testing: Are the location parameters of the distribution of x the same in each group?
kruskal.test(x = df[,"fit.unique"], g = df[, "mcc.class.id"])

    Kruskal-Wallis rank sum test

data:  df[, "fit.unique"] and df[, "mcc.class.id"]
Kruskal-Wallis chi-squared = 12.543, df = 4, p-value = 0.01374
pair.res = pairwise.wilcox.test(x = df[,"fit.unique"], g = df[, "mcc.class.id"], p.adjust.method = "BH")
p.value.mat = pair.res$p.value
col_fun = colorRamp2(breaks = c(0, 0.05, 0.5, 1), c("green", "white", "orange", "red"))
ht = Heatmap(matrix = p.value.mat, cluster_rows = FALSE, cluster_columns = FALSE,
  na_col = "white", name = "p-value", col = col_fun, column_names_rot = 0,
  row_title = "MCC class id", row_title_rot = 90, row_names_side = "left",
  column_title = "P-values (Pairwise Wilcoxon tests) - MCC-classified fitnesses", column_title_gp = gpar(fontsize = 20),
  cell_fun = function(j, i, x, y, width, height, fill) {
    if (!is.na(p.value.mat[i,j]))
        grid.text(sprintf("%.6f", p.value.mat[i, j]), x, y, gp = gpar(fontsize = 20))
})
draw(ht)

Extended Analysis

In this section, we get new steady states to test the fitness of our model datasets against. Note that the old ones used were calculated by using both the PARADIGM tool and tools to infer transcription factor activity (in the CLSS workflow). The new ones will use only PARADIGM. See README.md for more info on how to generate the new steady states.

The idea here is that the absense of correlation between fitness and performance shown above had to do with wrongly inferred steady states. So, even though the models have not be trained to these new steady states, their stable states remain the same and new fitness scores can be evaluated against these. Thus, the correlation of the models performance correspoding model fitness in each dataset.

Input

First, we get the new steady states:

steady.state.per.cell.line.new = list()
for (cell.line in cell.lines) {
  file = paste0(getwd(), "/steady-states/", cell.line, "_steady_state")
  ss.df = read.table(file, sep = "\t", stringsAsFactors = FALSE)
  
  steady.state = ss.df[,2]
  names(steady.state) = ss.df[,1]
  
  # change value to NA for nodes for which there was no activity found (dash)
  steady.state[steady.state == "-"] = NA
  
  # keep only the nodes that are included in the analysis
  steady.state = prune_and_reorder_vector(steady.state, node.names)
    
  # check
  stopifnot(names(steady.state) %in% node.names)
  
  # return an integer vector since the activity values are binarized (0,1)
  steady.state.per.cell.line.new[[cell.line]] = sapply(steady.state, as.integer)
}

# Which nodes are not in the steady state vector? (genes: xxx_g)
#node.names[which((node.names %in% names(steady.state.per.cell.line.new$SW620) == FALSE))]

We check to see if the new steady states are different from before by checking the percentage of common states:

for (cell.line in cell.lines) {
  percentage.match = get_percentage_of_matches(steady.state.per.cell.line.new[[cell.line]], 
    steady.state.per.cell.line[[cell.line]])
  expres = paste0(cell.line, ": ", specify_decimal(percentage.match, digits.to.keep = 2))
  if (cell.line == cell.lines[1])
    pretty_print_string(string = expres)
  else
    pretty_print_string(string = expres, with.gt = FALSE)
  print_empty_line(html.output = TRUE)
}

A498: 0.42
AGS: 0.48
DU145: 0.47
colo205: 0.52
SW620: 0.47
SF295: 0.48
UACC62: 0.49
MDA-MB-468: 0.53

So, we see that the new steady state vector for each cell line is around half different than the previous calculated one, so pretty much random!


We also get the gold/curation-based steady state for the AGS cell line which was one of the datasets given as supplementary material in the paper Discovery of Drug Synergies in Gastric Cancer Cells Predicted by Logical Modeling - see Table S4. Here is a version that is applicable to the node set in the Cascade 2.0 topology (that is used in all our simulations) with the addition of the steady state values for Prosurvival and Antisurvival nodes:

ags.literature.file = paste0(getwd(), "/steady-states/AGS_literature_steady_state")
ss.df = read.table(ags.literature.file, sep = "\t", stringsAsFactors = FALSE)
ags.literature.ss = ss.df[,2]
names(ags.literature.ss) = ss.df[,1]
stopifnot(names(ags.literature.ss) %in% node.names)

Comparing the common nodes between the AGS steady state vectors found with the 2 previous methods and the literature-curated profile in order to see how much more different the computationally predicted states for the AGS cell line are from the gold standard, we see that:

ags.ss.old = prune_and_reorder_vector(steady.state.per.cell.line$AGS, names(ags.literature.ss))
ags.ss.new = prune_and_reorder_vector(steady.state.per.cell.line.new$AGS, names(ags.literature.ss))

pretty_print_string(paste0("Match between the 2 AGS steady state vectors pruned to the literature-based nodes: ", specify_decimal(get_percentage_of_matches(ags.ss.old, ags.ss.new), digits.to.keep = 2)))

Match between the 2 AGS steady state vectors pruned to the literature-based nodes: 0.35

pretty_print_string(paste0("Match between the old AGS steady state vector and the literature-based one: ", specify_decimal(get_percentage_of_matches(ags.ss.old, ags.literature.ss), digits.to.keep = 2)))

Match between the old AGS steady state vector and the literature-based one: 0.57

pretty_print_string(paste0("Match between the new AGS steady state vector and the literature-based one: ", specify_decimal(get_percentage_of_matches(ags.ss.new, ags.literature.ss), digits.to.keep = 2)))

Match between the new AGS steady state vector and the literature-based one: 0.43

So, it seems that the 2 computational methods are very different even when compared on the node subset that constitutes the gold standard. Both of them seem to be random compared to the true (curated) steady state predictions.

Models fitness

Given the new steady states, we re-calculate the models fitness for both the cell-specific models and the random ones:

models.fitness.per.cell.line.new = list()
random.models.fitness.per.cell.line.new = list()

for (cell.line in cell.lines) {
  steady.state = steady.state.per.cell.line.new[[cell.line]]
  models.stable.state = models.stable.state.per.cell.line[[cell.line]]
  
  # Fitness per model contrasted to steady state from cell line
  models.fitness.per.cell.line.new[[cell.line]] = apply(
    models.stable.state[,names(steady.state)], 1, get_percentage_of_matches, steady.state
  )
  
  random.models.fitness.per.cell.line.new[[cell.line]] = apply(
    random.models.stable.state[,names(steady.state)], 1, 
    get_percentage_of_matches, steady.state
  )
}

We calculate the fitness of the AGS-trained models and the random ones to the literature-curated steady state for the AGS cell line:

ags.models.stable.state = models.stable.state.per.cell.line$AGS

models.fitness.to.ags.literature.ss = apply(
  ags.models.stable.state[,names(ags.literature.ss)], 1, 
  get_percentage_of_matches, ags.literature.ss)
random.models.fitness.to.ags.literature.ss = apply(
  random.models.stable.state[,names(ags.literature.ss)], 1, 
  get_percentage_of_matches, ags.literature.ss)

Choose best dataset

Following the same procedure as before, we try to find the best dataset to do the correlation analysis on. Such dataset will have models that result in a large MCC + fitness range when contrasted to the new steady states.

The next two tables will help us determine exactly which cell line and dataset to use:

cell.specific.model.data = matrix(data = NA, nrow = length(cell.lines) + 1, ncol = 11)
rownames(cell.specific.model.data) = c(cell.lines, "AGS-gold")
colnames(cell.specific.model.data) = c("fitness range", "Min fitness", 
  "Max fitness", "Mean fitness", "Median fitness", "MCC range", "Min MCC", 
  "Max MCC", "Mean MCC", "Median MCC", "Max TPR")

for (cell.line in c(cell.lines, "AGS-gold")) {
  if (cell.line == "AGS-gold") {
    models.fitness = models.fitness.to.ags.literature.ss
    models.mcc = models.mcc.per.cell.line$AGS
    models.tp = models.tp.per.cell.line$AGS
  } else {
    models.fitness = models.fitness.per.cell.line.new[[cell.line]]
    models.mcc = models.mcc.per.cell.line[[cell.line]]
    models.tp = models.tp.per.cell.line[[cell.line]]
  }
  
  fit.summary = unclass(summary(models.fitness))
  mcc.summary = unclass(summary(models.mcc))
  if (cell.line == "AGS-gold") {
    max.tpr = max(models.tp) / length(observed.synergies.per.cell.line$AGS)
  } else {
    max.tpr = max(models.tp) / length(observed.synergies.per.cell.line[[cell.line]])
  }
  
  cell.specific.model.data[cell.line, "fitness range"] = fit.summary["Max."] - fit.summary["Min."]
  cell.specific.model.data[cell.line, "Min fitness"] = fit.summary["Min."]
  cell.specific.model.data[cell.line, "Max fitness"] = fit.summary["Max."]
  cell.specific.model.data[cell.line, "Mean fitness"] = fit.summary["Mean"]
  cell.specific.model.data[cell.line, "Median fitness"] = fit.summary["Median"]
  
  cell.specific.model.data[cell.line, "MCC range"] = mcc.summary["Max."] - mcc.summary["Min."]
  cell.specific.model.data[cell.line, "Min MCC"] = mcc.summary["Min."]
  cell.specific.model.data[cell.line, "Max MCC"] = mcc.summary["Max."]
  cell.specific.model.data[cell.line, "Mean MCC"] = mcc.summary["Mean"]
  cell.specific.model.data[cell.line, "Median MCC"] = mcc.summary["Median"]
  
  cell.specific.model.data[cell.line, "Max TPR"] = max.tpr
}

# color columns
fit.breaks = quantile(cell.specific.model.data[,"fitness range"], probs = seq(.05, .95, .05), na.rm = TRUE)
fit.colors = round(seq(255, 40, length.out = length(fit.breaks) + 1), 0) %>%
  {paste0("rgb(255,", ., ",", ., ")")} # red
mcc.breaks = quantile(cell.specific.model.data[,"MCC range"], probs = seq(.05, .95, .05), na.rm = TRUE)
mcc.colors = round(seq(255, 40, length.out = length(mcc.breaks) + 1), 0) %>%
  {paste0("rgb(", ., ",255,", ., ")")} # green

caption.title = "Table 3: Fitness, MCC scores and TP (True positives) for the Cell-specific model predictions across 8 Cell Lines"
datatable(data = cell.specific.model.data, 
          options = list(dom = "t"), # just show the table
          caption = htmltools::tags$caption(caption.title, style="color:#dd4814; font-size: 18px")) %>% 
  formatRound(1:11, digits = 3) %>%
  formatStyle(columns = c("fitness range"), backgroundColor = styleInterval(fit.breaks, fit.colors)) %>%
  formatStyle(columns = c("MCC range"), backgroundColor = styleInterval(mcc.breaks, mcc.colors))
random.model.data = matrix(data = NA, nrow = length(cell.lines) + 1, ncol = 11)
rownames(random.model.data) = c(cell.lines, "AGS-gold")
colnames(random.model.data) = c("fitness range", "Min fitness", 
  "Max fitness", "Mean fitness", "Median fitness", "MCC range", "Min MCC", 
  "Max MCC", "Mean MCC", "Median MCC", "Max TPR")

for (cell.line in c(cell.lines, "AGS-gold")) {
  if (cell.line == "AGS-gold") {
    models.fitness = random.models.fitness.to.ags.literature.ss
    models.mcc = random.models.mcc.per.cell.line$AGS
    models.tp = random.models.tp.per.cell.line$AGS
  } else {
    models.fitness = random.models.fitness.per.cell.line.new[[cell.line]]
    models.mcc = random.models.mcc.per.cell.line[[cell.line]]
    models.tp = random.models.tp.per.cell.line[[cell.line]]
  }
  
  fit.summary = unclass(summary(models.fitness))
  mcc.summary = unclass(summary(models.mcc))
  if (cell.line == "AGS-gold") {
    max.tpr = max(models.tp) / length(observed.synergies.per.cell.line$AGS)
  } else {
    max.tpr = max(models.tp) / length(observed.synergies.per.cell.line[[cell.line]])
  }
  
  random.model.data[cell.line, "fitness range"] = fit.summary["Max."] - fit.summary["Min."]
  random.model.data[cell.line, "Min fitness"] = fit.summary["Min."]
  random.model.data[cell.line, "Max fitness"] = fit.summary["Max."]
  random.model.data[cell.line, "Mean fitness"] = fit.summary["Mean"]
  random.model.data[cell.line, "Median fitness"] = fit.summary["Median"]
  
  random.model.data[cell.line, "MCC range"] = mcc.summary["Max."] - mcc.summary["Min."]
  random.model.data[cell.line, "Min MCC"] = mcc.summary["Min."]
  random.model.data[cell.line, "Max MCC"] = mcc.summary["Max."]
  random.model.data[cell.line, "Mean MCC"] = mcc.summary["Mean"]
  random.model.data[cell.line, "Median MCC"] = mcc.summary["Median"]
  
  random.model.data[cell.line, "Max TPR"] = max.tpr
}

# color columns
fit.breaks = quantile(random.model.data[,"fitness range"], probs = seq(.05, .95, .05), na.rm = TRUE)
fit.colors = round(seq(255, 40, length.out = length(fit.breaks) + 1), 0) %>%
  {paste0("rgb(255,", ., ",", ., ")")} # red
mcc.breaks = quantile(random.model.data[,"MCC range"], probs = seq(.05, .95, .05), na.rm = TRUE)
mcc.colors = round(seq(255, 40, length.out = length(mcc.breaks) + 1), 0) %>%
  {paste0("rgb(", ., ",255,", ., ")")} # green

caption.title = "Table 4: Fitness, MCC scores and TP (True positives) for the random model predictions across 8 Cell Lines"
datatable(data = random.model.data, 
          options = list(dom = "t"), # just show the table
          caption = htmltools::tags$caption(caption.title, style="color:#dd4814; font-size: 18px")) %>% 
  formatRound(1:11, digits = 3) %>%
  formatStyle(columns = c("fitness range"), backgroundColor = styleInterval(fit.breaks, fit.colors)) %>%
  formatStyle(columns = c("MCC range"), backgroundColor = styleInterval(mcc.breaks, mcc.colors))

The below box plots compare the new fitness scores across all cell lines:

num.of.models = nrow(random.models.stable.state)
data.list = list()

for (cell.line in c(cell.lines, "AGS-gold")) {
  cell.line.vec = as.data.frame(rep(cell.line, num.of.models), stringsAsFactors = FALSE)
  
  if (cell.line == "AGS-gold") {
    models.fitness.cell.specific = remove_rownames(as.data.frame(models.fitness.to.ags.literature.ss))
    models.fitness.random        = remove_rownames(as.data.frame(random.models.fitness.to.ags.literature.ss))
  } else {
    models.fitness.cell.specific = remove_rownames(as.data.frame(models.fitness.per.cell.line.new[[cell.line]]))
    models.fitness.random        = remove_rownames(as.data.frame(random.models.fitness.per.cell.line.new[[cell.line]]))
  }
  
  colnames(models.fitness.cell.specific) = NULL
  colnames(models.fitness.random) = NULL
  
  data.list[[cell.line]] = 
    bind_cols(cell.line.vec, models.fitness.cell.specific, models.fitness.random)
}

data = bind_rows(data.list)
colnames(data) = c("cell.line", "fitness cell-specific", "fitness random")
ggboxplot(data, x = "cell.line", y = c("fitness cell-specific", "fitness random"),
          palette = brewer.pal(3, "Set1"), merge = "asis",
          xlab = "Cell Lines", ylab = "Fitness values", add = "point", add.params = list(size = 0.5))

We observe from Tables 3 and 4 and the boxplots above that all models show a wide range of fitness values since no models were trained to the new steady states (it’s like all models are random-generated). Also, the fitness of the AGS-specific and random models to the literature-curated steady state has the largest possible fitness range and small number of unique fitness values among the models (total of \(13\)).


We checked the following datasets and found no statistically significant correlation between fitness scores and performance metric (TP or MCC):

  • SF295, DU145 with cell-specific models (from Table 3)
  • A498, UACC62, DU145, AGS with random models (from Table 4)


Next, we investigate the AGS-gold dataset (row with the higher fitness and MCC range from Table 3). In this dataset the performance (MCC score) of the cell-specific AGS models is associated with the models fitness to the literature-curated AGS steady state.

AGS-gold Statistical Analysis

Firstly, we:

  • Filter the data by finding the unique models - those that have strictly different boolean equations
  • Remove the unique models with NaN MCC scores
  • Merge the fitness scores and MCC values in a common data.frame object
best.cell.line = "AGS"

# AGS-Gold cell-specific data
fit = models.fitness.to.ags.literature.ss
tp  = models.tp.per.cell.line[[best.cell.line]]
mcc = models.mcc.per.cell.line[[best.cell.line]]

unique.models = rownames(unique(models.link.operators.per.cell.line[[best.cell.line]]))
unique.models = unique.models[!unique.models %in% names(which(is.na(mcc)))]

fit.unique = fit[names(fit) %in% unique.models]
tp.unique  = tp[names(tp) %in% unique.models]
mcc.unique = mcc[names(mcc) %in% unique.models]

df = as.data.frame(cbind(fit.unique, mcc.unique, tp.unique))

Then, we check if the our data is normally distributed (using the Shapiro-Wilk test for normality and the Q-Q plots):

ggqqplot(data = df, x = "fit.unique", ylab = "Fitness")

ggqqplot(data = df, x = "mcc.unique", ylab = "MCC")

shapiro.test(x = sample(x = df$fit.unique, size = 1000))

    Shapiro-Wilk normality test

data:  sample(x = df$fit.unique, size = 1000)
W = 0.739, p-value < 2.2e-16
shapiro.test(x = sample(x = df$mcc.unique, size = 1000))

    Shapiro-Wilk normality test

data:  sample(x = df$mcc.unique, size = 1000)
W = 0.96911, p-value = 9.663e-14

From the above results we observe that both the fitness and MCC scores in both datasets are surely not normally distributed (with statistical significance). Thus, we will use non-parametric correlation scores, namely the Spearman and Kendall rank-based correlation tests, to check the correlation between the two continuous variables (models fitness values and their corresponding MCC score):

ggscatter(df, x = "fit.unique", y = "mcc.unique", 
          title = "AGS-Gold (Cell-specific): Fitness vs Performance - Spearman Correlation",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "spearman"),
          xlab = "Fitness Values", ylab = "MCC scores")

ggscatter(df, x = "fit.unique", y = "mcc.unique", 
          title = "AGS-Gold (Cell-specific): Fitness vs Performance - Kendall Correlation",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall"),
          xlab = "Fitness Values", ylab = "MCC scores")

Fitness vs TP/MCC-class

Split the models to MCC classes:

num.of.mcc.classes = 5
mcc.class.ids = 1:num.of.mcc.classes

# find the clusters
res = Ckmeans.1d.dp(x = df$mcc.unique, k = num.of.mcc.classes)
mcc.class.id = res$cluster
df = cbind(df, mcc.class.id)

plot_mcc_classes_hist(df$mcc.unique, df$mcc.class.id, 
  num.of.mcc.classes, mcc.class.ids)

Box plots to show the difference of the different TP/MCC-classes in terms of fitness score:

ggboxplot(df, x = "mcc.class.id", y = "fit.unique", color = "mcc.class.id",
  palette = usefun:::colors.100[1:num.of.mcc.classes],
  xlab = "MCC class", ylab = "Fitness values", 
  add = c("mean", "mean_sd"), error.plot = "errorbar")

ggboxplot(df, x = "tp.unique", y = "fit.unique", color = "tp.unique",
  palette = usefun:::colors.100[1:length(unique(tp.unique))],
  xlab = "TP class", ylab = "Fitness values", 
  add = c("mean", "mean_sd"), remove = 5)

Fitness-class vs mean MCC

Since the gold-standard AGS steady state has only 23 nodes, our models generate a small number of unique fitness scores to that steady state (total of 13 unique fitness classes). So, instead of grouping the models per performance class (TP or MCC) we will group them by fitness class and show the average MCC value in each class:

num.of.fit.classes = length(unique(df$fit.unique))
fit.class.ids = 1:num.of.fit.classes

# find the fitness classes
res = Ckmeans.1d.dp(x = df$fit.unique, k = num.of.fit.classes)
fit.class.id = res$cluster
df = cbind(df, fit.class.id)

unique.fit.stats = desc_statby(df, measure.var = "mcc.unique", grps = c("fit.unique", "fit.class.id"), ci = 0.95)

ggscatter(unique.fit.stats, x = "fit.unique", y = "mean", 
          title = "Fitness Class vs Performance (MCC) - Kendall Correlation",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall", label.x.npc = 0.7, label.y.npc = 0.1),
          xlab = "Unique Fitness values", ylab = "Mean MCC")

ggbarplot(unique.fit.stats, x = "fit.unique", y = "mean", title = "Fitness Class vs Performance (MCC)", fill = "fit.class.id",
  xlab = "Fitness Score", ylab = "Mean MCC", ylim = c(0, max(unique.fit.stats$mean) + 0.1),
  label = unique.fit.stats$length, lab.vjust = -2) + 
  scale_x_discrete(labels = round(unique.fit.stats$fit.unique, digits = 2)) + 
  scale_fill_gradient(low = "lightblue", high="red", name = "Fitness Class", guide = "legend", breaks = c(1,4,7,10,13)) +
  geom_errorbar(aes(ymin=mean-2*se, ymax=mean+2*se), width = 0.2, position = position_dodge(0.9)) +
  annotate("text", x = 7.5, y = 0.5, label = "Number of models per fitness class are displayed above 95% CI bars for the mean MCC")

# mean -+ 2*se => 95% CI for mean. Use when you are interested in the precision of the means or in comparing and testing differences between means.

There is evident statistical correlation between the models fitness to a curation-based steady state and their performance (measured by MCC score). Models with higher fitness to the steady state tend to have higher MCC score on average.


AGS-PARADIGM(+TF) analysis

What if we take the AGS steady state calculated with the PARADIGM tool (with or without the Transcription Factor activity) and prune to the nodes that constitute the AGS-gold steady state: will there be a correlation between fitness and performance then?


The AGS-Gold nodes are:

pretty_print_vector_names(ags.literature.ss)

23 nodes: AKT_f, Antisurvival, BAX, BCL2, CASP3, CASP8, CCND1, CTNNB1, ERK_f, GSK3_f, JNK_f, KRAS, MAPK14, MMP_f, MYC, NFKB_f, PIK3CA, Prosurvival, PTEN, RAC_f, S6K_f, TCF7_f, TP53

We first find the fitness of the models to the AGS steady state (Paradigm with and without TF included in the calculation) pruned to the AGS-gold nodes:

ags.models.stable.state = models.stable.state.per.cell.line$AGS

models.fitness.to.ags.ss.old = apply(
  ags.models.stable.state[,names(ags.ss.old)], 1, 
  get_percentage_of_matches, ags.ss.old)
models.fitness.to.ags.ss.new = apply(
  ags.models.stable.state[,names(ags.ss.new)], 1, 
  get_percentage_of_matches, ags.ss.new)

fit.old = models.fitness.to.ags.ss.old
fit.new = models.fitness.to.ags.ss.new
fit.unique.old = fit.old[names(fit.old) %in% unique.models]
fit.unique.new = fit.new[names(fit.new) %in% unique.models]

df.old = as.data.frame(cbind(fit.unique.old, mcc.unique, tp.unique))
df.new = as.data.frame(cbind(fit.unique.new, mcc.unique, tp.unique))

Then, we see if there is any correlation between fitness and performance (MCC) using general scatter plots:

ggscatter(df.old, x = "fit.unique.old", y = "mcc.unique", 
          title = "AGS (Paradigm+TF) - Cell-specific: Fitness vs Performance - Kendall's tau",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall"),
          xlab = "Fitness Values", ylab = "MCC scores")

ggscatter(df.new, x = "fit.unique.new", y = "mcc.unique", 
          title = "AGS (Paradigm) - Cell-specific: Fitness vs Performance - Kendall's tau",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall"),
          xlab = "Fitness Values", ylab = "MCC scores")

There is almost no correlation between fitness and performance when the fitness of the models is calculated with the same nodes as in AGS-gold steady state, but with the values as they were calculated by PARADIGM or PARADIGM including TF activity. This proves that the best models are those that fit to the true steady state and not the wrongly assessed ones!


To better visualize the absense of correlation, we group the models to distinct fitness classes and show the average MCC per class with the corresponding 95% CI.

First for the PARADIGM+TF (old) calculated steady state for AGS, pruned to the AGS-gold nodes:

num.of.fit.classes = length(unique(df.old$fit.unique.old))
fit.class.ids = 1:num.of.fit.classes

# find the fitness classes
res = Ckmeans.1d.dp(x = df.old$fit.unique.old, k = num.of.fit.classes)
fit.class.id = res$cluster
df.old = cbind(df.old, fit.class.id)

unique.fit.stats.old = desc_statby(df.old, measure.var = "mcc.unique", grps = c("fit.unique.old", "fit.class.id"), ci = 0.95)
  
ggbarplot(unique.fit.stats.old, x = "fit.unique.old", y = "mean", title = "Fitness Class vs Performance (MCC) - Paradigm + TF", fill = "fit.class.id",
  xlab = "Fitness Score", ylab = "Mean MCC", ylim = c(0, max(unique.fit.stats.old$mean) + 0.1),
  label = unique.fit.stats.old$length, lab.vjust = -2) + 
  scale_x_discrete(labels = round(unique.fit.stats.old$fit.unique.old, digits = 2)) + 
  scale_fill_gradient(low = "lightblue", high="red", name = "Fitness Class", guide = "legend", breaks = c(1,4,7)) +
  geom_errorbar(aes(ymin=mean-2*se, ymax=mean+2*se), width = 0.2, position = position_dodge(0.9)) +
  annotate("text", x = 4.5, y = 0.38, label = "Number of models per fitness class are displayed above 95% CI bars for the mean MCC")

# mean -+ 2*se => 95% CI for mean. Use when you are interested in the precision of the means or in comparing and testing differences between means.

And secondly for the PARADIGM only (new) calculated steady state for AGS, pruned to the AGS-gold nodes:

num.of.fit.classes = length(unique(df.new$fit.unique.new))
fit.class.ids = 1:num.of.fit.classes

# find the fitness classes
res = Ckmeans.1d.dp(x = df.new$fit.unique.new, k = num.of.fit.classes)
fit.class.id = res$cluster
df.new = cbind(df.new, fit.class.id)

unique.fit.stats.new = desc_statby(df.new, measure.var = "mcc.unique", grps = c("fit.unique.new", "fit.class.id"), ci = 0.95)

ggbarplot(unique.fit.stats.new, x = "fit.unique.new", y = "mean", title = "Fitness Class vs Performance (MCC) - Paradigm only", fill = "fit.class.id",
  xlab = "Fitness Score", ylab = "Mean MCC", ylim = c(0, max(unique.fit.stats.new$mean) + 0.1),
  label = unique.fit.stats.new$length, lab.vjust = -2) + 
  scale_x_discrete(labels = round(unique.fit.stats.new$fit.unique.new, digits = 2)) + 
  scale_fill_gradient(low = "lightblue", high="red", name = "Fitness Class", guide = "legend", breaks = c(1,4,7)) +
  geom_errorbar(aes(ymin=mean-2*se, ymax=mean+2*se), width = 0.2, position = position_dodge(0.9)) +
  annotate("text", x = 4.5, y = 0.35, label = "Number of models per fitness class are displayed above 95% CI bars for the mean MCC")

# mean -+ 2*se => 95% CI for mean. Use when you are interested in the precision of the means or in comparing and testing differences between means.

ASG-Gold Random analysis

In this section we want to investigate how the random models performance correlates with the fitness to the AGS-gold steady state, as well as the pruned PARADIGM steady state (with or with TF) - pruned to the 23 nodes of the AGS-gold ss.


First, we calculate the new fitness values for the random models, for each steady state case (AGS-Gold, PARADIGM only (new), PARADIGM+TF (old)):

# Get the MCC scores of the random models for the AGS
mcc = random.models.mcc.per.cell.line$AGS

# find the unique models
unique.models = rownames(unique(random.models.link.operator))

# remove models with NaN MCC scores
unique.models = unique.models[!unique.models %in% names(which(is.na(mcc)))]

# Prune MCC scores to unique models
mcc.unique = mcc[names(mcc) %in% unique.models]

# Find fitness of the random models to the 3 steady states
# random.models.fitness.to.ags.literature.ss (already calculated for AGS-Gold)
random.models.fitness.to.ags.ss.old = apply(
  random.models.stable.state[,names(ags.ss.old)], 1, 
  get_percentage_of_matches, ags.ss.old)
random.models.fitness.to.ags.ss.new = apply(
  random.models.stable.state[,names(ags.ss.new)], 1, 
  get_percentage_of_matches, ags.ss.new)

fit.gold = random.models.fitness.to.ags.literature.ss
fit.old = random.models.fitness.to.ags.ss.old
fit.new = random.models.fitness.to.ags.ss.new

fit.unique.gold = fit.gold[names(fit.gold) %in% unique.models]
fit.unique.old = fit.old[names(fit.old) %in% unique.models]
fit.unique.new = fit.new[names(fit.new) %in% unique.models]

df.gold.random = as.data.frame(cbind(fit.unique.gold, mcc.unique))
df.old.random = as.data.frame(cbind(fit.unique.old, mcc.unique))
df.new.random = as.data.frame(cbind(fit.unique.new, mcc.unique))

Then, we check to see if there is any correlation between fitness and performance (MCC) using general scatter plots:

ggscatter(df.gold.random, x = "fit.unique.gold", y = "mcc.unique", 
          title = "AGS-Gold - Random: Fitness vs Performance - Kendall's tau",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall"),
          xlab = "Fitness Values", ylab = "MCC scores")

ggscatter(df.old.random, x = "fit.unique.old", y = "mcc.unique", 
          title = "AGS (Paradigm+TF) - Random: Fitness vs Performance - Kendall's tau",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall"),
          xlab = "Fitness Values", ylab = "MCC scores")

ggscatter(df.new.random, x = "fit.unique.new", y = "mcc.unique", 
          title = "AGS (Paradigm) - Random: Fitness vs Performance - Kendall's tau",
          add = "reg.line", conf.int = TRUE, 
          cor.coef = TRUE, cor.coeff.args = list(method = "kendall"),
          xlab = "Fitness Values", ylab = "MCC scores")

There is very small correlation between fitness and performance in the case of fitness to the AGS-Gold steady state and the one that is calculated with the same nodes as in AGS-gold steady state, but with the values as they were calculated by PARADIGM including TF activity.


To better visualize the correlation relationship (if there is any), we group the models to distinct fitness classes and show the average MCC per class with the corresponding 95% CI.

First for the AGS-Gold steady state:

num.of.fit.classes = length(unique(df.gold.random$fit.unique.gold))
fit.class.ids = 1:num.of.fit.classes

# find the fitness classes
res = Ckmeans.1d.dp(x = df.gold.random$fit.unique.gold, k = num.of.fit.classes)
fit.class.id = res$cluster
df.gold.random = cbind(df.gold.random, fit.class.id)

unique.fit.stats.gold = desc_statby(df.gold.random, measure.var = "mcc.unique", grps = c("fit.unique.gold", "fit.class.id"), ci = 0.95)

ggbarplot(unique.fit.stats.gold, x = "fit.unique.gold", y = "mean", title = "Fitness Class vs Performance (MCC) - AGS-Gold - Random", fill = "fit.class.id",
  xlab = "Fitness Score", ylab = "Mean MCC", ylim = c(0, max(unique.fit.stats.gold$mean) + 0.1),
  label = unique.fit.stats.gold$length, lab.vjust = -2) + 
  scale_x_discrete(labels = round(unique.fit.stats.gold$fit.unique.gold, digits = 2)) + 
  scale_fill_gradient(low = "lightblue", high="red", name = "Fitness Class", guide = "legend", breaks = c(1,4,7,10)) +
  geom_errorbar(aes(ymin=mean-2*se, ymax=mean+2*se), width = 0.2, position = position_dodge(0.9)) +
  annotate("text", x = 5.5, y = 0.48, label = "Number of models per fitness class are displayed above 95% CI bars for the mean MCC")

# mean -+ 2*se => 95% CI for mean. Use when you are interested in the precision of the means or in comparing and testing differences between means.

Secondly, for the PARADIGM+TF (old) calculated steady state for AGS, pruned to the AGS-gold nodes:

num.of.fit.classes = length(unique(df.old.random$fit.unique.old))
fit.class.ids = 1:num.of.fit.classes

# find the fitness classes
res = Ckmeans.1d.dp(x = df.old.random$fit.unique.old, k = num.of.fit.classes)
fit.class.id = res$cluster
df.old.random = cbind(df.old.random, fit.class.id)

unique.fit.stats.old = desc_statby(df.old.random, measure.var = "mcc.unique", grps = c("fit.unique.old", "fit.class.id"), ci = 0.95)

ggbarplot(unique.fit.stats.old, x = "fit.unique.old", y = "mean", title = "Fitness Class vs Performance (MCC) - Paradigm+TF - Random", fill = "fit.class.id",
  xlab = "Fitness Score", ylab = "Mean MCC", ylim = c(0, max(unique.fit.stats.old$mean) + 0.1),
  label = unique.fit.stats.old$length, lab.vjust = -2) + 
  scale_x_discrete(labels = round(unique.fit.stats.old$fit.unique.old, digits = 2)) + 
  scale_fill_gradient(low = "lightblue", high="red", name = "Fitness Class", guide = "legend", breaks = c(1,4,7,9)) +
  geom_errorbar(aes(ymin=mean-2*se, ymax=mean+2*se), width = 0.2, position = position_dodge(0.9)) +
  annotate("text", x = 5.5, y = 0.48, label = "Number of models per fitness class are displayed above 95% CI bars for the mean MCC")

# mean -+ 2*se => 95% CI for mean. Use when you are interested in the precision of the means or in comparing and testing differences between means.

Lastly, for the PARADIGM (new) calculated steady state for AGS, pruned to the AGS-gold nodes:

num.of.fit.classes = length(unique(df.new.random$fit.unique.new))
fit.class.ids = 1:num.of.fit.classes

# find the fitness classes
res = Ckmeans.1d.dp(x = df.new.random$fit.unique.new, k = num.of.fit.classes)
fit.class.id = res$cluster
df.new.random = cbind(df.new.random, fit.class.id)

unique.fit.stats.new = desc_statby(df.new.random, measure.var = "mcc.unique", grps = c("fit.unique.new", "fit.class.id"), ci = 0.95)

ggbarplot(unique.fit.stats.new, x = "fit.unique.new", y = "mean", title = "Fitness Class vs Performance (MCC) - Paradigm - Random", fill = "fit.class.id",
  xlab = "Fitness Score", ylab = "Mean MCC", ylim = c(0, max(unique.fit.stats.new$mean) + 0.1),
  label = unique.fit.stats.new$length, lab.vjust = -2) + 
  scale_x_discrete(labels = round(unique.fit.stats.new$fit.unique.new, digits = 2)) + 
  scale_fill_gradient(low = "lightblue", high="red", name = "Fitness Class", guide = "legend", breaks = c(1,4,7,10)) +
  geom_errorbar(aes(ymin=mean-2*se, ymax=mean+2*se), width = 0.2, position = position_dodge(0.9)) +
  annotate("text", x = 5.5, y = 0.48, label = "Number of models per fitness class are displayed above 95% CI bars for the mean MCC")

# mean -+ 2*se => 95% CI for mean. Use when you are interested in the precision of the means or in comparing and testing differences between means.

R session info

xfun::session_info()
R version 3.6.1 (2019-07-05)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 18.04.3 LTS, RStudio 1.2.5001

Locale:
  LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
  LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
  LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
  LC_PAPER=en_US.UTF-8       LC_NAME=C                 
  LC_ADDRESS=C               LC_TELEPHONE=C            
  LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

Package version:
  assertthat_0.2.1     backports_1.1.5      base64enc_0.1-3     
  BH_1.69.0.1          bibtex_0.4.2         circlize_0.4.8      
  Ckmeans.1d.dp_4.3.0  cli_1.1.0            clue_0.3-57         
  cluster_2.1.0        codetools_0.2-16     colorspace_1.4-1    
  compiler_3.6.1       ComplexHeatmap_2.0.0 cowplot_1.0.0       
  crayon_1.3.4         crosstalk_1.0.0      datasets_3.6.1      
  digest_0.6.21        dplyr_0.8.3          DT_0.9              
  ellipsis_0.3.0       emba_0.1.2           evaluate_0.14       
  fansi_0.4.0          fastmap_1.0.1        gbRd_0.4-11         
  GetoptLong_0.1.7     ggplot2_3.2.1        ggpubr_0.2.3        
  ggrepel_0.8.1        ggsci_2.9            ggsignif_0.6.0      
  GlobalOptions_0.1.1  glue_1.3.1           graphics_3.6.1      
  grDevices_3.6.1      grid_3.6.1           gridExtra_2.3       
  gtable_0.3.0         highr_0.8            htmltools_0.4.0     
  htmlwidgets_1.5.1    httpuv_1.5.2         igraph_1.2.4.1      
  jsonlite_1.6         knitr_1.25           labeling_0.3        
  later_1.0.0          lattice_0.20.38      lazyeval_0.2.2      
  lifecycle_0.1.0      magrittr_1.5         markdown_1.1        
  MASS_7.3-51.4        Matrix_1.2.17        methods_3.6.1       
  mgcv_1.8.29          mime_0.7             munsell_0.5.0       
  nlme_3.1.141         nnet_7.3-12          packrat_0.5.0       
  parallel_3.6.1       pillar_1.4.2         pkgconfig_2.0.3     
  plogr_0.2.0          plyr_1.8.4           png_0.1-7           
  polynom_1.4.0        promises_1.1.0       pscl_1.5.2          
  purrr_0.3.2          R6_2.4.0             RColorBrewer_1.1-2  
  Rcpp_1.0.2           Rdpack_0.11-0        reshape2_1.4.3      
  rje_1.10.10          rjson_0.2.20         rlang_0.4.0         
  rmarkdown_1.16       rstudioapi_0.10      scales_1.0.0        
  shape_1.4.4          shiny_1.4.0          sourcetools_0.1.7   
  splines_3.6.1        stats_3.6.1          stringi_1.4.3       
  stringr_1.4.0        tibble_2.1.3         tidyr_1.0.0         
  tidyselect_0.2.5     tinytex_0.16         tools_3.6.1         
  usefun_0.4.3         utf8_1.1.4           utils_3.6.1         
  vctrs_0.2.0          viridisLite_0.3.0    visNetwork_2.0.8    
  withr_2.1.2          xfun_0.10            xtable_1.8-4        
  yaml_2.2.0           zeallot_0.1.0       
LS0tCnRpdGxlOiAiQ2FzY2FkZSBQZXJmb3JtYW5jZSB2cyBGaXRuZXNzIE1vZGVsIEFuYWx5c2lzIgphdXRob3I6ICJbSm9obiBab2JvbGFzXShodHRwczovL2dpdGh1Yi5jb20vYmJsb2Rmb24pIgpkYXRlOiAiTGFzdCB1cGRhdGVkOiBgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVkICVCLCAlWScpYCIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBjc3M6IHN0eWxlLmNzcwogICAgdGhlbWU6IHVuaXRlZAogICAgdG9jOiB0cnVlCiAgICB0b2NfZmxvYXQ6CiAgICAgIGNvbGxhcHNlZDogZmFsc2UKICAgICAgc21vb3RoX3Njcm9sbDogdHJ1ZQogICAgbnVtYmVyX3NlY3Rpb25zOiBmYWxzZQogICAgY29kZV9mb2xkaW5nOiBoaWRlCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCi0tLQoKYGBge3IgUmVuZGVyIGNvbW1hbmQsIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CiNybWFya2Rvd246OnJlbmRlcihpbnB1dCA9ICIuL3BlcmZvcm1hbmNlX3ZzX2ZpdG5lc3MuUm1kIiwgb3V0cHV0X2Zvcm1hdCA9ICJodG1sX2RvY3VtZW50Iiwgb3V0cHV0X2RpciA9ICIuLi8uLi9kb2NzL2Nhc2NhZGUvY2VsbC1saW5lcy0yNTAwLyIpCmBgYAoKIyMgSW50cm8gey19CgpUaGUgcHVycG9zZSBvZiB0aGlzIGFuYWx5c2lzIGlzIHRvIGZpbmQgYSBjb3JyZWxhdGlvbiBiZXR3ZWVuIHRoZSBib29sZWFuIG1vZGVscwoqKmZpdG5lc3MgdG8gYSBzdGVhZHkgc3RhdGUgYWN0aXZpdHkgcHJvZmlsZSoqIGFuZCB0aGVpciAqKnBlcmZvcm1hbmNlKiogaW4gdGVybXMgb2YgdGhlCm51bWJlciBvZiAqVHJ1ZSBQb3NpdGl2ZSogKFRQKSBzeW5lcmdpZXMgcHJlZGljdGVkIGFuZC9vciB0aGUgb3ZlcmFsbCAqTUNDIHNjb3JlKgooTWF0dGhld3MgQ29ycmVsYXRpb24gQ29lZmZpY2llbnQgc2NvcmUpLiBXZSB3YW50IHRvIHNob3cgdGhhdCAqKmEgY2xvc2VyIGZpdG5lc3MgCnRvIHRoZSBzdGVhZHkgc3RhdGUgc3VnZ2VzdHMgbW9yZSBwcmVkaWN0aXZlIG1vZGVscyoqLCBjb3Jyb2JvcmF0aW5nIHRodXMgb3VyIHByb29mIG9mIGNvbmNlcHQgb2YgdXNpbmcgYW4gZW5zZW1ibGUtYmFzZWQgYXBwcm9hY2ggd2hlcmUgbW9kZWxzIGFyZSB0cmFpbmVkIHRvd2FyZHMgYSAKc3BlY2lmaWMgc3RlYWR5IHN0YXRlIHNpZ25hbGluZyBwYXR0ZXJuIGZvciBkcnVnIGNvbWJpbmF0aW9uIHByZWRpY3Rpb25zLgoKVGhlIGJvb2xlYW4gbW9kZWwgZGF0YXNldHMgd2Ugd2lsbCB1c2UgYXJlIGluIHRvdGFsICQ5JDogb25lIGZvciBlYWNoIGNlbGwgbGluZSAKb2YgaW50ZXJlc3QgKDggY2VsbCBsaW5lcykgd2hlcmUgdGhlIG1vZGVscyB3ZXJlICoqZml0dGVkIHRvIGEgc3BlY2lmaWMgc3RlYWR5IHN0YXRlKiogaW4gZWFjaCAKY2FzZSBhbmQgb25lIGZvciB0aGUgc28tY2FsbGVkICoqcmFuZG9tIG1vZGVscyoqIHdoaWNoIHdlcmUgZ2VuZXJhdGVkICpyYW5kb21seSogaW4gCnRoZSBzZW5zZSB0aGF0IHdlcmUgZml0dGVkIG9ubHkgdG8gYSBwcm9saWZlcmF0aW9uIHN0YXRlIChzaW11bGF0aW9ucyB3ZXJlIGRvbmUgdXNpbmcgCnRoZSBEcnVnTG9naWNzIHNvZnR3YXJlIG1vZHVsZXMgYEdpdHNiZWAgYW5kIGBEcmFibWVgKS4KCkVhY2ggYm9vbGVhbiBtb2RlbCBkYXRhc2V0IGNvbnN0aXR1ZXMgb2Y6CgotIFRoZSAqKm1vZGVsIHByZWRpY3Rpb25zKiogZmlsZSB3aGljaCBoYXMgZm9yIGVhY2ggbW9kZWwgdGhlIHByZWRpY3Rpb24gZm9yIAplYWNoIGRydWcgY29tYmluYXRpb24gdGVzdGVkICgqMCogPSBubyBzeW5lcmd5IHByZWRpY3RlZCwgKjEqID0gc3luZXJneSAKcHJlZGljdGVkLCAqTkEqID0gY291bGRuJ3QgZmluZCBzdGFibGUgc3RhdGVzIGluIGVpdGhlciB0aGUgZHJ1ZyBjb21iaW5hdGlvbiAKaW5oaWJpdGVkIG1vZGVsIG9yIGluIGFueSBvZiB0aGUgdHdvIHNpbmdsZS1kcnVnIGluaGliaXRlZCBtb2RlbHMpCi0gVGhlICoqbW9kZWxzIHN0YWJsZSBzdGF0ZSoqIChvbmUgcGVyIG1vZGVsKS4gQSAqKmZpdG5lc3Mgc2NvcmUqKgpmb3IgZWFjaCBtb2RlbCBjYW4gZWFzaWx5IGJlIGNhbGN1bGF0ZWQgdGhlbiBieSBtYXRjaGluZyB0aGUgbW9kZWwncyBzdGFibGUgCnN0YXRlICh3aGljaCBpcyBzb21ldGhpbmcgaW5oZXJlbnQgaW4gdGhlIGJvb2xlYW4ncyBtb2RlbCBzdHJ1Y3R1cmUsIGEgdW5pcXVlIApmaXhwb2ludCBhdHRyYWN0b3IpIHdpdGggdGhlIHN0ZWFkeSBzdGF0ZSBvZiBpbnRlcmVzdCwgbm9kZSBwZXIgbm9kZS4KQSAqKmhpZ2hlciBmaXRuZXNzIHNjb3JlKiogd291bGQgbWVhbiBhIGJldHRlciBtYXRjaCBvZiBhIG1vZGVsJ3MgCnN0YWJsZSBzdGF0ZSB0byB0aGUgY2VsbCBsaW5lIGRlcml2ZWQgc3RlYWR5IHN0YXRlIChhIHBlcmZlY3QgbWF0Y2ggd291bGQgcmVzdWx0IAppbiBhIGZpdG5lc3Mgb2YgMSkuCi0gVGhlICoqbW9kZWxzIGxpbmsgb3BlcmF0b3JzKiogd2hpY2ggaXMgYSByZXByZXNlbnRhdGlvbiBvZiB0aGUgYm9vbGVhbiBlcXVhdGlvbnMKb2YgZWFjaCBtb2RlbC4gRWFjaCBib29sZWFuIGVxdWF0aW9uIGlzIGluIHRoZSBmb3JtOiAqKlRhcmdldCAqPSAoQWN0aXZhdG9yIE9SIEFjdGl2YXRvciBPUi4uLikgQU5EIE5PVCAoSW5oaWJpdG9yIE9SIEluaGliaXRvciBPUi4uLikqKiBhbmQgdGhlIGRpZmZlcmVuY2UgYmV0d2VlbiB0aGUgbW9kZWxzIGNhbiBiZSBmb3VuZCBpbiB0aGUgCipsaW5rIG9wZXJhdG9yKiAoKjEqID0gJ09SIE5PVCcsICowKiA9ICdBTkQgTk9UJywgb3IgYWJzZW50KSB3aGljaCBoYXMgYmVlbiAKbXV0YXRlZCAoY2hhbmdlZCkgdGhyb3VnaCB0aGUgZ2VuZXRpYyBhbGdvcml0aG0gaW4gYEdpdHNiZWAuIE5vdGUgdGhhdCB0aGUgZXF1YXRpb25zIHRoYXQgZG8gCm5vdCBoYXZlIGxpbmsgb3BlcmF0b3JzIGFyZSAqdGhlIHNhbWUgZm9yIGV2ZXJ5IG1vZGVsKiBhbmQgYXJlIHRodXMgZGlzY2FyZGVkLgotIFRoZSAqKm9ic2VydmVkIHN5bmVyZ2llcyoqIGZpbGUgd2hpY2ggbGlzdHMgdGhlIGRydWcgY29tYmluYXRpb25zIHRoYXQgd2VyZSAKb2JzZXJ2ZWQgYXMgc3luZXJnaXN0aWMgZm9yIGVhY2ggY2VsbCBsaW5lLgotIFRoZSAqKnN0ZWFkeSBzdGF0ZSoqIGZpbGUgd2hpY2ggbGlzdHMgdGhlIG5ldHdvcmsgbm9kZXMgKHByb3RlaW4sIGdlbmUsIGNvbXBsZXhlcwpuYW1lcywgZXRjKSBhbmQgdGhlaXIgYWN0aXZpdHkgdmFsdWUgKDAgb3IgMSwgcmVwcmVzZW50aW5nIGFuIGluaGliaXRlZCBvciBhY3RpdmUKbm9kZSByZXNwZWN0aXZlbHkpLiAKVGhpcyBpbnB1dCBpcyBwcm92aWRlZCBwZXIgY2VsbCBsaW5lIGFuZCBub3QgZm9yIHRoZSByYW5kb20gbW9kZWxzIHNpbmNlIHRoZXkgYXJlIGp1c3QgdHJhaW5lZCB0byBhIHByb2ZpbGVyYXRpb24gc3RhdGUuCgojIyBJbnB1dCB7LX0KCkxvYWRpbmcgbGlicmFyaWVzOgpgYGB7ciBMb2FkIGxpYnJhcmllcywgbWVzc2FnZSA9IEZBTFNFfQpsaWJyYXJ5KERUKQpsaWJyYXJ5KGdncHVicikKbGlicmFyeShlbWJhKQpsaWJyYXJ5KHVzZWZ1bikKbGlicmFyeShubmV0KQpsaWJyYXJ5KHBzY2wpCmxpYnJhcnkoQ29tcGxleEhlYXRtYXApCmxpYnJhcnkoY2lyY2xpemUpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkodGliYmxlKQpsaWJyYXJ5KENrbWVhbnMuMWQuZHApCmxpYnJhcnkoUkNvbG9yQnJld2VyKQpgYGAKCkZpcnN0IHdlIGxvYWQgdGhlIGNlbGwtc3BlY2lmaWMgaW5wdXQgZGF0YToKYGBge3IgQ2VsbC1zcGVjaWZpYyBJbnB1dCwgY2FjaGU9VFJVRX0KIyBDZWxsIExpbmVzCmNlbGwubGluZXMgPSBjKCJBNDk4IiwgIkFHUyIsICJEVTE0NSIsICJjb2xvMjA1IiwgIlNXNjIwIiwgIlNGMjk1IiwgIlVBQ0M2MiIsICJNREEtTUItNDY4IikKCmNlbGwubGluZS5kaXJzID0gc2FwcGx5KGNlbGwubGluZXMsIGZ1bmN0aW9uKGNlbGwubGluZSkgewogIHBhc3RlMChnZXR3ZCgpLCAiLyIsIGNlbGwubGluZSkKfSkKCiMgTW9kZWwgcHJlZGljdGlvbnMKbW9kZWwucHJlZGljdGlvbnMuZmlsZXMgPSBzYXBwbHkoY2VsbC5saW5lLmRpcnMsIGZ1bmN0aW9uKGNlbGwubGluZS5kaXIpIHsKICBwYXN0ZTAoY2VsbC5saW5lLmRpciwgIi9tb2RlbF9wcmVkaWN0aW9ucyIpCn0pCgptb2RlbC5wcmVkaWN0aW9ucy5wZXIuY2VsbC5saW5lID0gbGFwcGx5KG1vZGVsLnByZWRpY3Rpb25zLmZpbGVzLCAKICBmdW5jdGlvbihmaWxlKSB7CiAgICBnZXRfbW9kZWxfcHJlZGljdGlvbnMoZmlsZSkKICB9CikKCiMgT2JzZXJ2ZWQgc3luZXJnaWVzCm9ic2VydmVkLnN5bmVyZ2llcy5maWxlcyA9IHNhcHBseShjZWxsLmxpbmUuZGlycywgZnVuY3Rpb24oY2VsbC5saW5lLmRpcikgewogIHBhc3RlMChjZWxsLmxpbmUuZGlyLCAiL29ic2VydmVkX3N5bmVyZ2llcyIpCn0pCgpvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZSA9IGxhcHBseShvYnNlcnZlZC5zeW5lcmdpZXMuZmlsZXMsIAogIGZ1bmN0aW9uKGZpbGUpIHsKICAgIGdldF9vYnNlcnZlZF9zeW5lcmdpZXMoZmlsZSkKICB9CikKCiMgTW9kZWxzIFN0YWJsZSBTdGF0ZSAoMSBwZXIgbW9kZWwpCm1vZGVscy5zdGFibGUuc3RhdGUuZmlsZXMgPSBzYXBwbHkoY2VsbC5saW5lLmRpcnMsIGZ1bmN0aW9uKGNlbGwubGluZS5kaXIpIHsKICBwYXN0ZTAoY2VsbC5saW5lLmRpciwgIi9tb2RlbHNfc3RhYmxlX3N0YXRlIikKfSkKCm1vZGVscy5zdGFibGUuc3RhdGUucGVyLmNlbGwubGluZSA9IGxhcHBseShtb2RlbHMuc3RhYmxlLnN0YXRlLmZpbGVzLAogIGZ1bmN0aW9uKGZpbGUpIHsKICAgIGFzLm1hdHJpeChyZWFkLnRhYmxlKGZpbGUsIGNoZWNrLm5hbWVzID0gRkFMU0UpKQogIH0KKQoKIyBNb2RlbHMgTGluayBPcGVyYXRvcnMKbW9kZWxzLmxpbmsub3BlcmF0b3IuZmlsZXMgPSBzYXBwbHkoY2VsbC5saW5lLmRpcnMsIGZ1bmN0aW9uKGNlbGwubGluZS5kaXIpIHsKICBwYXN0ZTAoY2VsbC5saW5lLmRpciwgIi9tb2RlbHNfbGlua19vcGVyYXRvciIpCn0pCgptb2RlbHMubGluay5vcGVyYXRvcnMucGVyLmNlbGwubGluZSA9IGxhcHBseShtb2RlbHMubGluay5vcGVyYXRvci5maWxlcywKICBmdW5jdGlvbihmaWxlKSB7CiAgICBhcy5tYXRyaXgocmVhZC50YWJsZShmaWxlLCBjaGVjay5uYW1lcyA9IEZBTFNFKSkKICB9CikKCiMgdGhlIG5vZGUgbmFtZXMgdXNlZCBpbiBvdXIgYW5hbHlzaXMKbm9kZS5uYW1lcyA9IGNvbG5hbWVzKG1vZGVscy5zdGFibGUuc3RhdGUucGVyLmNlbGwubGluZVtbMV1dKQoKIyBTdGVhZHkgU3RhdGVzCnN0ZWFkeS5zdGF0ZS5maWxlcyA9IHNhcHBseShjZWxsLmxpbmUuZGlycywgZnVuY3Rpb24oY2VsbC5saW5lLmRpcikgewogIHBhc3RlMChjZWxsLmxpbmUuZGlyLCAiL3N0ZWFkeV9zdGF0ZSIpCn0pCgpzdGVhZHkuc3RhdGUucGVyLmNlbGwubGluZSA9IGxhcHBseShzdGVhZHkuc3RhdGUuZmlsZXMsCiAgZnVuY3Rpb24oZmlsZSkgewogICAgc3MuZGYgPSByZWFkLnRhYmxlKGZpbGUsIHNlcCA9ICJcdCIsIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkKICAgIHN0ZWFkeS5zdGF0ZSA9IHNzLmRmWywyXQogICAgbmFtZXMoc3RlYWR5LnN0YXRlKSA9IHNzLmRmWywxXQogICAgCiAgICAjIGNoYW5nZSB2YWx1ZSB0byBOQSBmb3Igbm9kZXMgZm9yIHdoaWNoIHRoZXJlIHdhcyBubyBhY3Rpdml0eSBmb3VuZCAoZGFzaCkKICAgIHN0ZWFkeS5zdGF0ZVtzdGVhZHkuc3RhdGUgPT0gIi0iXSA9IE5BCiAgICAKICAgICMga2VlcCBvbmx5IHRoZSBub2RlcyB0aGF0IGFyZSBpbmNsdWRlZCBpbiB0aGUgYW5hbHlzaXMKICAgIHN0ZWFkeS5zdGF0ZSA9IHBydW5lX2FuZF9yZW9yZGVyX3ZlY3RvcihzdGVhZHkuc3RhdGUsIG5vZGUubmFtZXMpCiAgICAKICAgICMgY2hlY2sKICAgIHN0b3BpZm5vdChuYW1lcyhzdGVhZHkuc3RhdGUpICVpbiUgbm9kZS5uYW1lcykKICAgIAogICAgIyByZXR1cm4gYW4gaW50ZWdlciB2ZWN0b3Igc2luY2UgdGhlIGFjdGl2aXR5IHZhbHVlcyBhcmUgYmluYXJpemVkICgwLDEpCiAgICByZXR1cm4oc2FwcGx5KHN0ZWFkeS5zdGF0ZSwgYXMuaW50ZWdlcikpCiAgfQopCmBgYAoKVGhlIHJhbmRvbSBtb2RlbCBpbnB1dCBkYXRhOgpgYGB7ciBSYW5kb20gbW9kZWwgSW5wdXR9CnJhbmRvbS5kaXIgPSBwYXN0ZTAoZ2V0d2QoKSwgIi9yYW5kb20iKQpyYW5kb20ubW9kZWwucHJlZGljdGlvbnMgPSBnZXRfbW9kZWxfcHJlZGljdGlvbnMocGFzdGUwKHJhbmRvbS5kaXIsICIvbW9kZWxfcHJlZGljdGlvbnMiKSkKCnJhbmRvbS5tb2RlbHMuc3RhYmxlLnN0YXRlID0gYXMubWF0cml4KAogIHJlYWQudGFibGUoZmlsZSA9IHBhc3RlMChyYW5kb20uZGlyLCAiL21vZGVsc19zdGFibGVfc3RhdGUiKSwgY2hlY2submFtZXMgPSBGQUxTRSkKKQoKcmFuZG9tLm1vZGVscy5saW5rLm9wZXJhdG9yID0KICBhcy5tYXRyaXgocmVhZC50YWJsZShmaWxlID0gcGFzdGUwKHJhbmRvbS5kaXIsICIvbW9kZWxzX2xpbmtfb3BlcmF0b3IiKSwgY2hlY2submFtZXMgPSBGQUxTRSkpCmBgYAoKIyMgTW9kZWwgQW5hbHlzaXMgey19CgpJbiBvcmRlciB0byBmaW5kIHRoZSBudW1iZXIgb2YgdHJ1ZSBwb3NpdGl2ZSBwcmVkaWN0ZWQgc3luZXJnaWVzLCBNQ0Mgc2NvcmVzIGFuZCBmaXRuZXNzIHNjb3JlcyBmb3IgZWFjaCBvZiB0aGUgbW9kZWxzIGluIGVhY2ggb2YgdGhlIDkgZGF0YXNldHMsIHdlIHVzZSBmdW5jdGlvbnMgZnJvbSB0aGUgW2VtYmFdKGh0dHBzOi8vZ2l0aHViLmNvbS9iYmxvZGZvbi9lbWJhKSBSIHBhY2thZ2UuCgojIyMgQ2VsbC1zcGVjaWZpYyB7LX0KCldlIGZpbmQgdGhlIE1DQywgVFAgYW5kIGZpdG5lc3MgdmFsdWVzIGZvciBlYWNoIG1vZGVsIHBlciBjZWxsIGxpbmUgKG5vdGUKdGhhdCBlYWNoIG1vZGVsJ3Mgc3RhYmxlIHN0YXRlIGluIGEgc3BlY2lmaWMgY2VsbCBsaW5lIGlzIG1hdGNoZWQgYWdhaW5zdCB0aGUgCnN0ZWFkeSBzdGF0ZSBmcm9tIHRoYXQgY2VsbCBsaW5lKToKYGBge3IgQ2VsbC1zcGVjaWZpYyBNb2RlbHMgVFAgKyBNQ0MgKyBmaXRuZXNzIHBlciBjZWxsIGxpbmUsIGNhY2hlPVRSVUV9Cm1vZGVscy50cC5wZXIuY2VsbC5saW5lID0gbGlzdCgpCm1vZGVscy5tY2MucGVyLmNlbGwubGluZSA9IGxpc3QoKQptb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lID0gbGlzdCgpCgpmb3IgKGNlbGwubGluZSBpbiBjZWxsLmxpbmVzKSB7CiAgbW9kZWwucHJlZGljdGlvbnMgPSBtb2RlbC5wcmVkaWN0aW9ucy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIG9ic2VydmVkLnN5bmVyZ2llcyA9IG9ic2VydmVkLnN5bmVyZ2llcy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIG51bWJlci5vZi5kcnVnLmNvbWIudGVzdGVkID0gbmNvbChtb2RlbC5wcmVkaWN0aW9ucy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXSkKICAKICAjIFNwbGl0IG1vZGVsLnByZWRpY3Rpb25zIHRvIHBvc2l0aXZlIChvYnNlcnZlZCkgYW5kIG5lZ2F0aXZlIChub24tb2JzZXJ2ZWQpIHJlc3VsdHMKICBvYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucyA9CiAgICBnZXRfb2JzZXJ2ZWRfbW9kZWxfcHJlZGljdGlvbnMobW9kZWwucHJlZGljdGlvbnMsIG9ic2VydmVkLnN5bmVyZ2llcykKICB1bm9ic2VydmVkLm1vZGVsLnByZWRpY3Rpb25zID0KICAgIGdldF91bm9ic2VydmVkX21vZGVsX3ByZWRpY3Rpb25zKG1vZGVsLnByZWRpY3Rpb25zLCBvYnNlcnZlZC5zeW5lcmdpZXMpCiAgCiAgIyBDb3VudCB0aGUgcHJlZGljdGlvbnMgb2YgdGhlIG9ic2VydmVkIHN5bmVyZ2llcyBwZXIgbW9kZWwgKFRQKQogIG1vZGVscy50cC5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXSA9IGNhbGN1bGF0ZV9tb2RlbHNfc3luZXJnaWVzX3RwKG9ic2VydmVkLm1vZGVsLnByZWRpY3Rpb25zKQogIAogICMgQ2FsY3VsYXRlIE1hdHRoZXdzIENvcnJlbGF0aW9uIENvZWZmaWNpZW50IChNQ0MpIGZvciBldmVyeSBtb2RlbAogIG1vZGVscy5tY2MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0gPSAKICAgIGNhbGN1bGF0ZV9tb2RlbHNfbWNjKG9ic2VydmVkLm1vZGVsLnByZWRpY3Rpb25zLAogICAgICAgICAgICAgICAgICAgICAgICAgdW5vYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucywKICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlci5vZi5kcnVnLmNvbWIudGVzdGVkKQogIAogICMgRml0bmVzcyBwZXIgbW9kZWwgY29udHJhc3RlZCB0byBzdGVhZHkgc3RhdGUgZnJvbSBjZWxsIGxpbmUKICBzdGVhZHkuc3RhdGUgPSBzdGVhZHkuc3RhdGUucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICBtb2RlbHMuc3RhYmxlLnN0YXRlID0gbW9kZWxzLnN0YWJsZS5zdGF0ZS5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIG1vZGVscy5maXRuZXNzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dID0gYXBwbHkoCiAgICBtb2RlbHMuc3RhYmxlLnN0YXRlWyxuYW1lcyhzdGVhZHkuc3RhdGUpXSwgMSwgZ2V0X3BlcmNlbnRhZ2Vfb2ZfbWF0Y2hlcywgc3RlYWR5LnN0YXRlCiAgKQp9CmBgYAoKIyMjIFJhbmRvbSBNb2RlbHMgey19CgpGaW5kaW5nIHRoZSBNQ0MsIFRQIGFuZCBmaXRuZXNzIHZhbHVlcyBmb3IgZWFjaCBtb2RlbCBmcm9tIHRoZSBhYm92ZSByZXN1bHQgKG5vdGUKdGhhdCBlYWNoIG1vZGVsJ3Mgc3RhYmxlIHN0YXRlIGluIGEgc3BlY2lmaWMgY2VsbCBsaW5lIGlzIG1hdGNoZWQgYWdhaW5zdCB0aGUgCnN0ZWFkeSBzdGF0ZSBmcm9tIHRoYXQgY2VsbCBsaW5lIGFuZCB0aGF0IHRoZSByYW5kb20gbW9kZWxzJyBzdGFibGUgc3RhdGUgZGF0YQpkb2VzIG5vdCBjaGFuZ2UgcGVyIGNlbGwgbGluZSwgaS5lLiBzYW1lIGByYW5kb20ubW9kZWxzLnN0YWJsZS5zdGF0ZWAgb2JqZWN0KToKYGBge3IgUmFuZG9tIE1vZGVscyBUUCArIE1DQyArIGZpdG5lc3MgcGVyIGNlbGwgbGluZSwgY2FjaGU9VFJVRX0KcmFuZG9tLm1vZGVscy5tY2MucGVyLmNlbGwubGluZSA9IGxpc3QoKQpyYW5kb20ubW9kZWxzLnRwLnBlci5jZWxsLmxpbmUgPSBsaXN0KCkKcmFuZG9tLm1vZGVscy5maXRuZXNzLnBlci5jZWxsLmxpbmUgPSBsaXN0KCkKCmZvciAoY2VsbC5saW5lIGluIGNlbGwubGluZXMpIHsKICBvYnNlcnZlZC5zeW5lcmdpZXMgPSBvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICBudW1iZXIub2YuZHJ1Zy5jb21iLnRlc3RlZCA9IG5jb2wocmFuZG9tLm1vZGVsLnByZWRpY3Rpb25zKQogIAogICMgU3BsaXQgbW9kZWwucHJlZGljdGlvbnMgdG8gcG9zaXRpdmUgKG9ic2VydmVkKSBhbmQgbmVnYXRpdmUgKG5vbi1vYnNlcnZlZCkgcmVzdWx0cwogIG9ic2VydmVkLm1vZGVsLnByZWRpY3Rpb25zID0KICAgIGdldF9vYnNlcnZlZF9tb2RlbF9wcmVkaWN0aW9ucyhyYW5kb20ubW9kZWwucHJlZGljdGlvbnMsIG9ic2VydmVkLnN5bmVyZ2llcykKICB1bm9ic2VydmVkLm1vZGVsLnByZWRpY3Rpb25zID0KICAgIGdldF91bm9ic2VydmVkX21vZGVsX3ByZWRpY3Rpb25zKHJhbmRvbS5tb2RlbC5wcmVkaWN0aW9ucywgb2JzZXJ2ZWQuc3luZXJnaWVzKQogIAogICAjIENvdW50IHRoZSBwcmVkaWN0aW9ucyBvZiB0aGUgb2JzZXJ2ZWQgc3luZXJnaWVzIHBlciBtb2RlbCAoVFApCiAgcmFuZG9tLm1vZGVscy50cC5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXSA9IAogICAgY2FsY3VsYXRlX21vZGVsc19zeW5lcmdpZXNfdHAob2JzZXJ2ZWQubW9kZWwucHJlZGljdGlvbnMpCiAgCiAgIyBDYWxjdWxhdGUgTWF0dGhld3MgQ29ycmVsYXRpb24gQ29lZmZpY2llbnQgKE1DQykgZm9yIGV2ZXJ5IG1vZGVsCiAgcmFuZG9tLm1vZGVscy5tY2MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0gPSAKICAgIGNhbGN1bGF0ZV9tb2RlbHNfbWNjKG9ic2VydmVkLm1vZGVsLnByZWRpY3Rpb25zLAogICAgICAgICAgICAgICAgICAgICAgICAgdW5vYnNlcnZlZC5tb2RlbC5wcmVkaWN0aW9ucywKICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlci5vZi5kcnVnLmNvbWIudGVzdGVkKQogIAogICMgRml0bmVzcyBwZXIgbW9kZWwgY29udHJhc3RlZCB0byBzdGVhZHkgc3RhdGUgZnJvbSBjZWxsIGxpbmUgCiAgc3RlYWR5LnN0YXRlID0gc3RlYWR5LnN0YXRlLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgcmFuZG9tLm1vZGVscy5maXRuZXNzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dID0gYXBwbHkoCiAgICByYW5kb20ubW9kZWxzLnN0YWJsZS5zdGF0ZVssbmFtZXMoc3RlYWR5LnN0YXRlKV0sIDEsIAogICAgZ2V0X3BlcmNlbnRhZ2Vfb2ZfbWF0Y2hlcywgc3RlYWR5LnN0YXRlCiAgKQp9CmBgYAoKIyMgQ2hvb3NlIGJlc3QgZGF0YXNldCB7LX0KCldlIG5vdyB3YW50IHRvICoqZmluZCB0aGUgYmVzdCBkYXRhc2V0ICYgY2VsbCBsaW5lKiogZm9yIG91ciBzdWJzZXF1ZW50IGFuYWx5c2lzIC0gdGhhdCBpcyB0byBzaG93IHRoZSBwZXJmb3JtYW5jZSB2cyBmaXRuZXNzIGNvcnJlbGF0aW9uLiAKVGhlIGFyZ3VtZW50IGhlcmUgaXMgdGhhdCB3ZSB3YW50IHRvIGNob29zZSBhIGJvb2xlYW4gZGF0YXNldCB0aGF0IGhhcyBhICoqbGFyZ2UgZW5vdWdoIGZpdG5lc3MgdmFsdWUgcmFuZ2UqKiBhbG9uZyB3aXRoIGEgbGFyZ2UgKipUUCBhbmQvb3IgTUNDIHZhbHVlIHJhbmdlKiosIHNpbmNlIHdpdGggc21hbGxlciB2YWx1ZSByYW5nZXMgaXQgd291bGQgYmUgaGFyZGVyIHRvIGRpc3Rpbmd1aXNoIHRoZSBkaWZmZXJlbmNlIG9mIHRoZSBlc3RpbWF0ZWQgZGlzdHJpYnV0aW9ucyBvZiB0aGUgZml0bmVzcyBzY29yZXMgYmVsb25naW5nIHRvIGRpZmZlcmVudCBwZXJmb3JtYW5jZSBjbGFzc2VzIChpLmUuIG1vZGVscycgZml0bmVzc2VzIHRoYXQgYmVsb25nIHRvIGRpZmZlcmVudCBjbGFzc2lmaWNhdGlvbiBncm91cHMgd2l0aCB0aGUgbWV0cmljIGJlaW5nIGVpdGhlciB0aGUgbnVtYmVyIG9mIFRQcyBvciB0aGUgTUNDIHNjb3JlKS4KClRoZSBuZXh0IHR3byB0YWJsZXMgd2lsbCBoZWxwIHVzIGRldGVybWluZSBleGFjdGx5IHdoaWNoIGNlbGwgbGluZSBhbmQgZGF0YXNldAp0byB1c2U6CmBgYHtyIENlbGwtc3BlY2lmaWMgTW9kZWxzIFRQICsgTUNDICsgZml0bmVzcyBwZXIgY2VsbCBsaW5lIHN0YXRzfQpjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGEgPSBtYXRyaXgoZGF0YSA9IE5BLCBucm93ID0gbGVuZ3RoKGNlbGwubGluZXMpLCBuY29sID0gMTEpCnJvd25hbWVzKGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YSkgPSBjZWxsLmxpbmVzCmNvbG5hbWVzKGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YSkgPSBjKCJmaXRuZXNzIHJhbmdlIiwgIk1pbiBmaXRuZXNzIiwgCiAgIk1heCBmaXRuZXNzIiwgIk1lYW4gZml0bmVzcyIsICJNZWRpYW4gZml0bmVzcyIsICJNQ0MgcmFuZ2UiLCAiTWluIE1DQyIsIAogICJNYXggTUNDIiwgIk1lYW4gTUNDIiwgIk1lZGlhbiBNQ0MiLCAiTWF4IFRQUiIpCgpmb3IgKGNlbGwubGluZSBpbiBjZWxsLmxpbmVzKSB7CiAgbW9kZWxzLmZpdG5lc3MgPSBtb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIG1vZGVscy5tY2MgPSBtb2RlbHMubWNjLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgbW9kZWxzLnRwID0gbW9kZWxzLnRwLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgCiAgZml0LnN1bW1hcnkgPSB1bmNsYXNzKHN1bW1hcnkobW9kZWxzLmZpdG5lc3MpKQogIG1jYy5zdW1tYXJ5ID0gdW5jbGFzcyhzdW1tYXJ5KG1vZGVscy5tY2MpKQogIG1heC50cHIgPSBtYXgobW9kZWxzLnRwKSAvIGxlbmd0aChvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pCiAgCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgImZpdG5lc3MgcmFuZ2UiXSA9IGZpdC5zdW1tYXJ5WyJNYXguIl0gLSBmaXQuc3VtbWFyeVsiTWluLiJdCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1pbiBmaXRuZXNzIl0gPSBmaXQuc3VtbWFyeVsiTWluLiJdCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1heCBmaXRuZXNzIl0gPSBmaXQuc3VtbWFyeVsiTWF4LiJdCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1lYW4gZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1lYW4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWRpYW4gZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1lZGlhbiJdCiAgCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1DQyByYW5nZSJdID0gbWNjLnN1bW1hcnlbIk1heC4iXSAtIG1jYy5zdW1tYXJ5WyJNaW4uIl0KICBjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWluIE1DQyJdID0gbWNjLnN1bW1hcnlbIk1pbi4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNYXggTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWF4LiJdCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1lYW4gTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWVhbiJdCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1lZGlhbiBNQ0MiXSA9IG1jYy5zdW1tYXJ5WyJNZWRpYW4iXQogIAogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNYXggVFBSIl0gPSBtYXgudHByCn0KCiMgY29sb3IgY29sdW1ucwpmaXQuYnJlYWtzID0gcXVhbnRpbGUoY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhWywiZml0bmVzcyByYW5nZSJdLCBwcm9icyA9IHNlcSguMDUsIC45NSwgLjA1KSwgbmEucm0gPSBUUlVFKQpmaXQuY29sb3JzID0gcm91bmQoc2VxKDI1NSwgNDAsIGxlbmd0aC5vdXQgPSBsZW5ndGgoZml0LmJyZWFrcykgKyAxKSwgMCkgJT4lCiAge3Bhc3RlMCgicmdiKDI1NSwiLCAuLCAiLCIsIC4sICIpIil9ICMgcmVkCm1jYy5icmVha3MgPSBxdWFudGlsZShjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbLCJNQ0MgcmFuZ2UiXSwgcHJvYnMgPSBzZXEoLjA1LCAuOTUsIC4wNSksIG5hLnJtID0gVFJVRSkKbWNjLmNvbG9ycyA9IHJvdW5kKHNlcSgyNTUsIDQwLCBsZW5ndGgub3V0ID0gbGVuZ3RoKG1jYy5icmVha3MpICsgMSksIDApICU+JQogIHtwYXN0ZTAoInJnYigiLCAuLCAiLDI1NSwiLCAuLCAiKSIpfSAjIGdyZWVuCgpjYXB0aW9uLnRpdGxlID0gIlRhYmxlIDE6IEZpdG5lc3MsIE1DQyBzY29yZXMgYW5kIFRQIChUcnVlIHBvc2l0aXZlcykgZm9yIHRoZSBDZWxsLXNwZWNpZmljIG1vZGVsIHByZWRpY3Rpb25zIGFjcm9zcyA4IENlbGwgTGluZXMiCmRhdGF0YWJsZShkYXRhID0gY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhLCAKICAgICAgICAgIG9wdGlvbnMgPSBsaXN0KGRvbSA9ICJ0IiksICMganVzdCBzaG93IHRoZSB0YWJsZQogICAgICAgICAgY2FwdGlvbiA9IGh0bWx0b29sczo6dGFncyRjYXB0aW9uKGNhcHRpb24udGl0bGUsIHN0eWxlPSJjb2xvcjojZGQ0ODE0OyBmb250LXNpemU6IDE4cHgiKSkgJT4lIAogIGZvcm1hdFJvdW5kKDE6MTEsIGRpZ2l0cyA9IDMpICU+JQogIGZvcm1hdFN0eWxlKGNvbHVtbnMgPSBjKCJmaXRuZXNzIHJhbmdlIiksIGJhY2tncm91bmRDb2xvciA9IHN0eWxlSW50ZXJ2YWwoZml0LmJyZWFrcywgZml0LmNvbG9ycykpICU+JQogIGZvcm1hdFN0eWxlKGNvbHVtbnMgPSBjKCJNQ0MgcmFuZ2UiKSwgYmFja2dyb3VuZENvbG9yID0gc3R5bGVJbnRlcnZhbChtY2MuYnJlYWtzLCBtY2MuY29sb3JzKSkKYGBgCgpgYGB7ciBSYW5kb20gTW9kZWxzIFRQICsgTUNDICsgZml0bmVzcyBwZXIgY2VsbCBsaW5lIHN0YXRzfQpyYW5kb20ubW9kZWwuZGF0YSA9IG1hdHJpeChkYXRhID0gTkEsIG5yb3cgPSBsZW5ndGgoY2VsbC5saW5lcyksIG5jb2wgPSAxMSkKcm93bmFtZXMocmFuZG9tLm1vZGVsLmRhdGEpID0gY2VsbC5saW5lcwpjb2xuYW1lcyhyYW5kb20ubW9kZWwuZGF0YSkgPSBjKCJmaXRuZXNzIHJhbmdlIiwgIk1pbiBmaXRuZXNzIiwgCiAgIk1heCBmaXRuZXNzIiwgIk1lYW4gZml0bmVzcyIsICJNZWRpYW4gZml0bmVzcyIsICJNQ0MgcmFuZ2UiLCAiTWluIE1DQyIsIAogICJNYXggTUNDIiwgIk1lYW4gTUNDIiwgIk1lZGlhbiBNQ0MiLCAiTWF4IFRQUiIpCgpmb3IgKGNlbGwubGluZSBpbiBjZWxsLmxpbmVzKSB7CiAgbW9kZWxzLmZpdG5lc3MgPSByYW5kb20ubW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICBtb2RlbHMubWNjID0gcmFuZG9tLm1vZGVscy5tY2MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICBtb2RlbHMudHAgPSByYW5kb20ubW9kZWxzLnRwLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgCiAgZml0LnN1bW1hcnkgPSB1bmNsYXNzKHN1bW1hcnkobW9kZWxzLmZpdG5lc3MpKQogIG1jYy5zdW1tYXJ5ID0gdW5jbGFzcyhzdW1tYXJ5KG1vZGVscy5tY2MpKQogIG1heC50cHIgPSBtYXgobW9kZWxzLnRwKSAvIGxlbmd0aChvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pCiAgCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiZml0bmVzcyByYW5nZSJdID0gZml0LnN1bW1hcnlbIk1heC4iXSAtIGZpdC5zdW1tYXJ5WyJNaW4uIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNaW4gZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1pbi4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1heCBmaXRuZXNzIl0gPSBmaXQuc3VtbWFyeVsiTWF4LiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWVhbiBmaXRuZXNzIl0gPSBmaXQuc3VtbWFyeVsiTWVhbiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWVkaWFuIGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNZWRpYW4iXQogIAogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1DQyByYW5nZSJdID0gbWNjLnN1bW1hcnlbIk1heC4iXSAtIG1jYy5zdW1tYXJ5WyJNaW4uIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNaW4gTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWluLiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWF4IE1DQyJdID0gbWNjLnN1bW1hcnlbIk1heC4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1lYW4gTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWVhbiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWVkaWFuIE1DQyJdID0gbWNjLnN1bW1hcnlbIk1lZGlhbiJdCiAgCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWF4IFRQUiJdID0gbWF4LnRwcgp9CgojIGNvbG9yIGNvbHVtbnMKZml0LmJyZWFrcyA9IHF1YW50aWxlKHJhbmRvbS5tb2RlbC5kYXRhWywiZml0bmVzcyByYW5nZSJdLCBwcm9icyA9IHNlcSguMDUsIC45NSwgLjA1KSwgbmEucm0gPSBUUlVFKQpmaXQuY29sb3JzID0gcm91bmQoc2VxKDI1NSwgNDAsIGxlbmd0aC5vdXQgPSBsZW5ndGgoZml0LmJyZWFrcykgKyAxKSwgMCkgJT4lCiAge3Bhc3RlMCgicmdiKDI1NSwiLCAuLCAiLCIsIC4sICIpIil9ICMgcmVkCm1jYy5icmVha3MgPSBxdWFudGlsZShyYW5kb20ubW9kZWwuZGF0YVssIk1DQyByYW5nZSJdLCBwcm9icyA9IHNlcSguMDUsIC45NSwgLjA1KSwgbmEucm0gPSBUUlVFKQptY2MuY29sb3JzID0gcm91bmQoc2VxKDI1NSwgNDAsIGxlbmd0aC5vdXQgPSBsZW5ndGgobWNjLmJyZWFrcykgKyAxKSwgMCkgJT4lCiAge3Bhc3RlMCgicmdiKCIsIC4sICIsMjU1LCIsIC4sICIpIil9ICMgZ3JlZW4KCmNhcHRpb24udGl0bGUgPSAiVGFibGUgMjogRml0bmVzcywgTUNDIHNjb3JlcyBhbmQgVFAgKFRydWUgcG9zaXRpdmVzKSBmb3IgdGhlIHJhbmRvbSBtb2RlbCBwcmVkaWN0aW9ucyBhY3Jvc3MgOCBDZWxsIExpbmVzIgpkYXRhdGFibGUoZGF0YSA9IHJhbmRvbS5tb2RlbC5kYXRhLCAKICAgICAgICAgIG9wdGlvbnMgPSBsaXN0KGRvbSA9ICJ0IiksICMganVzdCBzaG93IHRoZSB0YWJsZQogICAgICAgICAgY2FwdGlvbiA9IGh0bWx0b29sczo6dGFncyRjYXB0aW9uKGNhcHRpb24udGl0bGUsIHN0eWxlPSJjb2xvcjojZGQ0ODE0OyBmb250LXNpemU6IDE4cHgiKSkgJT4lIAogIGZvcm1hdFJvdW5kKDE6MTEsIGRpZ2l0cyA9IDMpICU+JQogIGZvcm1hdFN0eWxlKGNvbHVtbnMgPSBjKCJmaXRuZXNzIHJhbmdlIiksIGJhY2tncm91bmRDb2xvciA9IHN0eWxlSW50ZXJ2YWwoZml0LmJyZWFrcywgZml0LmNvbG9ycykpICU+JQogIGZvcm1hdFN0eWxlKGNvbHVtbnMgPSBjKCJNQ0MgcmFuZ2UiKSwgYmFja2dyb3VuZENvbG9yID0gc3R5bGVJbnRlcnZhbChtY2MuYnJlYWtzLCBtY2MuY29sb3JzKSkKYGBgCgpUaGUgYmVsb3cgYm94IHBsb3RzIGNvbXBhcmUgdGhlIE1DQyB2YWx1ZXMgYW5kIGZpdG5lc3Mgc2NvcmVzIGFjcm9zcyBhbGwgY2VsbCBsaW5lcyBiZXR3ZWVuCnRoZSBjZWxsLXNwZWNpZmljIG1vZGVscyAodHJhaW5lZCB0byBzdGVhZHkgc3RhdGUpIGFuZCB0aGUgcmFuZG9tIG9uZXMgKHRyYWluZWQgdG8gcHJvbGlmZXJhdGlvbik6CgpgYGB7ciBDb21iaW5lIGRhdGEgaW50byBvbmUgZGF0YSBmcmFtZX0KbnVtLm9mLm1vZGVscyA9IG5yb3cocmFuZG9tLm1vZGVscy5zdGFibGUuc3RhdGUpCmRhdGEubGlzdCA9IGxpc3QoKQoKZm9yIChjZWxsLmxpbmUgaW4gY2VsbC5saW5lcykgewogIGNlbGwubGluZS52ZWMgPSBhcy5kYXRhLmZyYW1lKHJlcChjZWxsLmxpbmUsIG51bS5vZi5tb2RlbHMpLCBzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UpCiAgbW9kZWxzLm1jYy5jZWxsLnNwZWNpZmljID0gcmVtb3ZlX3Jvd25hbWVzKGFzLmRhdGEuZnJhbWUobW9kZWxzLm1jYy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXSkpCiAgbW9kZWxzLm1jYy5yYW5kb20gICAgICAgID0gcmVtb3ZlX3Jvd25hbWVzKGFzLmRhdGEuZnJhbWUocmFuZG9tLm1vZGVscy5tY2MucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pKQogIAogIG1vZGVscy5maXRuZXNzLmNlbGwuc3BlY2lmaWMgPSByZW1vdmVfcm93bmFtZXMoYXMuZGF0YS5mcmFtZShtb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXSkpCiAgbW9kZWxzLmZpdG5lc3MucmFuZG9tICAgICAgICA9IHJlbW92ZV9yb3duYW1lcyhhcy5kYXRhLmZyYW1lKHJhbmRvbS5tb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXSkpCiAgCiAgZGF0YS5saXN0W1tjZWxsLmxpbmVdXSA9IGJpbmRfY29scyhjZWxsLmxpbmUudmVjLCBtb2RlbHMubWNjLmNlbGwuc3BlY2lmaWMsIAogICAgbW9kZWxzLm1jYy5yYW5kb20sIG1vZGVscy5maXRuZXNzLmNlbGwuc3BlY2lmaWMsIG1vZGVscy5maXRuZXNzLnJhbmRvbSkKfQoKZGF0YSA9IGJpbmRfcm93cyhkYXRhLmxpc3QpCmNvbG5hbWVzKGRhdGEpID0gYygiY2VsbC5saW5lIiwgIk1DQyBjZWxsLXNwZWNpZmljIiwgIk1DQyByYW5kb20iLCAKICAgICAgICAgICAgICAgICAgICJmaXRuZXNzIGNlbGwtc3BlY2lmaWMiLCAiZml0bmVzcyByYW5kb20iKQpgYGAKCmBgYHtyIEJveHBsb3RzOiBNQ0MgYW5kIGZpdG5lc3MgdmFsdWVzIGJldHdlZW4gY2VsbC1zcGVjaWZpYyBhbmQgcmFuZG9tIG1vZGVscywgZmlnLndpZHRoPTksIHdhcm5pbmc9RkFMU0UsIGNhY2hlPVRSVUV9CiMgTm90ZSB0aGUgY2VsbC1zcGVjaWZpYyBtb2RlbHMgaGF2ZSBOYU4gTUNDIHZhbHVlcwpnZ2JveHBsb3QoZGF0YSwgeCA9ICJjZWxsLmxpbmUiLCB5ID0gYygiTUNDIGNlbGwtc3BlY2lmaWMiLCAiTUNDIHJhbmRvbSIpLAogICAgICAgICAgcGFsZXR0ZSA9IGJyZXdlci5wYWwoMywgIlNldDEiKSwgbWVyZ2UgPSAiYXNpcyIsCiAgICAgICAgICB4bGFiID0gIkNlbGwgTGluZXMiLCB5bGFiID0gIk1DQyB2YWx1ZXMiLCBhZGQgPSAicG9pbnQiLCBhZGQucGFyYW1zID0gbGlzdChzaXplID0gMC41KSkKZ2dib3hwbG90KGRhdGEsIHggPSAiY2VsbC5saW5lIiwgeSA9IGMoImZpdG5lc3MgY2VsbC1zcGVjaWZpYyIsICJmaXRuZXNzIHJhbmRvbSIpLAogICAgICAgICAgcGFsZXR0ZSA9IGJyZXdlci5wYWwoMywgIlNldDEiKSwgbWVyZ2UgPSAiYXNpcyIsCiAgICAgICAgICB4bGFiID0gIkNlbGwgTGluZXMiLCB5bGFiID0gIkZpdG5lc3MgdmFsdWVzIiwgYWRkID0gInBvaW50IiwgYWRkLnBhcmFtcyA9IGxpc3Qoc2l6ZSA9IDAuNSkpCmBgYAoKU28sIGZyb20gdGhlIHR3byB0YWJsZXMgYW5kIHRoZSB0d28gYm94IHBsb3RzIGFib3ZlIHdlIGNvbmNsdWRlIHRoYXQ6CgotIEluIGdlbmVyYWwsIHRoZSAqKnJhbmRvbSBtb2RlbHMgb2ZmZXIgYSBsYXJnZXIgcmFuZ2Ugb2YgCmZpdG5lc3MgdmFsdWVzKiogd2hlbiB0aGVpciBzdGFibGUgc3RhdGVzIGFyZSBtYXRjaGVkIGFnYWluc3QgdGhlIHN0ZWFkeSBzdGF0ZSAKb2YgZWFjaCBwYXJ0aWN1bGFyIGNlbGwgbGluZS4gClRoaXMgaXMgc29tZXRoaW5nIHdlIGV4cGVjdGVkIHNpbmNlIHRoZXNlIG1vZGVscyB3ZXJlbid0IGZpdHRlZCB0byBhIApzcGVjaWZpYyBjZWxsLWxpbmUgc3RlYWR5IHN0YXRlIChtZWFuaW5nIHRoYXQgdGhleSB3ZXJlbid0IGNob3NlbiBmcm9tIHRoZSAKYEdpdHNiZWAgbW9kdWxlIGFzIHRoZSAzIGJlc3QgZnJvbSBlYWNoIHNpbXVsYXRpb24gdGhhdCBtYXRjaCB0aGF0IHN0ZWFkeSBzdGF0ZSAKYXMgYmVzdCBhcyBwb3NzaWJsZSkgYnV0IHJhdGhlciB0byBhIG1vcmUgZ2VuZXJpYyBzdGF0ZSBvZiBwcm9saWZlcmF0aW9uLiAKVGh1cywgdGhleSByZXByZXNlbnQgYSBzZXQgb2YgbW9kZWxzIHdpdGggbGFyZ2VyIHZhcmlhdGlvbiBpbiB0ZXJtcyBvZiAKc3RydWN0dXJlIChib29sZWFuIG1vZGVsIGVxdWF0aW9ucykgY29tcGFyZWQgdG8gdGhlIGNlbGwtc3BlY2lmaWMgZ2VuZXJhdGVkIG9uZXMuCi0gKipUaGUgY2VsbC1zcGVjaWZpYyBtb2RlbHMgaGF2ZSBhbHdheXMgYSBsYXJnZXIgbWF4aW11bSBmaXRuZXNzIGFuZCBtZWRpYW4gdmFsdWUqKiAKY29tcGFyZWQgdG8gdGhlIHJhbmRvbSBvbmVzIGZvciBlYWNoIHJlc3BlY3RpdmUgY2VsbCBsaW5lLgotIEluIG1vc3Qgb2YgdGhlIGNlbGwgbGluZXMsICoqdGhlIGNlbGwtc3BlY2lmaWMgbW9kZWxzIHNob3cgYW4gZXF1YWwgb3IgbGVzcyAKcGVyZm9ybWFuY2UgKHNtYWxsZXIgTUNDIHZhbHVlcykgdGhhbiB0aGUgcmFuZG9tIG9uZXMqKgoKQWxsIGluIGFsbCwgd2Ugd2lsbCB1c2UgdGhlICoqcmFuZG9tIG1vZGVscyBkYXRhKiosIGNvbnRyYXN0ZWQgdG8gdGhlIHN0ZWFkeSAKc3RhdGUgb2YgdGhlIGBBR1NgIGNlbGwgbGluZSAoc2VlIFRhYmxlIDIpLiBUaGlzIGRhdGFzZXQgaGFzIHRoZSBsYXJnZXN0IApNQ0MgcmFuZ2UgYW5kIFRQUiB2YWx1ZSAodGhpcmQgbGFyZ2VzdCBmaXRuZXNzIHJhbmdlIHZhbHVlIGFzIHdlbGwpIGNvbWJpbmVkIAppbiBib3RoIHRhYmxlcyBhYm92ZS4KCmBgYHtyIFNhdmUgYmVzdCBkYXRhc2V0fQpiZXN0LmNlbGwubGluZSA9ICJBR1MiCgpmaXQgPSByYW5kb20ubW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZVtbYmVzdC5jZWxsLmxpbmVdXQp0cCAgPSByYW5kb20ubW9kZWxzLnRwLnBlci5jZWxsLmxpbmVbW2Jlc3QuY2VsbC5saW5lXV0KbWNjID0gcmFuZG9tLm1vZGVscy5tY2MucGVyLmNlbGwubGluZVtbYmVzdC5jZWxsLmxpbmVdXQpgYGAKCiMjIFN0YXRpc3RpY2FsIEFuYWx5c2lzIHstfQoKIyMjIERhdGEgcHJlcHJvY2Vzc2luZyB7LX0KCkZpcnN0bHksIHdlIGZpbHRlciB0aGUgZGF0YSBieSBmaW5kaW5nIHRoZSAqKnVuaXF1ZSBtb2RlbHMqKiAtIHRob3NlIHRoYXQgaGF2ZSAKc3RyaWN0bHkgZGlmZmVyZW50IGJvb2xlYW4gZXF1YXRpb25zLiBUaGVuLCB3ZSB0YWtlIGEgcmFuZG9tIHNhbXBsZSBvdXQgb2YgdGhlc2UgCih3aGlsZSBzdGFiaWxpemluZyB0aGUgc2VlZCBudW1iZXIgZm9yIHJlcHJvZHVjaWJpbGl0eSBwdXJwb3Nlcyk6CmBgYHtyIFNhbXBsZSBiZXN0IGRhdGFzZXR9CiMgRm9yIHJlcHJvZHVjaWJpbGl0eQpzZXQuc2VlZCgxMCkKc2FtcGxlLnNpemUgPSAxMDAwCgojIGZpbmQgdGhlIHVuaXF1ZSBtb2RlbHMKdW5pcXVlLm1vZGVscyA9IHJvd25hbWVzKHVuaXF1ZShyYW5kb20ubW9kZWxzLmxpbmsub3BlcmF0b3IpKQoKIyByZW1vdmUgbW9kZWxzIHdpdGggTmFOIE1DQyBzY29yZXMKdW5pcXVlLm1vZGVscyA9IHVuaXF1ZS5tb2RlbHNbIXVuaXF1ZS5tb2RlbHMgJWluJSBuYW1lcyh3aGljaChpcy5uYShtY2MpKSldCgojIHRha2UgYSBzYW1wbGUKdW5pcXVlLm1vZGVscy5zYW1wbGUgPSBzYW1wbGUodW5pcXVlLm1vZGVscywgc2l6ZSA9IHNhbXBsZS5zaXplKQoKZml0LnVuaXF1ZSA9IGZpdFtuYW1lcyhmaXQpICVpbiUgdW5pcXVlLm1vZGVscy5zYW1wbGVdCnRwLnVuaXF1ZSAgPSB0cFtuYW1lcyh0cCkgJWluJSB1bmlxdWUubW9kZWxzLnNhbXBsZV0KbWNjLnVuaXF1ZSA9IG1jY1tuYW1lcyhtY2MpICVpbiUgdW5pcXVlLm1vZGVscy5zYW1wbGVdCgpkZiA9IGFzLmRhdGEuZnJhbWUoY2JpbmQoZml0LnVuaXF1ZSwgbWNjLnVuaXF1ZSwgdHAudW5pcXVlKSkKYGBgCgpOb3RlIHRoYXQgdGhlICoqZml0bmVzcyBhbmQgTUNDIHNjb3JlIGFyZSBjb250aW51b3VzIHZhcmlhYmxlcyB3aGlsZSB0aGUgbnVtYmVyIApvZiB0cnVlIHBvc2l0aXZlcyAoVFApIGlzIGRpc2NyZXRlKiouCgojIyMgQ29ycmVsYXRpb24gUGxvdHMgey19CgpUaGVuLCB3ZSBjaGVjayBpZiB0aGUgb3VyIGRhdGEgaXMgbm9ybWFsbHkgZGlzdHJpYnV0ZWQgKHVzaW5nIHRoZSBTaGFwaXJvLVdpbGsKdGVzdCBmb3Igbm9ybWFsaXR5IGFuZCB0aGUgUS1RIHBsb3RzKToKYGBge3IgTm9ybWFsaXR5IHRlc3RpbmcsIHdhcm5pbmc9RkFMU0UsIGNvbW1lbnQ9IiIsIGNhY2hlPVRSVUV9CmdncXFwbG90KGRhdGEgPSBkZiwgeCA9ICJmaXQudW5pcXVlIiwgeWxhYiA9ICJGaXRuZXNzIikKZ2dxcXBsb3QoZGF0YSA9IGRmLCB4ID0gIm1jYy51bmlxdWUiLCB5bGFiID0gIk1DQyIpCgpzaGFwaXJvLnRlc3QoeCA9IGZpdC51bmlxdWUpCnNoYXBpcm8udGVzdCh4ID0gbWNjLnVuaXF1ZSkKYGBgCgpGcm9tIHRoZSBhYm92ZSByZXN1bHRzIHdlIG9ic2VydmUgdGhhdCBib3RoIHRoZSBmaXRuZXNzIGFuZCBNQ0Mgc2NvcmVzIGFyZSBzdXJlbHkKbm90IG5vcm1hbGx5IGRpc3RyaWJ1dGVkICh3aXRoIHN0YXRpc3RpY2FsIHNpZ25pZmljYW5jZSkuClRodXMsIHdlIHdpbGwgdXNlICoqbm9uLXBhcmFtZXRyaWMgY29ycmVsYXRpb24gc2NvcmVzLCBuYW1lbHkgdGhlIFNwZWFybWFuIGFuZCBLZW5kYWxsIHJhbmstYmFzZWQgY29ycmVsYXRpb24gdGVzdHMqKiwKdG8gY2hlY2sgdGhlIGNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIHR3byBjb250aW51b3VzIHZhcmlhYmxlcyAobW9kZWxzIGZpdG5lc3MgCnZhbHVlcyBhbmQgdGhlaXIgY29ycmVzcG9uZGluZyBNQ0Mgc2NvcmUpOgpgYGB7ciBSYW5rIGNvcnJlbGF0aW9uIHBsb3RzOiBGaXRuZXNzIHZzIE1DQywgd2FybmluZz1GQUxTRX0KZ2dzY2F0dGVyKGRmLCB4ID0gIm1jYy51bmlxdWUiLCB5ID0gImZpdC51bmlxdWUiLCAKICAgICAgICAgIHRpdGxlID0gIkZpdG5lc3MgdnMgUGVyZm9ybWFuY2UgKE1DQykgLSBTcGVhcm1hbiBDb3JyZWxhdGlvbiIsCiAgICAgICAgICBhZGQgPSAicmVnLmxpbmUiLCBjb25mLmludCA9IFRSVUUsIAogICAgICAgICAgY29yLmNvZWYgPSBUUlVFLCBjb3IuY29lZmYuYXJncyA9IGxpc3QobWV0aG9kID0gInNwZWFybWFuIiwgbGFiZWwueC5ucGMgPSAwLjcsIGxhYmVsLnkubnBjID0gMC4xKSwKICAgICAgICAgIHhsYWIgPSAiTUNDIHNjb3JlcyIsIHlsYWIgPSAiRml0bmVzcyBWYWx1ZXMiKQpnZ3NjYXR0ZXIoZGYsIHggPSAibWNjLnVuaXF1ZSIsIHkgPSAiZml0LnVuaXF1ZSIsIAogICAgICAgICAgdGl0bGUgPSAiRml0bmVzcyB2cyBQZXJmb3JtYW5jZSAoTUNDKSAtIEtlbmRhbGwgQ29ycmVsYXRpb24iLAogICAgICAgICAgYWRkID0gInJlZy5saW5lIiwgY29uZi5pbnQgPSBUUlVFLCAKICAgICAgICAgIGNvci5jb2VmID0gVFJVRSwgY29yLmNvZWZmLmFyZ3MgPSBsaXN0KG1ldGhvZCA9ICJrZW5kYWxsIiwgbGFiZWwueC5ucGMgPSAwLjcsIGxhYmVsLnkubnBjID0gMC4xKSwKICAgICAgICAgIHhsYWIgPSAiTUNDIHNjb3JlcyIsIHlsYWIgPSAiRml0bmVzcyBWYWx1ZXMiKQpgYGAKCkZyb20gdGhlIGNvcnJlbGF0aW9uIHBsb3RzIGFib3ZlLCB3ZSBvYnNlcnZlIGEgKip3ZWFrL3NtYWxsIHBvc2l0aXZlIGNvcnJlbGF0aW9uIGJldHdlZW4KcGVyZm9ybWFuY2UgYW5kIGZpdG5lc3MgdG8gdGhlIHN0ZWFkeSBzdGF0ZSoqLgoKVG8gYXNzZXNzIHRoZSAqKmNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIG51bWJlciBvZiBUUCBwcmVkaWN0aW9ucyBvZiB0aGUgbW9kZWxzIAooZGlzY3JldGUgdmFyaWFibGUpIGFuZCB0aGUgbW9kZWxzIGZpdG5lc3MgKGNvbnRpbnVvdXMgdmFyaWFibGUpKiosIHdlIGNvbnN0cnVjdCBhIApwcmVkaWN0b3Igb2YgdGhlIGNhdGVnb3JpY2FsIHZhcmlhYmxlIGZyb20gdGhlIGNvbnRpbnVvdXMgdmFyaWFibGU6IGlmIHRoZSAKcmVzdWx0aW5nIGNsYXNzaWZpZXIgaGFzIGEgKipoaWdoIGRlZ3JlZSBvZiBmaXQqKiB3ZSBjYW4gY29uY2x1ZGUgdGhlIHR3byB2YXJpYWJsZXMgCnNoYXJlIGEgcmVsYXRpb25zaGlwIGFuZCBhcmUgaW5kZWVkIGNvcnJlbGF0ZWQuIFNpbmNlIHRoZXJlIGFyZSBtb3JlIHRoYW4gMiBUUApjbGFzc2VzICh2YWx1ZXMpIGluIHRoZSBkYXRhc2V0LCB3ZSB3aWxsIHVzZSAqKk11bHRpbm9taWFsIExvZ2lzdGljIFJlZ3Jlc3Npb24qKgphbmQgZml0IGxvZy1saW5lYXIgbW9kZWxzIHZpYSBuZXVyYWwgbmV0d29ya3M6CgpgYGB7ciBNdWx0aW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uOiBUUCB+IEZpdG5lc3MsIGNvbW1lbnQ9IiJ9Cm1vZGVsLmNsYXNzaWZpZXIgPSBtdWx0aW5vbShkYXRhID0gZGYsIGZvcm11bGEgPSB0cC51bmlxdWUgfiBmaXQudW5pcXVlKQpwc2V1ZG8ucjIubWVhc3VyZXMgPSBwUjIobW9kZWwuY2xhc3NpZmllcikKcHNldWRvLnIyLm1lYXN1cmVzWyJNY0ZhZGRlbiJdCmBgYAoKYGBge3IgVGVzdCBwcmVkaWN0aW9uIGFjY3VyYWN5IG9uIHNhbWUgZGF0YXNldCwgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0KbGlicmFyeShjYXJldCkKCnByZWRpY3Rpb25zID0gcHJlZGljdChvYmplY3QgPSBtb2RlbC5jbGFzc2lmaWVyLCBuZXdkYXRhID0gZGYkdHAudW5pcXVlLCB0eXBlID0gImNsYXNzIikKcG9zdFJlc2FtcGxlKHByZWQgPSBwcmVkaWN0aW9ucywgb2JzID0gZmFjdG9yKGRmJHRwLnVuaXF1ZSkpCmBgYAoKVG8gbWVhc3VyZSB0aGUgZ29vZG5lc3Mtb2YtZml0IGZvciBvdXIgbW9kZWwsIHRoZXJlIGFyZSBzZXZlcmFsIG1lYXN1cmVzIHByb3Bvc2VkIApmb3IgbG9naXN0aWMgcmVncmVzc2lvbi4gV2UgZW1waGFzaXplIG9uIHRoZSBNY0ZhZGRlbnMncyBwc2V1ZG8tJFJeMiQgbWVhc3VyZSAKZm9yIG91ciBjbGFzc2lmaWVyLCBmb3Igd2hpY2ggYSB2YWx1ZSBvZiAkMC4yLTAuNCQgd291bGQgaW5kaWNhdGUgYW4gZXhjZWxsZW50IGZpdCBbW3NvdXJjZV0oaHR0cHM6Ly9zdGF0cy5zdGFja2V4Y2hhbmdlLmNvbS9xdWVzdGlvbnMvODIxMDUvbWNmYWRkZW5zLXBzZXVkby1yMi1pbnRlcnByZXRhdGlvbildLgpTaW5jZSB3ZSBmb3VuZCBsZXNzLCB3ZSBjYW4gYWxzbyBhc3N1bWUgdGhhdCB0aGVyZSBpcyBvbmx5IGEgKip3ZWFrL3NtYWxsIHBvc2l0aXZlIApjb3JyZWxhdGlvbiBiZXR3ZWVuIHRoZSBudW1iZXIgb2YgVFBzIGFuZCB0aGUgZml0bmVzcyBzY29yZSBvZiB0aGUgbW9kZWxzKiouCgpOZXh0LCB3ZSB3aWxsIG5leHQgcHJvY2VlZCB3aXRoIGEgbW9yZSBlbGFib3JhdGUgYW5hbHlzaXMsIHdoZXJlIHRoZSBtb2RlbHMgd2lsbCBiZSBzcGxpdCB0byBkaWZmZXJlbnQgCnBlcmZvcm1hbmNlIGNsYXNzZXMgKFRQIG9yIE1DQyBzY29yZS1kZXJpdmVkKSBhbmQgdGhlIHN0YXRpc3RpY2FsIGNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIAppbmRpdmlkdWFsIGdyb3VwcyB3aWxsIGJlIHRlc3RlZCAod2l0aCByZWdhcmRzIHRvIHRoZWlyIGZpdG5lc3Mgc2NvcmVzKS4KCiMjIyBUUC1jbGFzcyB2cyBmaXRuZXNzIHstfQoKRmlyc3QsIHNvbWUgYm94IGFuZCBkZW5zaXR5IHBsb3RzIHRvIHNlZSBwcmFjdGljYWxseSAqKndoYXQgYW5kIHdoZXJlIGlzIHRoZSBkaWZmZXJlbmNlIApiZXR3ZWVuIHRoZSBtb2RlbHMgZml0bmVzcyB2YWx1ZXMgYmVsb25naW5nIHRvIGRpZmZlcmVudCBUUC1jbGFzc2VzKio6CmBgYHtyIFRQLWNsYXNzaWZpY2F0aW9uOiBQbG90c30KIyBCb3ggcGxvdHMKZ2dib3hwbG90KGRmLCB4ID0gInRwLnVuaXF1ZSIsIHkgPSAiZml0LnVuaXF1ZSIsIGNvbG9yID0gInRwLnVuaXF1ZSIsCiAgICAgICAgICBwYWxldHRlID0gdXNlZnVuOjo6Y29sb3JzLjEwMFsxOmxlbmd0aCh1bmlxdWUodHAudW5pcXVlKSldLAogICAgICAgICAgeGxhYiA9ICJUcnVlIFBvc2l0aXZlcyAoVFApIiwgeWxhYiA9ICJGaXRuZXNzIHZhbHVlcyIpCgojIERlbnNpdHkgUGxvdHMKZGVuc2l0aWVzID0gbGlzdCgpCmZvciAodHAubnVtIGluIHNvcnQodW5pcXVlKHRwLnVuaXF1ZSkpKSB7CiAgeCA9IGRmICU+JQogICAgZmlsdGVyKHRwLnVuaXF1ZSA9PSB0cC5udW0pICU+JQogICAgc2VsZWN0X2F0KC52YXJzID0gYygiZml0LnVuaXF1ZSIpKQogIGRlbiA9IGRlbnNpdHkoeCRmaXQudW5pcXVlKQogIGRlbnNpdGllc1tbcGFzdGUwKHRwLm51bSwgIiAoIiwgZGVuJG4sICIpIildXSA9IGRlbgp9CgptYWtlX211bHRpcGxlX2RlbnNpdHlfcGxvdChkZW5zaXRpZXMsIGxlZ2VuZC50aXRsZSA9ICJUUCBjbGFzc2VzICgjbW9kZWxzKSIsCiAgICAgICAgdGl0bGUgPSAiRGVuc2l0eSBFc3RpbWF0aW9uIiwgeC5heGlzLmxhYmVsID0gIkZpdG5lc3Mgc2NvcmUiKQpgYGAKCkFzIHdlIGNhbiBzZWUgZnJvbSB0aGUgYWJvdmUgcGxvdHMsICoqdGhlcmUgZG9lc24ndCBzZWVtIHRvIGJlIGFueSBzaWduaWZpY2FudCBkaWZmZXJlbmNlIApiZXR3ZWVuIHRoZSBmaXRuZXNzIHZhbHVlcyBvZiB0aGUgZGlmZmVyZW50IFRQIGNsYXNzZXMqKi4KTWF0aGVtYXRpY2FsbHksIHdlIGNhbiBzaG93IHRoYXQgYnkgdXNpbmcgdGhlICoqS3J1c2thbC1XYWxsaXMgUmFuayBTdW0gVGVzdCoqIAoobm90aWNlIHRoZSBwLXZhbHVlIC0gaXQgaXMgbGFyZ2VyIHRoYW4gJDAuMDAxJCk6CgpgYGB7ciBUUC1jbGFzc2lmaWNhdGlvbjogS3J1c2thbCBUZXN0LCBjb21tZW50PSIifQojIEh5cG90aGVzaXMgdGVzdGluZzogQXJlIHRoZSBsb2NhdGlvbiBwYXJhbWV0ZXJzIG9mIHRoZSBkaXN0cmlidXRpb24gb2YgeCB0aGUgc2FtZSBpbiBlYWNoIGdyb3VwPwprcnVza2FsLnRlc3QoeCA9IGRmWywiZml0LnVuaXF1ZSJdLCBnID0gZGZbLCAidHAudW5pcXVlIl0pCmBgYAoKVG8gZGVtb25zdHJhdGUgdGhhdCBhbG1vc3QgYWxsIHBhaXIgb2YgZ3JvdXBzIGFyZSB0aGUgc2FtZSwgKip3ZSBwZXJmb3JtIHBhaXJ3aXNlCldpbGNveG9uIHJhbmsgc3VtIHRlc3RzIGFuZCB3ZSBkcmF3IGEgaGVhdG1hcCBvZiB0aGUgZWFjaCB0ZXN0J3MgcC12YWx1ZSAobm90ZSAKdGhhdCBhbGwgZXhjZXB0IG9uZSBhcmUgbm90IHN0YXRpc3RpY2FsbHkgc2lnbmlmaWNhbnQpKio6CgpgYGB7ciBUUC1jbGFzc2lmaWNhdGlvbjogUGFpcndpc2UgV2lsY294IFRlc3RzLCB3YXJuaW5nPUZBTFNFfQpyZXMgPSBwYWlyd2lzZS53aWxjb3gudGVzdCh4ID0gZGZbLCJmaXQudW5pcXVlIl0sIGcgPSBkZlssICJ0cC51bmlxdWUiXSwgcC5hZGp1c3QubWV0aG9kID0gIkJIIikKcC52YWx1ZS5tYXQgPSByZXMkcC52YWx1ZQpgYGAKCmBgYHtyIFRQLWNsYXNzaWZpY2F0aW9uOiBwLXZhbHVlIEhlYXRtYXAgb2YgUGFpcndpc2UgV2lsY294IFRlc3RzLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9OSwgZHBpPTMwMH0KY29sX2Z1biA9IGNvbG9yUmFtcDIoYnJlYWtzID0gYygwLCAwLjA1LCAwLjUsIDEpLCBjKCJncmVlbiIsICJ3aGl0ZSIsICJvcmFuZ2UiLCAicmVkIikpCmh0ID0gSGVhdG1hcChtYXRyaXggPSBwLnZhbHVlLm1hdCwgY2x1c3Rlcl9yb3dzID0gRkFMU0UsIGNsdXN0ZXJfY29sdW1ucyA9IEZBTFNFLAogIG5hX2NvbCA9ICJ3aGl0ZSIsIG5hbWUgPSAicC12YWx1ZSIsIGNvbCA9IGNvbF9mdW4sIGNvbHVtbl9uYW1lc19yb3QgPSAwLAogIHJvd190aXRsZSA9ICJUUCIsIHJvd190aXRsZV9yb3QgPSAwLCByb3dfbmFtZXNfc2lkZSA9ICJsZWZ0IiwKICBjb2x1bW5fdGl0bGUgPSAiUC12YWx1ZXMgKFBhaXJ3aXNlIFdpbGNveG9uIHRlc3RzKSAtIFRQLWNsYXNzaWZpZWQgZml0bmVzc2VzIiwgY29sdW1uX3RpdGxlX2dwID0gZ3Bhcihmb250c2l6ZSA9IDIwKSwKICBjZWxsX2Z1biA9IGZ1bmN0aW9uKGosIGksIHgsIHksIHdpZHRoLCBoZWlnaHQsIGZpbGwpIHsKICAgIGlmICghaXMubmEocC52YWx1ZS5tYXRbaSxqXSkpCiAgICAgICAgZ3JpZC50ZXh0KHNwcmludGYoIiUuNmYiLCBwLnZhbHVlLm1hdFtpLCBqXSksIHgsIHksIGdwID0gZ3Bhcihmb250c2l6ZSA9IDE2KSkKfSkKZHJhdyhodCkKYGBgCgpOb3RlIHRoYXQgdGhlICoqbnVtYmVyIG9mIHRydWUgcG9zaXRpdmUgcHJlZGljdGlvbnMgaXMgYW4gb25lLWRpbWVuc2lvbmFsIG1ldHJpYyoqIGFuZCBieSAKZXhjbHVkaW5nIHRoZSBUTiwgRlAgYW5kIEZOcyB3ZSBoYXZlIGEgbGVzcy1pbmZvcm1hbnQgKGFuZCBhcmd1YWJseSBpbmNvcnJlY3QpIApwaWN0dXJlIG9mIHRoZSBtb2RlbHMgcGVyZm9ybWFuY2UgY2xhc3NpZmljYXRpb24uIApUaGF0J3Mgd2h5IHdlIHdpbGwgcHJvY2VlZCB3aXRoIHRoZSBtb3JlIGJhbGFuY2VkIE1DQyBzY29yZSBmb3IgY2xhc3NpZmluZyB0aGUgbW9kZWxzIGZpdG5lc3NlcyAKdG8gZGlmZmVyZW50IHBlcmZvcm1hbmNlIGdyb3Vwcy4KCiMjIyBNQ0MtY2xhc3MgdnMgZml0bmVzcyB7LX0KCkZpcnN0bHksIHdlIHBlcmZvcm0gYSAqdW5pdmFyaWF0ZSBrLW1lYW5zIGNsdXN0ZXJpbmcqIHRvIAoqKnNwbGl0IHRoZSBtb2RlbHMgTUNDIHZhbHVlcyB0byBkaWZmZXJlbnQgY2xhc3NlcyoqIGFuZCBwbG90IHRoZSBkYXRhIGhpc3RvZ3JhbS4KVGhlICoqbnVtYmVyIG9mIE1DQyBjbGFzc2VzKiogdG8gc3BsaXQgdGhlIGRhdGEgY2FuIGJlIGFyYml0cmFyaWx5IGNob3NlbiBhbmQgc28Kd2UgY2hvc2UgdGhlIHNhbWUgYXMgdGhlIG51bWJlciBvZiBUUCBjbGFzc2VzOgoKYGBge3IgTUNDLWNsYXNzaWZpY2F0aW9uOiBGaW5kIHRoZSBjbHVzdGVyc30KbnVtLm9mLm1jYy5jbGFzc2VzID0gNQptY2MuY2xhc3MuaWRzID0gMTpudW0ub2YubWNjLmNsYXNzZXMKCiMgZmluZCB0aGUgY2x1c3RlcnMKcmVzID0gQ2ttZWFucy4xZC5kcCh4ID0gZGZbLCJtY2MudW5pcXVlIl0sIGsgPSBudW0ub2YubWNjLmNsYXNzZXMpCm1jYy5jbGFzcy5pZCA9IHJlcyRjbHVzdGVyCmRmID0gY2JpbmQoZGYsIG1jYy5jbGFzcy5pZCkKCnBsb3RfbWNjX2NsYXNzZXNfaGlzdChkZlssIm1jYy51bmlxdWUiXSwgZGZbLCJtY2MuY2xhc3MuaWQiXSwKICAgICAgICAgICAgICAgICAgICAgIG51bS5vZi5tY2MuY2xhc3NlcywgbWNjLmNsYXNzLmlkcykKYGBgCgpUaGVuIHdlIHNob3cgc29tZSBib3ggYW5kIGRlbnNpdHkgcGxvdHMgdG8gc2VlIHByYWN0aWNhbGx5ICoqd2hhdCBhbmQgd2hlcmUgaXMgCnRoZSBkaWZmZXJlbmNlIGJldHdlZW4gdGhlIG1vZGVscyBmaXRuZXNzIHZhbHVlcyBiZWxvbmdpbmcgdG8gZGlmZmVyZW50IE1DQy1jbGFzc2VzKio6CmBgYHtyIE1DQy1jbGFzc2lmaWNhdGlvbjogUGxvdHN9CiMgQm94IHBsb3RzCmdnYm94cGxvdChkZiwgeCA9ICJtY2MuY2xhc3MuaWQiLCB5ID0gImZpdC51bmlxdWUiLCBjb2xvciA9ICJtY2MuY2xhc3MuaWQiLAogICAgICAgICAgcGFsZXR0ZSA9IHVzZWZ1bjo6OmNvbG9ycy4xMDBbMTpudW0ub2YubWNjLmNsYXNzZXNdLAogICAgICAgICAgeGxhYiA9ICJNQ0MgY2xhc3MiLCB5bGFiID0gIkZpdG5lc3MgdmFsdWVzIikKCiMgRGVuc2l0eSBQbG90cwpkZW5zaXRpZXMgPSBsaXN0KCkKZm9yIChpZCBpbiBtY2MuY2xhc3MuaWRzKSB7CiAgeCA9IGRmICU+JQogICAgZmlsdGVyKG1jYy5jbGFzcy5pZCA9PSBpZCkgJT4lCiAgICBzZWxlY3RfYXQoLnZhcnMgPSBjKCJmaXQudW5pcXVlIikpCiAgZGVuID0gZGVuc2l0eSh4JGZpdC51bmlxdWUpCiAgZGVuc2l0aWVzW1twYXN0ZTAoaWQsICIgKCIsIHJlcyRzaXplW2lkXSwgIikiKV1dID0gZGVuCn0KCm1ha2VfbXVsdGlwbGVfZGVuc2l0eV9wbG90KGRlbnNpdGllcywgbGVnZW5kLnRpdGxlID0gIk1DQyBjbGFzc2VzICgjbW9kZWxzKSIsCiAgICAgICAgdGl0bGUgPSAiRGVuc2l0eSBFc3RpbWF0aW9uIiwgeC5heGlzLmxhYmVsID0gIkZpdG5lc3Mgc2NvcmUiKQpgYGAKCkFnYWluIHdlIG9ic2VydmUgdGhhdCAqKnRoZXJlIGRvZXNu4oCZdCBzZWVtIHRvIGJlIGFueSBzaWduaWZpY2FudCBkaWZmZXJlbmNlIApiZXR3ZWVuIHRoZSBmaXRuZXNzIHZhbHVlcyBvZiB0aGUgZGlmZmVyZW50IE1DQyBjbGFzc2VzKiouIFRoZSBuZXh0IHN0YXRpc3RpY2FsCnRlc3RzIGFuZCBoZWF0bWFwIHNob3cgdGhlIHNhbWU6CgpgYGB7ciBNQ0MtY2xhc3NpZmljYXRpb246IEtydXNrYWwgVGVzdCwgY29tbWVudD0iIn0KIyBIeXBvdGhlc2lzIHRlc3Rpbmc6IEFyZSB0aGUgbG9jYXRpb24gcGFyYW1ldGVycyBvZiB0aGUgZGlzdHJpYnV0aW9uIG9mIHggdGhlIHNhbWUgaW4gZWFjaCBncm91cD8Ka3J1c2thbC50ZXN0KHggPSBkZlssImZpdC51bmlxdWUiXSwgZyA9IGRmWywgIm1jYy5jbGFzcy5pZCJdKQpgYGAKCmBgYHtyIE1DQy1jbGFzc2lmaWNhdGlvbjogUGFpcndpc2UgV2lsY294IFRlc3RzLCB3YXJuaW5nPUZBTFNFfQpwYWlyLnJlcyA9IHBhaXJ3aXNlLndpbGNveC50ZXN0KHggPSBkZlssImZpdC51bmlxdWUiXSwgZyA9IGRmWywgIm1jYy5jbGFzcy5pZCJdLCBwLmFkanVzdC5tZXRob2QgPSAiQkgiKQpwLnZhbHVlLm1hdCA9IHBhaXIucmVzJHAudmFsdWUKYGBgCgpgYGB7ciBNQ0MtY2xhc3NpZmljYXRpb246IHAtdmFsdWUgSGVhdG1hcCBvZiBQYWlyd2lzZSBXaWxjb3ggVGVzdHMsIHdhcm5pbmc9RkFMU0UsIGZpZy53aWR0aD05LCBkcGk9MzAwfQpjb2xfZnVuID0gY29sb3JSYW1wMihicmVha3MgPSBjKDAsIDAuMDUsIDAuNSwgMSksIGMoImdyZWVuIiwgIndoaXRlIiwgIm9yYW5nZSIsICJyZWQiKSkKaHQgPSBIZWF0bWFwKG1hdHJpeCA9IHAudmFsdWUubWF0LCBjbHVzdGVyX3Jvd3MgPSBGQUxTRSwgY2x1c3Rlcl9jb2x1bW5zID0gRkFMU0UsCiAgbmFfY29sID0gIndoaXRlIiwgbmFtZSA9ICJwLXZhbHVlIiwgY29sID0gY29sX2Z1biwgY29sdW1uX25hbWVzX3JvdCA9IDAsCiAgcm93X3RpdGxlID0gIk1DQyBjbGFzcyBpZCIsIHJvd190aXRsZV9yb3QgPSA5MCwgcm93X25hbWVzX3NpZGUgPSAibGVmdCIsCiAgY29sdW1uX3RpdGxlID0gIlAtdmFsdWVzIChQYWlyd2lzZSBXaWxjb3hvbiB0ZXN0cykgLSBNQ0MtY2xhc3NpZmllZCBmaXRuZXNzZXMiLCBjb2x1bW5fdGl0bGVfZ3AgPSBncGFyKGZvbnRzaXplID0gMjApLAogIGNlbGxfZnVuID0gZnVuY3Rpb24oaiwgaSwgeCwgeSwgd2lkdGgsIGhlaWdodCwgZmlsbCkgewogICAgaWYgKCFpcy5uYShwLnZhbHVlLm1hdFtpLGpdKSkKICAgICAgICBncmlkLnRleHQoc3ByaW50ZigiJS42ZiIsIHAudmFsdWUubWF0W2ksIGpdKSwgeCwgeSwgZ3AgPSBncGFyKGZvbnRzaXplID0gMjApKQp9KQpkcmF3KGh0KQpgYGAKCiMjIEV4dGVuZGVkIEFuYWx5c2lzIHstfQoKSW4gdGhpcyBzZWN0aW9uLCB3ZSBnZXQgbmV3IHN0ZWFkeSBzdGF0ZXMgdG8gdGVzdCB0aGUgZml0bmVzcyBvZiBvdXIgbW9kZWwgZGF0YXNldHMgYWdhaW5zdC4KTm90ZSB0aGF0IHRoZSBvbGQgb25lcyB1c2VkIHdlcmUgY2FsY3VsYXRlZCBieSB1c2luZyBib3RoIHRoZSBQQVJBRElHTSB0b29sIGFuZCB0b29scyB0byBpbmZlciB0cmFuc2NyaXB0aW9uIGZhY3RvciBhY3Rpdml0eSAoaW4gdGhlIENMU1Mgd29ya2Zsb3cpLiBUaGUgbmV3IG9uZXMgd2lsbCB1c2Ugb25seSBQQVJBRElHTS4KU2VlIFtSRUFETUUubWRdKGh0dHBzOi8vZ2l0aHViLmNvbS9iYmxvZGZvbi9naXRzYmUtbW9kZWwtYW5hbHlzaXMvYmxvYi9tYXN0ZXIvY2FzY2FkZS9jZWxsLWxpbmVzLTI1MDAvc3RlYWR5LXN0YXRlcy9SRUFETUUubWQpIGZvciBtb3JlIGluZm8gb24gaG93IHRvIGdlbmVyYXRlIHRoZSBuZXcgc3RlYWR5IHN0YXRlcy4KClRoZSBpZGVhIGhlcmUgaXMgdGhhdCB0aGUgYWJzZW5zZSBvZiBjb3JyZWxhdGlvbiBiZXR3ZWVuIGZpdG5lc3MgYW5kIHBlcmZvcm1hbmNlIHNob3duIGFib3ZlIGhhZCB0byBkbyB3aXRoICoqd3JvbmdseSBpbmZlcnJlZCBzdGVhZHkgc3RhdGVzKiouClNvLCBldmVuIHRob3VnaCB0aGUgbW9kZWxzIGhhdmUgbm90IGJlIHRyYWluZWQgdG8gdGhlc2UgbmV3IHN0ZWFkeSBzdGF0ZXMsIHRoZWlyIHN0YWJsZSBzdGF0ZXMgcmVtYWluIHRoZSBzYW1lIGFuZCBuZXcgZml0bmVzcyBzY29yZXMgY2FuIGJlIGV2YWx1YXRlZCBhZ2FpbnN0IHRoZXNlLiBUaHVzLCB0aGUgY29ycmVsYXRpb24gb2YgdGhlIG1vZGVscyBwZXJmb3JtYW5jZSBjb3JyZXNwb2RpbmcgbW9kZWwgZml0bmVzcyBpbiBlYWNoIGRhdGFzZXQuCgojIyMgSW5wdXQgey19CgpGaXJzdCwgd2UgZ2V0IHRoZSBuZXcgc3RlYWR5IHN0YXRlczoKYGBge3IgTmV3IFN0ZWFkeSBzdGF0ZXN9CnN0ZWFkeS5zdGF0ZS5wZXIuY2VsbC5saW5lLm5ldyA9IGxpc3QoKQpmb3IgKGNlbGwubGluZSBpbiBjZWxsLmxpbmVzKSB7CiAgZmlsZSA9IHBhc3RlMChnZXR3ZCgpLCAiL3N0ZWFkeS1zdGF0ZXMvIiwgY2VsbC5saW5lLCAiX3N0ZWFkeV9zdGF0ZSIpCiAgc3MuZGYgPSByZWFkLnRhYmxlKGZpbGUsIHNlcCA9ICJcdCIsIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkKICAKICBzdGVhZHkuc3RhdGUgPSBzcy5kZlssMl0KICBuYW1lcyhzdGVhZHkuc3RhdGUpID0gc3MuZGZbLDFdCiAgCiAgIyBjaGFuZ2UgdmFsdWUgdG8gTkEgZm9yIG5vZGVzIGZvciB3aGljaCB0aGVyZSB3YXMgbm8gYWN0aXZpdHkgZm91bmQgKGRhc2gpCiAgc3RlYWR5LnN0YXRlW3N0ZWFkeS5zdGF0ZSA9PSAiLSJdID0gTkEKICAKICAjIGtlZXAgb25seSB0aGUgbm9kZXMgdGhhdCBhcmUgaW5jbHVkZWQgaW4gdGhlIGFuYWx5c2lzCiAgc3RlYWR5LnN0YXRlID0gcHJ1bmVfYW5kX3Jlb3JkZXJfdmVjdG9yKHN0ZWFkeS5zdGF0ZSwgbm9kZS5uYW1lcykKICAgIAogICMgY2hlY2sKICBzdG9waWZub3QobmFtZXMoc3RlYWR5LnN0YXRlKSAlaW4lIG5vZGUubmFtZXMpCiAgCiAgIyByZXR1cm4gYW4gaW50ZWdlciB2ZWN0b3Igc2luY2UgdGhlIGFjdGl2aXR5IHZhbHVlcyBhcmUgYmluYXJpemVkICgwLDEpCiAgc3RlYWR5LnN0YXRlLnBlci5jZWxsLmxpbmUubmV3W1tjZWxsLmxpbmVdXSA9IHNhcHBseShzdGVhZHkuc3RhdGUsIGFzLmludGVnZXIpCn0KCiMgV2hpY2ggbm9kZXMgYXJlIG5vdCBpbiB0aGUgc3RlYWR5IHN0YXRlIHZlY3Rvcj8gKGdlbmVzOiB4eHhfZykKI25vZGUubmFtZXNbd2hpY2goKG5vZGUubmFtZXMgJWluJSBuYW1lcyhzdGVhZHkuc3RhdGUucGVyLmNlbGwubGluZS5uZXckU1c2MjApID09IEZBTFNFKSldCmBgYAoKV2UgY2hlY2sgdG8gc2VlIGlmIHRoZSAqKm5ldyBzdGVhZHkgc3RhdGVzIGFyZSBkaWZmZXJlbnQgZnJvbSBiZWZvcmUqKiBieSBjaGVja2luZyB0aGUgcGVyY2VudGFnZSBvZiBjb21tb24gc3RhdGVzOgoKYGBge3IgQ29tcGFyaW5nIHN0ZWFkeSBzdGF0ZXMgYmV0d2VlbiBvbGQgYW5kIG5ldyBtZXRob2QsIHJlc3VsdHM9J2FzaXMnfQpmb3IgKGNlbGwubGluZSBpbiBjZWxsLmxpbmVzKSB7CiAgcGVyY2VudGFnZS5tYXRjaCA9IGdldF9wZXJjZW50YWdlX29mX21hdGNoZXMoc3RlYWR5LnN0YXRlLnBlci5jZWxsLmxpbmUubmV3W1tjZWxsLmxpbmVdXSwgCiAgICBzdGVhZHkuc3RhdGUucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pCiAgZXhwcmVzID0gcGFzdGUwKGNlbGwubGluZSwgIjogIiwgc3BlY2lmeV9kZWNpbWFsKHBlcmNlbnRhZ2UubWF0Y2gsIGRpZ2l0cy50by5rZWVwID0gMikpCiAgaWYgKGNlbGwubGluZSA9PSBjZWxsLmxpbmVzWzFdKQogICAgcHJldHR5X3ByaW50X3N0cmluZyhzdHJpbmcgPSBleHByZXMpCiAgZWxzZQogICAgcHJldHR5X3ByaW50X3N0cmluZyhzdHJpbmcgPSBleHByZXMsIHdpdGguZ3QgPSBGQUxTRSkKICBwcmludF9lbXB0eV9saW5lKGh0bWwub3V0cHV0ID0gVFJVRSkKfQpgYGAKCjxkaXYgY2xhc3M9ImdyZWVuLWJveCI+ClNvLCB3ZSBzZWUgdGhhdCB0aGUgbmV3IHN0ZWFkeSBzdGF0ZSB2ZWN0b3IgZm9yIGVhY2ggY2VsbCBsaW5lIGlzIGFyb3VuZCBoYWxmIGRpZmZlcmVudCB0aGFuIHRoZSBwcmV2aW91cyBjYWxjdWxhdGVkIG9uZSwgc28gcHJldHR5IG11Y2ggcmFuZG9tIQo8L2Rpdj4KPC9icj4KCldlIGFsc28gZ2V0IHRoZSAqZ29sZC9jdXJhdGlvbi1iYXNlZCogKipzdGVhZHkgc3RhdGUgZm9yIHRoZSBBR1MgY2VsbCBsaW5lKiogd2hpY2ggd2FzIG9uZSBvZiB0aGUgZGF0YXNldHMgZ2l2ZW4gYXMgc3VwcGxlbWVudGFyeSBtYXRlcmlhbCBpbiB0aGUgcGFwZXIgW0Rpc2NvdmVyeSBvZiBEcnVnIFN5bmVyZ2llcyBpbiBHYXN0cmljIENhbmNlciBDZWxscyBQcmVkaWN0ZWQgYnkgTG9naWNhbCBNb2RlbGluZ10oaHR0cHM6Ly9kb2kub3JnLzEwLjEzNzEvam91cm5hbC5wY2JpLjEwMDQ0MjYpIC0gc2VlIFRhYmxlIFM0LiBIZXJlIGlzIGEgdmVyc2lvbiB0aGF0IGlzIGFwcGxpY2FibGUgdG8gdGhlIG5vZGUgc2V0IGluIHRoZSBDYXNjYWRlIDIuMCB0b3BvbG9neSAodGhhdCBpcyB1c2VkIGluIGFsbCBvdXIgc2ltdWxhdGlvbnMpIHdpdGggdGhlIGFkZGl0aW9uIG9mIHRoZSBzdGVhZHkgc3RhdGUgdmFsdWVzIGZvciBgUHJvc3Vydml2YWxgIGFuZCBgQW50aXN1cnZpdmFsYCBub2RlczoKCmBgYHtyIEFHUyBsaXRlcmF0dXJlIHN0ZWFkeSBzdGF0ZX0KYWdzLmxpdGVyYXR1cmUuZmlsZSA9IHBhc3RlMChnZXR3ZCgpLCAiL3N0ZWFkeS1zdGF0ZXMvQUdTX2xpdGVyYXR1cmVfc3RlYWR5X3N0YXRlIikKc3MuZGYgPSByZWFkLnRhYmxlKGFncy5saXRlcmF0dXJlLmZpbGUsIHNlcCA9ICJcdCIsIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkKYWdzLmxpdGVyYXR1cmUuc3MgPSBzcy5kZlssMl0KbmFtZXMoYWdzLmxpdGVyYXR1cmUuc3MpID0gc3MuZGZbLDFdCnN0b3BpZm5vdChuYW1lcyhhZ3MubGl0ZXJhdHVyZS5zcykgJWluJSBub2RlLm5hbWVzKQpgYGAKCkNvbXBhcmluZyB0aGUgY29tbW9uIG5vZGVzIGJldHdlZW4gdGhlIEFHUyBzdGVhZHkgc3RhdGUgdmVjdG9ycyBmb3VuZCB3aXRoIHRoZSAyIHByZXZpb3VzIG1ldGhvZHMgYW5kIHRoZSAqbGl0ZXJhdHVyZS1jdXJhdGVkKiBwcm9maWxlIGluIG9yZGVyIHRvIHNlZSAqKmhvdyBtdWNoIG1vcmUgZGlmZmVyZW50IHRoZSBjb21wdXRhdGlvbmFsbHkgcHJlZGljdGVkIHN0YXRlcyBmb3IgdGhlIEFHUyBjZWxsIGxpbmUqKiBhcmUgZnJvbSB0aGUgZ29sZCBzdGFuZGFyZCwgd2Ugc2VlIHRoYXQ6CgpgYGB7ciBBR1MgZ29sZCB2cyAyIENMU1MtY2FsY3VsYXRlZCBBR1Mgc3RlYWR5IHN0YXRlIHZlY3RvcnMsIHJlc3VsdHM9J2FzaXMnfQphZ3Muc3Mub2xkID0gcHJ1bmVfYW5kX3Jlb3JkZXJfdmVjdG9yKHN0ZWFkeS5zdGF0ZS5wZXIuY2VsbC5saW5lJEFHUywgbmFtZXMoYWdzLmxpdGVyYXR1cmUuc3MpKQphZ3Muc3MubmV3ID0gcHJ1bmVfYW5kX3Jlb3JkZXJfdmVjdG9yKHN0ZWFkeS5zdGF0ZS5wZXIuY2VsbC5saW5lLm5ldyRBR1MsIG5hbWVzKGFncy5saXRlcmF0dXJlLnNzKSkKCnByZXR0eV9wcmludF9zdHJpbmcocGFzdGUwKCJNYXRjaCBiZXR3ZWVuIHRoZSAyIEFHUyBzdGVhZHkgc3RhdGUgdmVjdG9ycyBwcnVuZWQgdG8gdGhlIGxpdGVyYXR1cmUtYmFzZWQgbm9kZXM6ICIsIHNwZWNpZnlfZGVjaW1hbChnZXRfcGVyY2VudGFnZV9vZl9tYXRjaGVzKGFncy5zcy5vbGQsIGFncy5zcy5uZXcpLCBkaWdpdHMudG8ua2VlcCA9IDIpKSkKcHJldHR5X3ByaW50X3N0cmluZyhwYXN0ZTAoIk1hdGNoIGJldHdlZW4gdGhlIG9sZCBBR1Mgc3RlYWR5IHN0YXRlIHZlY3RvciBhbmQgdGhlIGxpdGVyYXR1cmUtYmFzZWQgb25lOiAiLCBzcGVjaWZ5X2RlY2ltYWwoZ2V0X3BlcmNlbnRhZ2Vfb2ZfbWF0Y2hlcyhhZ3Muc3Mub2xkLCBhZ3MubGl0ZXJhdHVyZS5zcyksIGRpZ2l0cy50by5rZWVwID0gMikpKQpwcmV0dHlfcHJpbnRfc3RyaW5nKHBhc3RlMCgiTWF0Y2ggYmV0d2VlbiB0aGUgbmV3IEFHUyBzdGVhZHkgc3RhdGUgdmVjdG9yIGFuZCB0aGUgbGl0ZXJhdHVyZS1iYXNlZCBvbmU6ICIsIHNwZWNpZnlfZGVjaW1hbChnZXRfcGVyY2VudGFnZV9vZl9tYXRjaGVzKGFncy5zcy5uZXcsIGFncy5saXRlcmF0dXJlLnNzKSwgZGlnaXRzLnRvLmtlZXAgPSAyKSkpCmBgYAoKPGRpdiBjbGFzcz0ib3JhbmdlLWJveCI+ClNvLCBpdCBzZWVtcyB0aGF0IHRoZSAyIGNvbXB1dGF0aW9uYWwgbWV0aG9kcyBhcmUgdmVyeSBkaWZmZXJlbnQgZXZlbiB3aGVuIGNvbXBhcmVkIG9uIHRoZSBub2RlIHN1YnNldCB0aGF0IGNvbnN0aXR1dGVzIHRoZSBnb2xkIHN0YW5kYXJkLiBCb3RoIG9mIHRoZW0gc2VlbSB0byBiZSByYW5kb20gY29tcGFyZWQgdG8gdGhlIHRydWUgKGN1cmF0ZWQpIHN0ZWFkeSBzdGF0ZSBwcmVkaWN0aW9ucy4KPC9kaXY+CjwvYj4KCiMjIyBNb2RlbHMgZml0bmVzcyB7LX0KCkdpdmVuIHRoZSBuZXcgc3RlYWR5IHN0YXRlcywgd2UgcmUtY2FsY3VsYXRlIHRoZSBtb2RlbHMgZml0bmVzcyBmb3IgYm90aCB0aGUgY2VsbC1zcGVjaWZpYyBtb2RlbHMgYW5kIHRoZSByYW5kb20gb25lczoKYGBge3IgQ2VsbC1zcGVjaWZpYyBhbmQgUmFuZG9tIE1vZGVscyBuZXcgZml0bmVzcyBwZXIgY2VsbCBsaW5lfQptb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lLm5ldyA9IGxpc3QoKQpyYW5kb20ubW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZS5uZXcgPSBsaXN0KCkKCmZvciAoY2VsbC5saW5lIGluIGNlbGwubGluZXMpIHsKICBzdGVhZHkuc3RhdGUgPSBzdGVhZHkuc3RhdGUucGVyLmNlbGwubGluZS5uZXdbW2NlbGwubGluZV1dCiAgbW9kZWxzLnN0YWJsZS5zdGF0ZSA9IG1vZGVscy5zdGFibGUuc3RhdGUucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0KICAKICAjIEZpdG5lc3MgcGVyIG1vZGVsIGNvbnRyYXN0ZWQgdG8gc3RlYWR5IHN0YXRlIGZyb20gY2VsbCBsaW5lCiAgbW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZS5uZXdbW2NlbGwubGluZV1dID0gYXBwbHkoCiAgICBtb2RlbHMuc3RhYmxlLnN0YXRlWyxuYW1lcyhzdGVhZHkuc3RhdGUpXSwgMSwgZ2V0X3BlcmNlbnRhZ2Vfb2ZfbWF0Y2hlcywgc3RlYWR5LnN0YXRlCiAgKQogIAogIHJhbmRvbS5tb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lLm5ld1tbY2VsbC5saW5lXV0gPSBhcHBseSgKICAgIHJhbmRvbS5tb2RlbHMuc3RhYmxlLnN0YXRlWyxuYW1lcyhzdGVhZHkuc3RhdGUpXSwgMSwgCiAgICBnZXRfcGVyY2VudGFnZV9vZl9tYXRjaGVzLCBzdGVhZHkuc3RhdGUKICApCn0KYGBgCgpXZSBjYWxjdWxhdGUgdGhlIGZpdG5lc3Mgb2YgdGhlIEFHUy10cmFpbmVkIG1vZGVscyBhbmQgdGhlIHJhbmRvbSBvbmVzIHRvIHRoZSAqKmxpdGVyYXR1cmUtY3VyYXRlZCBzdGVhZHkgc3RhdGUgZm9yIHRoZSBBR1MgY2VsbCBsaW5lKio6CmBgYHtyIEFHUy1zcGVjaWZpYyBhbmQgUmFuZG9tIE1vZGVscyBmaXRuZXNzIHRvIGxpdGVyYXR1cmUtY3VyYXRlZCBBR1Mgc3RlYWR5IHN0YXRlfQphZ3MubW9kZWxzLnN0YWJsZS5zdGF0ZSA9IG1vZGVscy5zdGFibGUuc3RhdGUucGVyLmNlbGwubGluZSRBR1MKCm1vZGVscy5maXRuZXNzLnRvLmFncy5saXRlcmF0dXJlLnNzID0gYXBwbHkoCiAgYWdzLm1vZGVscy5zdGFibGUuc3RhdGVbLG5hbWVzKGFncy5saXRlcmF0dXJlLnNzKV0sIDEsIAogIGdldF9wZXJjZW50YWdlX29mX21hdGNoZXMsIGFncy5saXRlcmF0dXJlLnNzKQpyYW5kb20ubW9kZWxzLmZpdG5lc3MudG8uYWdzLmxpdGVyYXR1cmUuc3MgPSBhcHBseSgKICByYW5kb20ubW9kZWxzLnN0YWJsZS5zdGF0ZVssbmFtZXMoYWdzLmxpdGVyYXR1cmUuc3MpXSwgMSwgCiAgZ2V0X3BlcmNlbnRhZ2Vfb2ZfbWF0Y2hlcywgYWdzLmxpdGVyYXR1cmUuc3MpCmBgYAoKIyMjIENob29zZSBiZXN0IGRhdGFzZXQgey19CgpGb2xsb3dpbmcgdGhlIHNhbWUgcHJvY2VkdXJlIGFzIGJlZm9yZSwgd2UgdHJ5IHRvIGZpbmQgdGhlIGJlc3QgZGF0YXNldCB0byBkbyB0aGUgY29ycmVsYXRpb24gYW5hbHlzaXMgb24uIFN1Y2ggZGF0YXNldCB3aWxsIGhhdmUgbW9kZWxzIHRoYXQgcmVzdWx0IGluIGEgKipsYXJnZSBNQ0MgKyBmaXRuZXNzIHJhbmdlKiogd2hlbiBjb250cmFzdGVkIHRvIHRoZSBuZXcgc3RlYWR5IHN0YXRlcy4KClRoZSBuZXh0IHR3byB0YWJsZXMgd2lsbCBoZWxwIHVzIGRldGVybWluZSBleGFjdGx5IHdoaWNoIGNlbGwgbGluZSBhbmQgZGF0YXNldAp0byB1c2U6CmBgYHtyIENlbGwtc3BlY2lmaWMgTW9kZWxzIFRQICsgTUNDICsgZml0bmVzcyBwZXIgY2VsbCBsaW5lIHN0YXRzIHdpdGggbmV3IHN0ZWFkeSBzdGF0ZXN9CmNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YSA9IG1hdHJpeChkYXRhID0gTkEsIG5yb3cgPSBsZW5ndGgoY2VsbC5saW5lcykgKyAxLCBuY29sID0gMTEpCnJvd25hbWVzKGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YSkgPSBjKGNlbGwubGluZXMsICJBR1MtZ29sZCIpCmNvbG5hbWVzKGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YSkgPSBjKCJmaXRuZXNzIHJhbmdlIiwgIk1pbiBmaXRuZXNzIiwgCiAgIk1heCBmaXRuZXNzIiwgIk1lYW4gZml0bmVzcyIsICJNZWRpYW4gZml0bmVzcyIsICJNQ0MgcmFuZ2UiLCAiTWluIE1DQyIsIAogICJNYXggTUNDIiwgIk1lYW4gTUNDIiwgIk1lZGlhbiBNQ0MiLCAiTWF4IFRQUiIpCgpmb3IgKGNlbGwubGluZSBpbiBjKGNlbGwubGluZXMsICJBR1MtZ29sZCIpKSB7CiAgaWYgKGNlbGwubGluZSA9PSAiQUdTLWdvbGQiKSB7CiAgICBtb2RlbHMuZml0bmVzcyA9IG1vZGVscy5maXRuZXNzLnRvLmFncy5saXRlcmF0dXJlLnNzCiAgICBtb2RlbHMubWNjID0gbW9kZWxzLm1jYy5wZXIuY2VsbC5saW5lJEFHUwogICAgbW9kZWxzLnRwID0gbW9kZWxzLnRwLnBlci5jZWxsLmxpbmUkQUdTCiAgfSBlbHNlIHsKICAgIG1vZGVscy5maXRuZXNzID0gbW9kZWxzLmZpdG5lc3MucGVyLmNlbGwubGluZS5uZXdbW2NlbGwubGluZV1dCiAgICBtb2RlbHMubWNjID0gbW9kZWxzLm1jYy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogICAgbW9kZWxzLnRwID0gbW9kZWxzLnRwLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dCiAgfQogIAogIGZpdC5zdW1tYXJ5ID0gdW5jbGFzcyhzdW1tYXJ5KG1vZGVscy5maXRuZXNzKSkKICBtY2Muc3VtbWFyeSA9IHVuY2xhc3Moc3VtbWFyeShtb2RlbHMubWNjKSkKICBpZiAoY2VsbC5saW5lID09ICJBR1MtZ29sZCIpIHsKICAgIG1heC50cHIgPSBtYXgobW9kZWxzLnRwKSAvIGxlbmd0aChvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZSRBR1MpCiAgfSBlbHNlIHsKICAgIG1heC50cHIgPSBtYXgobW9kZWxzLnRwKSAvIGxlbmd0aChvYnNlcnZlZC5zeW5lcmdpZXMucGVyLmNlbGwubGluZVtbY2VsbC5saW5lXV0pCiAgfQogIAogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJmaXRuZXNzIHJhbmdlIl0gPSBmaXQuc3VtbWFyeVsiTWF4LiJdIC0gZml0LnN1bW1hcnlbIk1pbi4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNaW4gZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1pbi4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNYXggZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1heC4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWFuIGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNZWFuIl0KICBjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWVkaWFuIGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNZWRpYW4iXQogIAogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNQ0MgcmFuZ2UiXSA9IG1jYy5zdW1tYXJ5WyJNYXguIl0gLSBtY2Muc3VtbWFyeVsiTWluLiJdCiAgY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1pbiBNQ0MiXSA9IG1jYy5zdW1tYXJ5WyJNaW4uIl0KICBjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWF4IE1DQyJdID0gbWNjLnN1bW1hcnlbIk1heC4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWFuIE1DQyJdID0gbWNjLnN1bW1hcnlbIk1lYW4iXQogIGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWRpYW4gTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWVkaWFuIl0KICAKICBjZWxsLnNwZWNpZmljLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWF4IFRQUiJdID0gbWF4LnRwcgp9CgojIGNvbG9yIGNvbHVtbnMKZml0LmJyZWFrcyA9IHF1YW50aWxlKGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YVssImZpdG5lc3MgcmFuZ2UiXSwgcHJvYnMgPSBzZXEoLjA1LCAuOTUsIC4wNSksIG5hLnJtID0gVFJVRSkKZml0LmNvbG9ycyA9IHJvdW5kKHNlcSgyNTUsIDQwLCBsZW5ndGgub3V0ID0gbGVuZ3RoKGZpdC5icmVha3MpICsgMSksIDApICU+JQogIHtwYXN0ZTAoInJnYigyNTUsIiwgLiwgIiwiLCAuLCAiKSIpfSAjIHJlZAptY2MuYnJlYWtzID0gcXVhbnRpbGUoY2VsbC5zcGVjaWZpYy5tb2RlbC5kYXRhWywiTUNDIHJhbmdlIl0sIHByb2JzID0gc2VxKC4wNSwgLjk1LCAuMDUpLCBuYS5ybSA9IFRSVUUpCm1jYy5jb2xvcnMgPSByb3VuZChzZXEoMjU1LCA0MCwgbGVuZ3RoLm91dCA9IGxlbmd0aChtY2MuYnJlYWtzKSArIDEpLCAwKSAlPiUKICB7cGFzdGUwKCJyZ2IoIiwgLiwgIiwyNTUsIiwgLiwgIikiKX0gIyBncmVlbgoKY2FwdGlvbi50aXRsZSA9ICJUYWJsZSAzOiBGaXRuZXNzLCBNQ0Mgc2NvcmVzIGFuZCBUUCAoVHJ1ZSBwb3NpdGl2ZXMpIGZvciB0aGUgQ2VsbC1zcGVjaWZpYyBtb2RlbCBwcmVkaWN0aW9ucyBhY3Jvc3MgOCBDZWxsIExpbmVzIgpkYXRhdGFibGUoZGF0YSA9IGNlbGwuc3BlY2lmaWMubW9kZWwuZGF0YSwgCiAgICAgICAgICBvcHRpb25zID0gbGlzdChkb20gPSAidCIpLCAjIGp1c3Qgc2hvdyB0aGUgdGFibGUKICAgICAgICAgIGNhcHRpb24gPSBodG1sdG9vbHM6OnRhZ3MkY2FwdGlvbihjYXB0aW9uLnRpdGxlLCBzdHlsZT0iY29sb3I6I2RkNDgxNDsgZm9udC1zaXplOiAxOHB4IikpICU+JSAKICBmb3JtYXRSb3VuZCgxOjExLCBkaWdpdHMgPSAzKSAlPiUKICBmb3JtYXRTdHlsZShjb2x1bW5zID0gYygiZml0bmVzcyByYW5nZSIpLCBiYWNrZ3JvdW5kQ29sb3IgPSBzdHlsZUludGVydmFsKGZpdC5icmVha3MsIGZpdC5jb2xvcnMpKSAlPiUKICBmb3JtYXRTdHlsZShjb2x1bW5zID0gYygiTUNDIHJhbmdlIiksIGJhY2tncm91bmRDb2xvciA9IHN0eWxlSW50ZXJ2YWwobWNjLmJyZWFrcywgbWNjLmNvbG9ycykpCmBgYAoKYGBge3IgUmFuZG9tIE1vZGVscyBUUCArIE1DQyArIGZpdG5lc3MgcGVyIGNlbGwgbGluZSBzdGF0cyB3aXRoIG5ldyBzdGVhZHkgc3RhdGVzfQpyYW5kb20ubW9kZWwuZGF0YSA9IG1hdHJpeChkYXRhID0gTkEsIG5yb3cgPSBsZW5ndGgoY2VsbC5saW5lcykgKyAxLCBuY29sID0gMTEpCnJvd25hbWVzKHJhbmRvbS5tb2RlbC5kYXRhKSA9IGMoY2VsbC5saW5lcywgIkFHUy1nb2xkIikKY29sbmFtZXMocmFuZG9tLm1vZGVsLmRhdGEpID0gYygiZml0bmVzcyByYW5nZSIsICJNaW4gZml0bmVzcyIsIAogICJNYXggZml0bmVzcyIsICJNZWFuIGZpdG5lc3MiLCAiTWVkaWFuIGZpdG5lc3MiLCAiTUNDIHJhbmdlIiwgIk1pbiBNQ0MiLCAKICAiTWF4IE1DQyIsICJNZWFuIE1DQyIsICJNZWRpYW4gTUNDIiwgIk1heCBUUFIiKQoKZm9yIChjZWxsLmxpbmUgaW4gYyhjZWxsLmxpbmVzLCAiQUdTLWdvbGQiKSkgewogIGlmIChjZWxsLmxpbmUgPT0gIkFHUy1nb2xkIikgewogICAgbW9kZWxzLmZpdG5lc3MgPSByYW5kb20ubW9kZWxzLmZpdG5lc3MudG8uYWdzLmxpdGVyYXR1cmUuc3MKICAgIG1vZGVscy5tY2MgPSByYW5kb20ubW9kZWxzLm1jYy5wZXIuY2VsbC5saW5lJEFHUwogICAgbW9kZWxzLnRwID0gcmFuZG9tLm1vZGVscy50cC5wZXIuY2VsbC5saW5lJEFHUwogIH0gZWxzZSB7CiAgICBtb2RlbHMuZml0bmVzcyA9IHJhbmRvbS5tb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lLm5ld1tbY2VsbC5saW5lXV0KICAgIG1vZGVscy5tY2MgPSByYW5kb20ubW9kZWxzLm1jYy5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogICAgbW9kZWxzLnRwID0gcmFuZG9tLm1vZGVscy50cC5wZXIuY2VsbC5saW5lW1tjZWxsLmxpbmVdXQogIH0KICAKICBmaXQuc3VtbWFyeSA9IHVuY2xhc3Moc3VtbWFyeShtb2RlbHMuZml0bmVzcykpCiAgbWNjLnN1bW1hcnkgPSB1bmNsYXNzKHN1bW1hcnkobW9kZWxzLm1jYykpCiAgaWYgKGNlbGwubGluZSA9PSAiQUdTLWdvbGQiKSB7CiAgICBtYXgudHByID0gbWF4KG1vZGVscy50cCkgLyBsZW5ndGgob2JzZXJ2ZWQuc3luZXJnaWVzLnBlci5jZWxsLmxpbmUkQUdTKQogIH0gZWxzZSB7CiAgICBtYXgudHByID0gbWF4KG1vZGVscy50cCkgLyBsZW5ndGgob2JzZXJ2ZWQuc3luZXJnaWVzLnBlci5jZWxsLmxpbmVbW2NlbGwubGluZV1dKQogIH0KICAKICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJmaXRuZXNzIHJhbmdlIl0gPSBmaXQuc3VtbWFyeVsiTWF4LiJdIC0gZml0LnN1bW1hcnlbIk1pbi4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1pbiBmaXRuZXNzIl0gPSBmaXQuc3VtbWFyeVsiTWluLiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWF4IGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNYXguIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWFuIGZpdG5lc3MiXSA9IGZpdC5zdW1tYXJ5WyJNZWFuIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWRpYW4gZml0bmVzcyJdID0gZml0LnN1bW1hcnlbIk1lZGlhbiJdCiAgCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTUNDIHJhbmdlIl0gPSBtY2Muc3VtbWFyeVsiTWF4LiJdIC0gbWNjLnN1bW1hcnlbIk1pbi4iXQogIHJhbmRvbS5tb2RlbC5kYXRhW2NlbGwubGluZSwgIk1pbiBNQ0MiXSA9IG1jYy5zdW1tYXJ5WyJNaW4uIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNYXggTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWF4LiJdCiAgcmFuZG9tLm1vZGVsLmRhdGFbY2VsbC5saW5lLCAiTWVhbiBNQ0MiXSA9IG1jYy5zdW1tYXJ5WyJNZWFuIl0KICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNZWRpYW4gTUNDIl0gPSBtY2Muc3VtbWFyeVsiTWVkaWFuIl0KICAKICByYW5kb20ubW9kZWwuZGF0YVtjZWxsLmxpbmUsICJNYXggVFBSIl0gPSBtYXgudHByCn0KCiMgY29sb3IgY29sdW1ucwpmaXQuYnJlYWtzID0gcXVhbnRpbGUocmFuZG9tLm1vZGVsLmRhdGFbLCJmaXRuZXNzIHJhbmdlIl0sIHByb2JzID0gc2VxKC4wNSwgLjk1LCAuMDUpLCBuYS5ybSA9IFRSVUUpCmZpdC5jb2xvcnMgPSByb3VuZChzZXEoMjU1LCA0MCwgbGVuZ3RoLm91dCA9IGxlbmd0aChmaXQuYnJlYWtzKSArIDEpLCAwKSAlPiUKICB7cGFzdGUwKCJyZ2IoMjU1LCIsIC4sICIsIiwgLiwgIikiKX0gIyByZWQKbWNjLmJyZWFrcyA9IHF1YW50aWxlKHJhbmRvbS5tb2RlbC5kYXRhWywiTUNDIHJhbmdlIl0sIHByb2JzID0gc2VxKC4wNSwgLjk1LCAuMDUpLCBuYS5ybSA9IFRSVUUpCm1jYy5jb2xvcnMgPSByb3VuZChzZXEoMjU1LCA0MCwgbGVuZ3RoLm91dCA9IGxlbmd0aChtY2MuYnJlYWtzKSArIDEpLCAwKSAlPiUKICB7cGFzdGUwKCJyZ2IoIiwgLiwgIiwyNTUsIiwgLiwgIikiKX0gIyBncmVlbgoKY2FwdGlvbi50aXRsZSA9ICJUYWJsZSA0OiBGaXRuZXNzLCBNQ0Mgc2NvcmVzIGFuZCBUUCAoVHJ1ZSBwb3NpdGl2ZXMpIGZvciB0aGUgcmFuZG9tIG1vZGVsIHByZWRpY3Rpb25zIGFjcm9zcyA4IENlbGwgTGluZXMiCmRhdGF0YWJsZShkYXRhID0gcmFuZG9tLm1vZGVsLmRhdGEsIAogICAgICAgICAgb3B0aW9ucyA9IGxpc3QoZG9tID0gInQiKSwgIyBqdXN0IHNob3cgdGhlIHRhYmxlCiAgICAgICAgICBjYXB0aW9uID0gaHRtbHRvb2xzOjp0YWdzJGNhcHRpb24oY2FwdGlvbi50aXRsZSwgc3R5bGU9ImNvbG9yOiNkZDQ4MTQ7IGZvbnQtc2l6ZTogMThweCIpKSAlPiUgCiAgZm9ybWF0Um91bmQoMToxMSwgZGlnaXRzID0gMykgJT4lCiAgZm9ybWF0U3R5bGUoY29sdW1ucyA9IGMoImZpdG5lc3MgcmFuZ2UiKSwgYmFja2dyb3VuZENvbG9yID0gc3R5bGVJbnRlcnZhbChmaXQuYnJlYWtzLCBmaXQuY29sb3JzKSkgJT4lCiAgZm9ybWF0U3R5bGUoY29sdW1ucyA9IGMoIk1DQyByYW5nZSIpLCBiYWNrZ3JvdW5kQ29sb3IgPSBzdHlsZUludGVydmFsKG1jYy5icmVha3MsIG1jYy5jb2xvcnMpKQpgYGAKClRoZSBiZWxvdyBib3ggcGxvdHMgY29tcGFyZSB0aGUgbmV3IGZpdG5lc3Mgc2NvcmVzIGFjcm9zcyBhbGwgY2VsbCBsaW5lczoKYGBge3IgQ29tYmluZSBmaXRuZXNzIGRhdGEgaW50byBvbmUgZGF0YSBmcmFtZX0KbnVtLm9mLm1vZGVscyA9IG5yb3cocmFuZG9tLm1vZGVscy5zdGFibGUuc3RhdGUpCmRhdGEubGlzdCA9IGxpc3QoKQoKZm9yIChjZWxsLmxpbmUgaW4gYyhjZWxsLmxpbmVzLCAiQUdTLWdvbGQiKSkgewogIGNlbGwubGluZS52ZWMgPSBhcy5kYXRhLmZyYW1lKHJlcChjZWxsLmxpbmUsIG51bS5vZi5tb2RlbHMpLCBzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UpCiAgCiAgaWYgKGNlbGwubGluZSA9PSAiQUdTLWdvbGQiKSB7CiAgICBtb2RlbHMuZml0bmVzcy5jZWxsLnNwZWNpZmljID0gcmVtb3ZlX3Jvd25hbWVzKGFzLmRhdGEuZnJhbWUobW9kZWxzLmZpdG5lc3MudG8uYWdzLmxpdGVyYXR1cmUuc3MpKQogICAgbW9kZWxzLmZpdG5lc3MucmFuZG9tICAgICAgICA9IHJlbW92ZV9yb3duYW1lcyhhcy5kYXRhLmZyYW1lKHJhbmRvbS5tb2RlbHMuZml0bmVzcy50by5hZ3MubGl0ZXJhdHVyZS5zcykpCiAgfSBlbHNlIHsKICAgIG1vZGVscy5maXRuZXNzLmNlbGwuc3BlY2lmaWMgPSByZW1vdmVfcm93bmFtZXMoYXMuZGF0YS5mcmFtZShtb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lLm5ld1tbY2VsbC5saW5lXV0pKQogICAgbW9kZWxzLmZpdG5lc3MucmFuZG9tICAgICAgICA9IHJlbW92ZV9yb3duYW1lcyhhcy5kYXRhLmZyYW1lKHJhbmRvbS5tb2RlbHMuZml0bmVzcy5wZXIuY2VsbC5saW5lLm5ld1tbY2VsbC5saW5lXV0pKQogIH0KICAKICBjb2xuYW1lcyhtb2RlbHMuZml0bmVzcy5jZWxsLnNwZWNpZmljKSA9IE5VTEwKICBjb2xuYW1lcyhtb2RlbHMuZml0bmVzcy5yYW5kb20pID0gTlVMTAogIAogIGRhdGEubGlzdFtbY2VsbC5saW5lXV0gPSAKICAgIGJpbmRfY29scyhjZWxsLmxpbmUudmVjLCBtb2RlbHMuZml0bmVzcy5jZWxsLnNwZWNpZmljLCBtb2RlbHMuZml0bmVzcy5yYW5kb20pCn0KCmRhdGEgPSBiaW5kX3Jvd3MoZGF0YS5saXN0KQpjb2xuYW1lcyhkYXRhKSA9IGMoImNlbGwubGluZSIsICJmaXRuZXNzIGNlbGwtc3BlY2lmaWMiLCAiZml0bmVzcyByYW5kb20iKQpgYGAKCmBgYHtyIEJveHBsb3RzOiBGaXRuZXNzIHZhbHVlcyBiZXR3ZWVuIGNlbGwtc3BlY2lmaWMgYW5kIHJhbmRvbSBtb2RlbHMgZm9yIHRoZSBuZXcgc3RlYWR5IHN0YXRlcywgZmlnLndpZHRoPTEwLCBjYWNoZT1UUlVFfQpnZ2JveHBsb3QoZGF0YSwgeCA9ICJjZWxsLmxpbmUiLCB5ID0gYygiZml0bmVzcyBjZWxsLXNwZWNpZmljIiwgImZpdG5lc3MgcmFuZG9tIiksCiAgICAgICAgICBwYWxldHRlID0gYnJld2VyLnBhbCgzLCAiU2V0MSIpLCBtZXJnZSA9ICJhc2lzIiwKICAgICAgICAgIHhsYWIgPSAiQ2VsbCBMaW5lcyIsIHlsYWIgPSAiRml0bmVzcyB2YWx1ZXMiLCBhZGQgPSAicG9pbnQiLCBhZGQucGFyYW1zID0gbGlzdChzaXplID0gMC41KSkKYGBgCgo8ZGl2IGNsYXNzPSJncmVlbi1ib3giPgpXZSBvYnNlcnZlIGZyb20gVGFibGVzIDMgYW5kIDQgYW5kIHRoZSBib3hwbG90cyBhYm92ZSB0aGF0IGFsbCBtb2RlbHMgc2hvdyBhIHdpZGUgcmFuZ2Ugb2YgZml0bmVzcyB2YWx1ZXMgc2luY2Ugbm8gbW9kZWxzIHdlcmUgKnRyYWluZWQqIHRvIHRoZSBuZXcgc3RlYWR5IHN0YXRlcyAoaXQncyBsaWtlIGFsbCBtb2RlbHMgYXJlIHJhbmRvbS1nZW5lcmF0ZWQpLiBBbHNvLCAqKnRoZSBmaXRuZXNzIG9mIHRoZSBBR1Mtc3BlY2lmaWMgYW5kIHJhbmRvbSBtb2RlbHMgdG8gdGhlIGxpdGVyYXR1cmUtY3VyYXRlZCBzdGVhZHkgc3RhdGUqKiBoYXMgdGhlIGxhcmdlc3QgcG9zc2libGUgZml0bmVzcyByYW5nZSBhbmQgc21hbGwgbnVtYmVyIG9mIHVuaXF1ZSBmaXRuZXNzIHZhbHVlcyBhbW9uZyB0aGUgbW9kZWxzICh0b3RhbCBvZiAkMTMkKS4KPC9kaXY+CjwvYnI+Cgo8ZGl2IGNsYXNzPSJvcmFuZ2UtYm94Ij4KV2UgY2hlY2tlZCB0aGUgZm9sbG93aW5nIGRhdGFzZXRzIGFuZCBmb3VuZCAqKm5vIHN0YXRpc3RpY2FsbHkgc2lnbmlmaWNhbnQgY29ycmVsYXRpb24qKiBiZXR3ZWVuIGZpdG5lc3Mgc2NvcmVzIGFuZCBwZXJmb3JtYW5jZSBtZXRyaWMgKFRQIG9yIE1DQyk6CgotIGBTRjI5NWAsIGBEVTE0NWAgd2l0aCBjZWxsLXNwZWNpZmljIG1vZGVscyAoZnJvbSBUYWJsZSAzKQotIGBBNDk4YCwgYFVBQ0M2MmAsIGBEVTE0NWAsIGBBR1NgIHdpdGggcmFuZG9tIG1vZGVscyAoZnJvbSBUYWJsZSA0KQo8L2Rpdj4KPC9icj4KCioqTmV4dCwgd2UgaW52ZXN0aWdhdGUgdGhlIEFHUy1nb2xkIGRhdGFzZXQqKiAgKHJvdyB3aXRoIHRoZSBoaWdoZXIgZml0bmVzcyBhbmQgTUNDIHJhbmdlIGZyb20gVGFibGUgMykuIEluIHRoaXMgZGF0YXNldCB0aGUgcGVyZm9ybWFuY2UgKE1DQyBzY29yZSkgb2YgdGhlIGNlbGwtc3BlY2lmaWMgQUdTIG1vZGVscyBpcyBhc3NvY2lhdGVkIHdpdGggdGhlIG1vZGVscyBmaXRuZXNzIHRvIHRoZSBsaXRlcmF0dXJlLWN1cmF0ZWQgQUdTIHN0ZWFkeSBzdGF0ZS4KCiMjIyBBR1MtZ29sZCBTdGF0aXN0aWNhbCBBbmFseXNpcyB7LX0KCkZpcnN0bHksIHdlOgoKLSBGaWx0ZXIgdGhlIGRhdGEgYnkgZmluZGluZyB0aGUgKip1bmlxdWUgbW9kZWxzKiogLSB0aG9zZSB0aGF0IGhhdmUgc3RyaWN0bHkgZGlmZmVyZW50IGJvb2xlYW4gZXF1YXRpb25zCi0gUmVtb3ZlIHRoZSB1bmlxdWUgbW9kZWxzIHdpdGggTmFOIE1DQyBzY29yZXMKLSBNZXJnZSB0aGUgZml0bmVzcyBzY29yZXMgYW5kIE1DQyB2YWx1ZXMgaW4gYSBjb21tb24gZGF0YS5mcmFtZSBvYmplY3QKCmBgYHtyIFNhdmUgYmVzdCBkYXRhc2V0cyAod2l0aCBuZXcgc3RlYWR5IHN0YXRlcyl9CmJlc3QuY2VsbC5saW5lID0gIkFHUyIKCiMgQUdTLUdvbGQgY2VsbC1zcGVjaWZpYyBkYXRhCmZpdCA9IG1vZGVscy5maXRuZXNzLnRvLmFncy5saXRlcmF0dXJlLnNzCnRwICA9IG1vZGVscy50cC5wZXIuY2VsbC5saW5lW1tiZXN0LmNlbGwubGluZV1dCm1jYyA9IG1vZGVscy5tY2MucGVyLmNlbGwubGluZVtbYmVzdC5jZWxsLmxpbmVdXQoKdW5pcXVlLm1vZGVscyA9IHJvd25hbWVzKHVuaXF1ZShtb2RlbHMubGluay5vcGVyYXRvcnMucGVyLmNlbGwubGluZVtbYmVzdC5jZWxsLmxpbmVdXSkpCnVuaXF1ZS5tb2RlbHMgPSB1bmlxdWUubW9kZWxzWyF1bmlxdWUubW9kZWxzICVpbiUgbmFtZXMod2hpY2goaXMubmEobWNjKSkpXQoKZml0LnVuaXF1ZSA9IGZpdFtuYW1lcyhmaXQpICVpbiUgdW5pcXVlLm1vZGVsc10KdHAudW5pcXVlICA9IHRwW25hbWVzKHRwKSAlaW4lIHVuaXF1ZS5tb2RlbHNdCm1jYy51bmlxdWUgPSBtY2NbbmFtZXMobWNjKSAlaW4lIHVuaXF1ZS5tb2RlbHNdCgpkZiA9IGFzLmRhdGEuZnJhbWUoY2JpbmQoZml0LnVuaXF1ZSwgbWNjLnVuaXF1ZSwgdHAudW5pcXVlKSkKYGBgCgpUaGVuLCB3ZSBjaGVjayBpZiB0aGUgb3VyIGRhdGEgaXMgbm9ybWFsbHkgZGlzdHJpYnV0ZWQgKHVzaW5nIHRoZSBTaGFwaXJvLVdpbGsKdGVzdCBmb3Igbm9ybWFsaXR5IGFuZCB0aGUgUS1RIHBsb3RzKToKYGBge3IgTm9ybWFsaXR5IHRlc3RpbmcgMiwgd2FybmluZz1GQUxTRSwgY29tbWVudD0iIiwgY2FjaGU9VFJVRX0KZ2dxcXBsb3QoZGF0YSA9IGRmLCB4ID0gImZpdC51bmlxdWUiLCB5bGFiID0gIkZpdG5lc3MiKQpnZ3FxcGxvdChkYXRhID0gZGYsIHggPSAibWNjLnVuaXF1ZSIsIHlsYWIgPSAiTUNDIikKCnNoYXBpcm8udGVzdCh4ID0gc2FtcGxlKHggPSBkZiRmaXQudW5pcXVlLCBzaXplID0gMTAwMCkpCnNoYXBpcm8udGVzdCh4ID0gc2FtcGxlKHggPSBkZiRtY2MudW5pcXVlLCBzaXplID0gMTAwMCkpCmBgYAoKRnJvbSB0aGUgYWJvdmUgcmVzdWx0cyB3ZSBvYnNlcnZlIHRoYXQgYm90aCB0aGUgZml0bmVzcyBhbmQgTUNDIHNjb3JlcyBpbiBib3RoIGRhdGFzZXRzIGFyZSBzdXJlbHkgbm90IG5vcm1hbGx5IGRpc3RyaWJ1dGVkICh3aXRoIHN0YXRpc3RpY2FsIHNpZ25pZmljYW5jZSkuClRodXMsIHdlIHdpbGwgdXNlICoqbm9uLXBhcmFtZXRyaWMgY29ycmVsYXRpb24gc2NvcmVzLCBuYW1lbHkgdGhlIFNwZWFybWFuIGFuZCBLZW5kYWxsIHJhbmstYmFzZWQgY29ycmVsYXRpb24gdGVzdHMqKiwgdG8gY2hlY2sgdGhlIGNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIHR3byBjb250aW51b3VzIHZhcmlhYmxlcyAobW9kZWxzIGZpdG5lc3MgdmFsdWVzIGFuZCB0aGVpciBjb3JyZXNwb25kaW5nIE1DQyBzY29yZSk6CmBgYHtyIFJhbmsgY29ycmVsYXRpb24gcGxvdHM6IEZpdG5lc3MgdG8gY3VyYXRlZCBBR1Mgc3MgdnMgTUNDLCBjYWNoZT1UUlVFfQpnZ3NjYXR0ZXIoZGYsIHggPSAiZml0LnVuaXF1ZSIsIHkgPSAibWNjLnVuaXF1ZSIsIAogICAgICAgICAgdGl0bGUgPSAiQUdTLUdvbGQgKENlbGwtc3BlY2lmaWMpOiBGaXRuZXNzIHZzIFBlcmZvcm1hbmNlIC0gU3BlYXJtYW4gQ29ycmVsYXRpb24iLAogICAgICAgICAgYWRkID0gInJlZy5saW5lIiwgY29uZi5pbnQgPSBUUlVFLCAKICAgICAgICAgIGNvci5jb2VmID0gVFJVRSwgY29yLmNvZWZmLmFyZ3MgPSBsaXN0KG1ldGhvZCA9ICJzcGVhcm1hbiIpLAogICAgICAgICAgeGxhYiA9ICJGaXRuZXNzIFZhbHVlcyIsIHlsYWIgPSAiTUNDIHNjb3JlcyIpCmdnc2NhdHRlcihkZiwgeCA9ICJmaXQudW5pcXVlIiwgeSA9ICJtY2MudW5pcXVlIiwgCiAgICAgICAgICB0aXRsZSA9ICJBR1MtR29sZCAoQ2VsbC1zcGVjaWZpYyk6IEZpdG5lc3MgdnMgUGVyZm9ybWFuY2UgLSBLZW5kYWxsIENvcnJlbGF0aW9uIiwKICAgICAgICAgIGFkZCA9ICJyZWcubGluZSIsIGNvbmYuaW50ID0gVFJVRSwgCiAgICAgICAgICBjb3IuY29lZiA9IFRSVUUsIGNvci5jb2VmZi5hcmdzID0gbGlzdChtZXRob2QgPSAia2VuZGFsbCIpLAogICAgICAgICAgeGxhYiA9ICJGaXRuZXNzIFZhbHVlcyIsIHlsYWIgPSAiTUNDIHNjb3JlcyIpCmBgYAoKIyMjIyBGaXRuZXNzIHZzIFRQL01DQy1jbGFzcyB7LX0KClNwbGl0IHRoZSBtb2RlbHMgdG8gTUNDIGNsYXNzZXM6CmBgYHtyIE1DQy1jbGFzc2lmaWNhdGlvbjogRmluZCB0aGUgY2x1c3RlcnMgMiwgY2FjaGU9VFJVRX0KbnVtLm9mLm1jYy5jbGFzc2VzID0gNQptY2MuY2xhc3MuaWRzID0gMTpudW0ub2YubWNjLmNsYXNzZXMKCiMgZmluZCB0aGUgY2x1c3RlcnMKcmVzID0gQ2ttZWFucy4xZC5kcCh4ID0gZGYkbWNjLnVuaXF1ZSwgayA9IG51bS5vZi5tY2MuY2xhc3NlcykKbWNjLmNsYXNzLmlkID0gcmVzJGNsdXN0ZXIKZGYgPSBjYmluZChkZiwgbWNjLmNsYXNzLmlkKQoKcGxvdF9tY2NfY2xhc3Nlc19oaXN0KGRmJG1jYy51bmlxdWUsIGRmJG1jYy5jbGFzcy5pZCwgCiAgbnVtLm9mLm1jYy5jbGFzc2VzLCBtY2MuY2xhc3MuaWRzKQpgYGAKCkJveCBwbG90cyB0byBzaG93IHRoZSBkaWZmZXJlbmNlIG9mIHRoZSBkaWZmZXJlbnQgVFAvTUNDLWNsYXNzZXMgaW4gdGVybXMgb2YgZml0bmVzcyBzY29yZToKYGBge3IgRml0bmVzcyB2cyBUUCBvciBNQ0MtY2xhc3MsIHdhcm5pbmc9RkFMU0UsIGNhY2hlPVRSVUV9CmdnYm94cGxvdChkZiwgeCA9ICJtY2MuY2xhc3MuaWQiLCB5ID0gImZpdC51bmlxdWUiLCBjb2xvciA9ICJtY2MuY2xhc3MuaWQiLAogIHBhbGV0dGUgPSB1c2VmdW46Ojpjb2xvcnMuMTAwWzE6bnVtLm9mLm1jYy5jbGFzc2VzXSwKICB4bGFiID0gIk1DQyBjbGFzcyIsIHlsYWIgPSAiRml0bmVzcyB2YWx1ZXMiLCAKICBhZGQgPSBjKCJtZWFuIiwgIm1lYW5fc2QiKSwgZXJyb3IucGxvdCA9ICJlcnJvcmJhciIpCgpnZ2JveHBsb3QoZGYsIHggPSAidHAudW5pcXVlIiwgeSA9ICJmaXQudW5pcXVlIiwgY29sb3IgPSAidHAudW5pcXVlIiwKICBwYWxldHRlID0gdXNlZnVuOjo6Y29sb3JzLjEwMFsxOmxlbmd0aCh1bmlxdWUodHAudW5pcXVlKSldLAogIHhsYWIgPSAiVFAgY2xhc3MiLCB5bGFiID0gIkZpdG5lc3MgdmFsdWVzIiwgCiAgYWRkID0gYygibWVhbiIsICJtZWFuX3NkIiksIHJlbW92ZSA9IDUpCmBgYAoKIyMjIyBGaXRuZXNzLWNsYXNzIHZzIG1lYW4gTUNDIHstfQoKU2luY2UgdGhlIGdvbGQtc3RhbmRhcmQgQUdTIHN0ZWFkeSBzdGF0ZSBoYXMgb25seSAyMyBub2Rlcywgb3VyIG1vZGVscyBnZW5lcmF0ZSBhIHNtYWxsIG51bWJlciBvZiB1bmlxdWUgZml0bmVzcyBzY29yZXMgdG8gdGhhdCBzdGVhZHkgc3RhdGUgKHRvdGFsIG9mIGByIGxlbmd0aCh1bmlxdWUoZGYkZml0LnVuaXF1ZSkpYCB1bmlxdWUgZml0bmVzcyBjbGFzc2VzKS4gU28sICoqaW5zdGVhZCBvZiBncm91cGluZyB0aGUgbW9kZWxzIHBlciBwZXJmb3JtYW5jZSBjbGFzcyAoVFAgb3IgTUNDKSB3ZSB3aWxsIGdyb3VwIHRoZW0gYnkgZml0bmVzcyBjbGFzcyBhbmQgc2hvdyB0aGUgYXZlcmFnZSBNQ0MgdmFsdWUgaW4gZWFjaCBjbGFzcyoqOgoKYGBge3IgRml0bmVzcyBDbGFzcyB2cyBtZWFuIE1DQywgY2FjaGU9VFJVRX0KbnVtLm9mLmZpdC5jbGFzc2VzID0gbGVuZ3RoKHVuaXF1ZShkZiRmaXQudW5pcXVlKSkKZml0LmNsYXNzLmlkcyA9IDE6bnVtLm9mLmZpdC5jbGFzc2VzCgojIGZpbmQgdGhlIGZpdG5lc3MgY2xhc3NlcwpyZXMgPSBDa21lYW5zLjFkLmRwKHggPSBkZiRmaXQudW5pcXVlLCBrID0gbnVtLm9mLmZpdC5jbGFzc2VzKQpmaXQuY2xhc3MuaWQgPSByZXMkY2x1c3RlcgpkZiA9IGNiaW5kKGRmLCBmaXQuY2xhc3MuaWQpCgp1bmlxdWUuZml0LnN0YXRzID0gZGVzY19zdGF0YnkoZGYsIG1lYXN1cmUudmFyID0gIm1jYy51bmlxdWUiLCBncnBzID0gYygiZml0LnVuaXF1ZSIsICJmaXQuY2xhc3MuaWQiKSwgY2kgPSAwLjk1KQoKZ2dzY2F0dGVyKHVuaXF1ZS5maXQuc3RhdHMsIHggPSAiZml0LnVuaXF1ZSIsIHkgPSAibWVhbiIsIAogICAgICAgICAgdGl0bGUgPSAiRml0bmVzcyBDbGFzcyB2cyBQZXJmb3JtYW5jZSAoTUNDKSAtIEtlbmRhbGwgQ29ycmVsYXRpb24iLAogICAgICAgICAgYWRkID0gInJlZy5saW5lIiwgY29uZi5pbnQgPSBUUlVFLCAKICAgICAgICAgIGNvci5jb2VmID0gVFJVRSwgY29yLmNvZWZmLmFyZ3MgPSBsaXN0KG1ldGhvZCA9ICJrZW5kYWxsIiwgbGFiZWwueC5ucGMgPSAwLjcsIGxhYmVsLnkubnBjID0gMC4xKSwKICAgICAgICAgIHhsYWIgPSAiVW5pcXVlIEZpdG5lc3MgdmFsdWVzIiwgeWxhYiA9ICJNZWFuIE1DQyIpCiAgCmdnYmFycGxvdCh1bmlxdWUuZml0LnN0YXRzLCB4ID0gImZpdC51bmlxdWUiLCB5ID0gIm1lYW4iLCB0aXRsZSA9ICJGaXRuZXNzIENsYXNzIHZzIFBlcmZvcm1hbmNlIChNQ0MpIiwgZmlsbCA9ICJmaXQuY2xhc3MuaWQiLAogIHhsYWIgPSAiRml0bmVzcyBTY29yZSIsIHlsYWIgPSAiTWVhbiBNQ0MiLCB5bGltID0gYygwLCBtYXgodW5pcXVlLmZpdC5zdGF0cyRtZWFuKSArIDAuMSksCiAgbGFiZWwgPSB1bmlxdWUuZml0LnN0YXRzJGxlbmd0aCwgbGFiLnZqdXN0ID0gLTIpICsgCiAgc2NhbGVfeF9kaXNjcmV0ZShsYWJlbHMgPSByb3VuZCh1bmlxdWUuZml0LnN0YXRzJGZpdC51bmlxdWUsIGRpZ2l0cyA9IDIpKSArIAogIHNjYWxlX2ZpbGxfZ3JhZGllbnQobG93ID0gImxpZ2h0Ymx1ZSIsIGhpZ2g9InJlZCIsIG5hbWUgPSAiRml0bmVzcyBDbGFzcyIsIGd1aWRlID0gImxlZ2VuZCIsIGJyZWFrcyA9IGMoMSw0LDcsMTAsMTMpKSArCiAgZ2VvbV9lcnJvcmJhcihhZXMoeW1pbj1tZWFuLTIqc2UsIHltYXg9bWVhbisyKnNlKSwgd2lkdGggPSAwLjIsIHBvc2l0aW9uID0gcG9zaXRpb25fZG9kZ2UoMC45KSkgKwogIGFubm90YXRlKCJ0ZXh0IiwgeCA9IDcuNSwgeSA9IDAuNSwgbGFiZWwgPSAiTnVtYmVyIG9mIG1vZGVscyBwZXIgZml0bmVzcyBjbGFzcyBhcmUgZGlzcGxheWVkIGFib3ZlIDk1JSBDSSBiYXJzIGZvciB0aGUgbWVhbiBNQ0MiKQojIG1lYW4gLSsgMipzZSA9PiA5NSUgQ0kgZm9yIG1lYW4uIFVzZSB3aGVuIHlvdSBhcmUgaW50ZXJlc3RlZCBpbiB0aGUgcHJlY2lzaW9uIG9mIHRoZSBtZWFucyBvciBpbiBjb21wYXJpbmcgYW5kIHRlc3RpbmcgZGlmZmVyZW5jZXMgYmV0d2VlbiBtZWFucy4KYGBgCgo8ZGl2IGNsYXNzPSJncmVlbi1ib3giPgpUaGVyZSBpcyBldmlkZW50IHN0YXRpc3RpY2FsIGNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIG1vZGVscyBmaXRuZXNzIHRvIGEgY3VyYXRpb24tYmFzZWQgc3RlYWR5IHN0YXRlIGFuZCB0aGVpciBwZXJmb3JtYW5jZSAobWVhc3VyZWQgYnkgTUNDIHNjb3JlKS4gKipNb2RlbHMgd2l0aCBoaWdoZXIgZml0bmVzcyB0byB0aGUgc3RlYWR5IHN0YXRlIHRlbmQgdG8gaGF2ZSBoaWdoZXIgTUNDIHNjb3JlIG9uIGF2ZXJhZ2UqKi4KPC9kaXY+CjwvYnI+CgojIyMgQUdTLVBBUkFESUdNKCtURikgYW5hbHlzaXMgey19Cgo8ZGl2IGNsYXNzPSJibHVlLWJveCI+CldoYXQgaWYgd2UgdGFrZSB0aGUgKipBR1Mgc3RlYWR5IHN0YXRlIGNhbGN1bGF0ZWQgd2l0aCB0aGUgUEFSQURJR00gdG9vbCoqICh3aXRoIG9yIHdpdGhvdXQgdGhlIFRyYW5zY3JpcHRpb24gRmFjdG9yIGFjdGl2aXR5KSBhbmQgKipwcnVuZSB0byB0aGUgbm9kZXMgdGhhdCBjb25zdGl0dXRlIHRoZSBBR1MtZ29sZCBzdGVhZHkgc3RhdGUqKjogd2lsbCB0aGVyZSBiZSBhIGNvcnJlbGF0aW9uIGJldHdlZW4gZml0bmVzcyBhbmQgcGVyZm9ybWFuY2UgdGhlbj8KPC9kaXY+CjwvYnI+CgpUaGUgQUdTLUdvbGQgbm9kZXMgYXJlOgpgYGB7ciBBR1MtR29sZCBub2RlcywgcmVzdWx0cz0nYXNpcyd9CnByZXR0eV9wcmludF92ZWN0b3JfbmFtZXMoYWdzLmxpdGVyYXR1cmUuc3MpCmBgYAoKV2UgZmlyc3QgZmluZCAqKnRoZSBmaXRuZXNzIG9mIHRoZSBtb2RlbHMgdG8gdGhlIEFHUyBzdGVhZHkgc3RhdGUgKFBhcmFkaWdtIHdpdGggYW5kIHdpdGhvdXQgVEYgaW5jbHVkZWQgaW4gdGhlIGNhbGN1bGF0aW9uKSBwcnVuZWQgdG8gdGhlIEFHUy1nb2xkIG5vZGVzKio6CmBgYHtyIE1vZGVscyBmaXRuZXNzIHRvIEFHUyBuZXcgYW5kIG9sZCBzdGVhZHkgc3RhdGVzIC0gcHJ1bmVkIHRvIEFHUyBsaXRlcmF0dXJlIG5vZGVzfQphZ3MubW9kZWxzLnN0YWJsZS5zdGF0ZSA9IG1vZGVscy5zdGFibGUuc3RhdGUucGVyLmNlbGwubGluZSRBR1MKCm1vZGVscy5maXRuZXNzLnRvLmFncy5zcy5vbGQgPSBhcHBseSgKICBhZ3MubW9kZWxzLnN0YWJsZS5zdGF0ZVssbmFtZXMoYWdzLnNzLm9sZCldLCAxLCAKICBnZXRfcGVyY2VudGFnZV9vZl9tYXRjaGVzLCBhZ3Muc3Mub2xkKQptb2RlbHMuZml0bmVzcy50by5hZ3Muc3MubmV3ID0gYXBwbHkoCiAgYWdzLm1vZGVscy5zdGFibGUuc3RhdGVbLG5hbWVzKGFncy5zcy5uZXcpXSwgMSwgCiAgZ2V0X3BlcmNlbnRhZ2Vfb2ZfbWF0Y2hlcywgYWdzLnNzLm5ldykKCmZpdC5vbGQgPSBtb2RlbHMuZml0bmVzcy50by5hZ3Muc3Mub2xkCmZpdC5uZXcgPSBtb2RlbHMuZml0bmVzcy50by5hZ3Muc3MubmV3CmZpdC51bmlxdWUub2xkID0gZml0Lm9sZFtuYW1lcyhmaXQub2xkKSAlaW4lIHVuaXF1ZS5tb2RlbHNdCmZpdC51bmlxdWUubmV3ID0gZml0Lm5ld1tuYW1lcyhmaXQubmV3KSAlaW4lIHVuaXF1ZS5tb2RlbHNdCgpkZi5vbGQgPSBhcy5kYXRhLmZyYW1lKGNiaW5kKGZpdC51bmlxdWUub2xkLCBtY2MudW5pcXVlLCB0cC51bmlxdWUpKQpkZi5uZXcgPSBhcy5kYXRhLmZyYW1lKGNiaW5kKGZpdC51bmlxdWUubmV3LCBtY2MudW5pcXVlLCB0cC51bmlxdWUpKQpgYGAKClRoZW4sIHdlIHNlZSBpZiB0aGVyZSBpcyBhbnkgY29ycmVsYXRpb24gYmV0d2VlbiBmaXRuZXNzIGFuZCBwZXJmb3JtYW5jZSAoTUNDKSB1c2luZyBnZW5lcmFsIHNjYXR0ZXIgcGxvdHM6CmBgYHtyIFJhbmsgY29ycmVsYXRpb24gcGxvdHM6IEZpdG5lc3MgdG8gUGFyYWRpZ20tY2FsY3VsYXRlZCBzcyAod2l0aCBhbmQgd2l0aG91dCBURikgLSBwcnVuZWQgdG8gQUdTLWdvbGQgbm9kZXMgdnMgTUNDLCBjYWNoZT1UUlVFfQpnZ3NjYXR0ZXIoZGYub2xkLCB4ID0gImZpdC51bmlxdWUub2xkIiwgeSA9ICJtY2MudW5pcXVlIiwgCiAgICAgICAgICB0aXRsZSA9ICJBR1MgKFBhcmFkaWdtK1RGKSAtIENlbGwtc3BlY2lmaWM6IEZpdG5lc3MgdnMgUGVyZm9ybWFuY2UgLSBLZW5kYWxsJ3MgdGF1IiwKICAgICAgICAgIGFkZCA9ICJyZWcubGluZSIsIGNvbmYuaW50ID0gVFJVRSwgCiAgICAgICAgICBjb3IuY29lZiA9IFRSVUUsIGNvci5jb2VmZi5hcmdzID0gbGlzdChtZXRob2QgPSAia2VuZGFsbCIpLAogICAgICAgICAgeGxhYiA9ICJGaXRuZXNzIFZhbHVlcyIsIHlsYWIgPSAiTUNDIHNjb3JlcyIpCmdnc2NhdHRlcihkZi5uZXcsIHggPSAiZml0LnVuaXF1ZS5uZXciLCB5ID0gIm1jYy51bmlxdWUiLCAKICAgICAgICAgIHRpdGxlID0gIkFHUyAoUGFyYWRpZ20pIC0gQ2VsbC1zcGVjaWZpYzogRml0bmVzcyB2cyBQZXJmb3JtYW5jZSAtIEtlbmRhbGwncyB0YXUiLAogICAgICAgICAgYWRkID0gInJlZy5saW5lIiwgY29uZi5pbnQgPSBUUlVFLCAKICAgICAgICAgIGNvci5jb2VmID0gVFJVRSwgY29yLmNvZWZmLmFyZ3MgPSBsaXN0KG1ldGhvZCA9ICJrZW5kYWxsIiksCiAgICAgICAgICB4bGFiID0gIkZpdG5lc3MgVmFsdWVzIiwgeWxhYiA9ICJNQ0Mgc2NvcmVzIikKYGBgCgo8ZGl2IGNsYXNzPSJvcmFuZ2UtYm94Ij4KVGhlcmUgaXMgYWxtb3N0IG5vIGNvcnJlbGF0aW9uIGJldHdlZW4gZml0bmVzcyBhbmQgcGVyZm9ybWFuY2Ugd2hlbiB0aGUgZml0bmVzcyBvZiB0aGUgbW9kZWxzIGlzIGNhbGN1bGF0ZWQgd2l0aCB0aGUgKipzYW1lIG5vZGVzIGFzIGluIEFHUy1nb2xkIHN0ZWFkeSBzdGF0ZSoqLCBidXQgKip3aXRoIHRoZSB2YWx1ZXMgYXMgdGhleSB3ZXJlIGNhbGN1bGF0ZWQgYnkgUEFSQURJR00gb3IgUEFSQURJR00gaW5jbHVkaW5nIFRGIGFjdGl2aXR5KiouIFRoaXMgcHJvdmVzIHRoYXQgdGhlIGJlc3QgbW9kZWxzIGFyZSB0aG9zZSB0aGF0IGZpdCB0byB0aGUgKnRydWUqIHN0ZWFkeSBzdGF0ZSBhbmQgbm90IHRoZSB3cm9uZ2x5IGFzc2Vzc2VkIG9uZXMhCjwvZGl2Pgo8L2JyPgoKVG8gYmV0dGVyIHZpc3VhbGl6ZSB0aGUgYWJzZW5zZSBvZiBjb3JyZWxhdGlvbiwgKip3ZSBncm91cCB0aGUgbW9kZWxzIHRvIGRpc3RpbmN0IGZpdG5lc3MgY2xhc3NlcyBhbmQgc2hvdyB0aGUgYXZlcmFnZSBNQ0MgcGVyIGNsYXNzIHdpdGggdGhlIGNvcnJlc3BvbmRpbmcgOTUlIENJKiouIAoKRmlyc3QgZm9yIHRoZSAqKlBBUkFESUdNK1RGIChvbGQpKiogY2FsY3VsYXRlZCBzdGVhZHkgc3RhdGUgZm9yIEFHUywgcHJ1bmVkIHRvIHRoZSBBR1MtZ29sZCBub2RlczoKCmBgYHtyIEZpdG5lc3MgQ2xhc3MgdnMgbWVhbiBNQ0MgKFBhcmFkaWdtK1RGIC0gcHJ1bmVkIHRvIEFHUy1nb2xkKSwgd2FybmluZz1GQUxTRSwgY2FjaGU9VFJVRX0KbnVtLm9mLmZpdC5jbGFzc2VzID0gbGVuZ3RoKHVuaXF1ZShkZi5vbGQkZml0LnVuaXF1ZS5vbGQpKQpmaXQuY2xhc3MuaWRzID0gMTpudW0ub2YuZml0LmNsYXNzZXMKCiMgZmluZCB0aGUgZml0bmVzcyBjbGFzc2VzCnJlcyA9IENrbWVhbnMuMWQuZHAoeCA9IGRmLm9sZCRmaXQudW5pcXVlLm9sZCwgayA9IG51bS5vZi5maXQuY2xhc3NlcykKZml0LmNsYXNzLmlkID0gcmVzJGNsdXN0ZXIKZGYub2xkID0gY2JpbmQoZGYub2xkLCBmaXQuY2xhc3MuaWQpCgp1bmlxdWUuZml0LnN0YXRzLm9sZCA9IGRlc2Nfc3RhdGJ5KGRmLm9sZCwgbWVhc3VyZS52YXIgPSAibWNjLnVuaXF1ZSIsIGdycHMgPSBjKCJmaXQudW5pcXVlLm9sZCIsICJmaXQuY2xhc3MuaWQiKSwgY2kgPSAwLjk1KQogIApnZ2JhcnBsb3QodW5pcXVlLmZpdC5zdGF0cy5vbGQsIHggPSAiZml0LnVuaXF1ZS5vbGQiLCB5ID0gIm1lYW4iLCB0aXRsZSA9ICJGaXRuZXNzIENsYXNzIHZzIFBlcmZvcm1hbmNlIChNQ0MpIC0gUGFyYWRpZ20gKyBURiIsIGZpbGwgPSAiZml0LmNsYXNzLmlkIiwKICB4bGFiID0gIkZpdG5lc3MgU2NvcmUiLCB5bGFiID0gIk1lYW4gTUNDIiwgeWxpbSA9IGMoMCwgbWF4KHVuaXF1ZS5maXQuc3RhdHMub2xkJG1lYW4pICsgMC4xKSwKICBsYWJlbCA9IHVuaXF1ZS5maXQuc3RhdHMub2xkJGxlbmd0aCwgbGFiLnZqdXN0ID0gLTIpICsgCiAgc2NhbGVfeF9kaXNjcmV0ZShsYWJlbHMgPSByb3VuZCh1bmlxdWUuZml0LnN0YXRzLm9sZCRmaXQudW5pcXVlLm9sZCwgZGlnaXRzID0gMikpICsgCiAgc2NhbGVfZmlsbF9ncmFkaWVudChsb3cgPSAibGlnaHRibHVlIiwgaGlnaD0icmVkIiwgbmFtZSA9ICJGaXRuZXNzIENsYXNzIiwgZ3VpZGUgPSAibGVnZW5kIiwgYnJlYWtzID0gYygxLDQsNykpICsKICBnZW9tX2Vycm9yYmFyKGFlcyh5bWluPW1lYW4tMipzZSwgeW1heD1tZWFuKzIqc2UpLCB3aWR0aCA9IDAuMiwgcG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgwLjkpKSArCiAgYW5ub3RhdGUoInRleHQiLCB4ID0gNC41LCB5ID0gMC4zOCwgbGFiZWwgPSAiTnVtYmVyIG9mIG1vZGVscyBwZXIgZml0bmVzcyBjbGFzcyBhcmUgZGlzcGxheWVkIGFib3ZlIDk1JSBDSSBiYXJzIGZvciB0aGUgbWVhbiBNQ0MiKQojIG1lYW4gLSsgMipzZSA9PiA5NSUgQ0kgZm9yIG1lYW4uIFVzZSB3aGVuIHlvdSBhcmUgaW50ZXJlc3RlZCBpbiB0aGUgcHJlY2lzaW9uIG9mIHRoZSBtZWFucyBvciBpbiBjb21wYXJpbmcgYW5kIHRlc3RpbmcgZGlmZmVyZW5jZXMgYmV0d2VlbiBtZWFucy4KYGBgCgpBbmQgc2Vjb25kbHkgZm9yICoqdGhlIFBBUkFESUdNIG9ubHkgKG5ldykqKiBjYWxjdWxhdGVkIHN0ZWFkeSBzdGF0ZSBmb3IgQUdTLCBwcnVuZWQgdG8gdGhlIEFHUy1nb2xkIG5vZGVzOgoKYGBge3IgRml0bmVzcyBDbGFzcyB2cyBtZWFuIE1DQyAoUGFyYWRpZ20gb25seSAtIHBydW5lZCB0byBBR1MtZ29sZCksIHdhcm5pbmc9RkFMU0UsIGNhY2hlPVRSVUV9Cm51bS5vZi5maXQuY2xhc3NlcyA9IGxlbmd0aCh1bmlxdWUoZGYubmV3JGZpdC51bmlxdWUubmV3KSkKZml0LmNsYXNzLmlkcyA9IDE6bnVtLm9mLmZpdC5jbGFzc2VzCgojIGZpbmQgdGhlIGZpdG5lc3MgY2xhc3NlcwpyZXMgPSBDa21lYW5zLjFkLmRwKHggPSBkZi5uZXckZml0LnVuaXF1ZS5uZXcsIGsgPSBudW0ub2YuZml0LmNsYXNzZXMpCmZpdC5jbGFzcy5pZCA9IHJlcyRjbHVzdGVyCmRmLm5ldyA9IGNiaW5kKGRmLm5ldywgZml0LmNsYXNzLmlkKQoKdW5pcXVlLmZpdC5zdGF0cy5uZXcgPSBkZXNjX3N0YXRieShkZi5uZXcsIG1lYXN1cmUudmFyID0gIm1jYy51bmlxdWUiLCBncnBzID0gYygiZml0LnVuaXF1ZS5uZXciLCAiZml0LmNsYXNzLmlkIiksIGNpID0gMC45NSkKCmdnYmFycGxvdCh1bmlxdWUuZml0LnN0YXRzLm5ldywgeCA9ICJmaXQudW5pcXVlLm5ldyIsIHkgPSAibWVhbiIsIHRpdGxlID0gIkZpdG5lc3MgQ2xhc3MgdnMgUGVyZm9ybWFuY2UgKE1DQykgLSBQYXJhZGlnbSBvbmx5IiwgZmlsbCA9ICJmaXQuY2xhc3MuaWQiLAogIHhsYWIgPSAiRml0bmVzcyBTY29yZSIsIHlsYWIgPSAiTWVhbiBNQ0MiLCB5bGltID0gYygwLCBtYXgodW5pcXVlLmZpdC5zdGF0cy5uZXckbWVhbikgKyAwLjEpLAogIGxhYmVsID0gdW5pcXVlLmZpdC5zdGF0cy5uZXckbGVuZ3RoLCBsYWIudmp1c3QgPSAtMikgKyAKICBzY2FsZV94X2Rpc2NyZXRlKGxhYmVscyA9IHJvdW5kKHVuaXF1ZS5maXQuc3RhdHMubmV3JGZpdC51bmlxdWUubmV3LCBkaWdpdHMgPSAyKSkgKyAKICBzY2FsZV9maWxsX2dyYWRpZW50KGxvdyA9ICJsaWdodGJsdWUiLCBoaWdoPSJyZWQiLCBuYW1lID0gIkZpdG5lc3MgQ2xhc3MiLCBndWlkZSA9ICJsZWdlbmQiLCBicmVha3MgPSBjKDEsNCw3KSkgKwogIGdlb21fZXJyb3JiYXIoYWVzKHltaW49bWVhbi0yKnNlLCB5bWF4PW1lYW4rMipzZSksIHdpZHRoID0gMC4yLCBwb3NpdGlvbiA9IHBvc2l0aW9uX2RvZGdlKDAuOSkpICsKICBhbm5vdGF0ZSgidGV4dCIsIHggPSA0LjUsIHkgPSAwLjM1LCBsYWJlbCA9ICJOdW1iZXIgb2YgbW9kZWxzIHBlciBmaXRuZXNzIGNsYXNzIGFyZSBkaXNwbGF5ZWQgYWJvdmUgOTUlIENJIGJhcnMgZm9yIHRoZSBtZWFuIE1DQyIpCiMgbWVhbiAtKyAyKnNlID0+IDk1JSBDSSBmb3IgbWVhbi4gVXNlIHdoZW4geW91IGFyZSBpbnRlcmVzdGVkIGluIHRoZSBwcmVjaXNpb24gb2YgdGhlIG1lYW5zIG9yIGluIGNvbXBhcmluZyBhbmQgdGVzdGluZyBkaWZmZXJlbmNlcyBiZXR3ZWVuIG1lYW5zLgpgYGAKCiMjIyBBU0ctR29sZCBSYW5kb20gYW5hbHlzaXMKCjxkaXYgY2xhc3M9ImJsdWUtYm94Ij4KSW4gdGhpcyBzZWN0aW9uIHdlIHdhbnQgdG8gaW52ZXN0aWdhdGUgaG93IHRoZSAqKnJhbmRvbSBtb2RlbHMgcGVyZm9ybWFuY2UqKiBjb3JyZWxhdGVzIHdpdGggdGhlICoqZml0bmVzcyB0byB0aGUgQUdTLWdvbGQgc3RlYWR5IHN0YXRlKiosIGFzIHdlbGwgYXMgdGhlICoqcHJ1bmVkIFBBUkFESUdNIHN0ZWFkeSBzdGF0ZSAod2l0aCBvciB3aXRoIFRGKSoqIC0gcHJ1bmVkIHRvIHRoZSAyMyBub2RlcyBvZiB0aGUgQUdTLWdvbGQgc3MuCjwvZGl2Pgo8L2JyPgoKRmlyc3QsIHdlIGNhbGN1bGF0ZSB0aGUgbmV3IGZpdG5lc3MgdmFsdWVzIGZvciB0aGUgcmFuZG9tIG1vZGVscywgZm9yIGVhY2ggc3RlYWR5IHN0YXRlIGNhc2UgKEFHUy1Hb2xkLCBQQVJBRElHTSBvbmx5IChuZXcpLCBQQVJBRElHTStURiAob2xkKSk6CmBgYHtyIEZpdG5lc3MgYW5kIE1DQyBzY29yZXMgZm9yIFJhbmRvbSBNb2RlbHMsIGNhY2hlPVRSVUV9CiMgR2V0IHRoZSBNQ0Mgc2NvcmVzIG9mIHRoZSByYW5kb20gbW9kZWxzIGZvciB0aGUgQUdTCm1jYyA9IHJhbmRvbS5tb2RlbHMubWNjLnBlci5jZWxsLmxpbmUkQUdTCgojIGZpbmQgdGhlIHVuaXF1ZSBtb2RlbHMKdW5pcXVlLm1vZGVscyA9IHJvd25hbWVzKHVuaXF1ZShyYW5kb20ubW9kZWxzLmxpbmsub3BlcmF0b3IpKQoKIyByZW1vdmUgbW9kZWxzIHdpdGggTmFOIE1DQyBzY29yZXMKdW5pcXVlLm1vZGVscyA9IHVuaXF1ZS5tb2RlbHNbIXVuaXF1ZS5tb2RlbHMgJWluJSBuYW1lcyh3aGljaChpcy5uYShtY2MpKSldCgojIFBydW5lIE1DQyBzY29yZXMgdG8gdW5pcXVlIG1vZGVscwptY2MudW5pcXVlID0gbWNjW25hbWVzKG1jYykgJWluJSB1bmlxdWUubW9kZWxzXQoKIyBGaW5kIGZpdG5lc3Mgb2YgdGhlIHJhbmRvbSBtb2RlbHMgdG8gdGhlIDMgc3RlYWR5IHN0YXRlcwojIHJhbmRvbS5tb2RlbHMuZml0bmVzcy50by5hZ3MubGl0ZXJhdHVyZS5zcyAoYWxyZWFkeSBjYWxjdWxhdGVkIGZvciBBR1MtR29sZCkKcmFuZG9tLm1vZGVscy5maXRuZXNzLnRvLmFncy5zcy5vbGQgPSBhcHBseSgKICByYW5kb20ubW9kZWxzLnN0YWJsZS5zdGF0ZVssbmFtZXMoYWdzLnNzLm9sZCldLCAxLCAKICBnZXRfcGVyY2VudGFnZV9vZl9tYXRjaGVzLCBhZ3Muc3Mub2xkKQpyYW5kb20ubW9kZWxzLmZpdG5lc3MudG8uYWdzLnNzLm5ldyA9IGFwcGx5KAogIHJhbmRvbS5tb2RlbHMuc3RhYmxlLnN0YXRlWyxuYW1lcyhhZ3Muc3MubmV3KV0sIDEsIAogIGdldF9wZXJjZW50YWdlX29mX21hdGNoZXMsIGFncy5zcy5uZXcpCgpmaXQuZ29sZCA9IHJhbmRvbS5tb2RlbHMuZml0bmVzcy50by5hZ3MubGl0ZXJhdHVyZS5zcwpmaXQub2xkID0gcmFuZG9tLm1vZGVscy5maXRuZXNzLnRvLmFncy5zcy5vbGQKZml0Lm5ldyA9IHJhbmRvbS5tb2RlbHMuZml0bmVzcy50by5hZ3Muc3MubmV3CgpmaXQudW5pcXVlLmdvbGQgPSBmaXQuZ29sZFtuYW1lcyhmaXQuZ29sZCkgJWluJSB1bmlxdWUubW9kZWxzXQpmaXQudW5pcXVlLm9sZCA9IGZpdC5vbGRbbmFtZXMoZml0Lm9sZCkgJWluJSB1bmlxdWUubW9kZWxzXQpmaXQudW5pcXVlLm5ldyA9IGZpdC5uZXdbbmFtZXMoZml0Lm5ldykgJWluJSB1bmlxdWUubW9kZWxzXQoKZGYuZ29sZC5yYW5kb20gPSBhcy5kYXRhLmZyYW1lKGNiaW5kKGZpdC51bmlxdWUuZ29sZCwgbWNjLnVuaXF1ZSkpCmRmLm9sZC5yYW5kb20gPSBhcy5kYXRhLmZyYW1lKGNiaW5kKGZpdC51bmlxdWUub2xkLCBtY2MudW5pcXVlKSkKZGYubmV3LnJhbmRvbSA9IGFzLmRhdGEuZnJhbWUoY2JpbmQoZml0LnVuaXF1ZS5uZXcsIG1jYy51bmlxdWUpKQpgYGAKClRoZW4sIHdlIGNoZWNrIHRvIHNlZSBpZiB0aGVyZSBpcyBhbnkgY29ycmVsYXRpb24gYmV0d2VlbiBmaXRuZXNzIGFuZCBwZXJmb3JtYW5jZSAoTUNDKSB1c2luZyBnZW5lcmFsIHNjYXR0ZXIgcGxvdHM6CmBgYHtyIFJhbmsgY29ycmVsYXRpb24gcGxvdHMgZm9yIFJhbmRvbSBNb2RlbHMsIGNhY2hlPVRSVUV9Cmdnc2NhdHRlcihkZi5nb2xkLnJhbmRvbSwgeCA9ICJmaXQudW5pcXVlLmdvbGQiLCB5ID0gIm1jYy51bmlxdWUiLCAKICAgICAgICAgIHRpdGxlID0gIkFHUy1Hb2xkIC0gUmFuZG9tOiBGaXRuZXNzIHZzIFBlcmZvcm1hbmNlIC0gS2VuZGFsbCdzIHRhdSIsCiAgICAgICAgICBhZGQgPSAicmVnLmxpbmUiLCBjb25mLmludCA9IFRSVUUsIAogICAgICAgICAgY29yLmNvZWYgPSBUUlVFLCBjb3IuY29lZmYuYXJncyA9IGxpc3QobWV0aG9kID0gImtlbmRhbGwiKSwKICAgICAgICAgIHhsYWIgPSAiRml0bmVzcyBWYWx1ZXMiLCB5bGFiID0gIk1DQyBzY29yZXMiKQpnZ3NjYXR0ZXIoZGYub2xkLnJhbmRvbSwgeCA9ICJmaXQudW5pcXVlLm9sZCIsIHkgPSAibWNjLnVuaXF1ZSIsIAogICAgICAgICAgdGl0bGUgPSAiQUdTIChQYXJhZGlnbStURikgLSBSYW5kb206IEZpdG5lc3MgdnMgUGVyZm9ybWFuY2UgLSBLZW5kYWxsJ3MgdGF1IiwKICAgICAgICAgIGFkZCA9ICJyZWcubGluZSIsIGNvbmYuaW50ID0gVFJVRSwgCiAgICAgICAgICBjb3IuY29lZiA9IFRSVUUsIGNvci5jb2VmZi5hcmdzID0gbGlzdChtZXRob2QgPSAia2VuZGFsbCIpLAogICAgICAgICAgeGxhYiA9ICJGaXRuZXNzIFZhbHVlcyIsIHlsYWIgPSAiTUNDIHNjb3JlcyIpCmdnc2NhdHRlcihkZi5uZXcucmFuZG9tLCB4ID0gImZpdC51bmlxdWUubmV3IiwgeSA9ICJtY2MudW5pcXVlIiwgCiAgICAgICAgICB0aXRsZSA9ICJBR1MgKFBhcmFkaWdtKSAtIFJhbmRvbTogRml0bmVzcyB2cyBQZXJmb3JtYW5jZSAtIEtlbmRhbGwncyB0YXUiLAogICAgICAgICAgYWRkID0gInJlZy5saW5lIiwgY29uZi5pbnQgPSBUUlVFLCAKICAgICAgICAgIGNvci5jb2VmID0gVFJVRSwgY29yLmNvZWZmLmFyZ3MgPSBsaXN0KG1ldGhvZCA9ICJrZW5kYWxsIiksCiAgICAgICAgICB4bGFiID0gIkZpdG5lc3MgVmFsdWVzIiwgeWxhYiA9ICJNQ0Mgc2NvcmVzIikKYGBgCgo8ZGl2IGNsYXNzPSJvcmFuZ2UtYm94Ij4KVGhlcmUgaXMgdmVyeSBzbWFsbCBjb3JyZWxhdGlvbiBiZXR3ZWVuIGZpdG5lc3MgYW5kIHBlcmZvcm1hbmNlIGluIHRoZSBjYXNlIG9mIGZpdG5lc3MgdG8gdGhlICoqQUdTLUdvbGQgc3RlYWR5IHN0YXRlKiogYW5kIHRoZSBvbmUgdGhhdCBpcyBjYWxjdWxhdGVkIHdpdGggdGhlICoqc2FtZSBub2RlcyBhcyBpbiBBR1MtZ29sZCBzdGVhZHkgc3RhdGUqKiwgYnV0ICoqd2l0aCB0aGUgdmFsdWVzIGFzIHRoZXkgd2VyZSBjYWxjdWxhdGVkIGJ5IFBBUkFESUdNIGluY2x1ZGluZyBURiBhY3Rpdml0eSoqLgo8L2Rpdj4KPC9icj4KClRvIGJldHRlciB2aXN1YWxpemUgdGhlIGNvcnJlbGF0aW9uIHJlbGF0aW9uc2hpcCAoaWYgdGhlcmUgaXMgYW55KSwgKip3ZSBncm91cCB0aGUgbW9kZWxzIHRvIGRpc3RpbmN0IGZpdG5lc3MgY2xhc3NlcyBhbmQgc2hvdyB0aGUgYXZlcmFnZSBNQ0MgcGVyIGNsYXNzIHdpdGggdGhlIGNvcnJlc3BvbmRpbmcgOTUlIENJKiouIAoKRmlyc3QgZm9yIHRoZSAqKkFHUy1Hb2xkKiogc3RlYWR5IHN0YXRlOgoKYGBge3IgRml0bmVzcyBDbGFzcyB2cyBtZWFuIE1DQyBmb3IgUmFuZG9tIE1vZGVscyAoQUdTLUdvbGQpLCB3YXJuaW5nPUZBTFNFLCBjYWNoZT1UUlVFfQpudW0ub2YuZml0LmNsYXNzZXMgPSBsZW5ndGgodW5pcXVlKGRmLmdvbGQucmFuZG9tJGZpdC51bmlxdWUuZ29sZCkpCmZpdC5jbGFzcy5pZHMgPSAxOm51bS5vZi5maXQuY2xhc3NlcwoKIyBmaW5kIHRoZSBmaXRuZXNzIGNsYXNzZXMKcmVzID0gQ2ttZWFucy4xZC5kcCh4ID0gZGYuZ29sZC5yYW5kb20kZml0LnVuaXF1ZS5nb2xkLCBrID0gbnVtLm9mLmZpdC5jbGFzc2VzKQpmaXQuY2xhc3MuaWQgPSByZXMkY2x1c3RlcgpkZi5nb2xkLnJhbmRvbSA9IGNiaW5kKGRmLmdvbGQucmFuZG9tLCBmaXQuY2xhc3MuaWQpCgp1bmlxdWUuZml0LnN0YXRzLmdvbGQgPSBkZXNjX3N0YXRieShkZi5nb2xkLnJhbmRvbSwgbWVhc3VyZS52YXIgPSAibWNjLnVuaXF1ZSIsIGdycHMgPSBjKCJmaXQudW5pcXVlLmdvbGQiLCAiZml0LmNsYXNzLmlkIiksIGNpID0gMC45NSkKCmdnYmFycGxvdCh1bmlxdWUuZml0LnN0YXRzLmdvbGQsIHggPSAiZml0LnVuaXF1ZS5nb2xkIiwgeSA9ICJtZWFuIiwgdGl0bGUgPSAiRml0bmVzcyBDbGFzcyB2cyBQZXJmb3JtYW5jZSAoTUNDKSAtIEFHUy1Hb2xkIC0gUmFuZG9tIiwgZmlsbCA9ICJmaXQuY2xhc3MuaWQiLAogIHhsYWIgPSAiRml0bmVzcyBTY29yZSIsIHlsYWIgPSAiTWVhbiBNQ0MiLCB5bGltID0gYygwLCBtYXgodW5pcXVlLmZpdC5zdGF0cy5nb2xkJG1lYW4pICsgMC4xKSwKICBsYWJlbCA9IHVuaXF1ZS5maXQuc3RhdHMuZ29sZCRsZW5ndGgsIGxhYi52anVzdCA9IC0yKSArIAogIHNjYWxlX3hfZGlzY3JldGUobGFiZWxzID0gcm91bmQodW5pcXVlLmZpdC5zdGF0cy5nb2xkJGZpdC51bmlxdWUuZ29sZCwgZGlnaXRzID0gMikpICsgCiAgc2NhbGVfZmlsbF9ncmFkaWVudChsb3cgPSAibGlnaHRibHVlIiwgaGlnaD0icmVkIiwgbmFtZSA9ICJGaXRuZXNzIENsYXNzIiwgZ3VpZGUgPSAibGVnZW5kIiwgYnJlYWtzID0gYygxLDQsNywxMCkpICsKICBnZW9tX2Vycm9yYmFyKGFlcyh5bWluPW1lYW4tMipzZSwgeW1heD1tZWFuKzIqc2UpLCB3aWR0aCA9IDAuMiwgcG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgwLjkpKSArCiAgYW5ub3RhdGUoInRleHQiLCB4ID0gNS41LCB5ID0gMC40OCwgbGFiZWwgPSAiTnVtYmVyIG9mIG1vZGVscyBwZXIgZml0bmVzcyBjbGFzcyBhcmUgZGlzcGxheWVkIGFib3ZlIDk1JSBDSSBiYXJzIGZvciB0aGUgbWVhbiBNQ0MiKQojIG1lYW4gLSsgMipzZSA9PiA5NSUgQ0kgZm9yIG1lYW4uIFVzZSB3aGVuIHlvdSBhcmUgaW50ZXJlc3RlZCBpbiB0aGUgcHJlY2lzaW9uIG9mIHRoZSBtZWFucyBvciBpbiBjb21wYXJpbmcgYW5kIHRlc3RpbmcgZGlmZmVyZW5jZXMgYmV0d2VlbiBtZWFucy4KYGBgCgpTZWNvbmRseSwgZm9yIHRoZSAqKlBBUkFESUdNK1RGIChvbGQpKiogY2FsY3VsYXRlZCBzdGVhZHkgc3RhdGUgZm9yIEFHUywgcHJ1bmVkIHRvIHRoZSBBR1MtZ29sZCBub2RlczoKCmBgYHtyIEZpdG5lc3MgQ2xhc3MgdnMgbWVhbiBNQ0MgZm9yIFJhbmRvbSBNb2RlbHMgKFBhcmFkaWdtK1RGIC0gcHJ1bmVkIHRvIEFHUy1nb2xkKSwgd2FybmluZz1GQUxTRSwgY2FjaGU9VFJVRX0KbnVtLm9mLmZpdC5jbGFzc2VzID0gbGVuZ3RoKHVuaXF1ZShkZi5vbGQucmFuZG9tJGZpdC51bmlxdWUub2xkKSkKZml0LmNsYXNzLmlkcyA9IDE6bnVtLm9mLmZpdC5jbGFzc2VzCgojIGZpbmQgdGhlIGZpdG5lc3MgY2xhc3NlcwpyZXMgPSBDa21lYW5zLjFkLmRwKHggPSBkZi5vbGQucmFuZG9tJGZpdC51bmlxdWUub2xkLCBrID0gbnVtLm9mLmZpdC5jbGFzc2VzKQpmaXQuY2xhc3MuaWQgPSByZXMkY2x1c3RlcgpkZi5vbGQucmFuZG9tID0gY2JpbmQoZGYub2xkLnJhbmRvbSwgZml0LmNsYXNzLmlkKQoKdW5pcXVlLmZpdC5zdGF0cy5vbGQgPSBkZXNjX3N0YXRieShkZi5vbGQucmFuZG9tLCBtZWFzdXJlLnZhciA9ICJtY2MudW5pcXVlIiwgZ3JwcyA9IGMoImZpdC51bmlxdWUub2xkIiwgImZpdC5jbGFzcy5pZCIpLCBjaSA9IDAuOTUpCgpnZ2JhcnBsb3QodW5pcXVlLmZpdC5zdGF0cy5vbGQsIHggPSAiZml0LnVuaXF1ZS5vbGQiLCB5ID0gIm1lYW4iLCB0aXRsZSA9ICJGaXRuZXNzIENsYXNzIHZzIFBlcmZvcm1hbmNlIChNQ0MpIC0gUGFyYWRpZ20rVEYgLSBSYW5kb20iLCBmaWxsID0gImZpdC5jbGFzcy5pZCIsCiAgeGxhYiA9ICJGaXRuZXNzIFNjb3JlIiwgeWxhYiA9ICJNZWFuIE1DQyIsIHlsaW0gPSBjKDAsIG1heCh1bmlxdWUuZml0LnN0YXRzLm9sZCRtZWFuKSArIDAuMSksCiAgbGFiZWwgPSB1bmlxdWUuZml0LnN0YXRzLm9sZCRsZW5ndGgsIGxhYi52anVzdCA9IC0yKSArIAogIHNjYWxlX3hfZGlzY3JldGUobGFiZWxzID0gcm91bmQodW5pcXVlLmZpdC5zdGF0cy5vbGQkZml0LnVuaXF1ZS5vbGQsIGRpZ2l0cyA9IDIpKSArIAogIHNjYWxlX2ZpbGxfZ3JhZGllbnQobG93ID0gImxpZ2h0Ymx1ZSIsIGhpZ2g9InJlZCIsIG5hbWUgPSAiRml0bmVzcyBDbGFzcyIsIGd1aWRlID0gImxlZ2VuZCIsIGJyZWFrcyA9IGMoMSw0LDcsOSkpICsKICBnZW9tX2Vycm9yYmFyKGFlcyh5bWluPW1lYW4tMipzZSwgeW1heD1tZWFuKzIqc2UpLCB3aWR0aCA9IDAuMiwgcG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgwLjkpKSArCiAgYW5ub3RhdGUoInRleHQiLCB4ID0gNS41LCB5ID0gMC40OCwgbGFiZWwgPSAiTnVtYmVyIG9mIG1vZGVscyBwZXIgZml0bmVzcyBjbGFzcyBhcmUgZGlzcGxheWVkIGFib3ZlIDk1JSBDSSBiYXJzIGZvciB0aGUgbWVhbiBNQ0MiKQojIG1lYW4gLSsgMipzZSA9PiA5NSUgQ0kgZm9yIG1lYW4uIFVzZSB3aGVuIHlvdSBhcmUgaW50ZXJlc3RlZCBpbiB0aGUgcHJlY2lzaW9uIG9mIHRoZSBtZWFucyBvciBpbiBjb21wYXJpbmcgYW5kIHRlc3RpbmcgZGlmZmVyZW5jZXMgYmV0d2VlbiBtZWFucy4KYGBgCgpMYXN0bHksIGZvciB0aGUgKipQQVJBRElHTSAobmV3KSoqIGNhbGN1bGF0ZWQgc3RlYWR5IHN0YXRlIGZvciBBR1MsIHBydW5lZCB0byB0aGUgQUdTLWdvbGQgbm9kZXM6CgpgYGB7ciBGaXRuZXNzIENsYXNzIHZzIG1lYW4gTUNDIGZvciBSYW5kb20gbW9kZWxzIChQYXJhZGlnbSAtIHBydW5lZCB0byBBR1MtZ29sZCksIHdhcm5pbmc9RkFMU0UsIGNhY2hlPVRSVUV9Cm51bS5vZi5maXQuY2xhc3NlcyA9IGxlbmd0aCh1bmlxdWUoZGYubmV3LnJhbmRvbSRmaXQudW5pcXVlLm5ldykpCmZpdC5jbGFzcy5pZHMgPSAxOm51bS5vZi5maXQuY2xhc3NlcwoKIyBmaW5kIHRoZSBmaXRuZXNzIGNsYXNzZXMKcmVzID0gQ2ttZWFucy4xZC5kcCh4ID0gZGYubmV3LnJhbmRvbSRmaXQudW5pcXVlLm5ldywgayA9IG51bS5vZi5maXQuY2xhc3NlcykKZml0LmNsYXNzLmlkID0gcmVzJGNsdXN0ZXIKZGYubmV3LnJhbmRvbSA9IGNiaW5kKGRmLm5ldy5yYW5kb20sIGZpdC5jbGFzcy5pZCkKCnVuaXF1ZS5maXQuc3RhdHMubmV3ID0gZGVzY19zdGF0YnkoZGYubmV3LnJhbmRvbSwgbWVhc3VyZS52YXIgPSAibWNjLnVuaXF1ZSIsIGdycHMgPSBjKCJmaXQudW5pcXVlLm5ldyIsICJmaXQuY2xhc3MuaWQiKSwgY2kgPSAwLjk1KQoKZ2diYXJwbG90KHVuaXF1ZS5maXQuc3RhdHMubmV3LCB4ID0gImZpdC51bmlxdWUubmV3IiwgeSA9ICJtZWFuIiwgdGl0bGUgPSAiRml0bmVzcyBDbGFzcyB2cyBQZXJmb3JtYW5jZSAoTUNDKSAtIFBhcmFkaWdtIC0gUmFuZG9tIiwgZmlsbCA9ICJmaXQuY2xhc3MuaWQiLAogIHhsYWIgPSAiRml0bmVzcyBTY29yZSIsIHlsYWIgPSAiTWVhbiBNQ0MiLCB5bGltID0gYygwLCBtYXgodW5pcXVlLmZpdC5zdGF0cy5uZXckbWVhbikgKyAwLjEpLAogIGxhYmVsID0gdW5pcXVlLmZpdC5zdGF0cy5uZXckbGVuZ3RoLCBsYWIudmp1c3QgPSAtMikgKyAKICBzY2FsZV94X2Rpc2NyZXRlKGxhYmVscyA9IHJvdW5kKHVuaXF1ZS5maXQuc3RhdHMubmV3JGZpdC51bmlxdWUubmV3LCBkaWdpdHMgPSAyKSkgKyAKICBzY2FsZV9maWxsX2dyYWRpZW50KGxvdyA9ICJsaWdodGJsdWUiLCBoaWdoPSJyZWQiLCBuYW1lID0gIkZpdG5lc3MgQ2xhc3MiLCBndWlkZSA9ICJsZWdlbmQiLCBicmVha3MgPSBjKDEsNCw3LDEwKSkgKwogIGdlb21fZXJyb3JiYXIoYWVzKHltaW49bWVhbi0yKnNlLCB5bWF4PW1lYW4rMipzZSksIHdpZHRoID0gMC4yLCBwb3NpdGlvbiA9IHBvc2l0aW9uX2RvZGdlKDAuOSkpICsKICBhbm5vdGF0ZSgidGV4dCIsIHggPSA1LjUsIHkgPSAwLjQ4LCBsYWJlbCA9ICJOdW1iZXIgb2YgbW9kZWxzIHBlciBmaXRuZXNzIGNsYXNzIGFyZSBkaXNwbGF5ZWQgYWJvdmUgOTUlIENJIGJhcnMgZm9yIHRoZSBtZWFuIE1DQyIpCiMgbWVhbiAtKyAyKnNlID0+IDk1JSBDSSBmb3IgbWVhbi4gVXNlIHdoZW4geW91IGFyZSBpbnRlcmVzdGVkIGluIHRoZSBwcmVjaXNpb24gb2YgdGhlIG1lYW5zIG9yIGluIGNvbXBhcmluZyBhbmQgdGVzdGluZyBkaWZmZXJlbmNlcyBiZXR3ZWVuIG1lYW5zLgpgYGAKCiMjIFIgc2Vzc2lvbiBpbmZvIHstfQoKYGBge3Igc2Vzc2lvbiBpbmZvLCBjb21tZW50PSIifQp4ZnVuOjpzZXNzaW9uX2luZm8oKQpgYGAK