SUMMARY

The ultimate goal of the capstone project is to build a predictive text model. The data used for for this project include blogs, news and twitters. The final product will be a Shiny application which can be used to predict the next word(s) when the user inputs a phrase. This milestone report for the capstone project is intended to present the data preprocessing and exploratory data analysis and sketch out the plans for the Data Science Capstone project.

The reference text corpora is available at https://d396qusza40orc.cloudfront.net/dsscapstone/dataset/Coursera-SwiftKey.zip. Content archived from heliohost.org on September 30, 2016 and retrieved via Wayback Machine on April 24, 207. https://web-beta.archive.org/web/20160930083655/http://www.corpora.heliohost.org/aboutcorpus.html. The corpora are collected from publicly available sources by a web crawler.

Text Corpora Description

The given text corpora contains 3 files across four languages (Russian, Finnish, German and English). This project will focus on the English language datasets. The names of the data files are as follows:

  1. en_US.blogs.txt
  2. en_US.twitter.txt
  3. en_US.news.txt

Load Packages

library(plyr)
library(stringi)
library(NLP)
library(tm)
library(wordcloud)
library(RWeka)
library(stringr)
library(ggplot2)
library(grid)
library(gridExtra)

Download and unzip the reference Text Corpora

if(!file.exists("Coursera-SwiftKey.zip")){
    #Download the specified training data
    download.file("https://d396qusza40orc.cloudfront.net/dsscapstone/dataset/Coursera-SwiftKey.zip", "Coursera-SwiftKey.zip")
}else{
    print("Reference training data is already downloaded!")
}
[1] "Reference training data is already downloaded!"
## check if the file is unzipped. Unpack only if it doesn't exist 
if(!file.exists("./final/en_US")){
  print("Unzipping Coursera-SwiftKey.zip!")
  unzip("./Coursera-SwiftKey.zip")
}

Exploratory Analysis

  1. Summary statistics of original datasets
  2. Since the data size is fairly big, we will conduct analysis with small sample (say 1%).
  3. In the english language, some of the most common word in sentences are the likes of articles and prepositions. However, if we want to extract important messages from the texts, we should try to eliminate these less meaningful words.
  4. Profanity can be in our data and we would like to get rid of it. However, I believe sometimes people express their emotions through their use of language so it can also be useful to analyze it seperately.
  5. Create TermDocumentMatrix and subsequently perform 1-gram, 2-gram, and 3-gram analysis

Summary Statistics of the complete dataset

Here we are collecting suammry statistics on all English files without loading them into memory.

dataFiles <- dir("./final/en_US", full.names=TRUE)
datasets.stats <- ldply(dataFiles, function(ds) {
    files.name <- basename(ds)
    files.size <- as.integer(format(file.info(ds)$size/1024^2))
  files.linesCount <- as.integer(sub(" .*$", "", system(paste("wc -l", ds), intern=TRUE)))
  files.wordsCount <- as.integer(sub(" .*$", "", system(paste("wc -w", ds), intern=TRUE)))
  files.maxLineLen <- as.integer(sub(" .*$", "", system(paste("wc -L", ds), intern=TRUE)))
    
    data.frame(files.name, files.size, files.linesCount, files.wordsCount, files.maxLineLen)
})
colnames(datasets.stats) <- c("File", "File Size (MB)", "Lines", "Words", "Max length")
datasets.stats
               File File Size (MB)   Lines    Words Max length
1   en_US.blogs.txt            200  899288 37272578      40832
2    en_US.news.txt            196 1010242 34309642      11384
3 en_US.twitter.txt            159 2360148 30341028        140

Sampling small portion of training data for Exploratory Data Analysis

As all the text files are of relatively large size (~200 MB), hence we are trying to perform EDA on small portion (1%) of the training dataset

seed <- 1234
blogsSampleId <- as.logical(rbinom(datasets.stats$Lines[1], 1, prob=0.01))
newsSampleId <- as.logical(rbinom(datasets.stats$Lines[2], 1, prob=0.01))
twittersSampleId <- as.logical(rbinom(datasets.stats$Lines[3], 1, prob=0.01))
blogsSample <- readLines(dataFiles[1], encoding="UTF-8", skipNul=TRUE)[blogsSampleId]
newsSample <- readLines(dataFiles[2], encoding="UTF-8", skipNul=TRUE)[newsSampleId]
incomplete final line found on './final/en_US/en_US.news.txt'
twittersSample <- readLines(dataFiles[3], encoding="UTF-8", skipNul=TRUE)[twittersSampleId]

Summary statistics of samples data before data clearning

Note that lines count and words count are almost 1/10th of original dataset except for news words count which is somehow disproprotional which needs to be investigated later (mostly data cleaning would have removed huge presence of redundant terms)

samples.list <- list(blog = blogsSample, news = newsSample, twitter = twittersSample)
samples.df <- data.frame(text.source = c("blog", "news", "twitter"), lines.count = NA, words.count = NA)
samples.df$lines.count <- sapply(samples.list, length)
samples.df$words.count <- stri_count_words(samples.list)
print("Summary statistics of samples data before data clearning")
[1] "Summary statistics of samples data before data clearning"
samples.df
  text.source lines.count words.count
1        blog        8877      366961
2        news       10140       35974
3     twitter       23612      301322

Data cleaning to prepare text corpus

NOTE that even 1% sample has 10k documents and 30k terms on average across blogs/news/twitters datasets (including some large no of irrelevant terms). It is costly and uneccessary to identify all unique terms in raw sample, rather sample data must be cleaned before constructing BOW (bag of words) representation such as TermDocumentMatrix and subsequent processing.

Rationale behind different kind of data cleaning: -
- Punctuations: Punctuation characters such as commas, parentheses and the like have little or no impact on word order and n-gram composition.
- Stop Words: Stopwords are words so common that their information value is very low.
- ToLower: Words themselves - not their case - are important for prediction. Note that the resulting predictive model will predict lowercase words only. For English text this is less of an issue than for a language like German that uses capitalized nouns.
- http/ftp/mail uris & Twitter hashtags and handles: These social media specific terms are not helpful for word prediction.
- Profanity words: Words dictionary has been downloaded from https://www.freewebheaders.com/full-list-of-bad-words-banned-by-google/ and it has been used to remove their occurances.

cleanUri <- function(text){
        ## remove html/ftp url
        text <- gsub("?(f|ht)tp(s?)://(.*)[.][a-z]+", "", text) 
        ## remove e-mail adresses
        text <- gsub("[[:alnum:].-]+@[[:alnum:].-]+", "", text) 
        return(text)
}
cleanTwitterTagsHandles <- function(text){
        ## remove twitter handles
        text <- gsub("@\\S+", "", text)
        ## remove twitter hashtags
        text <- gsub("#\\S+", "", text)
        return(text)
}
samplesList <- list(blogs = blogsSample, news = newsSample, twitters = twittersSample)
samplesCorpus <- VCorpus(VectorSource(samplesList))
rm(samplesList)
# Remove URIs & Twitter hashtag and handles from all types of documents/messages
samplesCorpus <- tm_map(samplesCorpus, content_transformer(cleanUri))
samplesCorpus <- tm_map(samplesCorpus, content_transformer(cleanTwitterTagsHandles))
samplesCorpus <- tm_map(samplesCorpus, removeWords, stopwords("english"))
samplesCorpus <- tm_map(samplesCorpus, content_transformer(function(x) iconv(x, from="UTF-8", to="ASCII", sub="")))
samplesCorpus <- tm_map(samplesCorpus, content_transformer(tolower))
samplesCorpus <- tm_map(samplesCorpus, removePunctuation)
samplesCorpus <- tm_map(samplesCorpus, removeNumbers)
samplesCorpus <- tm_map(samplesCorpus, stripWhitespace)
# Remove profanity words as per https://www.freewebheaders.com/full-list-of-bad-words-banned-by-google/ 
samplesCorpus <- tm_map(samplesCorpus, removeWords, readLines("./badWords_2018_03_26.txt"))
incomplete final line found on './badWords_2018_03_26.txt'
# Apply stemming to the resulting corpus
samplesCorpus <- tm_map(samplesCorpus, stemDocument)

Text analysis for deriving n-gram

## Tokenizers
UnigramTokenizer <- function(x) NGramTokenizer(x, Weka_control(min = 1, max = 1))
BigramTokenizer <- function(x) NGramTokenizer(x, Weka_control(min = 2, max = 2))
TrigramTokenizer <- function(x) NGramTokenizer(x, Weka_control(min = 3, max = 3))
## TermDocumentMatrix for 1/2/3-grams
tdmBlogs_1gram = TermDocumentMatrix(samplesCorpus["blogs"], control = list(tokenize = UnigramTokenizer, wordLengths = c(3,Inf)))
tdmNews_1gram = TermDocumentMatrix(samplesCorpus["news"], control = list(tokenize = UnigramTokenizer, wordLengths = c(3,Inf)))
tdmTwitters_1gram = TermDocumentMatrix(samplesCorpus["twitters"], control = list(tokenize = UnigramTokenizer, wordLengths = c(3,Inf)))
tdmBlogs_2gram = TermDocumentMatrix(samplesCorpus["blogs"], control = list(tokenize = BigramTokenizer, wordLengths = c(3,Inf)))
tdmNews_2gram = TermDocumentMatrix(samplesCorpus["news"], control = list(tokenize = BigramTokenizer, wordLengths = c(3,Inf)))
tdmTwitters_2gram = TermDocumentMatrix(samplesCorpus["twitters"], control = list(tokenize = BigramTokenizer, wordLengths = c(3,Inf)))
tdmBlogs_3gram = TermDocumentMatrix(samplesCorpus["blogs"], control = list(tokenize = TrigramTokenizer, wordLengths = c(3,Inf)))
tdmNews_3gram = TermDocumentMatrix(samplesCorpus["news"], control = list(tokenize = TrigramTokenizer, wordLengths = c(3,Inf)))
tdmTwitters_3gram = TermDocumentMatrix(samplesCorpus["twitters"], control = list(tokenize = TrigramTokenizer, wordLengths = c(3,Inf)))

Extract most occuring 1/2/3-grams along with their frequency

# put word count from term-document matrices into data frames
wfmBlogs_1gram <- data.frame(word = tdmBlogs_1gram$dimnames$Terms, frequency = tdmBlogs_1gram$v)
wfmNews_1gram <- data.frame(word = tdmNews_1gram$dimnames$Terms, frequency = tdmNews_1gram$v)
wfmTwitters_1gram <- data.frame(word = tdmTwitters_1gram$dimnames$Terms, frequency = tdmTwitters_1gram$v)
wfmBlogs_2gram <- data.frame(word = tdmBlogs_2gram$dimnames$Terms, frequency = tdmBlogs_2gram$v)
wfmNews_2gram <- data.frame(word = tdmNews_2gram$dimnames$Terms, frequency = tdmNews_2gram$v)
wfmTwitters_2gram <- data.frame(word = tdmTwitters_2gram$dimnames$Terms, frequency = tdmTwitters_2gram$v)
wfmBlogs_3gram <- data.frame(word = tdmBlogs_3gram$dimnames$Terms, frequency = tdmBlogs_3gram$v)
wfmNews_3gram <- data.frame(word = tdmNews_3gram$dimnames$Terms, frequency = tdmNews_3gram$v)
wfmTwitters_3gram <- data.frame(word = tdmTwitters_3gram$dimnames$Terms, frequency = tdmTwitters_3gram$v)
# reorder by descreasing frequency
wfmBlogs_1gram <- plyr::arrange(wfmBlogs_1gram, -frequency)
wfmNews_1gram <- plyr::arrange(wfmNews_1gram, -frequency)
wfmTwitters_1gram <- plyr::arrange(wfmTwitters_1gram, -frequency)
wfmBlogs_2gram <- plyr::arrange(wfmBlogs_2gram, -frequency)
wfmNews_2gram <- plyr::arrange(wfmNews_2gram, -frequency)
wfmTwitters_2gram <- plyr::arrange(wfmTwitters_2gram, -frequency)
wfmBlogs_3gram <- plyr::arrange(wfmBlogs_3gram, -frequency)
wfmNews_3gram <- plyr::arrange(wfmNews_3gram, -frequency)
wfmTwitters_3gram <- plyr::arrange(wfmTwitters_3gram, -frequency)

Plot most frequent Unigrams

n <- 20L # show 20 most frequently occuring terms/words
# isolate top n words by decreasing frequency
blogs.top <- wfmBlogs_1gram[1:n, ]
news.top <- wfmNews_1gram[1:n, ]
twitters.top <- wfmTwitters_1gram[1:n, ]
blogs.top$word <- reorder(blogs.top$word, blogs.top$frequency)
news.top$word <- reorder(news.top$word, news.top$frequency)
twitters.top$word <- reorder(twitters.top$word, twitters.top$frequency)
# plots
g.blogs.top <- ggplot(blogs.top, aes(x = word, y = frequency))
g.blogs.top <- g.blogs.top + geom_bar(stat = "identity") + coord_flip() +
  labs(title = "Uni-grams in Blogs")
g.news.top <- ggplot(news.top, aes(x = word, y = frequency))
g.news.top <- g.news.top + geom_bar(stat = "identity") + coord_flip() +
  labs(title = "Uni-grams in News")
g.twitters.top <- ggplot(twitters.top, aes(x = word, y = frequency))
g.twitters.top <- g.twitters.top + geom_bar(stat = "identity") + coord_flip() + 
  labs(title = "Uni-grams in Twitters")
grid.arrange(g.blogs.top, g.news.top, g.twitters.top, ncol = 3)

Plot most frequent Bigrams

# isolate top n words by decreasing frequency
blogs.top <- wfmBlogs_2gram[1:n, ]
news.top <- wfmNews_2gram[1:n, ]
twitters.top <- wfmTwitters_2gram[1:n, ]
blogs.top$word <- reorder(blogs.top$word, blogs.top$frequency)
news.top$word <- reorder(news.top$word, news.top$frequency)
twitters.top$word <- reorder(twitters.top$word, twitters.top$frequency)
# plots
g.blogs.top <- ggplot(blogs.top, aes(x = word, y = frequency))
g.blogs.top <- g.blogs.top + geom_bar(stat = "identity") + coord_flip() +
  labs(title = "Bigrams in Blogs")
g.news.top <- ggplot(news.top, aes(x = word, y = frequency))
g.news.top <- g.news.top + geom_bar(stat = "identity") + coord_flip() +
  labs(title = "Bigrams in News")
g.twitters.top <- ggplot(twitters.top, aes(x = word, y = frequency))
g.twitters.top <- g.twitters.top + geom_bar(stat = "identity") + coord_flip() + 
  labs(title = "Bigrams in Twitters")
grid.arrange(g.blogs.top, g.news.top, g.twitters.top, ncol = 3)

Plot most frequent Trigrams

# isolate top n words by decreasing frequency
blogs.top <- wfmBlogs_3gram[1:n, ]
news.top <- wfmNews_3gram[1:n, ]
twitters.top <- wfmTwitters_3gram[1:n, ]
blogs.top$word <- reorder(blogs.top$word, blogs.top$frequency)
news.top$word <- reorder(news.top$word, news.top$frequency)
twitters.top$word <- reorder(twitters.top$word, twitters.top$frequency)
# plots
g.blogs.top <- ggplot(blogs.top, aes(x = word, y = frequency))
g.blogs.top <- g.blogs.top + geom_bar(stat = "identity") + coord_flip() +
  labs(title = "Trigrams in Blogs")
g.news.top <- ggplot(news.top, aes(x = word, y = frequency))
g.news.top <- g.news.top + geom_bar(stat = "identity") + coord_flip() +
  labs(title = "Trigrams in News")
g.twitters.top <- ggplot(twitters.top, aes(x = word, y = frequency))
g.twitters.top <- g.twitters.top + geom_bar(stat = "identity") + coord_flip() + 
  labs(title = "Trigrams in Twitters")
grid.arrange(g.blogs.top, g.news.top, g.twitters.top, ncol = 3)

Next Steps…

The plan for completing the Capstone Project would mostly involve the following tasks: -
- Create Shiny app
- Plan for the approach to archive learning/training of either currently known basic n-gram or its better alternative; This will mostly involve ensuring to quantify probability and confidence interval of proposed word predictions
- Profile and optimize the solution to achieve good performance and accuracy
- Fix stopwords not removing common most frequent words like ‘I’, ‘the’, etc
- Find the impact/usefulness of existing data cleaning steps and revise to achieve better performance and accuracy in modeling and prediction
- Investigate if any dimentionality / sparsity reduction would be feasible and helpful
- Identify appropriate training size to balance bais and variance
- Save sample training dataset to disk to avoid loading full training dataset
determine the best data structure in R to store NGram language models - Make the code terse

LS0tDQp0aXRsZTogIk1pbGVzdG9uZSBSZXBvcnQgZm9yIERhdGEgU2NpZW5jZSBDYXBzdG9uZSBQcm9qZWN0Ig0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobz1UUlVFKQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGNhY2hlPVRSVUUpDQpgYGANCiANCiMjU1VNTUFSWSANClRoZSB1bHRpbWF0ZSBnb2FsIG9mIHRoZSBjYXBzdG9uZSBwcm9qZWN0IGlzIHRvIGJ1aWxkIGEgcHJlZGljdGl2ZSB0ZXh0IG1vZGVsLiBUaGUgZGF0YSB1c2VkIGZvciBmb3IgdGhpcyBwcm9qZWN0IGluY2x1ZGUgYmxvZ3MsIG5ld3MgYW5kIHR3aXR0ZXJzLiBUaGUgZmluYWwgcHJvZHVjdCB3aWxsIGJlIGEgU2hpbnkgYXBwbGljYXRpb24gd2hpY2ggY2FuIGJlIHVzZWQgdG8gcHJlZGljdCB0aGUgbmV4dCB3b3JkKHMpIHdoZW4gdGhlIHVzZXIgaW5wdXRzIGEgcGhyYXNlLiBUaGlzIG1pbGVzdG9uZSByZXBvcnQgZm9yIHRoZSBjYXBzdG9uZSBwcm9qZWN0IGlzIGludGVuZGVkIHRvIHByZXNlbnQgdGhlIGRhdGEgcHJlcHJvY2Vzc2luZyBhbmQgZXhwbG9yYXRvcnkgZGF0YSBhbmFseXNpcyBhbmQgc2tldGNoIG91dCB0aGUgcGxhbnMgZm9yIHRoZSBEYXRhIFNjaWVuY2UgQ2Fwc3RvbmUgcHJvamVjdC4gIA0KDQpUaGUgcmVmZXJlbmNlIHRleHQgY29ycG9yYSBpcyBhdmFpbGFibGUgYXQgaHR0cHM6Ly9kMzk2cXVzemE0MG9yYy5jbG91ZGZyb250Lm5ldC9kc3NjYXBzdG9uZS9kYXRhc2V0L0NvdXJzZXJhLVN3aWZ0S2V5LnppcC4gQ29udGVudCBhcmNoaXZlZCBmcm9tIGhlbGlvaG9zdC5vcmcgb24gU2VwdGVtYmVyIDMwLCAyMDE2IGFuZCByZXRyaWV2ZWQgdmlhIFdheWJhY2sgTWFjaGluZSBvbiBBcHJpbCAyNCwgMjA3LiBodHRwczovL3dlYi1iZXRhLmFyY2hpdmUub3JnL3dlYi8yMDE2MDkzMDA4MzY1NS9odHRwOi8vd3d3LmNvcnBvcmEuaGVsaW9ob3N0Lm9yZy9hYm91dGNvcnB1cy5odG1sLiBUaGUgY29ycG9yYSBhcmUgY29sbGVjdGVkIGZyb20gcHVibGljbHkgYXZhaWxhYmxlIHNvdXJjZXMgYnkgYSB3ZWIgY3Jhd2xlci4NCiAgIA0KIyNUZXh0IENvcnBvcmEgRGVzY3JpcHRpb24NClRoZSBnaXZlbiB0ZXh0IGNvcnBvcmEgY29udGFpbnMgMyBmaWxlcyBhY3Jvc3MgZm91ciBsYW5ndWFnZXMgKFJ1c3NpYW4sIEZpbm5pc2gsIEdlcm1hbiBhbmQgRW5nbGlzaCkuIFRoaXMgcHJvamVjdCB3aWxsIGZvY3VzIG9uIHRoZSBFbmdsaXNoIGxhbmd1YWdlIGRhdGFzZXRzLiBUaGUgbmFtZXMgb2YgdGhlIGRhdGEgZmlsZXMgYXJlIGFzIGZvbGxvd3M6IA0KICANCjEuIGVuX1VTLmJsb2dzLnR4dCAgDQoyLiBlbl9VUy50d2l0dGVyLnR4dCAgDQozLiBlbl9VUy5uZXdzLnR4dCAgDQogIA0KIyNMb2FkIFBhY2thZ2VzDQpgYGB7cn0NCmxpYnJhcnkocGx5cikNCmxpYnJhcnkoc3RyaW5naSkNCmxpYnJhcnkoTkxQKQ0KbGlicmFyeSh0bSkNCmxpYnJhcnkod29yZGNsb3VkKQ0KbGlicmFyeShSV2VrYSkNCmxpYnJhcnkoc3RyaW5ncikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoZ3JpZCkNCmxpYnJhcnkoZ3JpZEV4dHJhKQ0KYGBgDQogIA0KIyNEb3dubG9hZCBhbmQgdW56aXAgdGhlIHJlZmVyZW5jZSBUZXh0IENvcnBvcmEgDQpgYGB7cn0NCmlmKCFmaWxlLmV4aXN0cygiQ291cnNlcmEtU3dpZnRLZXkuemlwIikpew0KICAgICNEb3dubG9hZCB0aGUgc3BlY2lmaWVkIHRyYWluaW5nIGRhdGENCiAgICBkb3dubG9hZC5maWxlKCJodHRwczovL2QzOTZxdXN6YTQwb3JjLmNsb3VkZnJvbnQubmV0L2Rzc2NhcHN0b25lL2RhdGFzZXQvQ291cnNlcmEtU3dpZnRLZXkuemlwIiwgIkNvdXJzZXJhLVN3aWZ0S2V5LnppcCIpDQp9ZWxzZXsNCiAgICBwcmludCgiUmVmZXJlbmNlIHRyYWluaW5nIGRhdGEgaXMgYWxyZWFkeSBkb3dubG9hZGVkISIpDQp9DQoNCiMjIGNoZWNrIGlmIHRoZSBmaWxlIGlzIHVuemlwcGVkLiBVbnBhY2sgb25seSBpZiBpdCBkb2Vzbid0IGV4aXN0IA0KaWYoIWZpbGUuZXhpc3RzKCIuL2ZpbmFsL2VuX1VTIikpew0KICBwcmludCgiVW56aXBwaW5nIENvdXJzZXJhLVN3aWZ0S2V5LnppcCEiKQ0KICB1bnppcCgiLi9Db3Vyc2VyYS1Td2lmdEtleS56aXAiKQ0KfQ0KYGBgDQoNCiMjRXhwbG9yYXRvcnkgQW5hbHlzaXMgIA0KMS4gU3VtbWFyeSBzdGF0aXN0aWNzIG9mIG9yaWdpbmFsIGRhdGFzZXRzICANCjIuIFNpbmNlIHRoZSBkYXRhIHNpemUgaXMgZmFpcmx5IGJpZywgd2Ugd2lsbCBjb25kdWN0IGFuYWx5c2lzIHdpdGggc21hbGwgc2FtcGxlIChzYXkgMSUpLiAgDQozLiBJbiB0aGUgZW5nbGlzaCBsYW5ndWFnZSwgc29tZSBvZiB0aGUgbW9zdCBjb21tb24gd29yZCBpbiBzZW50ZW5jZXMgYXJlIHRoZSBsaWtlcyBvZiBhcnRpY2xlcyBhbmQgcHJlcG9zaXRpb25zLiBIb3dldmVyLCBpZiB3ZSB3YW50IHRvIGV4dHJhY3QgaW1wb3J0YW50IG1lc3NhZ2VzIGZyb20gdGhlIHRleHRzLCB3ZSBzaG91bGQgdHJ5IHRvIGVsaW1pbmF0ZSB0aGVzZSBsZXNzIG1lYW5pbmdmdWwgd29yZHMuICANCjQuIFByb2Zhbml0eSBjYW4gYmUgaW4gb3VyIGRhdGEgYW5kIHdlIHdvdWxkIGxpa2UgdG8gZ2V0IHJpZCBvZiBpdC4gSG93ZXZlciwgSSBiZWxpZXZlIHNvbWV0aW1lcyBwZW9wbGUgZXhwcmVzcyB0aGVpciBlbW90aW9ucyB0aHJvdWdoIHRoZWlyIHVzZSBvZiBsYW5ndWFnZSBzbyBpdCBjYW4gYWxzbyBiZSB1c2VmdWwgdG8gYW5hbHl6ZSBpdCBzZXBlcmF0ZWx5LiAgDQo1LiBDcmVhdGUgVGVybURvY3VtZW50TWF0cml4IGFuZCBzdWJzZXF1ZW50bHkgcGVyZm9ybSAxLWdyYW0sIDItZ3JhbSwgYW5kIDMtZ3JhbSBhbmFseXNpcyAgDQoNCiMjI1N1bW1hcnkgU3RhdGlzdGljcyBvZiB0aGUgY29tcGxldGUgZGF0YXNldCAgDQpIZXJlIHdlIGFyZSBjb2xsZWN0aW5nIHN1YW1tcnkgc3RhdGlzdGljcyBvbiBhbGwgRW5nbGlzaCBmaWxlcyB3aXRob3V0IGxvYWRpbmcgdGhlbSBpbnRvIG1lbW9yeS4gIA0KYGBge3J9DQpkYXRhRmlsZXMgPC0gZGlyKCIuL2ZpbmFsL2VuX1VTIiwgZnVsbC5uYW1lcz1UUlVFKQ0KZGF0YXNldHMuc3RhdHMgPC0gbGRwbHkoZGF0YUZpbGVzLCBmdW5jdGlvbihkcykgew0KCWZpbGVzLm5hbWUgPC0gYmFzZW5hbWUoZHMpDQoJZmlsZXMuc2l6ZSA8LSBhcy5pbnRlZ2VyKGZvcm1hdChmaWxlLmluZm8oZHMpJHNpemUvMTAyNF4yKSkNCiAgZmlsZXMubGluZXNDb3VudCA8LSBhcy5pbnRlZ2VyKHN1YigiIC4qJCIsICIiLCBzeXN0ZW0ocGFzdGUoIndjIC1sIiwgZHMpLCBpbnRlcm49VFJVRSkpKQ0KICBmaWxlcy53b3Jkc0NvdW50IDwtIGFzLmludGVnZXIoc3ViKCIgLiokIiwgIiIsIHN5c3RlbShwYXN0ZSgid2MgLXciLCBkcyksIGludGVybj1UUlVFKSkpDQogIGZpbGVzLm1heExpbmVMZW4gPC0gYXMuaW50ZWdlcihzdWIoIiAuKiQiLCAiIiwgc3lzdGVtKHBhc3RlKCJ3YyAtTCIsIGRzKSwgaW50ZXJuPVRSVUUpKSkNCgkNCglkYXRhLmZyYW1lKGZpbGVzLm5hbWUsIGZpbGVzLnNpemUsIGZpbGVzLmxpbmVzQ291bnQsIGZpbGVzLndvcmRzQ291bnQsIGZpbGVzLm1heExpbmVMZW4pDQp9KQ0KY29sbmFtZXMoZGF0YXNldHMuc3RhdHMpIDwtIGMoIkZpbGUiLCAiRmlsZSBTaXplIChNQikiLCAiTGluZXMiLCAiV29yZHMiLCAiTWF4IGxlbmd0aCIpDQoNCmRhdGFzZXRzLnN0YXRzDQpgYGANCg0KIyMjU2FtcGxpbmcgc21hbGwgcG9ydGlvbiBvZiB0cmFpbmluZyBkYXRhIGZvciBFeHBsb3JhdG9yeSBEYXRhIEFuYWx5c2lzICANCkFzIGFsbCB0aGUgdGV4dCBmaWxlcyBhcmUgb2YgcmVsYXRpdmVseSBsYXJnZSBzaXplICh+MjAwIE1CKSwgaGVuY2Ugd2UgYXJlIHRyeWluZyB0byBwZXJmb3JtIEVEQSBvbiBzbWFsbCBwb3J0aW9uICgxJSkgb2YgdGhlIHRyYWluaW5nIGRhdGFzZXQgIA0KYGBge3J9DQpzZWVkIDwtIDEyMzQNCmJsb2dzU2FtcGxlSWQgPC0gYXMubG9naWNhbChyYmlub20oZGF0YXNldHMuc3RhdHMkTGluZXNbMV0sIDEsIHByb2I9MC4wMSkpDQpuZXdzU2FtcGxlSWQgPC0gYXMubG9naWNhbChyYmlub20oZGF0YXNldHMuc3RhdHMkTGluZXNbMl0sIDEsIHByb2I9MC4wMSkpDQp0d2l0dGVyc1NhbXBsZUlkIDwtIGFzLmxvZ2ljYWwocmJpbm9tKGRhdGFzZXRzLnN0YXRzJExpbmVzWzNdLCAxLCBwcm9iPTAuMDEpKQ0KDQpibG9nc1NhbXBsZSA8LSByZWFkTGluZXMoZGF0YUZpbGVzWzFdLCBlbmNvZGluZz0iVVRGLTgiLCBza2lwTnVsPVRSVUUpW2Jsb2dzU2FtcGxlSWRdDQpuZXdzU2FtcGxlIDwtIHJlYWRMaW5lcyhkYXRhRmlsZXNbMl0sIGVuY29kaW5nPSJVVEYtOCIsIHNraXBOdWw9VFJVRSlbbmV3c1NhbXBsZUlkXQ0KdHdpdHRlcnNTYW1wbGUgPC0gcmVhZExpbmVzKGRhdGFGaWxlc1szXSwgZW5jb2Rpbmc9IlVURi04Iiwgc2tpcE51bD1UUlVFKVt0d2l0dGVyc1NhbXBsZUlkXQ0KYGBgDQoNCiMjI1N1bW1hcnkgc3RhdGlzdGljcyBvZiBzYW1wbGVzIGRhdGEgYmVmb3JlIGRhdGEgY2xlYXJuaW5nICANCk5vdGUgdGhhdCBsaW5lcyBjb3VudCBhbmQgd29yZHMgY291bnQgYXJlIGFsbW9zdCAxLzEwdGggb2Ygb3JpZ2luYWwgZGF0YXNldCBleGNlcHQgZm9yIG5ld3Mgd29yZHMgY291bnQgd2hpY2ggaXMgc29tZWhvdyAgZGlzcHJvcHJvdGlvbmFsIHdoaWNoIG5lZWRzIHRvIGJlIGludmVzdGlnYXRlZCBsYXRlciAobW9zdGx5IGRhdGEgY2xlYW5pbmcgd291bGQgaGF2ZSByZW1vdmVkIGh1Z2UgcHJlc2VuY2Ugb2YgcmVkdW5kYW50IHRlcm1zKSAgDQpgYGB7cn0NCnNhbXBsZXMubGlzdCA8LSBsaXN0KGJsb2cgPSBibG9nc1NhbXBsZSwgbmV3cyA9IG5ld3NTYW1wbGUsIHR3aXR0ZXIgPSB0d2l0dGVyc1NhbXBsZSkNCg0Kc2FtcGxlcy5kZiA8LSBkYXRhLmZyYW1lKHRleHQuc291cmNlID0gYygiYmxvZyIsICJuZXdzIiwgInR3aXR0ZXIiKSwgbGluZXMuY291bnQgPSBOQSwgd29yZHMuY291bnQgPSBOQSkNCnNhbXBsZXMuZGYkbGluZXMuY291bnQgPC0gc2FwcGx5KHNhbXBsZXMubGlzdCwgbGVuZ3RoKQ0Kc2FtcGxlcy5kZiR3b3Jkcy5jb3VudCA8LSBzdHJpX2NvdW50X3dvcmRzKHNhbXBsZXMubGlzdCkNCg0KcHJpbnQoIlN1bW1hcnkgc3RhdGlzdGljcyBvZiBzYW1wbGVzIGRhdGEgYmVmb3JlIGRhdGEgY2xlYXJuaW5nIikNCnNhbXBsZXMuZGYNCmBgYA0KDQojIyNEYXRhIGNsZWFuaW5nIHRvIHByZXBhcmUgdGV4dCBjb3JwdXMNCk5PVEUgdGhhdCBldmVuIDElIHNhbXBsZSBoYXMgMTBrIGRvY3VtZW50cyBhbmQgMzBrIHRlcm1zIG9uIGF2ZXJhZ2UgYWNyb3NzIGJsb2dzL25ld3MvdHdpdHRlcnMgZGF0YXNldHMgKGluY2x1ZGluZyBzb21lIGxhcmdlIG5vIG9mIGlycmVsZXZhbnQgdGVybXMpLiBJdCBpcyBjb3N0bHkgYW5kIHVuZWNjZXNzYXJ5IHRvIGlkZW50aWZ5IGFsbCB1bmlxdWUgdGVybXMgaW4gcmF3IHNhbXBsZSwgcmF0aGVyIHNhbXBsZSBkYXRhIG11c3QgYmUgY2xlYW5lZCBiZWZvcmUgY29uc3RydWN0aW5nIEJPVyAoYmFnIG9mIHdvcmRzKSByZXByZXNlbnRhdGlvbiBzdWNoIGFzIFRlcm1Eb2N1bWVudE1hdHJpeCBhbmQgc3Vic2VxdWVudCBwcm9jZXNzaW5nLiAgDQogICANClJhdGlvbmFsZSBiZWhpbmQgZGlmZmVyZW50IGtpbmQgb2YgZGF0YSBjbGVhbmluZzogLSAgDQotIFB1bmN0dWF0aW9uczogUHVuY3R1YXRpb24gY2hhcmFjdGVycyBzdWNoIGFzIGNvbW1hcywgcGFyZW50aGVzZXMgYW5kIHRoZSBsaWtlIGhhdmUgbGl0dGxlIG9yIG5vIGltcGFjdCBvbiB3b3JkIG9yZGVyIGFuZCBuLWdyYW0gY29tcG9zaXRpb24uICANCi0gU3RvcCBXb3JkczogU3RvcHdvcmRzIGFyZSB3b3JkcyBzbyBjb21tb24gdGhhdCB0aGVpciBpbmZvcm1hdGlvbiB2YWx1ZSBpcyB2ZXJ5IGxvdy4gIA0KLSBUb0xvd2VyOiBXb3JkcyB0aGVtc2VsdmVzIC0gbm90IHRoZWlyIGNhc2UgLSBhcmUgaW1wb3J0YW50IGZvciBwcmVkaWN0aW9uLiBOb3RlIHRoYXQgdGhlIHJlc3VsdGluZyBwcmVkaWN0aXZlIG1vZGVsIHdpbGwgcHJlZGljdCBsb3dlcmNhc2Ugd29yZHMgb25seS4gRm9yIEVuZ2xpc2ggdGV4dCB0aGlzIGlzIGxlc3Mgb2YgYW4gaXNzdWUgdGhhbiBmb3IgYSBsYW5ndWFnZSBsaWtlIEdlcm1hbiB0aGF0IHVzZXMgY2FwaXRhbGl6ZWQgbm91bnMuICANCi0gaHR0cC9mdHAvbWFpbCB1cmlzICYgVHdpdHRlciBoYXNodGFncyBhbmQgaGFuZGxlczogVGhlc2Ugc29jaWFsIG1lZGlhIHNwZWNpZmljIHRlcm1zIGFyZSBub3QgaGVscGZ1bCBmb3Igd29yZCBwcmVkaWN0aW9uLiAgDQotIFByb2Zhbml0eSB3b3JkczogV29yZHMgZGljdGlvbmFyeSBoYXMgYmVlbiBkb3dubG9hZGVkIGZyb20gaHR0cHM6Ly93d3cuZnJlZXdlYmhlYWRlcnMuY29tL2Z1bGwtbGlzdC1vZi1iYWQtd29yZHMtYmFubmVkLWJ5LWdvb2dsZS8gYW5kIGl0IGhhcyBiZWVuIHVzZWQgdG8gcmVtb3ZlIHRoZWlyIG9jY3VyYW5jZXMuICANCg0KYGBge3J9DQpjbGVhblVyaSA8LSBmdW5jdGlvbih0ZXh0KXsNCiAgICAgICAgIyMgcmVtb3ZlIGh0bWwvZnRwIHVybA0KICAgICAgICB0ZXh0IDwtIGdzdWIoIj8oZnxodCl0cChzPyk6Ly8oLiopWy5dW2Etel0rIiwgIiIsIHRleHQpIA0KICAgICAgICAjIyByZW1vdmUgZS1tYWlsIGFkcmVzc2VzDQogICAgICAgIHRleHQgPC0gZ3N1YigiW1s6YWxudW06XS4tXStAW1s6YWxudW06XS4tXSsiLCAiIiwgdGV4dCkgDQogICAgICAgIHJldHVybih0ZXh0KQ0KfQ0KDQpjbGVhblR3aXR0ZXJUYWdzSGFuZGxlcyA8LSBmdW5jdGlvbih0ZXh0KXsNCiAgICAgICAgIyMgcmVtb3ZlIHR3aXR0ZXIgaGFuZGxlcw0KICAgICAgICB0ZXh0IDwtIGdzdWIoIkBcXFMrIiwgIiIsIHRleHQpDQogICAgICAgICMjIHJlbW92ZSB0d2l0dGVyIGhhc2h0YWdzDQogICAgICAgIHRleHQgPC0gZ3N1YigiI1xcUysiLCAiIiwgdGV4dCkNCiAgICAgICAgcmV0dXJuKHRleHQpDQp9DQoNCnNhbXBsZXNMaXN0IDwtIGxpc3QoYmxvZ3MgPSBibG9nc1NhbXBsZSwgbmV3cyA9IG5ld3NTYW1wbGUsIHR3aXR0ZXJzID0gdHdpdHRlcnNTYW1wbGUpDQpzYW1wbGVzQ29ycHVzIDwtIFZDb3JwdXMoVmVjdG9yU291cmNlKHNhbXBsZXNMaXN0KSkNCnJtKHNhbXBsZXNMaXN0KQ0KDQojIFJlbW92ZSBVUklzICYgVHdpdHRlciBoYXNodGFnIGFuZCBoYW5kbGVzIGZyb20gYWxsIHR5cGVzIG9mIGRvY3VtZW50cy9tZXNzYWdlcw0Kc2FtcGxlc0NvcnB1cyA8LSB0bV9tYXAoc2FtcGxlc0NvcnB1cywgY29udGVudF90cmFuc2Zvcm1lcihjbGVhblVyaSkpDQpzYW1wbGVzQ29ycHVzIDwtIHRtX21hcChzYW1wbGVzQ29ycHVzLCBjb250ZW50X3RyYW5zZm9ybWVyKGNsZWFuVHdpdHRlclRhZ3NIYW5kbGVzKSkNCg0Kc2FtcGxlc0NvcnB1cyA8LSB0bV9tYXAoc2FtcGxlc0NvcnB1cywgcmVtb3ZlV29yZHMsIHN0b3B3b3JkcygiZW5nbGlzaCIpKQ0Kc2FtcGxlc0NvcnB1cyA8LSB0bV9tYXAoc2FtcGxlc0NvcnB1cywgY29udGVudF90cmFuc2Zvcm1lcihmdW5jdGlvbih4KSBpY29udih4LCBmcm9tPSJVVEYtOCIsIHRvPSJBU0NJSSIsIHN1Yj0iIikpKQ0Kc2FtcGxlc0NvcnB1cyA8LSB0bV9tYXAoc2FtcGxlc0NvcnB1cywgY29udGVudF90cmFuc2Zvcm1lcih0b2xvd2VyKSkNCnNhbXBsZXNDb3JwdXMgPC0gdG1fbWFwKHNhbXBsZXNDb3JwdXMsIHJlbW92ZVB1bmN0dWF0aW9uKQ0Kc2FtcGxlc0NvcnB1cyA8LSB0bV9tYXAoc2FtcGxlc0NvcnB1cywgcmVtb3ZlTnVtYmVycykNCnNhbXBsZXNDb3JwdXMgPC0gdG1fbWFwKHNhbXBsZXNDb3JwdXMsIHN0cmlwV2hpdGVzcGFjZSkNCg0KIyBSZW1vdmUgcHJvZmFuaXR5IHdvcmRzIGFzIHBlciBodHRwczovL3d3dy5mcmVld2ViaGVhZGVycy5jb20vZnVsbC1saXN0LW9mLWJhZC13b3Jkcy1iYW5uZWQtYnktZ29vZ2xlLyANCnNhbXBsZXNDb3JwdXMgPC0gdG1fbWFwKHNhbXBsZXNDb3JwdXMsIHJlbW92ZVdvcmRzLCByZWFkTGluZXMoIi4vYmFkV29yZHNfMjAxOF8wM18yNi50eHQiKSkNCg0KIyBBcHBseSBzdGVtbWluZyB0byB0aGUgcmVzdWx0aW5nIGNvcnB1cw0Kc2FtcGxlc0NvcnB1cyA8LSB0bV9tYXAoc2FtcGxlc0NvcnB1cywgc3RlbURvY3VtZW50KQ0KDQpgYGANCg0KIyMjVGV4dCBhbmFseXNpcyBmb3IgZGVyaXZpbmcgbi1ncmFtDQpgYGB7cn0NCg0KIyMgVG9rZW5pemVycw0KVW5pZ3JhbVRva2VuaXplciA8LSBmdW5jdGlvbih4KSBOR3JhbVRva2VuaXplcih4LCBXZWthX2NvbnRyb2wobWluID0gMSwgbWF4ID0gMSkpDQpCaWdyYW1Ub2tlbml6ZXIgPC0gZnVuY3Rpb24oeCkgTkdyYW1Ub2tlbml6ZXIoeCwgV2VrYV9jb250cm9sKG1pbiA9IDIsIG1heCA9IDIpKQ0KVHJpZ3JhbVRva2VuaXplciA8LSBmdW5jdGlvbih4KSBOR3JhbVRva2VuaXplcih4LCBXZWthX2NvbnRyb2wobWluID0gMywgbWF4ID0gMykpDQoNCiMjIFRlcm1Eb2N1bWVudE1hdHJpeCBmb3IgMS8yLzMtZ3JhbXMNCnRkbUJsb2dzXzFncmFtID0gVGVybURvY3VtZW50TWF0cml4KHNhbXBsZXNDb3JwdXNbImJsb2dzIl0sIGNvbnRyb2wgPSBsaXN0KHRva2VuaXplID0gVW5pZ3JhbVRva2VuaXplciwgd29yZExlbmd0aHMgPSBjKDMsSW5mKSkpDQp0ZG1OZXdzXzFncmFtID0gVGVybURvY3VtZW50TWF0cml4KHNhbXBsZXNDb3JwdXNbIm5ld3MiXSwgY29udHJvbCA9IGxpc3QodG9rZW5pemUgPSBVbmlncmFtVG9rZW5pemVyLCB3b3JkTGVuZ3RocyA9IGMoMyxJbmYpKSkNCnRkbVR3aXR0ZXJzXzFncmFtID0gVGVybURvY3VtZW50TWF0cml4KHNhbXBsZXNDb3JwdXNbInR3aXR0ZXJzIl0sIGNvbnRyb2wgPSBsaXN0KHRva2VuaXplID0gVW5pZ3JhbVRva2VuaXplciwgd29yZExlbmd0aHMgPSBjKDMsSW5mKSkpDQoNCnRkbUJsb2dzXzJncmFtID0gVGVybURvY3VtZW50TWF0cml4KHNhbXBsZXNDb3JwdXNbImJsb2dzIl0sIGNvbnRyb2wgPSBsaXN0KHRva2VuaXplID0gQmlncmFtVG9rZW5pemVyLCB3b3JkTGVuZ3RocyA9IGMoMyxJbmYpKSkNCnRkbU5ld3NfMmdyYW0gPSBUZXJtRG9jdW1lbnRNYXRyaXgoc2FtcGxlc0NvcnB1c1sibmV3cyJdLCBjb250cm9sID0gbGlzdCh0b2tlbml6ZSA9IEJpZ3JhbVRva2VuaXplciwgd29yZExlbmd0aHMgPSBjKDMsSW5mKSkpDQp0ZG1Ud2l0dGVyc18yZ3JhbSA9IFRlcm1Eb2N1bWVudE1hdHJpeChzYW1wbGVzQ29ycHVzWyJ0d2l0dGVycyJdLCBjb250cm9sID0gbGlzdCh0b2tlbml6ZSA9IEJpZ3JhbVRva2VuaXplciwgd29yZExlbmd0aHMgPSBjKDMsSW5mKSkpDQoNCnRkbUJsb2dzXzNncmFtID0gVGVybURvY3VtZW50TWF0cml4KHNhbXBsZXNDb3JwdXNbImJsb2dzIl0sIGNvbnRyb2wgPSBsaXN0KHRva2VuaXplID0gVHJpZ3JhbVRva2VuaXplciwgd29yZExlbmd0aHMgPSBjKDMsSW5mKSkpDQp0ZG1OZXdzXzNncmFtID0gVGVybURvY3VtZW50TWF0cml4KHNhbXBsZXNDb3JwdXNbIm5ld3MiXSwgY29udHJvbCA9IGxpc3QodG9rZW5pemUgPSBUcmlncmFtVG9rZW5pemVyLCB3b3JkTGVuZ3RocyA9IGMoMyxJbmYpKSkNCnRkbVR3aXR0ZXJzXzNncmFtID0gVGVybURvY3VtZW50TWF0cml4KHNhbXBsZXNDb3JwdXNbInR3aXR0ZXJzIl0sIGNvbnRyb2wgPSBsaXN0KHRva2VuaXplID0gVHJpZ3JhbVRva2VuaXplciwgd29yZExlbmd0aHMgPSBjKDMsSW5mKSkpDQpgYGANCg0KIyMjRXh0cmFjdCBtb3N0IG9jY3VyaW5nIDEvMi8zLWdyYW1zIGFsb25nIHdpdGggdGhlaXIgZnJlcXVlbmN5DQpgYGB7cn0NCg0KIyBwdXQgd29yZCBjb3VudCBmcm9tIHRlcm0tZG9jdW1lbnQgbWF0cmljZXMgaW50byBkYXRhIGZyYW1lcw0Kd2ZtQmxvZ3NfMWdyYW0gPC0gZGF0YS5mcmFtZSh3b3JkID0gdGRtQmxvZ3NfMWdyYW0kZGltbmFtZXMkVGVybXMsIGZyZXF1ZW5jeSA9IHRkbUJsb2dzXzFncmFtJHYpDQp3Zm1OZXdzXzFncmFtIDwtIGRhdGEuZnJhbWUod29yZCA9IHRkbU5ld3NfMWdyYW0kZGltbmFtZXMkVGVybXMsIGZyZXF1ZW5jeSA9IHRkbU5ld3NfMWdyYW0kdikNCndmbVR3aXR0ZXJzXzFncmFtIDwtIGRhdGEuZnJhbWUod29yZCA9IHRkbVR3aXR0ZXJzXzFncmFtJGRpbW5hbWVzJFRlcm1zLCBmcmVxdWVuY3kgPSB0ZG1Ud2l0dGVyc18xZ3JhbSR2KQ0KDQp3Zm1CbG9nc18yZ3JhbSA8LSBkYXRhLmZyYW1lKHdvcmQgPSB0ZG1CbG9nc18yZ3JhbSRkaW1uYW1lcyRUZXJtcywgZnJlcXVlbmN5ID0gdGRtQmxvZ3NfMmdyYW0kdikNCndmbU5ld3NfMmdyYW0gPC0gZGF0YS5mcmFtZSh3b3JkID0gdGRtTmV3c18yZ3JhbSRkaW1uYW1lcyRUZXJtcywgZnJlcXVlbmN5ID0gdGRtTmV3c18yZ3JhbSR2KQ0Kd2ZtVHdpdHRlcnNfMmdyYW0gPC0gZGF0YS5mcmFtZSh3b3JkID0gdGRtVHdpdHRlcnNfMmdyYW0kZGltbmFtZXMkVGVybXMsIGZyZXF1ZW5jeSA9IHRkbVR3aXR0ZXJzXzJncmFtJHYpDQoNCndmbUJsb2dzXzNncmFtIDwtIGRhdGEuZnJhbWUod29yZCA9IHRkbUJsb2dzXzNncmFtJGRpbW5hbWVzJFRlcm1zLCBmcmVxdWVuY3kgPSB0ZG1CbG9nc18zZ3JhbSR2KQ0Kd2ZtTmV3c18zZ3JhbSA8LSBkYXRhLmZyYW1lKHdvcmQgPSB0ZG1OZXdzXzNncmFtJGRpbW5hbWVzJFRlcm1zLCBmcmVxdWVuY3kgPSB0ZG1OZXdzXzNncmFtJHYpDQp3Zm1Ud2l0dGVyc18zZ3JhbSA8LSBkYXRhLmZyYW1lKHdvcmQgPSB0ZG1Ud2l0dGVyc18zZ3JhbSRkaW1uYW1lcyRUZXJtcywgZnJlcXVlbmN5ID0gdGRtVHdpdHRlcnNfM2dyYW0kdikNCg0KIyByZW9yZGVyIGJ5IGRlc2NyZWFzaW5nIGZyZXF1ZW5jeQ0Kd2ZtQmxvZ3NfMWdyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1CbG9nc18xZ3JhbSwgLWZyZXF1ZW5jeSkNCndmbU5ld3NfMWdyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1OZXdzXzFncmFtLCAtZnJlcXVlbmN5KQ0Kd2ZtVHdpdHRlcnNfMWdyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1Ud2l0dGVyc18xZ3JhbSwgLWZyZXF1ZW5jeSkNCg0Kd2ZtQmxvZ3NfMmdyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1CbG9nc18yZ3JhbSwgLWZyZXF1ZW5jeSkNCndmbU5ld3NfMmdyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1OZXdzXzJncmFtLCAtZnJlcXVlbmN5KQ0Kd2ZtVHdpdHRlcnNfMmdyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1Ud2l0dGVyc18yZ3JhbSwgLWZyZXF1ZW5jeSkNCg0Kd2ZtQmxvZ3NfM2dyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1CbG9nc18zZ3JhbSwgLWZyZXF1ZW5jeSkNCndmbU5ld3NfM2dyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1OZXdzXzNncmFtLCAtZnJlcXVlbmN5KQ0Kd2ZtVHdpdHRlcnNfM2dyYW0gPC0gcGx5cjo6YXJyYW5nZSh3Zm1Ud2l0dGVyc18zZ3JhbSwgLWZyZXF1ZW5jeSkNCmBgYA0KDQojIyNQbG90IG1vc3QgZnJlcXVlbnQgVW5pZ3JhbXMNCmBgYHtyfQ0KbiA8LSAyMEwgIyBzaG93IDIwIG1vc3QgZnJlcXVlbnRseSBvY2N1cmluZyB0ZXJtcy93b3Jkcw0KDQojIGlzb2xhdGUgdG9wIG4gd29yZHMgYnkgZGVjcmVhc2luZyBmcmVxdWVuY3kNCmJsb2dzLnRvcCA8LSB3Zm1CbG9nc18xZ3JhbVsxOm4sIF0NCm5ld3MudG9wIDwtIHdmbU5ld3NfMWdyYW1bMTpuLCBdDQp0d2l0dGVycy50b3AgPC0gd2ZtVHdpdHRlcnNfMWdyYW1bMTpuLCBdDQoNCmJsb2dzLnRvcCR3b3JkIDwtIHJlb3JkZXIoYmxvZ3MudG9wJHdvcmQsIGJsb2dzLnRvcCRmcmVxdWVuY3kpDQpuZXdzLnRvcCR3b3JkIDwtIHJlb3JkZXIobmV3cy50b3Akd29yZCwgbmV3cy50b3AkZnJlcXVlbmN5KQ0KdHdpdHRlcnMudG9wJHdvcmQgPC0gcmVvcmRlcih0d2l0dGVycy50b3Akd29yZCwgdHdpdHRlcnMudG9wJGZyZXF1ZW5jeSkNCg0KIyBwbG90cw0KZy5ibG9ncy50b3AgPC0gZ2dwbG90KGJsb2dzLnRvcCwgYWVzKHggPSB3b3JkLCB5ID0gZnJlcXVlbmN5KSkNCmcuYmxvZ3MudG9wIDwtIGcuYmxvZ3MudG9wICsgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgY29vcmRfZmxpcCgpICsNCiAgbGFicyh0aXRsZSA9ICJVbmktZ3JhbXMgaW4gQmxvZ3MiKQ0KDQpnLm5ld3MudG9wIDwtIGdncGxvdChuZXdzLnRvcCwgYWVzKHggPSB3b3JkLCB5ID0gZnJlcXVlbmN5KSkNCmcubmV3cy50b3AgPC0gZy5uZXdzLnRvcCArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGNvb3JkX2ZsaXAoKSArDQogIGxhYnModGl0bGUgPSAiVW5pLWdyYW1zIGluIE5ld3MiKQ0KDQpnLnR3aXR0ZXJzLnRvcCA8LSBnZ3Bsb3QodHdpdHRlcnMudG9wLCBhZXMoeCA9IHdvcmQsIHkgPSBmcmVxdWVuY3kpKQ0KZy50d2l0dGVycy50b3AgPC0gZy50d2l0dGVycy50b3AgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBjb29yZF9mbGlwKCkgKyANCiAgbGFicyh0aXRsZSA9ICJVbmktZ3JhbXMgaW4gVHdpdHRlcnMiKQ0KDQpncmlkLmFycmFuZ2UoZy5ibG9ncy50b3AsIGcubmV3cy50b3AsIGcudHdpdHRlcnMudG9wLCBuY29sID0gMykNCmBgYA0KDQojIyNQbG90IG1vc3QgZnJlcXVlbnQgQmlncmFtcw0KYGBge3J9DQojIGlzb2xhdGUgdG9wIG4gd29yZHMgYnkgZGVjcmVhc2luZyBmcmVxdWVuY3kNCmJsb2dzLnRvcCA8LSB3Zm1CbG9nc18yZ3JhbVsxOm4sIF0NCm5ld3MudG9wIDwtIHdmbU5ld3NfMmdyYW1bMTpuLCBdDQp0d2l0dGVycy50b3AgPC0gd2ZtVHdpdHRlcnNfMmdyYW1bMTpuLCBdDQoNCmJsb2dzLnRvcCR3b3JkIDwtIHJlb3JkZXIoYmxvZ3MudG9wJHdvcmQsIGJsb2dzLnRvcCRmcmVxdWVuY3kpDQpuZXdzLnRvcCR3b3JkIDwtIHJlb3JkZXIobmV3cy50b3Akd29yZCwgbmV3cy50b3AkZnJlcXVlbmN5KQ0KdHdpdHRlcnMudG9wJHdvcmQgPC0gcmVvcmRlcih0d2l0dGVycy50b3Akd29yZCwgdHdpdHRlcnMudG9wJGZyZXF1ZW5jeSkNCg0KIyBwbG90cw0KZy5ibG9ncy50b3AgPC0gZ2dwbG90KGJsb2dzLnRvcCwgYWVzKHggPSB3b3JkLCB5ID0gZnJlcXVlbmN5KSkNCmcuYmxvZ3MudG9wIDwtIGcuYmxvZ3MudG9wICsgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgY29vcmRfZmxpcCgpICsNCiAgbGFicyh0aXRsZSA9ICJCaWdyYW1zIGluIEJsb2dzIikNCg0KZy5uZXdzLnRvcCA8LSBnZ3Bsb3QobmV3cy50b3AsIGFlcyh4ID0gd29yZCwgeSA9IGZyZXF1ZW5jeSkpDQpnLm5ld3MudG9wIDwtIGcubmV3cy50b3AgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBjb29yZF9mbGlwKCkgKw0KICBsYWJzKHRpdGxlID0gIkJpZ3JhbXMgaW4gTmV3cyIpDQoNCmcudHdpdHRlcnMudG9wIDwtIGdncGxvdCh0d2l0dGVycy50b3AsIGFlcyh4ID0gd29yZCwgeSA9IGZyZXF1ZW5jeSkpDQpnLnR3aXR0ZXJzLnRvcCA8LSBnLnR3aXR0ZXJzLnRvcCArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGNvb3JkX2ZsaXAoKSArIA0KICBsYWJzKHRpdGxlID0gIkJpZ3JhbXMgaW4gVHdpdHRlcnMiKQ0KDQpncmlkLmFycmFuZ2UoZy5ibG9ncy50b3AsIGcubmV3cy50b3AsIGcudHdpdHRlcnMudG9wLCBuY29sID0gMykNCmBgYA0KDQojIyNQbG90IG1vc3QgZnJlcXVlbnQgVHJpZ3JhbXMNCmBgYHtyfQ0KIyBpc29sYXRlIHRvcCBuIHdvcmRzIGJ5IGRlY3JlYXNpbmcgZnJlcXVlbmN5DQpibG9ncy50b3AgPC0gd2ZtQmxvZ3NfM2dyYW1bMTpuLCBdDQpuZXdzLnRvcCA8LSB3Zm1OZXdzXzNncmFtWzE6biwgXQ0KdHdpdHRlcnMudG9wIDwtIHdmbVR3aXR0ZXJzXzNncmFtWzE6biwgXQ0KDQpibG9ncy50b3Akd29yZCA8LSByZW9yZGVyKGJsb2dzLnRvcCR3b3JkLCBibG9ncy50b3AkZnJlcXVlbmN5KQ0KbmV3cy50b3Akd29yZCA8LSByZW9yZGVyKG5ld3MudG9wJHdvcmQsIG5ld3MudG9wJGZyZXF1ZW5jeSkNCnR3aXR0ZXJzLnRvcCR3b3JkIDwtIHJlb3JkZXIodHdpdHRlcnMudG9wJHdvcmQsIHR3aXR0ZXJzLnRvcCRmcmVxdWVuY3kpDQoNCiMgcGxvdHMNCmcuYmxvZ3MudG9wIDwtIGdncGxvdChibG9ncy50b3AsIGFlcyh4ID0gd29yZCwgeSA9IGZyZXF1ZW5jeSkpDQpnLmJsb2dzLnRvcCA8LSBnLmJsb2dzLnRvcCArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGNvb3JkX2ZsaXAoKSArDQogIGxhYnModGl0bGUgPSAiVHJpZ3JhbXMgaW4gQmxvZ3MiKQ0KDQpnLm5ld3MudG9wIDwtIGdncGxvdChuZXdzLnRvcCwgYWVzKHggPSB3b3JkLCB5ID0gZnJlcXVlbmN5KSkNCmcubmV3cy50b3AgPC0gZy5uZXdzLnRvcCArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGNvb3JkX2ZsaXAoKSArDQogIGxhYnModGl0bGUgPSAiVHJpZ3JhbXMgaW4gTmV3cyIpDQoNCmcudHdpdHRlcnMudG9wIDwtIGdncGxvdCh0d2l0dGVycy50b3AsIGFlcyh4ID0gd29yZCwgeSA9IGZyZXF1ZW5jeSkpDQpnLnR3aXR0ZXJzLnRvcCA8LSBnLnR3aXR0ZXJzLnRvcCArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGNvb3JkX2ZsaXAoKSArIA0KICBsYWJzKHRpdGxlID0gIlRyaWdyYW1zIGluIFR3aXR0ZXJzIikNCg0KZ3JpZC5hcnJhbmdlKGcuYmxvZ3MudG9wLCBnLm5ld3MudG9wLCBnLnR3aXR0ZXJzLnRvcCwgbmNvbCA9IDMpDQpgYGANCg0KIyMgTmV4dCBTdGVwcy4uLg0KICANClRoZSBwbGFuIGZvciBjb21wbGV0aW5nIHRoZSBDYXBzdG9uZSBQcm9qZWN0IHdvdWxkIG1vc3RseSBpbnZvbHZlIHRoZSBmb2xsb3dpbmcgdGFza3M6IC0gIA0KLSBDcmVhdGUgU2hpbnkgYXBwICANCi0gUGxhbiBmb3IgdGhlIGFwcHJvYWNoIHRvIGFyY2hpdmUgbGVhcm5pbmcvdHJhaW5pbmcgb2YgZWl0aGVyIGN1cnJlbnRseSBrbm93biBiYXNpYyBuLWdyYW0gb3IgaXRzIGJldHRlciBhbHRlcm5hdGl2ZTsgVGhpcyB3aWxsIG1vc3RseSBpbnZvbHZlIGVuc3VyaW5nIHRvIHF1YW50aWZ5IHByb2JhYmlsaXR5IGFuZCBjb25maWRlbmNlIGludGVydmFsIG9mIHByb3Bvc2VkIHdvcmQgcHJlZGljdGlvbnMgICANCi0gUHJvZmlsZSBhbmQgb3B0aW1pemUgdGhlIHNvbHV0aW9uIHRvIGFjaGlldmUgZ29vZCBwZXJmb3JtYW5jZSBhbmQgYWNjdXJhY3kgIA0KLSBGaXggc3RvcHdvcmRzIG5vdCByZW1vdmluZyBjb21tb24gbW9zdCBmcmVxdWVudCB3b3JkcyBsaWtlICdJJywgJ3RoZScsIGV0YyAgDQotIEZpbmQgdGhlIGltcGFjdC91c2VmdWxuZXNzIG9mIGV4aXN0aW5nIGRhdGEgY2xlYW5pbmcgc3RlcHMgYW5kIHJldmlzZSB0byBhY2hpZXZlIGJldHRlciBwZXJmb3JtYW5jZSBhbmQgYWNjdXJhY3kgaW4gbW9kZWxpbmcgYW5kIHByZWRpY3Rpb24gIA0KLSBJbnZlc3RpZ2F0ZSBpZiBhbnkgZGltZW50aW9uYWxpdHkgLyBzcGFyc2l0eSByZWR1Y3Rpb24gd291bGQgYmUgZmVhc2libGUgYW5kIGhlbHBmdWwgIA0KLSBJZGVudGlmeSBhcHByb3ByaWF0ZSB0cmFpbmluZyBzaXplIHRvIGJhbGFuY2UgYmFpcyBhbmQgdmFyaWFuY2UgIA0KLSBTYXZlIHNhbXBsZSB0cmFpbmluZyBkYXRhc2V0IHRvIGRpc2sgdG8gYXZvaWQgbG9hZGluZyBmdWxsIHRyYWluaW5nIGRhdGFzZXQgIA0KZGV0ZXJtaW5lIHRoZSBiZXN0IGRhdGEgc3RydWN0dXJlIGluIFIgdG8gc3RvcmUgTkdyYW0gbGFuZ3VhZ2UgbW9kZWxzDQotIE1ha2UgdGhlIGNvZGUgdGVyc2UNCg==