(Installing and) Loading the required R packages

# install.packages("ggplot2")
library(ggplot2)

Dataset

Read the dataset.

An engineered and highly simplified version: “The Beatles songs dataset, v1, no NAs.csv”:

the.beatles.songs <- read.csv("The Beatles songs dataset, v1, no NAs.csv", 
                              stringsAsFactors = FALSE)
summary(the.beatles.songs)

A more realistic, but still very simple version:

the.beatles.songs <- read.csv("The Beatles songs dataset, v3.csv", 
                              stringsAsFactors = FALSE)
summary(the.beatles.songs)

Missing values

NAs are missing values; empty strings (“”) are not the same as NAs.

Different ways of checking if there are NAs and/or “”s:
summary(<dataframe>)
which(complete.cases(<dataframe>) == FALSE) # which rows contain NAs
length(which(complete.cases(<dataframe>) == FALSE)) # how many such rows
which(is.na(<dataframe>[<row>, ])) # NAs in <row>
which(is.na(<dataframe>$<column>)) # NAs in <column>
which(<dataframe>$<column> == "") # ""s in <column>
nrow(<dataframe>[<dataframe>$<column> == "", ]) # how many such rows

summary(the.beatles.songs)
which(complete.cases(the.beatles.songs) == FALSE)
which(is.na(the.beatles.songs$Top.50.Billboard))
which(the.beatles.songs$Single.A.side == "")
nrow(the.beatles.songs[(the.beatles.songs$Single.A.side == "") & (the.beatles.songs$Year == 1967), ])

Visualizing NAs

Use the Amelia package:
library(Amelia) # visualizes NAs, BUT NOT ""s !!!
+ # to visualize ""s with Amelia as well, replace ""s with NAs
par(mfrow=c(1,2)) # structure the display area to show two plots in the same row
missmap(obj = <dataframe>,
+ main = "<title>",
+ legend = FALSE)
par(mfrow=c(1,1)) # revert the plotting area to the default (one plot per row)
Amelia is not absolutely necessary for small-scale problems, since summary(dataframe) clearly shows NAs as well (and so do which(complete.cases(<dataframe>) == FALSE)) and which(is.na(<dataframe>$<column>))).

library(Amelia)
par(mfrow=c(1,2))
missmap(obj = the.beatles.songs, main = "The Beatles songs dataset NAs (1/2)", legend = FALSE)
missmap(obj = the.beatles.songs[, c(-3:-15)], main = "The Beatles songs dataset NAs (2/2)", legend = FALSE)
par(mfrow=c(1,1))

Handling NAs

Categorical variables with a small number of missing values

In a situation like this, the missing values are replaced by the ‘majority class’ (the dominant value).
unique(<dataframe>$<column>) # how many different values
xtabs(~<column>, data = <dataframe>) # show frequencies, but not for NAs
table(<dataframe>$<column>) # show frequencies, but not for NAs
table(<dataframe>$<column>, useNA = "ifany") # show frequencies for NAs as well

unique(the.beatles.songs$Year)                                      # how many different values of Year
which(the.beatles.songs$Year == "196?")                             # turn this one into NA
the.beatles.songs$Year[69] <- NA
the.beatles.songs$Year <- as.factor(the.beatles.songs$Year)         # represent Year as a factor
table(the.beatles.songs$Year, useNA = "ifany")                      # show frequencies, including NA
max(as.integer(table(the.beatles.songs$Year)))                      # find the 'majority class'
the.beatles.songs$Year[69] <- "1963"                                # replace the NA
xtabs(~Year, the.beatles.songs)                                     # verify the replacement
saveRDS(the.beatles.songs, "The Beatles songs dataset, v5.1.RData") # save this version for later use
# the.beatles.songs <- readRDS("The Beatles songs dataset, v5.1.RData")

Numeric variables with a small number of missing values (Alternative 1)

Replace the missing values with the average value of the variable on a subset of instances that are the closest (the most similar) to the instance(s) with the missing value. If the variable is normaly distributed (shapiro.test()), use the mean; otherwise, used the median. In the simplest case, use the entire range of instances.
summary(<dataframe>$<numeric column>) # inspect normality
plot(density((<dataframe>$<numeric column>), na.rm = TRUE) # inspect normality
shapiro.test(<dataframe>$<numeric column>) # inspect normality
<indices> <- which(is.na(<dataframe>$<numeric column>)) # get the indices of NAs in <dataframe>$<numeric column>
<dataframe>$<numeric column>[<indices>] <- # in case of normal distribution
+ mean(<dataframe>$<numeric column>, na.rm = TRUE)
<dataframe>$<numeric column>[<indices>] <- # in other cases
+ median(<dataframe>$<numeric column>, na.rm = TRUE)

summary(the.beatles.songs$Duration)
plot(density(the.beatles.songs$Duration, na.rm = TRUE))
shapiro.test(the.beatles.songs$Duration)
original.Duration <- the.beatles.songs$Duration               # save it for the other examples
indices <- which(is.na(the.beatles.songs$Duration))
the.beatles.songs$Duration[indices] <-                        # distribution is not normal, 
  as.integer(summary(the.beatles.songs$Duration)[3])          # so use the median

Numeric variables with a small number of missing values (Alternative 2)

Use linear regression to PREDICT the missing values, and replace the missing values with the predicted ones:
<indices of missing values> <-
+ which(is.na(<dataframe>$<numeric column>))
length(<indices of missing values>) # verify that the number is small
# install.packages("rpart")
library(rpart)
<regression tree> <-
+ rpart(<output variable> ~ # build the regression tree (the model)
+ <predictor variable 1> +
+ <predictor variable 2> + ..., # . to include all variables
+ data = # the entire dataframe, but
+ <dataframe>[-<indices of missing values>], # leaving out the rows with NAs
+ method = "anova") # build regression tree
# install.packages('rattle')
# install.packages('rpart.plot')
# install.packages('RColorBrewer')
library(rattle)
library(rpart.plot)
library(RColorBrewer)
fancyRpartPlot(<regression tree>) # plot the regression tree
<predicted values for NAs> <- # make predictions
+ predict(object = <regression tree>,
+ newdata = <dataframe>[<indices of missing values>, ])
<original <dataframe>$<numeric column> as a dataframe> <- # save it for later assessment
+ data.frame(<numeric column name> = <dataframe>$<numeric column>)
<original <dataframe>$<numeric column> as a dataframe>$lbl <- "before"
ggplot(<original <dataframe>$<numeric column> as a dataframe>, # optional: plot the density of
+ aes(x = <numeric column>)) + # the original <numeric column>
+ geom_density()
<dataframe>$<numeric column>[<indices of missing values>] <- # impute predicted values
+ as.integer(<predicted values for NAs>)
summary(<dataframe>$<numeric column>) # verify that NAs are eliminated
summary(<original <dataframe>$<numeric column> as a dataframe>$<numeric column>)
<modified <dataframe>$<numeric column> as a dataframe> <-
+ data.frame(<numeric column name> = <dataframe>$<numeric column>)
<modified <dataframe>$<numeric column> as a dataframe>$lbl <- "after"
<before-and-after dataframe> <- # append 2 new dataframes:
+ rbind(<original <dataframe>$<numeric column> as a dataframe>, # before and
+ <modified <dataframe>$<numeric column> as a dataframe>) # after the imputation
library(ggplot2)
ggplot(<before-and-after dataframe>, # plot the results
+ aes(x = <numeric column name>, fill = lbl)) +
+ geom_density(alpha = 0.2) + # alpha: plot transparency (0-1, optional)
+ theme_bw()

the.beatles.songs$Duration <- original.Duration           # restore the original Duration (with NAs)
indices <- which(is.na(the.beatles.songs$Duration))
library(rpart)
song.duration.model <- rpart(Duration ~ Year, 
                             data = the.beatles.songs,
                             method = "anova")
library(rattle)
library(rpart.plot)
library(RColorBrewer)
fancyRpartPlot(song.duration.model)                       # plot the regression tree
song.duration.predicted <- 
  predict(object = song.duration.model, 
          newdata = the.beatles.songs[indices, ])
original.Duration.df <- data.frame(Duration = the.beatles.songs$Duration)
original.Duration.df$lbl <- "before"
# ggplot(original.Duration.df, aes(x = Duration)) + geom_density()
the.beatles.songs$Duration[indices] <- song.duration.predicted
summary(the.beatles.songs$Duration)
summary(original.Duration.df$Duration)
modified.Duration.df <- data.frame(Duration = the.beatles.songs$Duration)
modified.Duration.df$lbl <- "after"
duration.df <- rbind(original.Duration.df, modified.Duration.df)
library(ggplot2)
ggplot(duration.df, aes(x = Duration, fill = lbl)) + 
  geom_density(alpha = 0.3) + 
  theme_bw()

Numeric variables with a small number of missing values (Alternative 3)

Use non-NA values from a subset of SIMILAR observations (similar in terms of having (nearly) the same values of other relevant features). Suitable when the number of NAs is very small, since the replacements typically go one by one. The example in the YouTube video Introduction to Data Science with R - Exploratory Modeling 2, from 1:24:38 to 1:28:04, l. 805-815, where one is looking at similar records in the dataset, in order to find a suitable replacement for the missing value. This approach has not been demonstrated here because no suitable feature is available in the dataset (a numeric feature with a very small number of NAs), but the approach is worth mentioning.

Variables with many missing values and/or missing values that are difficult to replace

A more sophisticated imputation is applied in such cases (out of scope of this course). It is, in fact, the task of predicting (good substitutes for) the missing values. The other option is to create some new variables (“proxies”) and do some feature engineering.

—————-

Additional data cleaning using a custom function

Get rid of all other NAs and factorize suitable features before attempting feature selection and engineering:

source("Get rid of NAs.R")
the.beatles.songs <- getRidOfNAs(the.beatles.songs)
saveRDS(the.beatles.songs, "The Beatles songs dataset, v5.2.RData")
the.beatles.songs <- factorize(the.beatles.songs)
saveRDS(the.beatles.songs, "The Beatles songs dataset, v5.3.RData")

Feature selection and engineering

Overall philosophy: pick features one by one, or in suitable pairs, and see how they can be engineered to increase the quality/precision of predictions. Generally, plot variables and their values (with na.rm = TRUE) whenever it makes sense. It gives you a better sense of the predictive power of each variable.

Split the dataset into train and test sets:
# install.packages("caret")
library(caret)
set.seed(<n>)
<train dataset indices> <- # stratified partitioning:
+ createDataPartition(<dataset>$<output variable>, # the same distribution of the output variable in both sets
+ p = .80, # 80/20% of data in train/test sets
+ list = FALSE) # don't make a list of results, make a matrix
<train dataset> <- <dataset>[<train dataset indices>, ]
<test dataset> <- <dataset>[-<train dataset indices>, ]

library(caret)
set.seed(444)
train.data.indices <- createDataPartition(the.beatles.songs$Top.50.Billboard, p = 0.80, list = FALSE)
train.data <- the.beatles.songs[train.data.indices, ]
test.data <- the.beatles.songs[-train.data.indices, ]

Examine the predictive power of variables from the data set by means of tables, frequencies and proportions. For a categorical predictor, check its frequencies and proportions in the dataset: summary(<dataset>$<predictor>) # frequencies
round(summary(<dataset>$<predictor>) / nrow(<dataset>), digits = 3) # proportions

summary(train.data$Single.certification)
round(summary(train.data$Single.certification) / nrow(train.data), digits = 3)

Then examine the frequencies and the proportions of the output variable values (classes) based on the predictor values:
xtabs(~<predictor> + <output variable>, data = <dataset>) # frequencies
prop.table(xtabs(~<predictor> + <output variable>, data = <dataset>), # proportions
+ margin = 1) # by row

xtabs(~Single.certification + Top.50.Billboard, data = train.data)
prop.table(xtabs(~Single.certification + Top.50.Billboard, data = train.data), margin = 1)

For a numeric predictor with a small number of values, first convert it into a factor:
<dataset>$<predictor> <- factor(<dataset>$<predictor>,
+ levels = c(<n1>, <n2>, ...),
+ labels = c("<l1>", "<l2>", ...))
However, make sure to keep the original (numeric) values from the integrated dataset (including both and test datasets) for possible later use:
<original numeric predictor> <- <integrated dataset>$<predictor>

unique(train.data$Weeks.at.No1.in.UK.The.Guardian)
original.Weeks.at.No1.in.UK.The.Guardian <-                               # keep it for later
  the.beatles.songs$Weeks.at.No1.in.UK.The.Guardian
train.data$Weeks.at.No1.in.UK.The.Guardian <- 
  factor(train.data$Weeks.at.No1.in.UK.The.Guardian, 
         levels = c(0, 2, 3, 4, 5, 6, 7), 
         labels = c("0", "2", "3", "4", "5", "6", "7"))

Then examine frequencies and proportions as above, or plot the output variable against the predictor:
<gg> <- ggplot(<dataset>, aes(x = <predictor name>, fill = <output variable name>)) +
+ geom_bar(position = "dodge", width = <bin width>) + # "dodge": bargraph, <bin width>: 0.2-0.4
+ labs(x = "<x-label>", y = "<y-label>", title = "<title>") +
+ theme_bw()
<gg>
<gf> <- <gg> + facet_wrap(~<another predictor name>) # examine 2 predictors together
<gf>

gg1 <- ggplot(data = train.data, aes(x = Weeks.at.No1.in.UK.The.Guardian, fill = Top.50.Billboard)) +
  geom_bar(position = "dodge", width = 0.6) +
  ylab("Number of Billboard Top 50 songs") + xlab("Weeks and No.1 in UK (The Guardian)") +
  theme_bw()
gg1
gg2 <- ggplot(data = train.data, aes(x = Year, fill = Top.50.Billboard)) +
  geom_bar(position = "dodge", width = 0.6) +
  ylab("Number of Billboard Top 50 songs") + xlab("Year") +
  theme_bw()
gg2
gg3 <- ggplot(data = train.data, aes(x = Single.certification, fill = Top.50.Billboard)) +
  geom_bar(position = "dodge", width = 0.6) +
  labs(y = "Number of Billboard Top 50 songs", x = "Single certification", title = "Certified") +
  theme_bw()
gg3
gg4 <- gg3 + facet_wrap(~Year)
gg4

Feature engineering

When creating new features (attributes) to be used for prediction purposes, we should merge the training and the test sets and develop new features on the merged data. Before that, we need to assure that the training and the test sets have exactly the same structure.

In the test set, do all the modifications that have been done in the train set:

unique(test.data$Weeks.at.No1.in.UK.The.Guardian)
test.data$Weeks.at.No1.in.UK.The.Guardian <- 
  factor(test.data$Weeks.at.No1.in.UK.The.Guardian, 
         levels = c(0, 2, 3, 4, 5, 6, 7), 
         labels = c("0", "2", "3", "4", "5", "6", "7"))

Merge the train and test datasets into one for creating new features:
<adapted dataset> <- rbind(<adapted train dataset>, <adapted test dataset>)
saveRDS(<adapted dataset>, "<RData filename>")

the.beatles.songs <- rbind(train.data, test.data)
saveRDS(the.beatles.songs, "The Beatles songs dataset, v5.4.RData")

Creating a proxy variable for a certain feature

It’s a new variable that approximates an original one, or is a good replacement for the original one.

How many Top 50 Billboard songs performed by the Beatles are covers of other authors’ songs?

which(the.beatles.songs$Cover == "Yes")                                  # how many cover songs (all)
top.50.bb.indices <- which(the.beatles.songs$Top.50.Billboard == "Yes")  # all Billboard Top 50 songs
top.50.bb.indices
which(the.beatles.songs$Cover[top.50.bb.indices] == "Yes")  # how many cover songs on Billboard Top 50

Very few Top 50 Billboard songs performed by the Beatles are covers of other authors’ songs, so take a closer look at the Songwriter feature. How many different Songwriter values are there?

unique(the.beatles.songs$Songwriter)

How many songs are covers?

length(which(the.beatles.songs$Cover == "Yes"))

It’s a considerable difference (82 vs. 71), so it’s better to create a proxy for song authorship.
How many songs have, say, John Lennon in the list of authors?
grepl(<substring>, <string>) # TRUE if <string> contains <substring>
<indices> <- # indices of <character variable> containing <substring>
+ grep(<substring>,
+ <dataframe>$<character variable>)

grepl("eat", "The Beatles")
i.lennon <- grep("Lennon", the.beatles.songs$Songwriter)
i.mccartney <- grep("McCartney", the.beatles.songs$Songwriter)
i.harrison <- grep("Harrison", the.beatles.songs$Songwriter)
i.starkey <- grep("Starkey", the.beatles.songs$Songwriter)

source("Song author proxies.R")
authors <- getSongAuthorProxies(i.lennon, i.mccartney, i.harrison, i.starkey)
lennon.songs <- authors[[1]]
mccartney.songs <- authors[[2]]
harrison.songs <- authors[[3]]
starkey.songs <- authors[[4]]
lennon.mccartney.songs <- authors[[5]]
mccartney.lennon.songs <- authors[[6]]
lennon.mccartney.harrison.starkey.songs <- authors[[7]]

Create proxy variables for different authors:

the.beatles.songs$Author <- "Other"
the.beatles.songs$Author[lennon.songs] <- "Lennon"
the.beatles.songs$Author[mccartney.songs] <- "McCartney"
the.beatles.songs$Author[harrison.songs] <- "Harrison"
the.beatles.songs$Author[starkey.songs] <- "Starkey"
the.beatles.songs$Author[lennon.mccartney.songs] <- "Lennon/McCartney"
the.beatles.songs$Author[mccartney.lennon.songs] <- "McCartney/Lennon"
the.beatles.songs$Author[lennon.mccartney.harrison.starkey.songs] <- "Lennon/McCartney/Harrison/Starkey"

Convert the new proxy vriable into a factor:

the.beatles.songs$Author <- factor(the.beatles.songs$Author)
summary(the.beatles.songs$Author)

Examine the predictive power of the newly created proxy variable:
ggplot(data = <dataset>[1:<training data length>, ], # only the training data from the merged dataset!
+ aes(x = <proxy variable name>,
+ fill = <output variable name>)) +
+ geom_bar(position = "dodge") + # bar graph
+ theme_bw()

ggplot(the.beatles.songs[1:249, ], 
       aes(x = Author, fill = Top.50.Billboard)) +
  geom_bar(position = "dodge") + 
  theme_bw()

Creating a new variable / new variables

Ideas about Chart.position.UK.Wikipedia, Chart.position.US.Wikipedia, Highest.position.The.Guardian, Weeks.on.chart.in.UK.The.Guardian and Weeks.at.No1.The.Guardian:
# Normalized chart position
+ if (<chart position> == 0) {
+ <normalized chart position> <- 0
+ }
+ if (<chart position> in (1:<max value>)) {
+ <normalized chart position> <- (<max value> - <chart position> + 1) / <max value>
+ }
# Normalized weeks-on-chart
+ <normalized weeks-on-chart> <- <weeks-on-chart> / <max value>
# Normalized weeks-at-No.1
<normalized weeks-at-No.1> <- <weeks-at-No.1> / <max value>
# Idea: chart presence
# Chart presence in UK/US:
+ <chart presence> <-
+ (<a> * <normalized chart position> +
+ <b> * <normalized weeks-on-chart> +
+ <c> * normalized weeks-at-no-1) /
+ (<a> + <b> + <c>)
# Chart presence in both UK and US (overall): ((a * ch.pr.1 + b * ch.pr.2) / (a + b))
+ <chart presence> <-
+ (<a> * <chart presence in UK> +
+ <b> * <chart presence in US>) /
+ (<a> + <b>)

New variables: Chart.presence.UK, Chart.presence.US, Chart.presence.UK.and.US

the.beatles.songs$Weeks.at.No1.in.UK.The.Guardian <-            # restore this as a numeric
  original.Weeks.at.No1.in.UK.The.Guardian
saveRDS(the.beatles.songs, "The Beatles songs dataset, v5.5.RData")

source("Chart presence.R")
ch.pos.UK.norm <- getNormalizedChartPosition(the.beatles.songs$Chart.position.UK.Wikipedia)
weeks.on.chart.UK.norm <- 
  getNormalizedWeeksOnChart(the.beatles.songs$Weeks.on.chart.in.UK.The.Guardian)
weeks.at.No1.UK.norm <- 
  getNormalizedWeeksAtNo1(the.beatles.songs$Weeks.at.No1.in.UK.The.Guardian)
ch.pos.US.norm <-                                               # that's all for US in the dataset
  getNormalizedChartPosition(the.beatles.songs$Chart.position.US.Wikipedia)
the.beatles.songs$Chart.presence.UK <- 
  getChartPresence(ch.pos.UK.norm, weeks.on.chart.UK.norm, weeks.at.No1.UK.norm, 
                   1, 1, 1)                                     # experiment with different weights
the.beatles.songs$Chart.presence.US <- 
  getChartPresence(ch.pos.US.norm,                              # that's all for US in the dataset
                   rep(0, nrow(the.beatles.songs)),             # dummy, for the sake of function format
                   rep(0, nrow(the.beatles.songs)),             # dummy, for the sake of function format
                   1, 0, 0)                                     # 0 weights for missing data
the.beatles.songs$Chart.presence.UK.and.US <- 
  getChartPresenceOverall(the.beatles.songs$Chart.presence.UK, 
                          the.beatles.songs$Chart.presence.US, 
                          1, 1)                                 # experiment with different weights

How relevant are the new variables for predicting Billboard Top 50 Beatles songs?

Discretize and factorize them first:
<dataset>$<new factor feature> <-
+ cut(<dataset>$<numeric feature>,
+ breaks = <n>, # number of intervals to cut the <numeric feature> into
+ labels = c("<lab 1>", "<lab 2>", ..., "<lab n>")) # factor labels

the.beatles.songs$Hype.UK <- 
  cut(the.beatles.songs$Chart.presence.UK, 
      breaks = 5, 
      labels = c("Very Low", "Low", "Neutral", "High", "Very High"))
the.beatles.songs$Hype.US <- 
  cut(the.beatles.songs$Chart.presence.US, 
      breaks = 5, 
      labels = c("Very Low", "Low", "Neutral", "High", "Very High"))
the.beatles.songs$Hype.UK.and.US <- 
  cut(the.beatles.songs$Chart.presence.UK.and.US, 
      breaks = 5, 
      labels = c("Very Low", "Low", "Neutral", "High", "Very High"))

And now plot them:

ggplot(the.beatles.songs[1:249, ], 
       aes(x = Hype.UK, fill = Top.50.Billboard)) +
  geom_bar(position = "dodge") + 
  theme_bw()
ggplot(the.beatles.songs[1:249, ], 
       aes(x = Hype.US, fill = Top.50.Billboard)) +
  geom_bar(position = "dodge") + 
  theme_bw()
ggplot(the.beatles.songs[1:249, ], 
       aes(x = Hype.UK.and.US, fill = Top.50.Billboard)) +
  geom_bar(position = "dodge") + 
  theme_bw()

Resources, readings, references

Classification vs. regression:
http://www.simafore.com/blog/bid/62482/2-main-differences-between-classification-and-regression-trees

LS0tDQp0aXRsZTogIkZlYXR1cmUgZW5naW5lZXJpbmciDQphdXRob3I6ICJWbGFkYW4gRGV2ZWR6aWMiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IGRlZmF1bHQNCiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdA0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KYGBgDQoNCiMjIChJbnN0YWxsaW5nIGFuZCkgTG9hZGluZyB0aGUgcmVxdWlyZWQgUiBwYWNrYWdlcw0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCiMgaW5zdGFsbC5wYWNrYWdlcygiZ2dwbG90MiIpDQpsaWJyYXJ5KGdncGxvdDIpDQpgYGANCg0KIyMgRGF0YXNldA0KDQpSZWFkIHRoZSBkYXRhc2V0LiAgDQoNCkFuIGVuZ2luZWVyZWQgYW5kIGhpZ2hseSBzaW1wbGlmaWVkIHZlcnNpb246ICJUaGUgQmVhdGxlcyBzb25ncyBkYXRhc2V0LCB2MSwgbm8gTkFzLmNzdiI6ICANCmBgYHtyfQ0KdGhlLmJlYXRsZXMuc29uZ3MgPC0gcmVhZC5jc3YoIlRoZSBCZWF0bGVzIHNvbmdzIGRhdGFzZXQsIHYxLCBubyBOQXMuY3N2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UpDQpzdW1tYXJ5KHRoZS5iZWF0bGVzLnNvbmdzKQ0KYGBgDQoNCkEgbW9yZSByZWFsaXN0aWMsIGJ1dCBzdGlsbCB2ZXJ5IHNpbXBsZSB2ZXJzaW9uOiAgDQpgYGB7cn0NCnRoZS5iZWF0bGVzLnNvbmdzIDwtIHJlYWQuY3N2KCJUaGUgQmVhdGxlcyBzb25ncyBkYXRhc2V0LCB2My5jc3YiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkNCnN1bW1hcnkodGhlLmJlYXRsZXMuc29uZ3MpDQpgYGANCg0KIyMgTWlzc2luZyB2YWx1ZXMNCg0KTkFzIGFyZSBtaXNzaW5nIHZhbHVlczsgZW1wdHkgc3RyaW5ncyAoIiIpIGFyZSBub3QgdGhlIHNhbWUgYXMgTkFzLiAgDQoNCkRpZmZlcmVudCB3YXlzIG9mIGNoZWNraW5nIGlmIHRoZXJlIGFyZSBOQXMgYW5kL29yICIiczogIA0KYHN1bW1hcnkoPGRhdGFmcmFtZT4pYCAgDQpgd2hpY2goY29tcGxldGUuY2FzZXMoPGRhdGFmcmFtZT4pID09IEZBTFNFKSAgICAgICAgICAgIyB3aGljaCByb3dzIGNvbnRhaW4gTkFzYCAgDQpgbGVuZ3RoKHdoaWNoKGNvbXBsZXRlLmNhc2VzKDxkYXRhZnJhbWU+KSA9PSBGQUxTRSkpICAgIyBob3cgbWFueSBzdWNoIHJvd3NgICANCmB3aGljaChpcy5uYSg8ZGF0YWZyYW1lPls8cm93PiwgXSkpICAgICAgICAgICAgICAgICAgICAjIE5BcyBpbiA8cm93PmAgIA0KYHdoaWNoKGlzLm5hKDxkYXRhZnJhbWU+JDxjb2x1bW4+KSkgICAgICAgICAgICAgICAgICAgICMgTkFzIGluIDxjb2x1bW4+YCAgDQpgd2hpY2goPGRhdGFmcmFtZT4kPGNvbHVtbj4gPT0gIiIpICAgICAgICAgICAgICAgICAgICAgIyAiInMgaW4gPGNvbHVtbj5gICANCmBucm93KDxkYXRhZnJhbWU+WzxkYXRhZnJhbWU+JDxjb2x1bW4+ID09ICIiLCBdKSAgICAgICAjIGhvdyBtYW55IHN1Y2ggcm93c2AgIA0KYGBge3J9DQpzdW1tYXJ5KHRoZS5iZWF0bGVzLnNvbmdzKQ0Kd2hpY2goY29tcGxldGUuY2FzZXModGhlLmJlYXRsZXMuc29uZ3MpID09IEZBTFNFKQ0Kd2hpY2goaXMubmEodGhlLmJlYXRsZXMuc29uZ3MkVG9wLjUwLkJpbGxib2FyZCkpDQp3aGljaCh0aGUuYmVhdGxlcy5zb25ncyRTaW5nbGUuQS5zaWRlID09ICIiKQ0KbnJvdyh0aGUuYmVhdGxlcy5zb25nc1sodGhlLmJlYXRsZXMuc29uZ3MkU2luZ2xlLkEuc2lkZSA9PSAiIikgJiAodGhlLmJlYXRsZXMuc29uZ3MkWWVhciA9PSAxOTY3KSwgXSkNCmBgYA0KDQojIyMgVmlzdWFsaXppbmcgTkFzDQoNClVzZSB0aGUgQW1lbGlhIHBhY2thZ2U6ICANCmBsaWJyYXJ5KEFtZWxpYSkgICAgICAgICAgICAgICAgICAgICAgICMgdmlzdWFsaXplcyBOQXMsIEJVVCBOT1QgIiJzICEhIWAgIA0KYCsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyB0byB2aXN1YWxpemUgIiJzIHdpdGggQW1lbGlhIGFzIHdlbGwsIHJlcGxhY2UgIiJzIHdpdGggTkFzYCAgDQpgcGFyKG1mcm93PWMoMSwyKSkgICAgICAgICAgICAgICAgICAgICAjIHN0cnVjdHVyZSB0aGUgZGlzcGxheSBhcmVhIHRvIHNob3cgdHdvIHBsb3RzIGluIHRoZSBzYW1lIHJvd2AgIA0KYG1pc3NtYXAob2JqID0gPGRhdGFmcmFtZT4sYCAgDQpgKyAgICAgICBtYWluID0gIjx0aXRsZT4iLGAgIA0KYCsgICAgICAgbGVnZW5kID0gRkFMU0UpYCAgDQpgcGFyKG1mcm93PWMoMSwxKSkgICAgICAgICAgICAgICAgICAgICAjIHJldmVydCB0aGUgcGxvdHRpbmcgYXJlYSB0byB0aGUgZGVmYXVsdCAob25lIHBsb3QgcGVyIHJvdylgICANCkFtZWxpYSBpcyBub3QgYWJzb2x1dGVseSBuZWNlc3NhcnkgZm9yIHNtYWxsLXNjYWxlIHByb2JsZW1zLCBzaW5jZSBzdW1tYXJ5KGRhdGFmcmFtZSkgY2xlYXJseSBzaG93cyBOQXMgYXMgd2VsbCAoYW5kIHNvIGRvIGB3aGljaChjb21wbGV0ZS5jYXNlcyg8ZGF0YWZyYW1lPikgPT0gRkFMU0UpKWAgYW5kIGB3aGljaChpcy5uYSg8ZGF0YWZyYW1lPiQ8Y29sdW1uPikpKWAuICANCmBgYHtyfQ0KbGlicmFyeShBbWVsaWEpDQpwYXIobWZyb3c9YygxLDIpKQ0KbWlzc21hcChvYmogPSB0aGUuYmVhdGxlcy5zb25ncywgbWFpbiA9ICJUaGUgQmVhdGxlcyBzb25ncyBkYXRhc2V0IE5BcyAoMS8yKSIsIGxlZ2VuZCA9IEZBTFNFKQ0KbWlzc21hcChvYmogPSB0aGUuYmVhdGxlcy5zb25nc1ssIGMoLTM6LTE1KV0sIG1haW4gPSAiVGhlIEJlYXRsZXMgc29uZ3MgZGF0YXNldCBOQXMgKDIvMikiLCBsZWdlbmQgPSBGQUxTRSkNCnBhcihtZnJvdz1jKDEsMSkpDQpgYGANCg0KIyMjIEhhbmRsaW5nIE5Bcw0KDQojIyMjIENhdGVnb3JpY2FsIHZhcmlhYmxlcyB3aXRoIGEgc21hbGwgbnVtYmVyIG9mIG1pc3NpbmcgdmFsdWVzDQoNCkluIGEgc2l0dWF0aW9uIGxpa2UgdGhpcywgdGhlIG1pc3NpbmcgdmFsdWVzIGFyZSByZXBsYWNlZCBieSB0aGUgJ21ham9yaXR5IGNsYXNzJyAodGhlIGRvbWluYW50IHZhbHVlKS4gIA0KYHVuaXF1ZSg8ZGF0YWZyYW1lPiQ8Y29sdW1uPikgICAgICAgICAgICAgICAgICAjIGhvdyBtYW55IGRpZmZlcmVudCB2YWx1ZXNgICANCmB4dGFicyh+PGNvbHVtbj4sIGRhdGEgPSA8ZGF0YWZyYW1lPikgICAgICAgICAgIyBzaG93IGZyZXF1ZW5jaWVzLCBidXQgbm90IGZvciBOQXNgICANCmB0YWJsZSg8ZGF0YWZyYW1lPiQ8Y29sdW1uPikgICAgICAgICAgICAgICAgICAgIyBzaG93IGZyZXF1ZW5jaWVzLCBidXQgbm90IGZvciBOQXNgICANCmB0YWJsZSg8ZGF0YWZyYW1lPiQ8Y29sdW1uPiwgdXNlTkEgPSAiaWZhbnkiKSAgIyBzaG93IGZyZXF1ZW5jaWVzIGZvciBOQXMgYXMgd2VsbGAgIA0KYGBge3J9DQp1bmlxdWUodGhlLmJlYXRsZXMuc29uZ3MkWWVhcikgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgaG93IG1hbnkgZGlmZmVyZW50IHZhbHVlcyBvZiBZZWFyDQp3aGljaCh0aGUuYmVhdGxlcy5zb25ncyRZZWFyID09ICIxOTY/IikgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgdHVybiB0aGlzIG9uZSBpbnRvIE5BDQp0aGUuYmVhdGxlcy5zb25ncyRZZWFyWzY5XSA8LSBOQQ0KdGhlLmJlYXRsZXMuc29uZ3MkWWVhciA8LSBhcy5mYWN0b3IodGhlLmJlYXRsZXMuc29uZ3MkWWVhcikgICAgICAgICAjIHJlcHJlc2VudCBZZWFyIGFzIGEgZmFjdG9yDQp0YWJsZSh0aGUuYmVhdGxlcy5zb25ncyRZZWFyLCB1c2VOQSA9ICJpZmFueSIpICAgICAgICAgICAgICAgICAgICAgICMgc2hvdyBmcmVxdWVuY2llcywgaW5jbHVkaW5nIE5BDQptYXgoYXMuaW50ZWdlcih0YWJsZSh0aGUuYmVhdGxlcy5zb25ncyRZZWFyKSkpICAgICAgICAgICAgICAgICAgICAgICMgZmluZCB0aGUgJ21ham9yaXR5IGNsYXNzJw0KdGhlLmJlYXRsZXMuc29uZ3MkWWVhcls2OV0gPC0gIjE5NjMiICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHJlcGxhY2UgdGhlIE5BDQp4dGFicyh+WWVhciwgdGhlLmJlYXRsZXMuc29uZ3MpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgdmVyaWZ5IHRoZSByZXBsYWNlbWVudA0Kc2F2ZVJEUyh0aGUuYmVhdGxlcy5zb25ncywgIlRoZSBCZWF0bGVzIHNvbmdzIGRhdGFzZXQsIHY1LjEuUkRhdGEiKSAjIHNhdmUgdGhpcyB2ZXJzaW9uIGZvciBsYXRlciB1c2UNCiMgdGhlLmJlYXRsZXMuc29uZ3MgPC0gcmVhZFJEUygiVGhlIEJlYXRsZXMgc29uZ3MgZGF0YXNldCwgdjUuMS5SRGF0YSIpDQpgYGANCg0KIyMjIyBOdW1lcmljIHZhcmlhYmxlcyB3aXRoIGEgc21hbGwgbnVtYmVyIG9mIG1pc3NpbmcgdmFsdWVzIChBbHRlcm5hdGl2ZSAxKQ0KDQpSZXBsYWNlIHRoZSBtaXNzaW5nIHZhbHVlcyB3aXRoIHRoZSBhdmVyYWdlIHZhbHVlIG9mIHRoZSB2YXJpYWJsZSBvbiBhIHN1YnNldCBvZiBpbnN0YW5jZXMgdGhhdCBhcmUgdGhlIGNsb3Nlc3QgKHRoZSBtb3N0IHNpbWlsYXIpIHRvIHRoZSBpbnN0YW5jZShzKSB3aXRoIHRoZSBtaXNzaW5nIHZhbHVlLiBJZiB0aGUgdmFyaWFibGUgaXMgbm9ybWFseSBkaXN0cmlidXRlZCAoYHNoYXBpcm8udGVzdCgpYCksIHVzZSB0aGUgbWVhbjsgb3RoZXJ3aXNlLCB1c2VkIHRoZSBtZWRpYW4uIEluIHRoZSBzaW1wbGVzdCBjYXNlLCB1c2UgdGhlIGVudGlyZSByYW5nZSBvZiBpbnN0YW5jZXMuICANCmBzdW1tYXJ5KDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4pICAgICAgICAgICAgICAgICAgICAgICAjIGluc3BlY3Qgbm9ybWFsaXR5YCAgDQpgcGxvdChkZW5zaXR5KCg8ZGF0YWZyYW1lPiQ8bnVtZXJpYyBjb2x1bW4+KSwgbmEucm0gPSBUUlVFKSAgIyBpbnNwZWN0IG5vcm1hbGl0eWAgIA0KYHNoYXBpcm8udGVzdCg8ZGF0YWZyYW1lPiQ8bnVtZXJpYyBjb2x1bW4+KSAgICAgICAgICAgICAgICAgICMgaW5zcGVjdCBub3JtYWxpdHlgICANCmA8aW5kaWNlcz4gPC0gd2hpY2goaXMubmEoPGRhdGFmcmFtZT4kPG51bWVyaWMgY29sdW1uPikpICAgICAjIGdldCB0aGUgaW5kaWNlcyBvZiBOQXMgaW4gPGRhdGFmcmFtZT4kPG51bWVyaWMgY29sdW1uPmAgIA0KYDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj5bPGluZGljZXM+XSA8LSAgICAgICAgICAgICAgICAgICMgaW4gY2FzZSBvZiBub3JtYWwgZGlzdHJpYnV0aW9uYCAgDQpgKyBtZWFuKDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4sIG5hLnJtID0gVFJVRSlgICANCmA8ZGF0YWZyYW1lPiQ8bnVtZXJpYyBjb2x1bW4+WzxpbmRpY2VzPl0gPC0gICAgICAgICAgICAgICAgICAjIGluIG90aGVyIGNhc2VzYCAgDQpgKyBtZWRpYW4oPGRhdGFmcmFtZT4kPG51bWVyaWMgY29sdW1uPiwgbmEucm0gPSBUUlVFKWAgIA0KYGBge3J9DQpzdW1tYXJ5KHRoZS5iZWF0bGVzLnNvbmdzJER1cmF0aW9uKQ0KcGxvdChkZW5zaXR5KHRoZS5iZWF0bGVzLnNvbmdzJER1cmF0aW9uLCBuYS5ybSA9IFRSVUUpKQ0Kc2hhcGlyby50ZXN0KHRoZS5iZWF0bGVzLnNvbmdzJER1cmF0aW9uKQ0Kb3JpZ2luYWwuRHVyYXRpb24gPC0gdGhlLmJlYXRsZXMuc29uZ3MkRHVyYXRpb24gICAgICAgICAgICAgICAjIHNhdmUgaXQgZm9yIHRoZSBvdGhlciBleGFtcGxlcw0KaW5kaWNlcyA8LSB3aGljaChpcy5uYSh0aGUuYmVhdGxlcy5zb25ncyREdXJhdGlvbikpDQp0aGUuYmVhdGxlcy5zb25ncyREdXJhdGlvbltpbmRpY2VzXSA8LSAgICAgICAgICAgICAgICAgICAgICAgICMgZGlzdHJpYnV0aW9uIGlzIG5vdCBub3JtYWwsIA0KICBhcy5pbnRlZ2VyKHN1bW1hcnkodGhlLmJlYXRsZXMuc29uZ3MkRHVyYXRpb24pWzNdKSAgICAgICAgICAjIHNvIHVzZSB0aGUgbWVkaWFuDQpgYGANCg0KIyMjIyBOdW1lcmljIHZhcmlhYmxlcyB3aXRoIGEgc21hbGwgbnVtYmVyIG9mIG1pc3NpbmcgdmFsdWVzIChBbHRlcm5hdGl2ZSAyKQ0KDQpVc2UgbGluZWFyIHJlZ3Jlc3Npb24gdG8gUFJFRElDVCB0aGUgbWlzc2luZyB2YWx1ZXMsIGFuZCByZXBsYWNlIHRoZSBtaXNzaW5nIHZhbHVlcyB3aXRoIHRoZSBwcmVkaWN0ZWQgb25lczogIA0KYDxpbmRpY2VzIG9mIG1pc3NpbmcgdmFsdWVzPiA8LWAgIA0KYCsgd2hpY2goaXMubmEoPGRhdGFmcmFtZT4kPG51bWVyaWMgY29sdW1uPikpYCAgDQpgbGVuZ3RoKDxpbmRpY2VzIG9mIG1pc3NpbmcgdmFsdWVzPikgICAgICAgICAgICAgICAgICAgICAjIHZlcmlmeSB0aGF0IHRoZSBudW1iZXIgaXMgc21hbGxgICANCmAjIGluc3RhbGwucGFja2FnZXMoInJwYXJ0IilgICANCmBsaWJyYXJ5KHJwYXJ0KWAgIA0KYDxyZWdyZXNzaW9uIHRyZWU+IDwtYCAgDQpgKyBycGFydCg8b3V0cHV0IHZhcmlhYmxlPiB+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIGJ1aWxkIHRoZSByZWdyZXNzaW9uIHRyZWUgKHRoZSBtb2RlbClgICANCmArICAgICAgICAgPHByZWRpY3RvciB2YXJpYWJsZSAxPiArYCAgDQpgKyAgICAgICAgIDxwcmVkaWN0b3IgdmFyaWFibGUgMj4gKyAuLi4sICAgICAgICAgICAgICAgICAjIC4gdG8gaW5jbHVkZSBhbGwgdmFyaWFibGVzYCAgDQpgKyAgICAgICBkYXRhID0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHRoZSBlbnRpcmUgZGF0YWZyYW1lLCBidXRgICANCmArICAgICAgICAgPGRhdGFmcmFtZT5bLTxpbmRpY2VzIG9mIG1pc3NpbmcgdmFsdWVzPl0sICAgICMgbGVhdmluZyBvdXQgdGhlIHJvd3Mgd2l0aCBOQXNgICANCmArICAgICAgIG1ldGhvZCA9ICJhbm92YSIpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgYnVpbGQgcmVncmVzc2lvbiB0cmVlYCAgDQpgIyBpbnN0YWxsLnBhY2thZ2VzKCdyYXR0bGUnKWAgIA0KYCMgaW5zdGFsbC5wYWNrYWdlcygncnBhcnQucGxvdCcpYCAgDQpgIyBpbnN0YWxsLnBhY2thZ2VzKCdSQ29sb3JCcmV3ZXInKWAgIA0KYGxpYnJhcnkocmF0dGxlKWAgIA0KYGxpYnJhcnkocnBhcnQucGxvdClgICANCmBsaWJyYXJ5KFJDb2xvckJyZXdlcilgICANCmBmYW5jeVJwYXJ0UGxvdCg8cmVncmVzc2lvbiB0cmVlPikgICAgICAgICAgICAgICAgICAgICAgICMgcGxvdCB0aGUgcmVncmVzc2lvbiB0cmVlYCAgDQpgPHByZWRpY3RlZCB2YWx1ZXMgZm9yIE5Bcz4gPC0gICAgICAgICAgICAgICAgICAgICAgICAgICAjIG1ha2UgcHJlZGljdGlvbnNgICANCmArIHByZWRpY3Qob2JqZWN0ID0gPHJlZ3Jlc3Npb24gdHJlZT4sYCAgDQpgKyAgICAgICAgIG5ld2RhdGEgPSA8ZGF0YWZyYW1lPls8aW5kaWNlcyBvZiBtaXNzaW5nIHZhbHVlcz4sIF0pYCAgDQpgPG9yaWdpbmFsIDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4gYXMgYSBkYXRhZnJhbWU+IDwtICAgICAgICAgICAgICMgc2F2ZSBpdCBmb3IgbGF0ZXIgYXNzZXNzbWVudGAgIA0KYCsgZGF0YS5mcmFtZSg8bnVtZXJpYyBjb2x1bW4gbmFtZT4gPSA8ZGF0YWZyYW1lPiQ8bnVtZXJpYyBjb2x1bW4+KWAgIA0KYDxvcmlnaW5hbCA8ZGF0YWZyYW1lPiQ8bnVtZXJpYyBjb2x1bW4+IGFzIGEgZGF0YWZyYW1lPiRsYmwgPC0gImJlZm9yZSJgICANCmBnZ3Bsb3QoPG9yaWdpbmFsIDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4gYXMgYSBkYXRhZnJhbWU+LCAgICAgICAgIyBvcHRpb25hbDogcGxvdCB0aGUgZGVuc2l0eSBvZmAgIA0KYCsgICAgICBhZXMoeCA9IDxudW1lcmljIGNvbHVtbj4pKSArICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHRoZSBvcmlnaW5hbCA8bnVtZXJpYyBjb2x1bW4+YCAgDQpgKyBnZW9tX2RlbnNpdHkoKWAgIA0KYDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj5bPGluZGljZXMgb2YgbWlzc2luZyB2YWx1ZXM+XSA8LSAgICAgICAgICAjIGltcHV0ZSBwcmVkaWN0ZWQgdmFsdWVzYCAgDQpgKyBhcy5pbnRlZ2VyKDxwcmVkaWN0ZWQgdmFsdWVzIGZvciBOQXM+KWAgIA0KYHN1bW1hcnkoPGRhdGFmcmFtZT4kPG51bWVyaWMgY29sdW1uPikgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHZlcmlmeSB0aGF0IE5BcyBhcmUgZWxpbWluYXRlZGAgIA0KYHN1bW1hcnkoPG9yaWdpbmFsIDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4gYXMgYSBkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4pYCAgDQpgPG1vZGlmaWVkIDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4gYXMgYSBkYXRhZnJhbWU+IDwtYCAgDQpgKyBkYXRhLmZyYW1lKDxudW1lcmljIGNvbHVtbiBuYW1lPiA9IDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4pYCAgDQpgPG1vZGlmaWVkIDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4gYXMgYSBkYXRhZnJhbWU+JGxibCA8LSAiYWZ0ZXIiYCAgDQpgPGJlZm9yZS1hbmQtYWZ0ZXIgZGF0YWZyYW1lPiA8LSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgYXBwZW5kIDIgbmV3IGRhdGFmcmFtZXM6YCAgDQpgKyByYmluZCg8b3JpZ2luYWwgPGRhdGFmcmFtZT4kPG51bWVyaWMgY29sdW1uPiBhcyBhIGRhdGFmcmFtZT4sICAgICAgICMgYmVmb3JlIGFuZGAgIA0KYCsgICAgICAgPG1vZGlmaWVkIDxkYXRhZnJhbWU+JDxudW1lcmljIGNvbHVtbj4gYXMgYSBkYXRhZnJhbWU+KSAgICAgICAjIGFmdGVyIHRoZSBpbXB1dGF0aW9uYCAgDQpgbGlicmFyeShnZ3Bsb3QyKWAgIA0KYGdncGxvdCg8YmVmb3JlLWFuZC1hZnRlciBkYXRhZnJhbWU+LCAgICAgICAgICAgICAgICAgICAgICAjIHBsb3QgdGhlIHJlc3VsdHNgICANCmArICAgICAgYWVzKHggPSA8bnVtZXJpYyBjb2x1bW4gbmFtZT4sIGZpbGwgPSBsYmwpKSArYCAgDQpgKyBnZW9tX2RlbnNpdHkoYWxwaGEgPSAwLjIpICsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgYWxwaGE6IHBsb3QgdHJhbnNwYXJlbmN5ICgwLTEsIG9wdGlvbmFsKWAgIA0KYCsgdGhlbWVfYncoKWAgIA0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnRoZS5iZWF0bGVzLnNvbmdzJER1cmF0aW9uIDwtIG9yaWdpbmFsLkR1cmF0aW9uICAgICAgICAgICAjIHJlc3RvcmUgdGhlIG9yaWdpbmFsIER1cmF0aW9uICh3aXRoIE5BcykNCmluZGljZXMgPC0gd2hpY2goaXMubmEodGhlLmJlYXRsZXMuc29uZ3MkRHVyYXRpb24pKQ0KbGlicmFyeShycGFydCkNCnNvbmcuZHVyYXRpb24ubW9kZWwgPC0gcnBhcnQoRHVyYXRpb24gfiBZZWFyLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IHRoZS5iZWF0bGVzLnNvbmdzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiYW5vdmEiKQ0KbGlicmFyeShyYXR0bGUpDQpsaWJyYXJ5KHJwYXJ0LnBsb3QpDQpsaWJyYXJ5KFJDb2xvckJyZXdlcikNCmZhbmN5UnBhcnRQbG90KHNvbmcuZHVyYXRpb24ubW9kZWwpICAgICAgICAgICAgICAgICAgICAgICAjIHBsb3QgdGhlIHJlZ3Jlc3Npb24gdHJlZQ0Kc29uZy5kdXJhdGlvbi5wcmVkaWN0ZWQgPC0gDQogIHByZWRpY3Qob2JqZWN0ID0gc29uZy5kdXJhdGlvbi5tb2RlbCwgDQogICAgICAgICAgbmV3ZGF0YSA9IHRoZS5iZWF0bGVzLnNvbmdzW2luZGljZXMsIF0pDQpvcmlnaW5hbC5EdXJhdGlvbi5kZiA8LSBkYXRhLmZyYW1lKER1cmF0aW9uID0gdGhlLmJlYXRsZXMuc29uZ3MkRHVyYXRpb24pDQpvcmlnaW5hbC5EdXJhdGlvbi5kZiRsYmwgPC0gImJlZm9yZSINCiMgZ2dwbG90KG9yaWdpbmFsLkR1cmF0aW9uLmRmLCBhZXMoeCA9IER1cmF0aW9uKSkgKyBnZW9tX2RlbnNpdHkoKQ0KdGhlLmJlYXRsZXMuc29uZ3MkRHVyYXRpb25baW5kaWNlc10gPC0gc29uZy5kdXJhdGlvbi5wcmVkaWN0ZWQNCnN1bW1hcnkodGhlLmJlYXRsZXMuc29uZ3MkRHVyYXRpb24pDQpzdW1tYXJ5KG9yaWdpbmFsLkR1cmF0aW9uLmRmJER1cmF0aW9uKQ0KbW9kaWZpZWQuRHVyYXRpb24uZGYgPC0gZGF0YS5mcmFtZShEdXJhdGlvbiA9IHRoZS5iZWF0bGVzLnNvbmdzJER1cmF0aW9uKQ0KbW9kaWZpZWQuRHVyYXRpb24uZGYkbGJsIDwtICJhZnRlciINCmR1cmF0aW9uLmRmIDwtIHJiaW5kKG9yaWdpbmFsLkR1cmF0aW9uLmRmLCBtb2RpZmllZC5EdXJhdGlvbi5kZikNCmxpYnJhcnkoZ2dwbG90MikNCmdncGxvdChkdXJhdGlvbi5kZiwgYWVzKHggPSBEdXJhdGlvbiwgZmlsbCA9IGxibCkpICsgDQogIGdlb21fZGVuc2l0eShhbHBoYSA9IDAuMykgKyANCiAgdGhlbWVfYncoKQ0KYGBgDQoNCiMjIyMgTnVtZXJpYyB2YXJpYWJsZXMgd2l0aCBhIHNtYWxsIG51bWJlciBvZiBtaXNzaW5nIHZhbHVlcyAoQWx0ZXJuYXRpdmUgMykNCg0KVXNlIG5vbi1OQSB2YWx1ZXMgZnJvbSBhIHN1YnNldCBvZiBTSU1JTEFSIG9ic2VydmF0aW9ucyAoc2ltaWxhciBpbiB0ZXJtcyBvZiBoYXZpbmcgKG5lYXJseSkgdGhlIHNhbWUgdmFsdWVzIG9mIG90aGVyIHJlbGV2YW50IGZlYXR1cmVzKS4gU3VpdGFibGUgd2hlbiB0aGUgbnVtYmVyIG9mIE5BcyBpcyB2ZXJ5IHNtYWxsLCBzaW5jZSB0aGUgcmVwbGFjZW1lbnRzIHR5cGljYWxseSBnbyBvbmUgYnkgb25lLiBUaGUgZXhhbXBsZSBpbiB0aGUgWW91VHViZSB2aWRlbyBJbnRyb2R1Y3Rpb24gdG8gRGF0YSBTY2llbmNlIHdpdGggUiAtIEV4cGxvcmF0b3J5IE1vZGVsaW5nIDIsIGZyb20gMToyNDozOCB0byAxOjI4OjA0LCBsLiA4MDUtODE1LCB3aGVyZSBvbmUgaXMgbG9va2luZyBhdCBzaW1pbGFyIHJlY29yZHMgaW4gdGhlIGRhdGFzZXQsIGluIG9yZGVyIHRvIGZpbmQgYSBzdWl0YWJsZSByZXBsYWNlbWVudCBmb3IgdGhlIG1pc3NpbmcgdmFsdWUuIFRoaXMgYXBwcm9hY2ggaGFzIG5vdCBiZWVuIGRlbW9uc3RyYXRlZCBoZXJlIGJlY2F1c2Ugbm8gc3VpdGFibGUgZmVhdHVyZSBpcyBhdmFpbGFibGUgaW4gdGhlIGRhdGFzZXQgKGEgbnVtZXJpYyBmZWF0dXJlIHdpdGggYSB2ZXJ5IHNtYWxsIG51bWJlciBvZiBOQXMpLCBidXQgdGhlIGFwcHJvYWNoIGlzIHdvcnRoIG1lbnRpb25pbmcuICANCg0KIyMjIyBWYXJpYWJsZXMgd2l0aCBtYW55IG1pc3NpbmcgdmFsdWVzIGFuZC9vciBtaXNzaW5nIHZhbHVlcyB0aGF0IGFyZSBkaWZmaWN1bHQgdG8gcmVwbGFjZQ0KDQpBIG1vcmUgc29waGlzdGljYXRlZCBpbXB1dGF0aW9uIGlzIGFwcGxpZWQgaW4gc3VjaCBjYXNlcyAob3V0IG9mIHNjb3BlIG9mIHRoaXMgY291cnNlKS4gSXQgaXMsIGluIGZhY3QsIHRoZSB0YXNrIG9mIHByZWRpY3RpbmcgKGdvb2Qgc3Vic3RpdHV0ZXMgZm9yKSB0aGUgbWlzc2luZyB2YWx1ZXMuIFRoZSBvdGhlciBvcHRpb24gaXMgdG8gY3JlYXRlIHNvbWUgbmV3IHZhcmlhYmxlcyAoInByb3hpZXMiKSBhbmQgZG8gc29tZSBmZWF0dXJlIGVuZ2luZWVyaW5nLiAgDQoNCjxjZW50ZXI+LS0tLS0tLS0tLS0tLS0tLTwvY2VudGVyPiAgDQogIA0KIyMjIyBBZGRpdGlvbmFsIGRhdGEgY2xlYW5pbmcgdXNpbmcgYSBjdXN0b20gZnVuY3Rpb24NCg0KR2V0IHJpZCBvZiBhbGwgb3RoZXIgTkFzIGFuZCBmYWN0b3JpemUgc3VpdGFibGUgZmVhdHVyZXMgYmVmb3JlIGF0dGVtcHRpbmcgZmVhdHVyZSBzZWxlY3Rpb24gYW5kIGVuZ2luZWVyaW5nOiAgDQpgYGB7cn0NCnNvdXJjZSgiR2V0IHJpZCBvZiBOQXMuUiIpDQp0aGUuYmVhdGxlcy5zb25ncyA8LSBnZXRSaWRPZk5Bcyh0aGUuYmVhdGxlcy5zb25ncykNCnNhdmVSRFModGhlLmJlYXRsZXMuc29uZ3MsICJUaGUgQmVhdGxlcyBzb25ncyBkYXRhc2V0LCB2NS4yLlJEYXRhIikNCnRoZS5iZWF0bGVzLnNvbmdzIDwtIGZhY3Rvcml6ZSh0aGUuYmVhdGxlcy5zb25ncykNCnNhdmVSRFModGhlLmJlYXRsZXMuc29uZ3MsICJUaGUgQmVhdGxlcyBzb25ncyBkYXRhc2V0LCB2NS4zLlJEYXRhIikNCmBgYA0KDQojIyMgRmVhdHVyZSBzZWxlY3Rpb24gYW5kIGVuZ2luZWVyaW5nDQoNCk92ZXJhbGwgcGhpbG9zb3BoeTogcGljayBmZWF0dXJlcyBvbmUgYnkgb25lLCBvciBpbiBzdWl0YWJsZSBwYWlycywgYW5kIHNlZSBob3cgdGhleSBjYW4gYmUgZW5naW5lZXJlZCB0byBpbmNyZWFzZSB0aGUgcXVhbGl0eS9wcmVjaXNpb24gb2YgcHJlZGljdGlvbnMuIEdlbmVyYWxseSwgcGxvdCB2YXJpYWJsZXMgYW5kIHRoZWlyIHZhbHVlcyAod2l0aCBuYS5ybSA9IFRSVUUpIHdoZW5ldmVyIGl0IG1ha2VzIHNlbnNlLiBJdCBnaXZlcyB5b3UgYSBiZXR0ZXIgc2Vuc2Ugb2YgdGhlIHByZWRpY3RpdmUgcG93ZXIgb2YgZWFjaCB2YXJpYWJsZS4gIA0KDQpTcGxpdCB0aGUgZGF0YXNldCBpbnRvIHRyYWluIGFuZCB0ZXN0IHNldHM6ICANCmAjIGluc3RhbGwucGFja2FnZXMoImNhcmV0IilgICANCmBsaWJyYXJ5KGNhcmV0KWAgIA0KYHNldC5zZWVkKDxuPilgICANCmA8dHJhaW4gZGF0YXNldCBpbmRpY2VzPiA8LSAgICAgICAgICAgICAgICAgICAgICAgICAgIyBzdHJhdGlmaWVkIHBhcnRpdGlvbmluZzpgICANCmArIGNyZWF0ZURhdGFQYXJ0aXRpb24oPGRhdGFzZXQ+JDxvdXRwdXQgdmFyaWFibGU+LCAgIyB0aGUgc2FtZSBkaXN0cmlidXRpb24gb2YgdGhlIG91dHB1dCB2YXJpYWJsZSBpbiBib3RoIHNldHNgICANCmArICAgICAgICAgICAgICAgICAgICAgIHAgPSAuODAsICAgICAgICAgICAgICAgICAgICAgIyA4MC8yMCUgb2YgZGF0YSBpbiB0cmFpbi90ZXN0IHNldHNgICANCmArICAgICAgICAgICAgICAgICAgICAgIGxpc3QgPSBGQUxTRSkgICAgICAgICAgICAgICAgIyBkb24ndCBtYWtlIGEgbGlzdCBvZiByZXN1bHRzLCBtYWtlIGEgbWF0cml4YCAgDQpgPHRyYWluIGRhdGFzZXQ+IDwtIDxkYXRhc2V0Pls8dHJhaW4gZGF0YXNldCBpbmRpY2VzPiwgXWAgIA0KYDx0ZXN0IGRhdGFzZXQ+ICA8LSA8ZGF0YXNldD5bLTx0cmFpbiBkYXRhc2V0IGluZGljZXM+LCBdYCAgDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShjYXJldCkNCnNldC5zZWVkKDQ0NCkNCnRyYWluLmRhdGEuaW5kaWNlcyA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHRoZS5iZWF0bGVzLnNvbmdzJFRvcC41MC5CaWxsYm9hcmQsIHAgPSAwLjgwLCBsaXN0ID0gRkFMU0UpDQp0cmFpbi5kYXRhIDwtIHRoZS5iZWF0bGVzLnNvbmdzW3RyYWluLmRhdGEuaW5kaWNlcywgXQ0KdGVzdC5kYXRhIDwtIHRoZS5iZWF0bGVzLnNvbmdzWy10cmFpbi5kYXRhLmluZGljZXMsIF0NCmBgYA0KDQpFeGFtaW5lIHRoZSBwcmVkaWN0aXZlIHBvd2VyIG9mIHZhcmlhYmxlcyBmcm9tIHRoZSBkYXRhIHNldCBieSBtZWFucyBvZiB0YWJsZXMsIGZyZXF1ZW5jaWVzIGFuZCBwcm9wb3J0aW9ucy4gRm9yIGEgY2F0ZWdvcmljYWwgcHJlZGljdG9yLCBjaGVjayBpdHMgZnJlcXVlbmNpZXMgYW5kIHByb3BvcnRpb25zIGluIHRoZSBkYXRhc2V0Og0KYHN1bW1hcnkoPGRhdGFzZXQ+JDxwcmVkaWN0b3I+KSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgZnJlcXVlbmNpZXNgICANCmByb3VuZChzdW1tYXJ5KDxkYXRhc2V0PiQ8cHJlZGljdG9yPikgLyBucm93KDxkYXRhc2V0PiksIGRpZ2l0cyA9IDMpICAgICAjIHByb3BvcnRpb25zYCAgDQpgYGB7cn0NCnN1bW1hcnkodHJhaW4uZGF0YSRTaW5nbGUuY2VydGlmaWNhdGlvbikNCnJvdW5kKHN1bW1hcnkodHJhaW4uZGF0YSRTaW5nbGUuY2VydGlmaWNhdGlvbikgLyBucm93KHRyYWluLmRhdGEpLCBkaWdpdHMgPSAzKQ0KYGBgDQoNClRoZW4gZXhhbWluZSB0aGUgZnJlcXVlbmNpZXMgYW5kIHRoZSBwcm9wb3J0aW9ucyBvZiB0aGUgb3V0cHV0IHZhcmlhYmxlIHZhbHVlcyAoY2xhc3NlcykgYmFzZWQgb24gdGhlIHByZWRpY3RvciB2YWx1ZXM6ICANCmB4dGFicyh+PHByZWRpY3Rvcj4gKyA8b3V0cHV0IHZhcmlhYmxlPiwgZGF0YSA9IDxkYXRhc2V0PikgICAgICAgICAgICAgICAjIGZyZXF1ZW5jaWVzYCAgDQpgcHJvcC50YWJsZSh4dGFicyh+PHByZWRpY3Rvcj4gKyA8b3V0cHV0IHZhcmlhYmxlPiwgZGF0YSA9IDxkYXRhc2V0PiksICAgIyBwcm9wb3J0aW9uc2AgIA0KYCsgICAgICAgICAgICAgICAgbWFyZ2luID0gMSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgYnkgcm93YCAgDQpgYGB7cn0NCnh0YWJzKH5TaW5nbGUuY2VydGlmaWNhdGlvbiArIFRvcC41MC5CaWxsYm9hcmQsIGRhdGEgPSB0cmFpbi5kYXRhKQ0KcHJvcC50YWJsZSh4dGFicyh+U2luZ2xlLmNlcnRpZmljYXRpb24gKyBUb3AuNTAuQmlsbGJvYXJkLCBkYXRhID0gdHJhaW4uZGF0YSksIG1hcmdpbiA9IDEpDQpgYGANCg0KRm9yIGEgbnVtZXJpYyBwcmVkaWN0b3Igd2l0aCBhIHNtYWxsIG51bWJlciBvZiB2YWx1ZXMsIGZpcnN0IGNvbnZlcnQgaXQgaW50byBhIGZhY3RvcjogIA0KYDxkYXRhc2V0PiQ8cHJlZGljdG9yPiA8LSBmYWN0b3IoPGRhdGFzZXQ+JDxwcmVkaWN0b3I+LGAgIA0KYCsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzID0gYyg8bjE+LCA8bjI+LCAuLi4pLGAgIA0KYCsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGFiZWxzID0gYygiPGwxPiIsICI8bDI+IiwgLi4uKSlgICANCkhvd2V2ZXIsIG1ha2Ugc3VyZSB0byBrZWVwIHRoZSBvcmlnaW5hbCAobnVtZXJpYykgdmFsdWVzIGZyb20gdGhlIGludGVncmF0ZWQgZGF0YXNldCAoaW5jbHVkaW5nIGJvdGggYW5kIHRlc3QgZGF0YXNldHMpIGZvciBwb3NzaWJsZSBsYXRlciB1c2U6ICANCmA8b3JpZ2luYWwgbnVtZXJpYyBwcmVkaWN0b3I+IDwtIDxpbnRlZ3JhdGVkIGRhdGFzZXQ+JDxwcmVkaWN0b3I+YCAgDQpgYGB7cn0NCnVuaXF1ZSh0cmFpbi5kYXRhJFdlZWtzLmF0Lk5vMS5pbi5VSy5UaGUuR3VhcmRpYW4pDQpvcmlnaW5hbC5XZWVrcy5hdC5ObzEuaW4uVUsuVGhlLkd1YXJkaWFuIDwtICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMga2VlcCBpdCBmb3IgbGF0ZXINCiAgdGhlLmJlYXRsZXMuc29uZ3MkV2Vla3MuYXQuTm8xLmluLlVLLlRoZS5HdWFyZGlhbg0KdHJhaW4uZGF0YSRXZWVrcy5hdC5ObzEuaW4uVUsuVGhlLkd1YXJkaWFuIDwtIA0KICBmYWN0b3IodHJhaW4uZGF0YSRXZWVrcy5hdC5ObzEuaW4uVUsuVGhlLkd1YXJkaWFuLCANCiAgICAgICAgIGxldmVscyA9IGMoMCwgMiwgMywgNCwgNSwgNiwgNyksIA0KICAgICAgICAgbGFiZWxzID0gYygiMCIsICIyIiwgIjMiLCAiNCIsICI1IiwgIjYiLCAiNyIpKQ0KYGBgDQoNClRoZW4gZXhhbWluZSBmcmVxdWVuY2llcyBhbmQgcHJvcG9ydGlvbnMgYXMgYWJvdmUsIG9yIHBsb3QgdGhlIG91dHB1dCB2YXJpYWJsZSBhZ2FpbnN0IHRoZSBwcmVkaWN0b3I6ICANCmA8Z2c+IDwtIGdncGxvdCg8ZGF0YXNldD4sIGFlcyh4ID0gPHByZWRpY3RvciBuYW1lPiwgZmlsbCA9IDxvdXRwdXQgdmFyaWFibGUgbmFtZT4pKSArYCAgDQpgKyBnZW9tX2Jhcihwb3NpdGlvbiA9ICJkb2RnZSIsIHdpZHRoID0gPGJpbiB3aWR0aD4pICsgICAgICAgIyAiZG9kZ2UiOiBiYXJncmFwaCwgPGJpbiB3aWR0aD46IDAuMi0wLjRgICANCmArIGxhYnMoeCA9ICI8eC1sYWJlbD4iLCB5ID0gIjx5LWxhYmVsPiIsIHRpdGxlID0gIjx0aXRsZT4iKSArYCAgDQpgKyB0aGVtZV9idygpYCAgDQpgPGdnPmAgIA0KYDxnZj4gPC0gPGdnPiArIGZhY2V0X3dyYXAofjxhbm90aGVyIHByZWRpY3RvciBuYW1lPikgICAgICAgICMgZXhhbWluZSAyIHByZWRpY3RvcnMgdG9nZXRoZXJgICANCmA8Z2Y+YCAgDQpgYGB7cn0NCmdnMSA8LSBnZ3Bsb3QoZGF0YSA9IHRyYWluLmRhdGEsIGFlcyh4ID0gV2Vla3MuYXQuTm8xLmluLlVLLlRoZS5HdWFyZGlhbiwgZmlsbCA9IFRvcC41MC5CaWxsYm9hcmQpKSArDQogIGdlb21fYmFyKHBvc2l0aW9uID0gImRvZGdlIiwgd2lkdGggPSAwLjYpICsNCiAgeWxhYigiTnVtYmVyIG9mIEJpbGxib2FyZCBUb3AgNTAgc29uZ3MiKSArIHhsYWIoIldlZWtzIGFuZCBOby4xIGluIFVLIChUaGUgR3VhcmRpYW4pIikgKw0KICB0aGVtZV9idygpDQpnZzENCmdnMiA8LSBnZ3Bsb3QoZGF0YSA9IHRyYWluLmRhdGEsIGFlcyh4ID0gWWVhciwgZmlsbCA9IFRvcC41MC5CaWxsYm9hcmQpKSArDQogIGdlb21fYmFyKHBvc2l0aW9uID0gImRvZGdlIiwgd2lkdGggPSAwLjYpICsNCiAgeWxhYigiTnVtYmVyIG9mIEJpbGxib2FyZCBUb3AgNTAgc29uZ3MiKSArIHhsYWIoIlllYXIiKSArDQogIHRoZW1lX2J3KCkNCmdnMg0KZ2czIDwtIGdncGxvdChkYXRhID0gdHJhaW4uZGF0YSwgYWVzKHggPSBTaW5nbGUuY2VydGlmaWNhdGlvbiwgZmlsbCA9IFRvcC41MC5CaWxsYm9hcmQpKSArDQogIGdlb21fYmFyKHBvc2l0aW9uID0gImRvZGdlIiwgd2lkdGggPSAwLjYpICsNCiAgbGFicyh5ID0gIk51bWJlciBvZiBCaWxsYm9hcmQgVG9wIDUwIHNvbmdzIiwgeCA9ICJTaW5nbGUgY2VydGlmaWNhdGlvbiIsIHRpdGxlID0gIkNlcnRpZmllZCIpICsNCiAgdGhlbWVfYncoKQ0KZ2czDQpnZzQgPC0gZ2czICsgZmFjZXRfd3JhcCh+WWVhcikNCmdnNA0KYGBgDQoNCiMjIyBGZWF0dXJlIGVuZ2luZWVyaW5nDQoNCldoZW4gY3JlYXRpbmcgbmV3IGZlYXR1cmVzIChhdHRyaWJ1dGVzKSB0byBiZSB1c2VkIGZvciBwcmVkaWN0aW9uIHB1cnBvc2VzLCB3ZSBzaG91bGQgbWVyZ2UgdGhlIHRyYWluaW5nIGFuZCB0aGUgdGVzdCBzZXRzIGFuZCBkZXZlbG9wIG5ldyBmZWF0dXJlcyBvbiB0aGUgbWVyZ2VkIGRhdGEuIEJlZm9yZSB0aGF0LCB3ZSBuZWVkIHRvIGFzc3VyZSB0aGF0IHRoZSB0cmFpbmluZyBhbmQgdGhlIHRlc3Qgc2V0cyBoYXZlIGV4YWN0bHkgdGhlIHNhbWUgc3RydWN0dXJlLiAgDQoNCkluIHRoZSB0ZXN0IHNldCwgZG8gYWxsIHRoZSBtb2RpZmljYXRpb25zIHRoYXQgaGF2ZSBiZWVuIGRvbmUgaW4gdGhlIHRyYWluIHNldDogIA0KYGBge3J9DQp1bmlxdWUodGVzdC5kYXRhJFdlZWtzLmF0Lk5vMS5pbi5VSy5UaGUuR3VhcmRpYW4pDQp0ZXN0LmRhdGEkV2Vla3MuYXQuTm8xLmluLlVLLlRoZS5HdWFyZGlhbiA8LSANCiAgZmFjdG9yKHRlc3QuZGF0YSRXZWVrcy5hdC5ObzEuaW4uVUsuVGhlLkd1YXJkaWFuLCANCiAgICAgICAgIGxldmVscyA9IGMoMCwgMiwgMywgNCwgNSwgNiwgNyksIA0KICAgICAgICAgbGFiZWxzID0gYygiMCIsICIyIiwgIjMiLCAiNCIsICI1IiwgIjYiLCAiNyIpKQ0KYGBgDQoNCk1lcmdlIHRoZSB0cmFpbiBhbmQgdGVzdCBkYXRhc2V0cyBpbnRvIG9uZSBmb3IgY3JlYXRpbmcgbmV3IGZlYXR1cmVzOiAgDQpgPGFkYXB0ZWQgZGF0YXNldD4gPC0gcmJpbmQoPGFkYXB0ZWQgdHJhaW4gZGF0YXNldD4sIDxhZGFwdGVkIHRlc3QgZGF0YXNldD4pYCAgDQpgc2F2ZVJEUyg8YWRhcHRlZCBkYXRhc2V0PiwgIjxSRGF0YSBmaWxlbmFtZT4iKWAgIA0KYGBge3J9DQp0aGUuYmVhdGxlcy5zb25ncyA8LSByYmluZCh0cmFpbi5kYXRhLCB0ZXN0LmRhdGEpDQpzYXZlUkRTKHRoZS5iZWF0bGVzLnNvbmdzLCAiVGhlIEJlYXRsZXMgc29uZ3MgZGF0YXNldCwgdjUuNC5SRGF0YSIpDQpgYGANCg0KIyMjIyBDcmVhdGluZyBhIHByb3h5IHZhcmlhYmxlIGZvciBhIGNlcnRhaW4gZmVhdHVyZQ0KDQpJdCdzIGEgbmV3IHZhcmlhYmxlIHRoYXQgYXBwcm94aW1hdGVzIGFuIG9yaWdpbmFsIG9uZSwgb3IgaXMgYSBnb29kIHJlcGxhY2VtZW50IGZvciB0aGUgb3JpZ2luYWwgb25lLiAgDQoNCkhvdyBtYW55IFRvcCA1MCBCaWxsYm9hcmQgc29uZ3MgcGVyZm9ybWVkIGJ5IHRoZSBCZWF0bGVzIGFyZSBjb3ZlcnMgb2Ygb3RoZXIgYXV0aG9ycycgc29uZ3M/ICANCmBgYHtyfQ0Kd2hpY2godGhlLmJlYXRsZXMuc29uZ3MkQ292ZXIgPT0gIlllcyIpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgaG93IG1hbnkgY292ZXIgc29uZ3MgKGFsbCkNCnRvcC41MC5iYi5pbmRpY2VzIDwtIHdoaWNoKHRoZS5iZWF0bGVzLnNvbmdzJFRvcC41MC5CaWxsYm9hcmQgPT0gIlllcyIpICAjIGFsbCBCaWxsYm9hcmQgVG9wIDUwIHNvbmdzDQp0b3AuNTAuYmIuaW5kaWNlcw0Kd2hpY2godGhlLmJlYXRsZXMuc29uZ3MkQ292ZXJbdG9wLjUwLmJiLmluZGljZXNdID09ICJZZXMiKSAgIyBob3cgbWFueSBjb3ZlciBzb25ncyBvbiBCaWxsYm9hcmQgVG9wIDUwDQpgYGANCg0KVmVyeSBmZXcgVG9wIDUwIEJpbGxib2FyZCBzb25ncyBwZXJmb3JtZWQgYnkgdGhlIEJlYXRsZXMgYXJlIGNvdmVycyBvZiBvdGhlciBhdXRob3JzJyBzb25ncywgc28gdGFrZSBhIGNsb3NlciBsb29rIGF0IHRoZSBTb25nd3JpdGVyIGZlYXR1cmUuIEhvdyBtYW55IGRpZmZlcmVudCBTb25nd3JpdGVyIHZhbHVlcyBhcmUgdGhlcmU/ICANCmBgYHtyfQ0KdW5pcXVlKHRoZS5iZWF0bGVzLnNvbmdzJFNvbmd3cml0ZXIpDQpgYGANCg0KSG93IG1hbnkgc29uZ3MgYXJlIGNvdmVycz8gIA0KYGBge3J9DQpsZW5ndGgod2hpY2godGhlLmJlYXRsZXMuc29uZ3MkQ292ZXIgPT0gIlllcyIpKQ0KYGBgDQoNCkl0J3MgYSBjb25zaWRlcmFibGUgZGlmZmVyZW5jZSAoODIgdnMuIDcxKSwgc28gaXQncyBiZXR0ZXIgdG8gY3JlYXRlIGEgcHJveHkgZm9yIHNvbmcgYXV0aG9yc2hpcC4gIA0KSG93IG1hbnkgc29uZ3MgaGF2ZSwgc2F5LCBKb2huIExlbm5vbiBpbiB0aGUgbGlzdCBvZiBhdXRob3JzPyAgDQpgZ3JlcGwoPHN1YnN0cmluZz4sIDxzdHJpbmc+KSAgICAgICAgICAgICAgIyBUUlVFIGlmIDxzdHJpbmc+IGNvbnRhaW5zIDxzdWJzdHJpbmc+YCAgDQpgPGluZGljZXM+IDwtICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBpbmRpY2VzIG9mIDxjaGFyYWN0ZXIgdmFyaWFibGU+IGNvbnRhaW5pbmcgPHN1YnN0cmluZz5gICANCmArIGdyZXAoPHN1YnN0cmluZz4sYCAgDQpgKyAgICAgIDxkYXRhZnJhbWU+JDxjaGFyYWN0ZXIgdmFyaWFibGU+KWAgIA0KYGBge3J9DQpncmVwbCgiZWF0IiwgIlRoZSBCZWF0bGVzIikNCmkubGVubm9uIDwtIGdyZXAoIkxlbm5vbiIsIHRoZS5iZWF0bGVzLnNvbmdzJFNvbmd3cml0ZXIpDQppLm1jY2FydG5leSA8LSBncmVwKCJNY0NhcnRuZXkiLCB0aGUuYmVhdGxlcy5zb25ncyRTb25nd3JpdGVyKQ0KaS5oYXJyaXNvbiA8LSBncmVwKCJIYXJyaXNvbiIsIHRoZS5iZWF0bGVzLnNvbmdzJFNvbmd3cml0ZXIpDQppLnN0YXJrZXkgPC0gZ3JlcCgiU3RhcmtleSIsIHRoZS5iZWF0bGVzLnNvbmdzJFNvbmd3cml0ZXIpDQoNCnNvdXJjZSgiU29uZyBhdXRob3IgcHJveGllcy5SIikNCmF1dGhvcnMgPC0gZ2V0U29uZ0F1dGhvclByb3hpZXMoaS5sZW5ub24sIGkubWNjYXJ0bmV5LCBpLmhhcnJpc29uLCBpLnN0YXJrZXkpDQpsZW5ub24uc29uZ3MgPC0gYXV0aG9yc1tbMV1dDQptY2NhcnRuZXkuc29uZ3MgPC0gYXV0aG9yc1tbMl1dDQpoYXJyaXNvbi5zb25ncyA8LSBhdXRob3JzW1szXV0NCnN0YXJrZXkuc29uZ3MgPC0gYXV0aG9yc1tbNF1dDQpsZW5ub24ubWNjYXJ0bmV5LnNvbmdzIDwtIGF1dGhvcnNbWzVdXQ0KbWNjYXJ0bmV5Lmxlbm5vbi5zb25ncyA8LSBhdXRob3JzW1s2XV0NCmxlbm5vbi5tY2NhcnRuZXkuaGFycmlzb24uc3RhcmtleS5zb25ncyA8LSBhdXRob3JzW1s3XV0NCmBgYA0KDQpDcmVhdGUgcHJveHkgdmFyaWFibGVzIGZvciBkaWZmZXJlbnQgYXV0aG9yczogIA0KYGBge3J9DQp0aGUuYmVhdGxlcy5zb25ncyRBdXRob3IgPC0gIk90aGVyIg0KdGhlLmJlYXRsZXMuc29uZ3MkQXV0aG9yW2xlbm5vbi5zb25nc10gPC0gIkxlbm5vbiINCnRoZS5iZWF0bGVzLnNvbmdzJEF1dGhvclttY2NhcnRuZXkuc29uZ3NdIDwtICJNY0NhcnRuZXkiDQp0aGUuYmVhdGxlcy5zb25ncyRBdXRob3JbaGFycmlzb24uc29uZ3NdIDwtICJIYXJyaXNvbiINCnRoZS5iZWF0bGVzLnNvbmdzJEF1dGhvcltzdGFya2V5LnNvbmdzXSA8LSAiU3RhcmtleSINCnRoZS5iZWF0bGVzLnNvbmdzJEF1dGhvcltsZW5ub24ubWNjYXJ0bmV5LnNvbmdzXSA8LSAiTGVubm9uL01jQ2FydG5leSINCnRoZS5iZWF0bGVzLnNvbmdzJEF1dGhvclttY2NhcnRuZXkubGVubm9uLnNvbmdzXSA8LSAiTWNDYXJ0bmV5L0xlbm5vbiINCnRoZS5iZWF0bGVzLnNvbmdzJEF1dGhvcltsZW5ub24ubWNjYXJ0bmV5LmhhcnJpc29uLnN0YXJrZXkuc29uZ3NdIDwtICJMZW5ub24vTWNDYXJ0bmV5L0hhcnJpc29uL1N0YXJrZXkiDQpgYGANCg0KQ29udmVydCB0aGUgbmV3IHByb3h5IHZyaWFibGUgaW50byBhIGZhY3RvcjogIA0KYGBge3J9DQp0aGUuYmVhdGxlcy5zb25ncyRBdXRob3IgPC0gZmFjdG9yKHRoZS5iZWF0bGVzLnNvbmdzJEF1dGhvcikNCnN1bW1hcnkodGhlLmJlYXRsZXMuc29uZ3MkQXV0aG9yKQ0KYGBgDQoNCkV4YW1pbmUgdGhlIHByZWRpY3RpdmUgcG93ZXIgb2YgdGhlIG5ld2x5IGNyZWF0ZWQgcHJveHkgdmFyaWFibGU6ICANCmBnZ3Bsb3QoZGF0YSA9IDxkYXRhc2V0PlsxOjx0cmFpbmluZyBkYXRhIGxlbmd0aD4sIF0sICAjIG9ubHkgdGhlIHRyYWluaW5nIGRhdGEgZnJvbSB0aGUgbWVyZ2VkIGRhdGFzZXQhYCAgDQpgKyAgICAgIGFlcyh4ID0gPHByb3h5IHZhcmlhYmxlIG5hbWU+LGAgIA0KYCsgICAgICAgICAgZmlsbCA9IDxvdXRwdXQgdmFyaWFibGUgbmFtZT4pKSArYCAgDQpgKyBnZW9tX2Jhcihwb3NpdGlvbiA9ICJkb2RnZSIpICsgICAgICAgICAgICAgICAgICAgICAgIyBiYXIgZ3JhcGhgICANCmArIHRoZW1lX2J3KClgICANCmBgYHtyfQ0KZ2dwbG90KHRoZS5iZWF0bGVzLnNvbmdzWzE6MjQ5LCBdLCANCiAgICAgICBhZXMoeCA9IEF1dGhvciwgZmlsbCA9IFRvcC41MC5CaWxsYm9hcmQpKSArDQogIGdlb21fYmFyKHBvc2l0aW9uID0gImRvZGdlIikgKyANCiAgdGhlbWVfYncoKQ0KYGBgDQoNCiMjIyBDcmVhdGluZyBhIG5ldyB2YXJpYWJsZSAvIG5ldyB2YXJpYWJsZXMNCg0KSWRlYXMgYWJvdXQgQ2hhcnQucG9zaXRpb24uVUsuV2lraXBlZGlhLCBDaGFydC5wb3NpdGlvbi5VUy5XaWtpcGVkaWEsIEhpZ2hlc3QucG9zaXRpb24uVGhlLkd1YXJkaWFuLCBXZWVrcy5vbi5jaGFydC5pbi5VSy5UaGUuR3VhcmRpYW4gYW5kIFdlZWtzLmF0Lk5vMS5UaGUuR3VhcmRpYW46ICANCmAjIE5vcm1hbGl6ZWQgY2hhcnQgcG9zaXRpb25gICANCmArIGlmICg8Y2hhcnQgcG9zaXRpb24+ID09IDApIHtgICANCmArICAgPG5vcm1hbGl6ZWQgY2hhcnQgcG9zaXRpb24+IDwtIDBgICANCmArIH1gICANCmArIGlmICg8Y2hhcnQgcG9zaXRpb24+IGluICgxOjxtYXggdmFsdWU+KSkge2AgIA0KYCsgICA8bm9ybWFsaXplZCBjaGFydCBwb3NpdGlvbj4gPC0gKDxtYXggdmFsdWU+IC0gPGNoYXJ0IHBvc2l0aW9uPiArIDEpIC8gPG1heCB2YWx1ZT5gICANCmArIH1gICANCmAjIE5vcm1hbGl6ZWQgd2Vla3Mtb24tY2hhcnRgICANCmArIDxub3JtYWxpemVkIHdlZWtzLW9uLWNoYXJ0PiA8LSA8d2Vla3Mtb24tY2hhcnQ+IC8gPG1heCB2YWx1ZT5gICANCmAjIE5vcm1hbGl6ZWQgd2Vla3MtYXQtTm8uMWAgIA0KYCAgPG5vcm1hbGl6ZWQgd2Vla3MtYXQtTm8uMT4gPC0gPHdlZWtzLWF0LU5vLjE+IC8gPG1heCB2YWx1ZT5gICANCmAjIElkZWE6IGNoYXJ0IHByZXNlbmNlYCAgDQpgIyBDaGFydCBwcmVzZW5jZSBpbiBVSy9VUzpgICANCmArIDxjaGFydCBwcmVzZW5jZT4gPC1gICANCmArICAgKDxhPiAqIDxub3JtYWxpemVkIGNoYXJ0IHBvc2l0aW9uPiArYCAgDQpgKyAgICA8Yj4gKiA8bm9ybWFsaXplZCB3ZWVrcy1vbi1jaGFydD4gK2AgIA0KYCsgICAgPGM+ICogbm9ybWFsaXplZCB3ZWVrcy1hdC1uby0xKSAvYCAgDQpgKyAgICg8YT4gKyA8Yj4gKyA8Yz4pYCAgDQpgIyBDaGFydCBwcmVzZW5jZSBpbiBib3RoIFVLIGFuZCBVUyAob3ZlcmFsbCk6ICgoYSAqIGNoLnByLjEgKyBiICogY2gucHIuMikgLyAoYSArIGIpKWAgIA0KYCsgPGNoYXJ0IHByZXNlbmNlPiA8LWAgIA0KYCsgICg8YT4gKiA8Y2hhcnQgcHJlc2VuY2UgaW4gVUs+ICtgICANCmArICAgPGI+ICogPGNoYXJ0IHByZXNlbmNlIGluIFVTPikgL2AgIA0KYCsgICg8YT4gKyA8Yj4pYCAgDQoNCk5ldyB2YXJpYWJsZXM6IENoYXJ0LnByZXNlbmNlLlVLLCBDaGFydC5wcmVzZW5jZS5VUywgQ2hhcnQucHJlc2VuY2UuVUsuYW5kLlVTDQpgYGB7cn0NCnRoZS5iZWF0bGVzLnNvbmdzJFdlZWtzLmF0Lk5vMS5pbi5VSy5UaGUuR3VhcmRpYW4gPC0gICAgICAgICAgICAjIHJlc3RvcmUgdGhpcyBhcyBhIG51bWVyaWMNCiAgb3JpZ2luYWwuV2Vla3MuYXQuTm8xLmluLlVLLlRoZS5HdWFyZGlhbg0Kc2F2ZVJEUyh0aGUuYmVhdGxlcy5zb25ncywgIlRoZSBCZWF0bGVzIHNvbmdzIGRhdGFzZXQsIHY1LjUuUkRhdGEiKQ0KDQpzb3VyY2UoIkNoYXJ0IHByZXNlbmNlLlIiKQ0KY2gucG9zLlVLLm5vcm0gPC0gZ2V0Tm9ybWFsaXplZENoYXJ0UG9zaXRpb24odGhlLmJlYXRsZXMuc29uZ3MkQ2hhcnQucG9zaXRpb24uVUsuV2lraXBlZGlhKQ0Kd2Vla3Mub24uY2hhcnQuVUsubm9ybSA8LSANCiAgZ2V0Tm9ybWFsaXplZFdlZWtzT25DaGFydCh0aGUuYmVhdGxlcy5zb25ncyRXZWVrcy5vbi5jaGFydC5pbi5VSy5UaGUuR3VhcmRpYW4pDQp3ZWVrcy5hdC5ObzEuVUsubm9ybSA8LSANCiAgZ2V0Tm9ybWFsaXplZFdlZWtzQXRObzEodGhlLmJlYXRsZXMuc29uZ3MkV2Vla3MuYXQuTm8xLmluLlVLLlRoZS5HdWFyZGlhbikNCmNoLnBvcy5VUy5ub3JtIDwtICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHRoYXQncyBhbGwgZm9yIFVTIGluIHRoZSBkYXRhc2V0DQogIGdldE5vcm1hbGl6ZWRDaGFydFBvc2l0aW9uKHRoZS5iZWF0bGVzLnNvbmdzJENoYXJ0LnBvc2l0aW9uLlVTLldpa2lwZWRpYSkNCnRoZS5iZWF0bGVzLnNvbmdzJENoYXJ0LnByZXNlbmNlLlVLIDwtIA0KICBnZXRDaGFydFByZXNlbmNlKGNoLnBvcy5VSy5ub3JtLCB3ZWVrcy5vbi5jaGFydC5VSy5ub3JtLCB3ZWVrcy5hdC5ObzEuVUsubm9ybSwgDQogICAgICAgICAgICAgICAgICAgMSwgMSwgMSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBleHBlcmltZW50IHdpdGggZGlmZmVyZW50IHdlaWdodHMNCnRoZS5iZWF0bGVzLnNvbmdzJENoYXJ0LnByZXNlbmNlLlVTIDwtIA0KICBnZXRDaGFydFByZXNlbmNlKGNoLnBvcy5VUy5ub3JtLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgdGhhdCdzIGFsbCBmb3IgVVMgaW4gdGhlIGRhdGFzZXQNCiAgICAgICAgICAgICAgICAgICByZXAoMCwgbnJvdyh0aGUuYmVhdGxlcy5zb25ncykpLCAgICAgICAgICAgICAjIGR1bW15LCBmb3IgdGhlIHNha2Ugb2YgZnVuY3Rpb24gZm9ybWF0DQogICAgICAgICAgICAgICAgICAgcmVwKDAsIG5yb3codGhlLmJlYXRsZXMuc29uZ3MpKSwgICAgICAgICAgICAgIyBkdW1teSwgZm9yIHRoZSBzYWtlIG9mIGZ1bmN0aW9uIGZvcm1hdA0KICAgICAgICAgICAgICAgICAgIDEsIDAsIDApICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgMCB3ZWlnaHRzIGZvciBtaXNzaW5nIGRhdGENCnRoZS5iZWF0bGVzLnNvbmdzJENoYXJ0LnByZXNlbmNlLlVLLmFuZC5VUyA8LSANCiAgZ2V0Q2hhcnRQcmVzZW5jZU92ZXJhbGwodGhlLmJlYXRsZXMuc29uZ3MkQ2hhcnQucHJlc2VuY2UuVUssIA0KICAgICAgICAgICAgICAgICAgICAgICAgICB0aGUuYmVhdGxlcy5zb25ncyRDaGFydC5wcmVzZW5jZS5VUywgDQogICAgICAgICAgICAgICAgICAgICAgICAgIDEsIDEpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBleHBlcmltZW50IHdpdGggZGlmZmVyZW50IHdlaWdodHMNCmBgYA0KDQpIb3cgcmVsZXZhbnQgYXJlIHRoZSBuZXcgdmFyaWFibGVzIGZvciBwcmVkaWN0aW5nIEJpbGxib2FyZCBUb3AgNTAgQmVhdGxlcyBzb25ncz8gIA0KDQpEaXNjcmV0aXplIGFuZCBmYWN0b3JpemUgdGhlbSBmaXJzdDogIA0KYDxkYXRhc2V0PiQ8bmV3IGZhY3RvciBmZWF0dXJlPiA8LWAgIA0KYCsgY3V0KDxkYXRhc2V0PiQ8bnVtZXJpYyBmZWF0dXJlPixgICANCmArICAgIGJyZWFrcyA9IDxuPiwgICAgICAgICAgICAgICAgICAgIyBudW1iZXIgb2YgaW50ZXJ2YWxzIHRvIGN1dCB0aGUgPG51bWVyaWMgZmVhdHVyZT4gaW50b2AgIA0KYCsgICAgbGFiZWxzID0gYygiPGxhYiAxPiIsICI8bGFiIDI+IiwgLi4uLCAiPGxhYiBuPiIpKSAgICAgICAjIGZhY3RvciBsYWJlbHNgICANCmBgYHtyfQ0KdGhlLmJlYXRsZXMuc29uZ3MkSHlwZS5VSyA8LSANCiAgY3V0KHRoZS5iZWF0bGVzLnNvbmdzJENoYXJ0LnByZXNlbmNlLlVLLCANCiAgICAgIGJyZWFrcyA9IDUsIA0KICAgICAgbGFiZWxzID0gYygiVmVyeSBMb3ciLCAiTG93IiwgIk5ldXRyYWwiLCAiSGlnaCIsICJWZXJ5IEhpZ2giKSkNCnRoZS5iZWF0bGVzLnNvbmdzJEh5cGUuVVMgPC0gDQogIGN1dCh0aGUuYmVhdGxlcy5zb25ncyRDaGFydC5wcmVzZW5jZS5VUywgDQogICAgICBicmVha3MgPSA1LCANCiAgICAgIGxhYmVscyA9IGMoIlZlcnkgTG93IiwgIkxvdyIsICJOZXV0cmFsIiwgIkhpZ2giLCAiVmVyeSBIaWdoIikpDQp0aGUuYmVhdGxlcy5zb25ncyRIeXBlLlVLLmFuZC5VUyA8LSANCiAgY3V0KHRoZS5iZWF0bGVzLnNvbmdzJENoYXJ0LnByZXNlbmNlLlVLLmFuZC5VUywgDQogICAgICBicmVha3MgPSA1LCANCiAgICAgIGxhYmVscyA9IGMoIlZlcnkgTG93IiwgIkxvdyIsICJOZXV0cmFsIiwgIkhpZ2giLCAiVmVyeSBIaWdoIikpDQpgYGANCg0KQW5kIG5vdyBwbG90IHRoZW06DQpgYGB7cn0NCmdncGxvdCh0aGUuYmVhdGxlcy5zb25nc1sxOjI0OSwgXSwgDQogICAgICAgYWVzKHggPSBIeXBlLlVLLCBmaWxsID0gVG9wLjUwLkJpbGxib2FyZCkpICsNCiAgZ2VvbV9iYXIocG9zaXRpb24gPSAiZG9kZ2UiKSArIA0KICB0aGVtZV9idygpDQpnZ3Bsb3QodGhlLmJlYXRsZXMuc29uZ3NbMToyNDksIF0sIA0KICAgICAgIGFlcyh4ID0gSHlwZS5VUywgZmlsbCA9IFRvcC41MC5CaWxsYm9hcmQpKSArDQogIGdlb21fYmFyKHBvc2l0aW9uID0gImRvZGdlIikgKyANCiAgdGhlbWVfYncoKQ0KZ2dwbG90KHRoZS5iZWF0bGVzLnNvbmdzWzE6MjQ5LCBdLCANCiAgICAgICBhZXMoeCA9IEh5cGUuVUsuYW5kLlVTLCBmaWxsID0gVG9wLjUwLkJpbGxib2FyZCkpICsNCiAgZ2VvbV9iYXIocG9zaXRpb24gPSAiZG9kZ2UiKSArIA0KICB0aGVtZV9idygpDQpgYGANCg0KIyMgUmVzb3VyY2VzLCByZWFkaW5ncywgcmVmZXJlbmNlcw0KDQpDbGFzc2lmaWNhdGlvbiB2cy4gcmVncmVzc2lvbjogIA0KPGh0dHA6Ly93d3cuc2ltYWZvcmUuY29tL2Jsb2cvYmlkLzYyNDgyLzItbWFpbi1kaWZmZXJlbmNlcy1iZXR3ZWVuLWNsYXNzaWZpY2F0aW9uLWFuZC1yZWdyZXNzaW9uLXRyZWVzPg0K