Introduction
This project serves to analyze a large corpus of text documents to discover the structure in the data and how words are put together. It will perform cleaning and exploratory analysis of the data, then building and sampling from a predictive text model. The final outcome is an application that can take an input text and predict the next word.
The data used for exploration and model training can be found at this link: https://d396qusza40orc.cloudfront.net/dsscapstone/dataset/Coursera-SwiftKey.zip
Understanding the data
This analysis starts by understanding the data files that need to be worked with. For the purpose of the exercise the files have been downloaded and stored in a subfolder called “data”, sitting inside the same folder as this markdown file.
First, we store the names of all the files sitting in the data folder in a list, then we run a function over each file that extracts file name, file size(MB), the number of lines, the longest line and its length, and the number of words for each file. We then print that in a table format and produce some plots.
setwd(env$dirData)
# store filenames in environment list
env$fileList <- list.files(recursive = T, pattern = "*.txt")
fileInfo = list()
fileInfo$info <- lapply(paste(env$fileList, sep = "/"), function(f) {
fileSize <- file.info(f)[1] / 1024 / 1024
con <- file(f, open = "r")
lines <- readLines(con)
nchars <- lapply(lines, nchar)
maxchars <- which.max(nchars)
maxlength <- nchar(lines[maxchars])
nwords <- sum(sapply(strsplit(lines, "\\s+"), length))
close(con)
return(c(
f,
format(round(fileSize, 2), nsmall = 2),
length(lines),
maxchars,
maxlength,
nwords
))
})
fileInfo$tbl <- data.frame(matrix(unlist(fileInfo$info), nrow = length(fileInfo$info), byrow = T))
colnames(fileInfo$tbl) <-
c("file_name",
"size_MB",
"num_lines",
"longest_line",
"max_line_length",
"num_words")
setwd(env$dirFiles)
# write this out to csv, if needed
# write.table(fileInfo, file = "fileInfo.csv", sep = ",")
fileInfo$tbl$num_lines <- as.numeric(as.character(fileInfo$tbl$num_lines))
fileInfo$tbl$max_line_length <- as.numeric(as.character(fileInfo$tbl$max_line_length))
fileInfo$tbl$num_words <- as.numeric(as.character(fileInfo$tbl$num_words))
fileInfo$tbl$size_MB <- as.numeric(as.character(fileInfo$tbl$size_MB))
fileInfo$plots = list()
fileInfo$plots[[1]] <- ggplot(data = fileInfo$tbl, aes(x = file_name, y = num_lines/1000)) +
geom_bar(stat = "identity", fill = "steelblue4") +
labs(y = "Number of lines in thousands", x = "") +
coord_flip()
fileInfo$plots[[2]] <- ggplot(data = fileInfo$tbl, aes(x = file_name, y = max_line_length)) +
geom_bar(stat = "identity", fill = "steelblue4") +
labs(y = "Maximum line length", x = "") +
coord_flip()
fileInfo$plots[[3]] <- ggplot(data = fileInfo$tbl, aes(x = file_name, y = num_words/1000)) +
geom_bar(stat = "identity", fill = "steelblue4") +
labs(y = "Number of words in thousands", x = "") +
coord_flip()
fileInfo$plots[[4]] <- ggplot(data = fileInfo$tbl, aes(x = file_name, y = size_MB)) +
geom_bar(stat = "identity", fill = "steelblue4") +
labs(y = "File size in MB", x = "") +
coord_flip()
grid.arrange(fileInfo$plots[[1]], fileInfo$plots[[2]], fileInfo$plots[[3]], fileInfo$plots[[4]], ncol = 1)

What these show is that the Twitter data has the highest number of lines, but the shortest lines. The news data has the smallest number of words. All three files are over 150MB in size.
Reading, sampling and tidying up the data
First we read the files into a corpus. The files are large, but since we don’t need all of the data for exporation and model training, we suffice with random samples consisting of 5% of the lines from each file.
swift <- VCorpus(DirSource(env$dirData, mode = "text", encoding = "UTF-8"),
readerControl = list(
reader = readPlain,
language = "english",
load = TRUE
))
# Give descriptions that help me to remember which one's which :-)
swift[[1]]$meta$description <- "Blogs"
swift[[2]]$meta$description <- "News"
swift[[3]]$meta$description <- "Twitter"
# Take random samples
set.seed(1200)
swift[[1]]$content <- sample(swift[[1]]$content, fileInfo$tbl[1,3]*0.05)
swift[[2]]$content <- sample(swift[[2]]$content, fileInfo$tbl[2,3]*0.05)
swift[[3]]$content <- sample(swift[[3]]$content, fileInfo$tbl[3,3]*0.05)
swiftBup <- swift # backup
swift <- swiftBup # restore
In order to get meaningful insights from the data, some cleaning needs to be done, such as removal of punctuations, graphic characters/emoji, white spaces, profanities and other content that does not have our interest. We also convert the data to lower case. For profanity filtering we use a list from Luis von Ahn at Carnegie Mellon University, downloadable at https://www.cs.cmu.edu/~biglou/resources/bad-words.txt. Since the final goal of this exercise is to predict words for users typing text, we choose not to perform stemming or stop word removal.
# Convert from UTF-8to ASCII
swift[[1]]$content <- iconv(swift[[1]]$content, "UTF-8", "ASCII", sub = "byte")
swift[[2]]$content <- iconv(swift[[2]]$content, "UTF-8", "ASCII", sub = "byte")
swift[[3]]$content <- iconv(swift[[3]]$content, "UTF-8", "ASCII", sub = "byte")
# Remove punctuation and numbers
swift <- tm_map(swift, content_transformer(removePunctuation))
swift <- tm_map(swift, content_transformer(removeNumbers))
# Convert to lower caps (only works after ASCII conversion)
swift <- tm_map(swift, content_transformer(tolower))
# Remove stopwords - chose to skip
# swift <- tm_map(swift, removeWords, stopwords("english"))
# Remove profanities
# Load profane words list
refData = list()
con = url("https://www.cs.cmu.edu/~biglou/resources/bad-words.txt")
refData$badWords = readLines(con)
close(con); rm(con)
# Remove them
swift <- tm_map(swift, removeWords, refData$badWords)
# Stemming - chose to skip
# swift <- tm_map(swift, stemDocument)
# Strip excess white spaces
swift <- tm_map(swift, stripWhitespace)
Exploratory data analysis
To get to know the data better, we can explore the most frequently occurring words and other characteristics of the data. To do so, first we build a term-document matrix, then extract the most frequently occurring words among the three documents and plot them. We also look at data density, and at n-grams.
Term-document matrix
A term-document matrix is used to map terms (words) used in a corpus against the documents in the corpus. Terms are displayed in rows, columns contain the frequencies at which these words occur in each document.
# Term Document Matrix
tdm = list()
tdm$tdm <- TermDocumentMatrix(swift)
tdm$tdm <- removeSparseTerms(tdm$tdm, 0.25)
# Find most frequent terms
tdm$mostFreq <- findMostFreqTerms(tdm$tdm, 25)
# Tabulate most frequent terms
tdm$tblMostFreq <- as.data.frame(tdm$mostFreq)
tdm$tblMostFreq$Term <- rownames(tdm$tblMostFreq)
tdm$tblMostFreqMelt <-
melt(
tdm$tblMostFreq,
id = c("Term"),
measure.vars = colnames(tdm$tblMostFreq[,1:3])
)
colnames(tdm$tblMostFreqMelt) <- c("Term", "Document", "Occurrence")
# Plot most frequent terms
tdm$plot <-
ggplot(data = tdm$tblMostFreqMelt, aes(x = reorder(Term, Occurrence),
y = Occurrence, fill = Document)) +
geom_bar(stat = "identity") +
geom_text(aes(label = Occurrence), size = 3, position = position_stack(vjust = 0.5)) +
coord_flip() +
labs(title = "Term-document matrix of US blogs, news and Twitter data", x = "Terms")
tdm$plot

Data density
Now that we know the terms in each document and their frequencies we can compare the three sources with a set of density plots that express how often each word occurs in each text, as a proportion of the total number of words instances in the text. This shows that even the most frequent words only make up less than 0.01% of the total text.
# Tabulate terms and frequencies in data frame
tdm$tblAll <- as.data.frame(findMostFreqTerms(tdm$tdm, tdm$tdm$nrow))
tdm$tblAll$terms <- rownames(tdm$tblAll)
colnames(tdm$tblAll) <- c("Blogs", "News", "Twitter", "Terms")
# Generate density plots
tdm$density$plot$blogs <- ggplot(data = tdm$tblAll, aes(x = Blogs/sum(Blogs), y = Terms)) +
geom_count(stat = "sum", size = 2, color = "steelblue4", alpha = 0.5) +
xlim(0, 0.01) +
theme(axis.text.y = element_blank(), axis.title.y = element_blank(),
axis.ticks = element_blank())
tdm$density$plot$news <- ggplot(data = tdm$tblAll, aes(x = News/sum(News), y = Terms)) +
geom_count(stat = "sum", size = 2, color = "steelblue4", alpha = 0.5) +
xlim(0, 0.01) +
theme(axis.text.y = element_blank(), axis.title.y = element_blank(),
axis.ticks = element_blank())
tdm$density$plot$twitter <- ggplot(data = tdm$tblAll, aes(x = Twitter/sum(Twitter), y = Terms)) +
geom_count(stat = "sum", size = 2, color = "steelblue4", alpha = 0.5) +
xlim(0, 0.01) +
theme(axis.text.y = element_blank(), axis.title.y = element_blank(),
axis.ticks = element_blank())
grid.arrange(tdm$density$plot$blogs, tdm$density$plot$news, tdm$density$plot$twitter, ncol = 1)

N-grams
Another way of looking at the data is via tokenization into chunks of two or more words, aka n-grams. Here, we use the RWeka package to find bi-grams and tri-grams. We tabulate and vizualize the output again using ggplot.
tdm2 = list()
# set tokenizer options
tdm2$tknz <- function(x) NGramTokenizer(x, Weka_control(min = 2, max = 2))
tdm2$tdm <- TermDocumentMatrix(swift, control = list(tokenize = tdm2$tknz))
tdm2$tdm <- removeSparseTerms(tdm2$tdm, 0.1)
# Find most frequent terms
tdm2$mostFreq <- findMostFreqTerms(tdm2$tdm, 30)
# Tabulate most frequent terms
tdm2$tblMostFreq <- as.data.frame(tdm2$mostFreq)
tdm2$tblMostFreq$Term <- rownames(tdm2$tblMostFreq)
tdm2$tblMostFreqMelt <-
melt(
tdm2$tblMostFreq,
id = c("Term"),
measure.vars = colnames(tdm2$tblMostFreq[,1:3])
)
colnames(tdm2$tblMostFreqMelt) <- c("Term", "Document", "Occurrence")
# Plot most frequent terms
tdm2$plot1 <- ggplot(data = tdm2$tblMostFreqMelt,
aes(x = reorder(Term, Occurrence), y = Occurrence, fill = Document)) +
geom_bar(stat = "identity") +
geom_text(aes(label = Occurrence), size = 3, position = position_stack(vjust = 0.5)) +
coord_flip() +
labs(title = "Bi-gram term-document matrix of US blogs, news and Twitter data",
x = "Terms")
tdm2$plot1

tdm3 = list()
# set tokenizer options
tdm3$tknz <- function(x) NGramTokenizer(x, Weka_control(min = 3, max = 3))
tdm3$tdm <- TermDocumentMatrix(swift, control = list(tokenize = tdm3$tknz))
tdm3$tdm <- removeSparseTerms(tdm3$tdm, 0.1)
# Find most frequent terms
tdm3$mostFreq <- findMostFreqTerms(tdm3$tdm, 30)
# Tabulate most frequent terms
tdm3$tblMostFreq <- as.data.frame(tdm3$mostFreq)
tdm3$tblMostFreq$Term <- rownames(tdm3$tblMostFreq)
tdm3$tblMostFreqMelt <-
melt(
tdm3$tblMostFreq,
id = c("Term"),
measure.vars = colnames(tdm3$tblMostFreq[,1:3])
)
colnames(tdm3$tblMostFreqMelt) <- c("Term", "Document", "Occurrence")
# Plot most frequent terms
tdm3$plot1 <- ggplot(data = tdm3$tblMostFreqMelt,
aes(x = reorder(Term, Occurrence), y = Occurrence, fill = Document)) +
geom_bar(stat = "identity") +
geom_text(aes(label = Occurrence), size = 3, position = position_stack(vjust = 0.5)) +
coord_flip() +
labs(title = "Tri-gram term-document matrix of US blogs, news and Twitter data",
x = "Terms")
tdm3$plot1

Percent coverage
Next, we will look at percent coverage, meaning: How many unique words found in the texts make up a given percentile of all word instances in the corpus?
# How many words cover 50% of the text? How many cover 90%
# Add up row sums
tdm$tblAll$totalOccur <- rowSums(tdm$tblAll[,1:3])
# Calculate cumulative sums
tdm$tblAll$cumTotalOccur <- cumsum(tdm$tblAll$totalOccur)
# Calculate percent of column
tdm$tblAll$percUnq <- tdm$tblAll$totalOccur / sum(tdm$tblAll$totalOccur)
# Calculate cumulative percent of column
tdm$tblAll$cumPercUnq <- cumsum(tdm$tblAll$percUnq)
# Look up how many words, percent of unique words, for each 10 percent of words
tdm$coverage$percentiles = data.frame()
for (i in 1:9) {
tdm$coverage$percentiles[i, 1] <- 10*i
tdm$coverage$percentiles[i, 2] <-
which(abs(tdm$tblAll$cumPercUnq - 0.1 * i) == min(abs(tdm$tblAll$cumPercUnq - 0.1 * i)))
tdm$coverage$percentiles[i, 3] <- round(100 * (which(
abs(tdm$tblAll$cumPercUnq - 0.1 * i) == min(abs(tdm$tblAll$cumPercUnq - 0.1 * i))
) / length(tdm$tblAll$totalOccur)), 2)
}
rm(i)
colnames(tdm$coverage$percentiles) <- c("percWords", "numWords", "percUnqWords")
tdm$coverage$percentiles
Given the samples we took, we find that 50% of all words instances in the corpus are covered by 141 unique words, which make up 1.22 percent of all unique words. 90% of all words in the corpus are covered by 2717 unique words, which make up 23.48 percent of all unique words.
Data modeling and word prediction
For data modeling and word prediction, the plan is to use the following strategy.
- Generate a Markov transition probability matrix or some other form of frequency table for unigrams, bigrams and trigrams found in the text files
- To predict the next word given a sentence, we’ll select the last 2 words, and from the matrix or tables get whatever next word has the highest frequency observed
- In case there is no match at a trigram level, we fall back on bigrams; if there also no match at bigram level, then we return the most probable unigram (which is ‘the’)
Some considerations to be made when building the model are:
- What is the most efficient way to build and store the training set?
- What is the best balance between model accuracy and model performance? For example, how many sparse terms can we remove in order to retain accuracy while improving model performance? Similarly, adding quad-grams to the matrix may yield better accuracy in some cases, but is also computationally more expensive.
- How do we evaluate the predictive performance of the model?
LS0tDQp0aXRsZTogIkNvdXJzZXJhL1N3aWZ0a2V5IFRleHQgTWluaW5nIGFuZCBOYXR1cmFsIExhbmd1YWdlIEFuYWx5c2lzIg0KYXV0aG9yOiAiTUxNViINCmRhdGU6ICJNYXkvSnVuZSAyMDE3Ig0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOg0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIHRoZW1lOiB1bml0ZWQNCiAgICB0b2M6IHllcw0KICBodG1sX2RvY3VtZW50OiBkZWZhdWx0DQogIHBkZl9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KLS0tDQoNCmBgYHtyIHNldHVwLCBlY2hvID0gRkFMU0UsIGVycm9yID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFfQ0KDQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IEZBTFNFLCBlcnJvciA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIHdhcm5pbmcgPSBGQUxTRSwgZmlnLmhlaWdodCA9IDQsIGZpZy53aWR0aCA9IDEyKQ0KDQpsaWJyYXJ5KHRtKQ0KbGlicmFyeShmaWxlaGFzaCkNCmxpYnJhcnkoU25vd2JhbGxDKQ0KbGlicmFyeShSV2VrYSkNCmxpYnJhcnkobmdyYW0pDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KGdyaWRFeHRyYSkNCmxpYnJhcnkocmVzaGFwZTIpDQpsaWJyYXJ5KGRwbHlyKQ0KDQpzZXR3ZCgifi9BbmFseXRpY3MvU3R1ZHkvSm9obnMgSG9wa2lucyBEYXRhIFNjaWVuY2UgMjAxNS0yMDE2L1ggLSBEYXRhIFNjaWVuY2UgQ2Fwc3RvbmUgUHJvamVjdC9Bc3NpZ25tZW50cyIpDQoNCiMgbGlzdCBmb3IgZW52aXJvbm1lbnQgdmFyaWFibGVzDQplbnYgPSBsaXN0KCkNCmVudiRkaXJGaWxlcyA8LSBnZXR3ZCgpDQplbnYkZGlyRGF0YSA8LSBwYXN0ZShlbnYkZGlyRmlsZXMsIi9kYXRhIiwgc2VwID0gIiIpDQoNCmBgYA0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KDQojIyMgSW50cm9kdWN0aW9uDQoNClRoaXMgcHJvamVjdCBzZXJ2ZXMgdG8gYW5hbHl6ZSBhIGxhcmdlIGNvcnB1cyBvZiB0ZXh0IGRvY3VtZW50cyB0byBkaXNjb3ZlciB0aGUgDQpzdHJ1Y3R1cmUgaW4gdGhlIGRhdGEgYW5kIGhvdyB3b3JkcyBhcmUgcHV0IHRvZ2V0aGVyLiBJdCB3aWxsIHBlcmZvcm0gY2xlYW5pbmcgDQphbmQgZXhwbG9yYXRvcnkgYW5hbHlzaXMgb2YgdGhlIGRhdGEsIHRoZW4gYnVpbGRpbmcgYW5kIHNhbXBsaW5nIGZyb20gYSANCnByZWRpY3RpdmUgdGV4dCBtb2RlbC4gVGhlIGZpbmFsIG91dGNvbWUgaXMgYW4gYXBwbGljYXRpb24gdGhhdCBjYW4gdGFrZSBhbg0KaW5wdXQgdGV4dCBhbmQgcHJlZGljdCB0aGUgbmV4dCB3b3JkLg0KDQpUaGUgZGF0YSB1c2VkIGZvciBleHBsb3JhdGlvbiBhbmQgbW9kZWwgdHJhaW5pbmcgY2FuIGJlIGZvdW5kIGF0IHRoaXMgbGluazoNCmh0dHBzOi8vZDM5NnF1c3phNDBvcmMuY2xvdWRmcm9udC5uZXQvZHNzY2Fwc3RvbmUvZGF0YXNldC9Db3Vyc2VyYS1Td2lmdEtleS56aXANCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyMjIFVuZGVyc3RhbmRpbmcgdGhlIGRhdGENCg0KVGhpcyBhbmFseXNpcyBzdGFydHMgYnkgdW5kZXJzdGFuZGluZyB0aGUgZGF0YSBmaWxlcyB0aGF0IG5lZWQgdG8gYmUgd29ya2VkDQp3aXRoLiBGb3IgdGhlIHB1cnBvc2Ugb2YgdGhlIGV4ZXJjaXNlIHRoZSBmaWxlcyBoYXZlIGJlZW4gZG93bmxvYWRlZCBhbmQgc3RvcmVkDQppbiBhIHN1YmZvbGRlciBjYWxsZWQgImRhdGEiLCBzaXR0aW5nIGluc2lkZSB0aGUgc2FtZSBmb2xkZXIgYXMgdGhpcyBtYXJrZG93bg0KZmlsZS4NCg0KRmlyc3QsIHdlIHN0b3JlIHRoZSBuYW1lcyBvZiBhbGwgdGhlIGZpbGVzIHNpdHRpbmcgaW4gdGhlIGRhdGEgZm9sZGVyIGluIGEgbGlzdCwNCnRoZW4gd2UgcnVuIGEgZnVuY3Rpb24gb3ZlciBlYWNoIGZpbGUgdGhhdCBleHRyYWN0cyBmaWxlIG5hbWUsIGZpbGUgc2l6ZShNQiksDQp0aGUgbnVtYmVyIG9mIGxpbmVzLCB0aGUgbG9uZ2VzdCBsaW5lIGFuZCBpdHMgbGVuZ3RoLCBhbmQgdGhlIG51bWJlciBvZiB3b3Jkcw0KZm9yIGVhY2ggZmlsZS4gV2UgdGhlbiBwcmludCB0aGF0IGluIGEgdGFibGUgZm9ybWF0IGFuZCBwcm9kdWNlIHNvbWUgcGxvdHMuDQoNCmBgYHtyIGZpbGVfaW5mb18xfQ0KDQpzZXR3ZChlbnYkZGlyRGF0YSkNCg0KIyBzdG9yZSBmaWxlbmFtZXMgaW4gZW52aXJvbm1lbnQgbGlzdA0KZW52JGZpbGVMaXN0IDwtIGxpc3QuZmlsZXMocmVjdXJzaXZlID0gVCwgcGF0dGVybiA9ICIqLnR4dCIpDQoNCmZpbGVJbmZvID0gbGlzdCgpDQpmaWxlSW5mbyRpbmZvIDwtIGxhcHBseShwYXN0ZShlbnYkZmlsZUxpc3QsIHNlcCA9ICIvIiksIGZ1bmN0aW9uKGYpIHsNCiAgICAgIGZpbGVTaXplIDwtIGZpbGUuaW5mbyhmKVsxXSAvIDEwMjQgLyAxMDI0DQogICAgICBjb24gPC0gZmlsZShmLCBvcGVuID0gInIiKQ0KICAgICAgbGluZXMgPC0gcmVhZExpbmVzKGNvbikNCiAgICAgIG5jaGFycyA8LSBsYXBwbHkobGluZXMsIG5jaGFyKQ0KICAgICAgbWF4Y2hhcnMgPC0gd2hpY2gubWF4KG5jaGFycykNCiAgICAgIG1heGxlbmd0aCA8LSBuY2hhcihsaW5lc1ttYXhjaGFyc10pDQogICAgICBud29yZHMgPC0gc3VtKHNhcHBseShzdHJzcGxpdChsaW5lcywgIlxccysiKSwgbGVuZ3RoKSkNCiAgICAgIGNsb3NlKGNvbikNCiAgICAgIHJldHVybihjKA0KICAgICAgICAgICAgZiwNCiAgICAgICAgICAgIGZvcm1hdChyb3VuZChmaWxlU2l6ZSwgMiksIG5zbWFsbCA9IDIpLA0KICAgICAgICAgICAgbGVuZ3RoKGxpbmVzKSwNCiAgICAgICAgICAgIG1heGNoYXJzLA0KICAgICAgICAgICAgbWF4bGVuZ3RoLA0KICAgICAgICAgICAgbndvcmRzDQogICAgICApKQ0KfSkNCg0KZmlsZUluZm8kdGJsIDwtIGRhdGEuZnJhbWUobWF0cml4KHVubGlzdChmaWxlSW5mbyRpbmZvKSwgbnJvdyA9IGxlbmd0aChmaWxlSW5mbyRpbmZvKSwgYnlyb3cgPSBUKSkNCmNvbG5hbWVzKGZpbGVJbmZvJHRibCkgPC0NCiAgICAgIGMoImZpbGVfbmFtZSIsDQogICAgICAgICJzaXplX01CIiwNCiAgICAgICAgIm51bV9saW5lcyIsDQogICAgICAgICJsb25nZXN0X2xpbmUiLA0KICAgICAgICAibWF4X2xpbmVfbGVuZ3RoIiwNCiAgICAgICAgIm51bV93b3JkcyIpDQoNCnNldHdkKGVudiRkaXJGaWxlcykNCiMgd3JpdGUgdGhpcyBvdXQgdG8gY3N2LCBpZiBuZWVkZWQNCiMgd3JpdGUudGFibGUoZmlsZUluZm8sIGZpbGUgPSAiZmlsZUluZm8uY3N2Iiwgc2VwID0gIiwiKQ0KDQpgYGANCg0KYGBge3IgZmlsZV9pbmZvXzJ9DQoNCmZpbGVJbmZvJHRibCRudW1fbGluZXMgPC0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoZmlsZUluZm8kdGJsJG51bV9saW5lcykpDQpmaWxlSW5mbyR0YmwkbWF4X2xpbmVfbGVuZ3RoIDwtIGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKGZpbGVJbmZvJHRibCRtYXhfbGluZV9sZW5ndGgpKQ0KZmlsZUluZm8kdGJsJG51bV93b3JkcyA8LSBhcy5udW1lcmljKGFzLmNoYXJhY3RlcihmaWxlSW5mbyR0YmwkbnVtX3dvcmRzKSkNCmZpbGVJbmZvJHRibCRzaXplX01CIDwtIGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKGZpbGVJbmZvJHRibCRzaXplX01CKSkNCg0KZmlsZUluZm8kcGxvdHMgPSBsaXN0KCkNCmZpbGVJbmZvJHBsb3RzW1sxXV0gPC0gZ2dwbG90KGRhdGEgPSBmaWxlSW5mbyR0YmwsIGFlcyh4ID0gZmlsZV9uYW1lLCB5ID0gbnVtX2xpbmVzLzEwMDApKSArDQogICAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgZmlsbCA9ICJzdGVlbGJsdWU0IikgKw0KICAgICAgbGFicyh5ID0gIk51bWJlciBvZiBsaW5lcyBpbiB0aG91c2FuZHMiLCB4ID0gIiIpICsNCiAgICAgIGNvb3JkX2ZsaXAoKQ0KZmlsZUluZm8kcGxvdHNbWzJdXSA8LSBnZ3Bsb3QoZGF0YSA9IGZpbGVJbmZvJHRibCwgYWVzKHggPSBmaWxlX25hbWUsIHkgPSBtYXhfbGluZV9sZW5ndGgpKSArDQogICAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgZmlsbCA9ICJzdGVlbGJsdWU0IikgKw0KICAgICAgbGFicyh5ID0gIk1heGltdW0gbGluZSBsZW5ndGgiLCB4ID0gIiIpICsNCiAgICAgIGNvb3JkX2ZsaXAoKQ0KZmlsZUluZm8kcGxvdHNbWzNdXSA8LSBnZ3Bsb3QoZGF0YSA9IGZpbGVJbmZvJHRibCwgYWVzKHggPSBmaWxlX25hbWUsIHkgPSBudW1fd29yZHMvMTAwMCkpICsNCiAgICAgIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBmaWxsID0gInN0ZWVsYmx1ZTQiKSArDQogICAgICBsYWJzKHkgPSAiTnVtYmVyIG9mIHdvcmRzIGluIHRob3VzYW5kcyIsIHggPSAiIikgKw0KICAgICAgY29vcmRfZmxpcCgpDQpmaWxlSW5mbyRwbG90c1tbNF1dIDwtIGdncGxvdChkYXRhID0gZmlsZUluZm8kdGJsLCBhZXMoeCA9IGZpbGVfbmFtZSwgeSA9IHNpemVfTUIpKSArDQogICAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgZmlsbCA9ICJzdGVlbGJsdWU0IikgKw0KICAgICAgbGFicyh5ID0gIkZpbGUgc2l6ZSBpbiBNQiIsIHggPSAiIikgKw0KICAgICAgY29vcmRfZmxpcCgpDQpncmlkLmFycmFuZ2UoZmlsZUluZm8kcGxvdHNbWzFdXSwgZmlsZUluZm8kcGxvdHNbWzJdXSwgZmlsZUluZm8kcGxvdHNbWzNdXSwgZmlsZUluZm8kcGxvdHNbWzRdXSwgbmNvbCA9IDEpDQoNCmBgYA0KV2hhdCB0aGVzZSBzaG93IGlzIHRoYXQgdGhlIFR3aXR0ZXIgZGF0YSBoYXMgdGhlIGhpZ2hlc3QgbnVtYmVyIG9mIGxpbmVzLCBidXQgDQp0aGUgc2hvcnRlc3QgbGluZXMuIFRoZSBuZXdzIGRhdGEgaGFzIHRoZSBzbWFsbGVzdCBudW1iZXIgb2Ygd29yZHMuIEFsbCB0aHJlZSANCmZpbGVzIGFyZSBvdmVyIDE1ME1CIGluIHNpemUuDQoNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMjIyBSZWFkaW5nLCBzYW1wbGluZyBhbmQgdGlkeWluZyB1cCB0aGUgZGF0YQ0KDQpGaXJzdCB3ZSByZWFkIHRoZSBmaWxlcyBpbnRvIGEgY29ycHVzLiBUaGUgZmlsZXMgYXJlIGxhcmdlLCBidXQgc2luY2Ugd2UgZG9uJ3QgDQpuZWVkIGFsbCBvZiB0aGUgZGF0YSBmb3IgZXhwb3JhdGlvbiBhbmQgbW9kZWwgdHJhaW5pbmcsIHdlIHN1ZmZpY2Ugd2l0aCByYW5kb20NCnNhbXBsZXMgY29uc2lzdGluZyBvZiA1JSBvZiB0aGUgbGluZXMgZnJvbSBlYWNoIGZpbGUuDQoNCmBgYHtyIGNvcnB1c30NCg0Kc3dpZnQgPC0gVkNvcnB1cyhEaXJTb3VyY2UoZW52JGRpckRhdGEsIG1vZGUgPSAidGV4dCIsIGVuY29kaW5nID0gIlVURi04IiksDQogICAgICAgICAgICAgICAgcmVhZGVyQ29udHJvbCA9IGxpc3QoDQogICAgICAgICAgICAgICAgICAgICAgcmVhZGVyID0gcmVhZFBsYWluLA0KICAgICAgICAgICAgICAgICAgICAgIGxhbmd1YWdlID0gImVuZ2xpc2giLA0KICAgICAgICAgICAgICAgICAgICAgIGxvYWQgPSBUUlVFDQogICAgICAgICAgICAgICAgKSkNCg0KIyBHaXZlIGRlc2NyaXB0aW9ucyB0aGF0IGhlbHAgbWUgdG8gcmVtZW1iZXIgd2hpY2ggb25lJ3Mgd2hpY2ggOi0pDQpzd2lmdFtbMV1dJG1ldGEkZGVzY3JpcHRpb24gPC0gIkJsb2dzIg0Kc3dpZnRbWzJdXSRtZXRhJGRlc2NyaXB0aW9uIDwtICJOZXdzIg0Kc3dpZnRbWzNdXSRtZXRhJGRlc2NyaXB0aW9uIDwtICJUd2l0dGVyIg0KDQojIFRha2UgcmFuZG9tIHNhbXBsZXMNCnNldC5zZWVkKDEyMDApDQpzd2lmdFtbMV1dJGNvbnRlbnQgPC0gc2FtcGxlKHN3aWZ0W1sxXV0kY29udGVudCwgZmlsZUluZm8kdGJsWzEsM10qMC4wNSkNCnN3aWZ0W1syXV0kY29udGVudCA8LSBzYW1wbGUoc3dpZnRbWzJdXSRjb250ZW50LCBmaWxlSW5mbyR0YmxbMiwzXSowLjA1KQ0Kc3dpZnRbWzNdXSRjb250ZW50IDwtIHNhbXBsZShzd2lmdFtbM11dJGNvbnRlbnQsIGZpbGVJbmZvJHRibFszLDNdKjAuMDUpDQoNCnN3aWZ0QnVwIDwtIHN3aWZ0ICMgYmFja3VwDQpzd2lmdCA8LSBzd2lmdEJ1cCAjIHJlc3RvcmUNCg0KYGBgDQoNCkluIG9yZGVyIHRvIGdldCBtZWFuaW5nZnVsIGluc2lnaHRzIGZyb20gdGhlIGRhdGEsIHNvbWUgY2xlYW5pbmcgbmVlZHMgdG8gYmUgDQpkb25lLCBzdWNoIGFzIHJlbW92YWwgb2YgcHVuY3R1YXRpb25zLCBncmFwaGljIGNoYXJhY3RlcnMvZW1vamksIHdoaXRlIHNwYWNlcywgDQpwcm9mYW5pdGllcyBhbmQgb3RoZXIgY29udGVudCB0aGF0IGRvZXMgbm90IGhhdmUgb3VyIGludGVyZXN0LiBXZSBhbHNvIGNvbnZlcnQgDQp0aGUgZGF0YSB0byBsb3dlciBjYXNlLiBGb3IgcHJvZmFuaXR5IGZpbHRlcmluZyB3ZSB1c2UgYSBsaXN0IGZyb20gTHVpcyB2b24gQWhuIA0KYXQgQ2FybmVnaWUgTWVsbG9uIFVuaXZlcnNpdHksIGRvd25sb2FkYWJsZSBhdCANCmh0dHBzOi8vd3d3LmNzLmNtdS5lZHUvfmJpZ2xvdS9yZXNvdXJjZXMvYmFkLXdvcmRzLnR4dC4gU2luY2UgdGhlIGZpbmFsIGdvYWwgb2YNCnRoaXMgZXhlcmNpc2UgaXMgdG8gcHJlZGljdCB3b3JkcyBmb3IgdXNlcnMgdHlwaW5nIHRleHQsIHdlIGNob29zZSBub3QgdG8NCnBlcmZvcm0gc3RlbW1pbmcgb3Igc3RvcCB3b3JkIHJlbW92YWwuDQoNCmBgYHtyIGNsZWFuX2RhdGFfMSwgZWNobz1GLCBldmFsPUZ9DQoNCiMgQ2hlY2sgbGluZXMgLSB0aGlzIGlzIGp1c3QgZm9yIG1lIHRvIGNoZWNrIHRoZSAnYmVmb3JlICYgYWZ0ZXInDQoNCiAgICAgICMgUmVtb3ZlIHdpdGggdG0NCiAgICAgIHN3aWZ0W1sxXV0kY29udGVudFtbNDQ0XV0gIyBwdW5jdHVhdGlvbg0KICAgICAgc3dpZnRbWzFdXSRjb250ZW50W1s0NjBdXSAjIGJyYWNrZXRzLCBleGNsYW1hdGlvbiBtYXJrcw0KICAgICAgc3dpZnRbWzFdXSRjb250ZW50W1s0NjNdXSAjIGFzdGVyaXNrcw0KICAgICAgc3dpZnRbWzFdXSRjb250ZW50W1s0NzFdXSAjIHF1b3Rlcw0KICAgICAgc3dpZnRbWzNdXSRjb250ZW50W1s0OTBdXSAjIHBlcmNlbnQNCiAgICAgIHN3aWZ0W1szXV0kY29udGVudFtbNTEzXV0gIyBADQogICAgICBzd2lmdFtbM11dJGNvbnRlbnRbWzUxNl1dICMgbnVtYmVycw0KICAgICAgc3dpZnRbWzNdXSRjb250ZW50W1s1MzRdXSAjIGhhc2h0YWdzDQogICAgICANCiAgICAgICMgUmVtb3ZlIGJ5IEFTQ0lJIGNvbnZlcnNpb24NCiAgICAgIHN3aWZ0W1sxXV0kY29udGVudFtbNDQyXV0gIyBoeXBoZW5zDQogICAgICBzd2lmdFtbMV1dJGNvbnRlbnRbWzQ3OF1dICMgbW9yZSBxdW90ZXMNCiAgICAgIHN3aWZ0W1szXV0kY29udGVudFtbNTExXV0gIyBzeW1ib2xzDQogICAgICBzd2lmdFtbMV1dJGNvbnRlbnRbWzQ0M11dICMgcHJvZmFuaXR5DQoNCmBgYA0KDQpgYGB7ciBjbGVhbl9kYXRhXzJ9DQoNCiMgQ29udmVydCBmcm9tIFVURi04dG8gQVNDSUkNCnN3aWZ0W1sxXV0kY29udGVudCA8LSBpY29udihzd2lmdFtbMV1dJGNvbnRlbnQsICJVVEYtOCIsICJBU0NJSSIsIHN1YiA9ICJieXRlIikNCnN3aWZ0W1syXV0kY29udGVudCA8LSBpY29udihzd2lmdFtbMl1dJGNvbnRlbnQsICJVVEYtOCIsICJBU0NJSSIsIHN1YiA9ICJieXRlIikNCnN3aWZ0W1szXV0kY29udGVudCA8LSBpY29udihzd2lmdFtbM11dJGNvbnRlbnQsICJVVEYtOCIsICJBU0NJSSIsIHN1YiA9ICJieXRlIikNCg0KIyBSZW1vdmUgcHVuY3R1YXRpb24gYW5kIG51bWJlcnMNCnN3aWZ0IDwtIHRtX21hcChzd2lmdCwgY29udGVudF90cmFuc2Zvcm1lcihyZW1vdmVQdW5jdHVhdGlvbikpDQpzd2lmdCA8LSB0bV9tYXAoc3dpZnQsIGNvbnRlbnRfdHJhbnNmb3JtZXIocmVtb3ZlTnVtYmVycykpDQoNCiMgQ29udmVydCB0byBsb3dlciBjYXBzIChvbmx5IHdvcmtzIGFmdGVyIEFTQ0lJIGNvbnZlcnNpb24pDQpzd2lmdCA8LSB0bV9tYXAoc3dpZnQsIGNvbnRlbnRfdHJhbnNmb3JtZXIodG9sb3dlcikpDQoNCiMgUmVtb3ZlIHN0b3B3b3JkcyAtIGNob3NlIHRvIHNraXANCiMgc3dpZnQgPC0gdG1fbWFwKHN3aWZ0LCByZW1vdmVXb3Jkcywgc3RvcHdvcmRzKCJlbmdsaXNoIikpDQoNCiMgUmVtb3ZlIHByb2Zhbml0aWVzDQogICAgICAjIExvYWQgcHJvZmFuZSB3b3JkcyBsaXN0DQogICAgICByZWZEYXRhID0gbGlzdCgpDQogICAgICBjb24gPSB1cmwoImh0dHBzOi8vd3d3LmNzLmNtdS5lZHUvfmJpZ2xvdS9yZXNvdXJjZXMvYmFkLXdvcmRzLnR4dCIpDQogICAgICByZWZEYXRhJGJhZFdvcmRzID0gcmVhZExpbmVzKGNvbikNCiAgICAgIGNsb3NlKGNvbik7IHJtKGNvbikNCiAgICAgICMgUmVtb3ZlIHRoZW0NCiAgICAgIHN3aWZ0IDwtIHRtX21hcChzd2lmdCwgcmVtb3ZlV29yZHMsIHJlZkRhdGEkYmFkV29yZHMpDQoNCiMgU3RlbW1pbmcgLSBjaG9zZSB0byBza2lwDQojIHN3aWZ0IDwtIHRtX21hcChzd2lmdCwgc3RlbURvY3VtZW50KQ0KDQojIFN0cmlwIGV4Y2VzcyB3aGl0ZSBzcGFjZXMNCnN3aWZ0IDwtIHRtX21hcChzd2lmdCwgc3RyaXBXaGl0ZXNwYWNlKQ0KDQpgYGANCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyMjIEV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXMNCg0KVG8gZ2V0IHRvIGtub3cgdGhlIGRhdGEgYmV0dGVyLCB3ZSBjYW4gZXhwbG9yZSB0aGUgbW9zdCBmcmVxdWVudGx5IG9jY3VycmluZw0Kd29yZHMgYW5kIG90aGVyIGNoYXJhY3RlcmlzdGljcyBvZiB0aGUgZGF0YS4gVG8gZG8gc28sIGZpcnN0IHdlIGJ1aWxkIGENCnRlcm0tZG9jdW1lbnQgbWF0cml4LCB0aGVuIGV4dHJhY3QgdGhlIG1vc3QgZnJlcXVlbnRseSBvY2N1cnJpbmcgd29yZHMgYW1vbmcgdGhlDQp0aHJlZSBkb2N1bWVudHMgYW5kIHBsb3QgdGhlbS4gV2UgYWxzbyBsb29rIGF0IGRhdGEgZGVuc2l0eSwgYW5kIGF0IG4tZ3JhbXMuDQoNCiMjIyMgVGVybS1kb2N1bWVudCBtYXRyaXgNCg0KQSB0ZXJtLWRvY3VtZW50IG1hdHJpeCBpcyB1c2VkIHRvIG1hcCB0ZXJtcyAod29yZHMpIHVzZWQgaW4gYSBjb3JwdXMgYWdhaW5zdCB0aGUNCmRvY3VtZW50cyBpbiB0aGUgY29ycHVzLiBUZXJtcyBhcmUgZGlzcGxheWVkIGluIHJvd3MsIGNvbHVtbnMgY29udGFpbiB0aGUNCmZyZXF1ZW5jaWVzIGF0IHdoaWNoIHRoZXNlIHdvcmRzIG9jY3VyIGluIGVhY2ggZG9jdW1lbnQuDQoNCmBgYHtyIGV4cGxvcmV9DQoNCiMgVGVybSBEb2N1bWVudCBNYXRyaXgNCnRkbSA9IGxpc3QoKQ0KdGRtJHRkbSA8LSBUZXJtRG9jdW1lbnRNYXRyaXgoc3dpZnQpDQp0ZG0kdGRtIDwtIHJlbW92ZVNwYXJzZVRlcm1zKHRkbSR0ZG0sIDAuMjUpDQoNCiMgRmluZCBtb3N0IGZyZXF1ZW50IHRlcm1zDQp0ZG0kbW9zdEZyZXEgPC0gZmluZE1vc3RGcmVxVGVybXModGRtJHRkbSwgMjUpDQoNCiMgVGFidWxhdGUgbW9zdCBmcmVxdWVudCB0ZXJtcw0KdGRtJHRibE1vc3RGcmVxIDwtIGFzLmRhdGEuZnJhbWUodGRtJG1vc3RGcmVxKQ0KdGRtJHRibE1vc3RGcmVxJFRlcm0gPC0gcm93bmFtZXModGRtJHRibE1vc3RGcmVxKQ0KdGRtJHRibE1vc3RGcmVxTWVsdCA8LQ0KICAgICAgbWVsdCgNCiAgICAgICAgICAgIHRkbSR0YmxNb3N0RnJlcSwNCiAgICAgICAgICAgIGlkID0gYygiVGVybSIpLA0KICAgICAgICAgICAgbWVhc3VyZS52YXJzID0gY29sbmFtZXModGRtJHRibE1vc3RGcmVxWywxOjNdKQ0KICAgICAgKQ0KY29sbmFtZXModGRtJHRibE1vc3RGcmVxTWVsdCkgPC0gYygiVGVybSIsICJEb2N1bWVudCIsICJPY2N1cnJlbmNlIikNCg0KIyBQbG90IG1vc3QgZnJlcXVlbnQgdGVybXMNCnRkbSRwbG90IDwtDQogICAgICBnZ3Bsb3QoZGF0YSA9IHRkbSR0YmxNb3N0RnJlcU1lbHQsIGFlcyh4ID0gcmVvcmRlcihUZXJtLCBPY2N1cnJlbmNlKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHkgPSBPY2N1cnJlbmNlLCBmaWxsID0gRG9jdW1lbnQpKSArDQogICAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKw0KICAgICAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IE9jY3VycmVuY2UpLCBzaXplID0gMywgcG9zaXRpb24gPSBwb3NpdGlvbl9zdGFjayh2anVzdCA9IDAuNSkpICsNCiAgICAgIGNvb3JkX2ZsaXAoKSArDQogICAgICBsYWJzKHRpdGxlID0gIlRlcm0tZG9jdW1lbnQgbWF0cml4IG9mIFVTIGJsb2dzLCBuZXdzIGFuZCBUd2l0dGVyIGRhdGEiLCB4ID0gIlRlcm1zIikNCnRkbSRwbG90DQoNCmBgYA0KDQojIyMjIERhdGEgZGVuc2l0eQ0KTm93IHRoYXQgd2Uga25vdyB0aGUgdGVybXMgaW4gZWFjaCBkb2N1bWVudCBhbmQgdGhlaXIgZnJlcXVlbmNpZXMgd2UgY2FuIGNvbXBhcmUNCnRoZSB0aHJlZSBzb3VyY2VzIHdpdGggYSBzZXQgb2YgZGVuc2l0eSBwbG90cyB0aGF0IGV4cHJlc3MgaG93IG9mdGVuIGVhY2ggd29yZA0Kb2NjdXJzIGluIGVhY2ggdGV4dCwgYXMgYSBwcm9wb3J0aW9uIG9mIHRoZSB0b3RhbCBudW1iZXIgb2Ygd29yZHMgaW5zdGFuY2VzIGluDQp0aGUgdGV4dC4gVGhpcyBzaG93cyB0aGF0IGV2ZW4gdGhlIG1vc3QgZnJlcXVlbnQgd29yZHMgb25seSBtYWtlIHVwIGxlc3MgdGhhbg0KMC4wMSUgb2YgdGhlIHRvdGFsIHRleHQuDQoNCmBgYHtyIGRlbnNpdHl9DQoNCiMgVGFidWxhdGUgdGVybXMgYW5kIGZyZXF1ZW5jaWVzIGluIGRhdGEgZnJhbWUNCnRkbSR0YmxBbGwgPC0gYXMuZGF0YS5mcmFtZShmaW5kTW9zdEZyZXFUZXJtcyh0ZG0kdGRtLCB0ZG0kdGRtJG5yb3cpKQ0KdGRtJHRibEFsbCR0ZXJtcyA8LSByb3duYW1lcyh0ZG0kdGJsQWxsKQ0KY29sbmFtZXModGRtJHRibEFsbCkgPC0gYygiQmxvZ3MiLCAiTmV3cyIsICJUd2l0dGVyIiwgIlRlcm1zIikNCg0KIyBHZW5lcmF0ZSBkZW5zaXR5IHBsb3RzDQp0ZG0kZGVuc2l0eSRwbG90JGJsb2dzIDwtIGdncGxvdChkYXRhID0gdGRtJHRibEFsbCwgYWVzKHggPSBCbG9ncy9zdW0oQmxvZ3MpLCB5ID0gVGVybXMpKSArDQogICAgICBnZW9tX2NvdW50KHN0YXQgPSAic3VtIiwgc2l6ZSA9IDIsIGNvbG9yID0gInN0ZWVsYmx1ZTQiLCBhbHBoYSA9IDAuNSkgKw0KICAgICAgeGxpbSgwLCAwLjAxKSArDQogICAgICB0aGVtZShheGlzLnRleHQueSA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aXRsZS55ID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICAgICAgYXhpcy50aWNrcyA9IGVsZW1lbnRfYmxhbmsoKSkNCnRkbSRkZW5zaXR5JHBsb3QkbmV3cyA8LSBnZ3Bsb3QoZGF0YSA9IHRkbSR0YmxBbGwsIGFlcyh4ID0gTmV3cy9zdW0oTmV3cyksIHkgPSBUZXJtcykpICsNCiAgICAgIGdlb21fY291bnQoc3RhdCA9ICJzdW0iLCBzaXplID0gMiwgY29sb3IgPSAic3RlZWxibHVlNCIsIGFscGhhID0gMC41KSArDQogICAgICB4bGltKDAsIDAuMDEpICsNCiAgICAgIHRoZW1lKGF4aXMudGV4dC55ID0gZWxlbWVudF9ibGFuaygpLCBheGlzLnRpdGxlLnkgPSBlbGVtZW50X2JsYW5rKCksDQogICAgICAgICAgICBheGlzLnRpY2tzID0gZWxlbWVudF9ibGFuaygpKQ0KdGRtJGRlbnNpdHkkcGxvdCR0d2l0dGVyIDwtIGdncGxvdChkYXRhID0gdGRtJHRibEFsbCwgYWVzKHggPSBUd2l0dGVyL3N1bShUd2l0dGVyKSwgeSA9IFRlcm1zKSkgKw0KICAgICAgZ2VvbV9jb3VudChzdGF0ID0gInN1bSIsIHNpemUgPSAyLCBjb2xvciA9ICJzdGVlbGJsdWU0IiwgYWxwaGEgPSAwLjUpICsNCiAgICAgIHhsaW0oMCwgMC4wMSkgKw0KICAgICAgdGhlbWUoYXhpcy50ZXh0LnkgPSBlbGVtZW50X2JsYW5rKCksIGF4aXMudGl0bGUueSA9IGVsZW1lbnRfYmxhbmsoKSwNCiAgICAgICAgICAgIGF4aXMudGlja3MgPSBlbGVtZW50X2JsYW5rKCkpDQpncmlkLmFycmFuZ2UodGRtJGRlbnNpdHkkcGxvdCRibG9ncywgdGRtJGRlbnNpdHkkcGxvdCRuZXdzLCB0ZG0kZGVuc2l0eSRwbG90JHR3aXR0ZXIsIG5jb2wgPSAxKQ0KDQpgYGANCg0KDQojIyMjIE4tZ3JhbXMNCg0KQW5vdGhlciB3YXkgb2YgbG9va2luZyBhdCB0aGUgZGF0YSBpcyB2aWEgdG9rZW5pemF0aW9uIGludG8gY2h1bmtzIG9mIHR3byBvciBtb3JlIHdvcmRzLCBha2Egbi1ncmFtcy4gSGVyZSwgd2UgdXNlIHRoZSBSV2VrYSBwYWNrYWdlIHRvIGZpbmQgYmktZ3JhbXMgYW5kIHRyaS1ncmFtcy4gV2UgdGFidWxhdGUgYW5kIHZpenVhbGl6ZSB0aGUgb3V0cHV0IGFnYWluIHVzaW5nIGdncGxvdC4NCg0KYGBge3IgYmlncmFtXzF9DQp0ZG0yID0gbGlzdCgpDQoNCiMgc2V0IHRva2VuaXplciBvcHRpb25zDQp0ZG0yJHRrbnogPC0gZnVuY3Rpb24oeCkgTkdyYW1Ub2tlbml6ZXIoeCwgV2VrYV9jb250cm9sKG1pbiA9IDIsIG1heCA9IDIpKQ0KdGRtMiR0ZG0gPC0gVGVybURvY3VtZW50TWF0cml4KHN3aWZ0LCBjb250cm9sID0gbGlzdCh0b2tlbml6ZSA9IHRkbTIkdGtueikpDQp0ZG0yJHRkbSA8LSByZW1vdmVTcGFyc2VUZXJtcyh0ZG0yJHRkbSwgMC4xKQ0KDQojIEZpbmQgbW9zdCBmcmVxdWVudCB0ZXJtcw0KdGRtMiRtb3N0RnJlcSA8LSBmaW5kTW9zdEZyZXFUZXJtcyh0ZG0yJHRkbSwgMzApDQoNCiMgVGFidWxhdGUgbW9zdCBmcmVxdWVudCB0ZXJtcw0KdGRtMiR0YmxNb3N0RnJlcSA8LSBhcy5kYXRhLmZyYW1lKHRkbTIkbW9zdEZyZXEpDQp0ZG0yJHRibE1vc3RGcmVxJFRlcm0gPC0gcm93bmFtZXModGRtMiR0YmxNb3N0RnJlcSkNCnRkbTIkdGJsTW9zdEZyZXFNZWx0IDwtDQogICAgICBtZWx0KA0KICAgICAgICAgICAgdGRtMiR0YmxNb3N0RnJlcSwNCiAgICAgICAgICAgIGlkID0gYygiVGVybSIpLA0KICAgICAgICAgICAgbWVhc3VyZS52YXJzID0gY29sbmFtZXModGRtMiR0YmxNb3N0RnJlcVssMTozXSkNCiAgICAgICkNCmNvbG5hbWVzKHRkbTIkdGJsTW9zdEZyZXFNZWx0KSA8LSBjKCJUZXJtIiwgIkRvY3VtZW50IiwgIk9jY3VycmVuY2UiKQ0KYGBgDQoNCmBgYHtyIGJpZ3JhbV8yfQ0KDQojIFBsb3QgbW9zdCBmcmVxdWVudCB0ZXJtcw0KdGRtMiRwbG90MSA8LSBnZ3Bsb3QoZGF0YSA9IHRkbTIkdGJsTW9zdEZyZXFNZWx0LCANCiAgICAgICAgICAgICAgICAgICAgIGFlcyh4ID0gcmVvcmRlcihUZXJtLCBPY2N1cnJlbmNlKSwgeSA9IE9jY3VycmVuY2UsIGZpbGwgPSBEb2N1bWVudCkpICsNCiAgICAgIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArDQogICAgICBnZW9tX3RleHQoYWVzKGxhYmVsID0gT2NjdXJyZW5jZSksIHNpemUgPSAzLCBwb3NpdGlvbiA9IHBvc2l0aW9uX3N0YWNrKHZqdXN0ID0gMC41KSkgKw0KICAgICAgY29vcmRfZmxpcCgpICsNCiAgICAgIGxhYnModGl0bGUgPSAiQmktZ3JhbSB0ZXJtLWRvY3VtZW50IG1hdHJpeCBvZiBVUyBibG9ncywgbmV3cyBhbmQgVHdpdHRlciBkYXRhIiwgDQogICAgICAgICAgIHggPSAiVGVybXMiKQ0KdGRtMiRwbG90MQ0KDQpgYGANCg0KYGBge3IgdHJpZ3JhbV8xfQ0KDQp0ZG0zID0gbGlzdCgpDQoNCiMgc2V0IHRva2VuaXplciBvcHRpb25zDQp0ZG0zJHRrbnogPC0gZnVuY3Rpb24oeCkgTkdyYW1Ub2tlbml6ZXIoeCwgV2VrYV9jb250cm9sKG1pbiA9IDMsIG1heCA9IDMpKQ0KdGRtMyR0ZG0gPC0gVGVybURvY3VtZW50TWF0cml4KHN3aWZ0LCBjb250cm9sID0gbGlzdCh0b2tlbml6ZSA9IHRkbTMkdGtueikpDQp0ZG0zJHRkbSA8LSByZW1vdmVTcGFyc2VUZXJtcyh0ZG0zJHRkbSwgMC4xKQ0KDQojIEZpbmQgbW9zdCBmcmVxdWVudCB0ZXJtcw0KdGRtMyRtb3N0RnJlcSA8LSBmaW5kTW9zdEZyZXFUZXJtcyh0ZG0zJHRkbSwgMzApDQoNCiMgVGFidWxhdGUgbW9zdCBmcmVxdWVudCB0ZXJtcw0KdGRtMyR0YmxNb3N0RnJlcSA8LSBhcy5kYXRhLmZyYW1lKHRkbTMkbW9zdEZyZXEpDQp0ZG0zJHRibE1vc3RGcmVxJFRlcm0gPC0gcm93bmFtZXModGRtMyR0YmxNb3N0RnJlcSkNCnRkbTMkdGJsTW9zdEZyZXFNZWx0IDwtDQogICAgICBtZWx0KA0KICAgICAgICAgICAgdGRtMyR0YmxNb3N0RnJlcSwNCiAgICAgICAgICAgIGlkID0gYygiVGVybSIpLA0KICAgICAgICAgICAgbWVhc3VyZS52YXJzID0gY29sbmFtZXModGRtMyR0YmxNb3N0RnJlcVssMTozXSkNCiAgICAgICkNCmNvbG5hbWVzKHRkbTMkdGJsTW9zdEZyZXFNZWx0KSA8LSBjKCJUZXJtIiwgIkRvY3VtZW50IiwgIk9jY3VycmVuY2UiKQ0KYGBgDQoNCmBgYHtyIHRyaWdyYW1fMn0NCiMgUGxvdCBtb3N0IGZyZXF1ZW50IHRlcm1zDQp0ZG0zJHBsb3QxIDwtIGdncGxvdChkYXRhID0gdGRtMyR0YmxNb3N0RnJlcU1lbHQsIA0KICAgICAgICAgICAgICAgICAgICAgYWVzKHggPSByZW9yZGVyKFRlcm0sIE9jY3VycmVuY2UpLCB5ID0gT2NjdXJyZW5jZSwgZmlsbCA9IERvY3VtZW50KSkgKw0KICAgICAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsNCiAgICAgIGdlb21fdGV4dChhZXMobGFiZWwgPSBPY2N1cnJlbmNlKSwgc2l6ZSA9IDMsIHBvc2l0aW9uID0gcG9zaXRpb25fc3RhY2sodmp1c3QgPSAwLjUpKSArDQogICAgICBjb29yZF9mbGlwKCkgKw0KICAgICAgbGFicyh0aXRsZSA9ICJUcmktZ3JhbSB0ZXJtLWRvY3VtZW50IG1hdHJpeCBvZiBVUyBibG9ncywgbmV3cyBhbmQgVHdpdHRlciBkYXRhIiwgDQogICAgICAgICAgIHggPSAiVGVybXMiKQ0KdGRtMyRwbG90MQ0KDQpgYGANCg0KIyMjIyBQZXJjZW50IGNvdmVyYWdlDQoNCk5leHQsIHdlIHdpbGwgbG9vayBhdCBwZXJjZW50IGNvdmVyYWdlLCBtZWFuaW5nOiBIb3cgbWFueSB1bmlxdWUgd29yZHMgZm91bmQgaW4NCnRoZSB0ZXh0cyBtYWtlIHVwIGEgZ2l2ZW4gcGVyY2VudGlsZSBvZiBhbGwgd29yZCBpbnN0YW5jZXMgaW4gdGhlIGNvcnB1cz8NCg0KYGBge3IgcGVyY2VudF9jb3ZlcmFnZX0NCiMgSG93IG1hbnkgd29yZHMgY292ZXIgNTAlIG9mIHRoZSB0ZXh0PyBIb3cgbWFueSBjb3ZlciA5MCUNCg0KIyBBZGQgdXAgcm93IHN1bXMNCnRkbSR0YmxBbGwkdG90YWxPY2N1ciA8LSByb3dTdW1zKHRkbSR0YmxBbGxbLDE6M10pDQojIENhbGN1bGF0ZSBjdW11bGF0aXZlIHN1bXMNCnRkbSR0YmxBbGwkY3VtVG90YWxPY2N1ciA8LSBjdW1zdW0odGRtJHRibEFsbCR0b3RhbE9jY3VyKQ0KIyBDYWxjdWxhdGUgcGVyY2VudCBvZiBjb2x1bW4NCnRkbSR0YmxBbGwkcGVyY1VucSA8LSB0ZG0kdGJsQWxsJHRvdGFsT2NjdXIgLyBzdW0odGRtJHRibEFsbCR0b3RhbE9jY3VyKQ0KIyBDYWxjdWxhdGUgY3VtdWxhdGl2ZSBwZXJjZW50IG9mIGNvbHVtbg0KdGRtJHRibEFsbCRjdW1QZXJjVW5xIDwtIGN1bXN1bSh0ZG0kdGJsQWxsJHBlcmNVbnEpDQoNCiMgTG9vayB1cCBob3cgbWFueSB3b3JkcywgcGVyY2VudCBvZiB1bmlxdWUgd29yZHMsIGZvciBlYWNoIDEwIHBlcmNlbnQgb2Ygd29yZHMNCnRkbSRjb3ZlcmFnZSRwZXJjZW50aWxlcyA9IGRhdGEuZnJhbWUoKQ0KZm9yIChpIGluIDE6OSkgew0KICAgICAgdGRtJGNvdmVyYWdlJHBlcmNlbnRpbGVzW2ksIDFdIDwtIDEwKmkNCiAgICAgIHRkbSRjb3ZlcmFnZSRwZXJjZW50aWxlc1tpLCAyXSA8LQ0KICAgICAgICAgICAgd2hpY2goYWJzKHRkbSR0YmxBbGwkY3VtUGVyY1VucSAtIDAuMSAqIGkpID09IG1pbihhYnModGRtJHRibEFsbCRjdW1QZXJjVW5xIC0gMC4xICogaSkpKQ0KICAgICAgdGRtJGNvdmVyYWdlJHBlcmNlbnRpbGVzW2ksIDNdIDwtIHJvdW5kKDEwMCAqICh3aGljaCgNCiAgICAgICAgICAgIGFicyh0ZG0kdGJsQWxsJGN1bVBlcmNVbnEgLSAwLjEgKiBpKSA9PSBtaW4oYWJzKHRkbSR0YmxBbGwkY3VtUGVyY1VucSAtIDAuMSAqIGkpKQ0KICAgICAgICAgICAgKSAvIGxlbmd0aCh0ZG0kdGJsQWxsJHRvdGFsT2NjdXIpKSwgMikNCn0gDQpybShpKQ0KY29sbmFtZXModGRtJGNvdmVyYWdlJHBlcmNlbnRpbGVzKSA8LSBjKCJwZXJjV29yZHMiLCAibnVtV29yZHMiLCAicGVyY1VucVdvcmRzIikNCnRkbSRjb3ZlcmFnZSRwZXJjZW50aWxlcw0KYGBgDQoNCkdpdmVuIHRoZSBzYW1wbGVzIHdlIHRvb2ssIHdlIGZpbmQgdGhhdCA1MCUgb2YgYWxsIHdvcmRzIGluc3RhbmNlcyBpbiB0aGUgY29ycHVzIGFyZSBjb3ZlcmVkIGJ5IGByIHRkbSRjb3ZlcmFnZSRwZXJjZW50aWxlc1s1LDJdYCB1bmlxdWUgd29yZHMsIHdoaWNoIG1ha2UgdXAgYHIgdGRtJGNvdmVyYWdlJHBlcmNlbnRpbGVzWzUsM11gIHBlcmNlbnQgb2YgYWxsIHVuaXF1ZSB3b3Jkcy4gOTAlIG9mIGFsbCB3b3JkcyBpbiB0aGUgY29ycHVzIGFyZSBjb3ZlcmVkIGJ5IGByIHRkbSRjb3ZlcmFnZSRwZXJjZW50aWxlc1s5LDJdYCB1bmlxdWUgd29yZHMsIHdoaWNoIG1ha2UgdXAgYHIgdGRtJGNvdmVyYWdlJHBlcmNlbnRpbGVzWzksM11gIHBlcmNlbnQgb2YgYWxsIHVuaXF1ZSB3b3Jkcy4NCg0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KDQojIyMgRGF0YSBtb2RlbGluZyBhbmQgd29yZCBwcmVkaWN0aW9uDQoNCkZvciBkYXRhIG1vZGVsaW5nIGFuZCB3b3JkIHByZWRpY3Rpb24sIHRoZSBwbGFuIGlzIHRvIHVzZSB0aGUgZm9sbG93aW5nDQpzdHJhdGVneS4NCg0KKiBHZW5lcmF0ZSBhIE1hcmtvdiB0cmFuc2l0aW9uIHByb2JhYmlsaXR5IG1hdHJpeCBvciBzb21lIG90aGVyIGZvcm0gb2YNCmZyZXF1ZW5jeSB0YWJsZSBmb3IgdW5pZ3JhbXMsIGJpZ3JhbXMgYW5kIHRyaWdyYW1zIGZvdW5kIGluIHRoZSB0ZXh0IGZpbGVzIA0KKiBUbyBwcmVkaWN0IHRoZSBuZXh0IHdvcmQgZ2l2ZW4gYSBzZW50ZW5jZSwgd2UnbGwgc2VsZWN0IHRoZSBsYXN0IDIgd29yZHMsIGFuZA0KZnJvbSB0aGUgbWF0cml4IG9yIHRhYmxlcyBnZXQgd2hhdGV2ZXIgbmV4dCB3b3JkIGhhcyB0aGUgaGlnaGVzdCBmcmVxdWVuY3kNCm9ic2VydmVkDQoqIEluIGNhc2UgdGhlcmUgaXMgbm8gbWF0Y2ggYXQgYSB0cmlncmFtIGxldmVsLCB3ZSBmYWxsIGJhY2sgb24gYmlncmFtczsgaWYNCnRoZXJlIGFsc28gbm8gbWF0Y2ggYXQgYmlncmFtIGxldmVsLCB0aGVuIHdlIHJldHVybiB0aGUgbW9zdCBwcm9iYWJsZSB1bmlncmFtDQood2hpY2ggaXMgJ3RoZScpDQoNClNvbWUgY29uc2lkZXJhdGlvbnMgdG8gYmUgbWFkZSB3aGVuIGJ1aWxkaW5nIHRoZSBtb2RlbCBhcmU6IA0KDQoqIFdoYXQgaXMgdGhlIG1vc3QgZWZmaWNpZW50IHdheSB0byBidWlsZCBhbmQgc3RvcmUgdGhlIHRyYWluaW5nIHNldD8NCiogV2hhdCBpcyB0aGUgYmVzdCBiYWxhbmNlIGJldHdlZW4gbW9kZWwgYWNjdXJhY3kgYW5kIG1vZGVsIHBlcmZvcm1hbmNlPyBGb3IgDQpleGFtcGxlLCBob3cgbWFueSBzcGFyc2UgdGVybXMgY2FuIHdlIHJlbW92ZSBpbiBvcmRlciB0byByZXRhaW4gYWNjdXJhY3kgd2hpbGUNCmltcHJvdmluZyBtb2RlbCBwZXJmb3JtYW5jZT8gU2ltaWxhcmx5LCBhZGRpbmcgcXVhZC1ncmFtcyB0byB0aGUgbWF0cml4IG1heQ0KeWllbGQgYmV0dGVyIGFjY3VyYWN5IGluIHNvbWUgY2FzZXMsIGJ1dCBpcyBhbHNvIGNvbXB1dGF0aW9uYWxseSBtb3JlIGV4cGVuc2l2ZS4NCiogSG93IGRvIHdlIGV2YWx1YXRlIHRoZSBwcmVkaWN0aXZlIHBlcmZvcm1hbmNlIG9mIHRoZSBtb2RlbD8NCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0K