1 Introduction

This short note introduces the steps for processing simple text format data for visualization and simple descriptive visual analysis using R.

In addition to the basic R string functions, we will use a popular R library for text mining for some text format data processing.

2 Basic String Functions in R

Several R string functions are commonly used in manipulating strings.

2.1 Character Translation

The base R has several character translation functions. The most frequently used functions are tolower and toupper. The following are a few illustrative examples.

my_string <- "Example STRING, with example numbers (12, 15 and also 10.2)?!"
tolower(my_string)
[1] "example string, with example numbers (12, 15 and also 10.2)?!"
toupper(my_string)
[1] "EXAMPLE STRING, WITH EXAMPLE NUMBERS (12, 15 AND ALSO 10.2)?!"
casefold(my_string, upper = TRUE)
[1] "EXAMPLE STRING, WITH EXAMPLE NUMBERS (12, 15 AND ALSO 10.2)?!"

2.2 Concatenating and Splitting Strings

when processing text data, sometimes we need to combine multiple strings to make a single string. While on some other occasions, we need to split a string into multiple substrings. The following is a simple example.

string1 = "It is easy to get started with R"
string2 = "However, it takes time to be a good R programmer"
###
str_split(string2, ",")
[[1]]
[1] "However"                                 
[2] " it takes time to be a good R programmer"

To obtain the actural string, the [[]] string operator needs to be used,

str_split(string2, ",")[[1]]
[1] "However"                                 
[2] " it takes time to be a good R programmer"

paste() is widely used to concatenate strings. It is very useful to pass values of parameters to the text of graphic annotations.

string12 = paste(string1, ".", string2, "!" )
string12    # There is white space before added punctuation.
[1] "It is easy to get started with R . However, it takes time to be a good R programmer !"
## To remove the white space, set argument sep = ""
string120 = paste(string1, ".", string2, "!", sep = "" )
string120
[1] "It is easy to get started with R.However, it takes time to be a good R programmer!"

An example of graphical annotation that passes values of a parameter.

myDat = data.frame(normal = rnorm(100),
                   exponential = rexp(100, rate=1),
                   gamma = rgamma(100, shape=0.5, rate = 1))
distName = c("Normal", "Exponential", "Gamma")

par(mfrow=c(1,3))
for (i in 1:3){
  plot(density(myDat[,i]), xlab=paste(distName[i], "score"), main = "")
  legend("topright", paste(distName[i], "distribution"), cex = 0.7, bty="n")
}

2.3 Searching & Replacing Symbols

Two R functions grep() and grepl() can be used to detect special symbols in a vector of strings. grep() returns the index of the string vector that contains the special symbol while grepl() returns the logical values indicating whether each component has the special symbol.

Since special characters (symbols) need to be escaped to make them literal, \\ needs to be added in front of special symbols. The following examples show how use grep() and grepl() to detect special characters in a string.

my_string <- "Example STRING, with example numbers (12, 15 and also 10.2)?!"
str_vec = str_split(my_string, ",")[[1]]
str_vec
[1] "Example STRING"            " with example numbers (12"
[3] " 15 and also 10.2)?!"     
grep("\\?",str_vec)  # returns the index(es)
[1] 3
grepl("\\?",str_vec)  # returns the logical vector
[1] FALSE FALSE  TRUE

The other two useful functions in processing strings are str_replace_all() and str_extract_all().

Before presenting illustrative examples, we introduce the term regex (regular expression) - a sequence of characters that forms a search pattern. regex can be used to check if a string contains the specified search pattern. for example, [0-9]+ match any substring that is one or more contiguous numbers. [a-zA-Z\s] is The letters A-Z, upper and lower case, plus whitespace. [^A-Za-z\s] is the negation of the previous regex. \\s is a white space.

my_string <- "Example STRING, with example numbers (12, 15 and also 10.2)?!"
str_replace_all(my_string, "e","___")   # replacement
[1] "Exampl___ STRING, with ___xampl___ numb___rs (12, 15 and also 10.2)?!"
###
str_extract_all(my_string,"[0-9]+")[[1]]
[1] "12" "15" "10" "2" 

2.4 A Wrap-up Function

The following is a function that calls the aforementioned functions to clean a string (sentences) and return a vector of words.

StrCleaning <- function(string){
    # Lowercase
    temp <- tolower(string)
    # Remove everything that is not a number or letter (may want to keep more 
    # stuff in your actual analyses). 
    temp <- stringr::str_replace_all(temp,"[^a-zA-Z\\s]", " ")
    # Shrink down to just one white space
    temp <- stringr::str_replace_all(temp,"[\\s]+", " ")
    # Split it
    temp <- stringr::str_split(temp, " ")[[1]]
    # Get rid of trailing "" if necessary
    indexes <- which(temp == "")
    if(length(indexes) > 0){
      temp <- temp[-indexes]
    } 
    return(temp)
}

The following example calls the above function to clean a sentence and make a vector of words.

sentence = "The term 'data science' (originally used interchangeably with 'datalogy') has existed for over thirty years and was used initially as a substitute for computer science by Peter Naur in 1960."
CleanSentence <- StrCleaning(sentence)
CleanSentence 
 [1] "the"             "term"            "data"            "science"        
 [5] "originally"      "used"            "interchangeably" "with"           
 [9] "datalogy"        "has"             "existed"         "for"            
[13] "over"            "thirty"          "years"           "and"            
[17] "was"             "used"            "initially"       "as"             
[21] "a"               "substitute"      "for"             "computer"       
[25] "science"         "by"              "peter"           "naur"           
[29] "in"             

The following R function cleans a text blocks.

# function to clean text
CleanTextDocs <- function(text){
    # Get rid of blank lines
    indexes <- which(text == "")
    if (length(indexes) > 0) {
        text <- text[-indexes]
    }
    # See if we are left with any valid text:
    if (length(text) == 0) {
        cat("There was no text in this document! \n")
        to_return <- list(num_tokens = 0, 
                             unique_tokens = 0, 
                             text = "")
    } else {
        # If there is valid text, process it.
        # Loop through the lines in the text and combine them:
        clean_text <- NULL
        for (i in 1:length(text)) {
            # add them to a vector 
            clean_text <- c(clean_text, StrCleaning(text[i]))
        }
        # Calculate the number of tokens and unique tokens and return them in a 
        # named list object.
        num_tok <- length(clean_text)
        num_uniq <- length(unique(clean_text))
        to_return <- list(num_tokens = num_tok, 
                             unique_tokens = num_uniq, 
                             text = clean_text)
    }
    
    return(to_return)
}
text <- readLines("https://pengdsci.github.io/STA553VIZ/w04/Obama_Speech_2-24-09.txt")
TextVec = CleanTextDocs(text)
sort(table(TextVec$text), decreasing = TRUE)[1:30]

     the      and       to     that       of        a       we      our 
     269      229      205      165      161      138      137      117 
      in     will      for        i       is     this       it      not 
     107       85       81       80       77       75       70       41 
     but        s     have      are       on      can       be       or 
      40       40       38       37       36       34       31       31 
     you     more    their        t american      now 
      31       30       28       27       26       26 

The above frequency table contains many words that have less information. In text mining, these types of words are called stopwords that need to be removed from the text analysis. In R text mining package tm lists about 175 stopwords. The detailed list can be found using the code

stopwords(kind = "en")

Next, we remove all stopwords and meaningless strings from the above partially processed text and make a frequency table of words in the speech.

removedText = removeWords(TextVec$text, c(stopwords("english"),"s", "t", "ve"))
## remove empty string ""
removedText =removedText[removedText!=""]
## Make a frequency table
freqVec = sort(table(removedText), decreasing = TRUE)
freqVec[1:20]
removedText
     will       can  american       now      know   economy       new    people 
       85        34        26        26        25        22        21        21 
     plan     every    health   america        us      care      also      time 
       21        20        20        19        19        18        16        16 
     must     years americans education 
       15        15        14        14 
xtick = names(freqVec[1:20])
x = barplot(freqVec[1:20], col = heat.colors(20), xaxt="n")
## xpd = TRUE or NA allows text annotation plotted outside the plot region
text(cex=0.75, x = x -.25, y = -10, labels = xtick, xpd=TRUE, srt=75, col = "navy")

The above bar chart shows the frequencies of the first 20 words with the highest frequency. We can see the rough idea of the speech: actions for the US economy, health care, education, etc.

3 Text File Processing with Library tm

R has a powerful library for text mining. The following two functions can be used to scrape internet files and clean them for basic text analytics including visual representation using wordcloud.

#++++++++++++++++++++++++++++++++++
# rquery.wordcloud() : Word cloud generator
# - http://www.sthda.com
#+++++++++++++++++++++++++++++++++++
# x : character string (plain text, web url, txt file path)
# type : specify whether x is a plain text, a web page url or a file path
# lang : the language of the text
# excludeWords : a vector of words to exclude from the text
# textStemming : reduces words to their root form
# colorPalette : the name of color palette taken from RColorBrewer package, 
  # or a color name, or a color code
# min.freq : words with frequency below min.freq will not be plotted
# max.words : Maximum number of words to be plotted. least frequent terms dropped

# value returned by the function : a list(tdm, freqTable)
rquery.wordcloud <- function(x, type=c("text", "url", "file"), 
                          lang="english", excludeWords=NULL, 
                          textStemming=FALSE,  colorPalette="Dark2",
                          min.freq=3, max.words=2000)
{ 
  if(type[1]=="file") text <- readLines(x)
  else if(type[1]=="url") text <- html_to_text(x)
  else if(type[1]=="text") text <- x
  
  # Load the text as a corpus
  docs <- Corpus(VectorSource(text))
  # Convert the text to lower case
  docs <- tm_map(docs, content_transformer(tolower))
  # Remove numbers
  docs <- tm_map(docs, removeNumbers)
  # Remove stopwords for the language 
  docs <- tm_map(docs, removeWords, stopwords(lang))
  # Remove punctuations
  docs <- tm_map(docs, removePunctuation)
  # Eliminate extra white spaces
  docs <- tm_map(docs, stripWhitespace)
  # Remove your own stopwords
  if(!is.null(excludeWords)) 
    docs <- tm_map(docs, removeWords, excludeWords) 
  # Text stemming
  if(textStemming) docs <- tm_map(docs, stemDocument)
  # Create term-document matrix
  tdm <- TermDocumentMatrix(docs)
  m <- as.matrix(tdm)
  v <- sort(rowSums(m),decreasing=TRUE)
  d <- data.frame(word = names(v),freq=v)
  # check the color palette name 
  if(!colorPalette %in% rownames(brewer.pal.info)) colors = colorPalette
  else colors = brewer.pal(8, colorPalette) 
  # Plot the word cloud
  set.seed(1234)
  wordcloud(d$word,d$freq, min.freq=min.freq, max.words=max.words,
            random.order=FALSE, rot.per=0.35, 
            use.r.layout=FALSE, colors=colors)
  
  invisible(list(tdm=tdm, freqTable = d))
}

#++++++++++++++++++++++
# Helper function
#++++++++++++++++++++++
# Download and parse webpage
html_to_text<-function(url){
  # download html
  html.doc <- getURL(url)  
  #convert to plain text
  doc = htmlParse(html.doc, asText=TRUE)
 # "//text()" returns all text outside of HTML tags.
 # We also don't want text such as style and script codes
  text <- xpathSApply(doc, "//text()[not(ancestor::script)][not(ancestor::style)][not(ancestor::noscript)][not(ancestor::form)]", xmlValue)
  # Format text vector into one character string
  return(paste(text, collapse = " "))
}
rquery.wordcloud("https://pengdsci.github.io/STA553VIZ/w04/Obama_Speech_2-24-09.txt", type="url")

LS0tDQp0aXRsZTogIkRhdGEgUHJvY2Vzc2luZyBmb3IgVmlzdWFsaXphdGlvbiINCmF1dGhvcjogIkNoZW5nIFBlbmciDQpkYXRlOiAiICINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6IDQNCiAgICB0b2NfZmxvYXQ6IHllcw0KICAgIGZpZ193aWR0aDogNg0KICAgIGZpZ19jYXB0aW9uOiB5ZXMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRvY19jb2xsYXBzZWQ6IHllcw0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGNvZGVfZG93bmxvYWQ6IHllcw0KICAgIHNtb290aF9zY3JvbGw6IHRydWUNCiAgICB0aGVtZTogcmVhZGFibGUNCiAgcGRmX2RvY3VtZW50OiANCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogNA0KICAgIGZpZ19jYXB0aW9uOiB5ZXMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIGZpZ193aWR0aDogNQ0KICAgIGZpZ19oZWlnaHQ6IDQNCi0tLQ0KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4NCg0KLyogVGFibGUgb2YgY29udGVudCAtIG5hdmlnYXRpb24gKi8NCmRpdiNUT0MgbGkgew0KICAgIGxpc3Qtc3R5bGU6bm9uZTsNCiAgICBiYWNrZ3JvdW5kLWNvbG9yOnNreWJsdWU7DQogICAgYmFja2dyb3VuZC1pbWFnZTpub25lOw0KICAgIGJhY2tncm91bmQtcmVwZWF0Om5vbmU7DQogICAgYmFja2dyb3VuZC1wb3NpdGlvbjowOw0KICAgIGZvbnQtZmFtaWx5OiBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmOw0KICAgIGNvbG9yOiBkYXJrcmVkOw0KfQ0KDQpoMS50aXRsZSB7DQogIGZvbnQtc2l6ZTogMjRweDsNCiAgY29sb3I6IERhcmtSZWQ7DQogIHRleHQtYWxpZ246IGNlbnRlcjsNCn0NCg0KaDQuYXV0aG9yIHsgLyogSGVhZGVyIDQgLSBhbmQgdGhlIGF1dGhvciBhbmQgZGF0YSBoZWFkZXJzIHVzZSB0aGlzIHRvbyAgKi8NCiAgICBmb250LXNpemU6IDE4cHg7DQogIGZvbnQtZmFtaWx5OiAiVGltZXMgTmV3IFJvbWFuIiwgVGltZXMsIHNlcmlmOw0KICBjb2xvcjogRGFya1JlZDsNCiAgdGV4dC1hbGlnbjogY2VudGVyOw0KfQ0KaDQuZGF0ZSB7IC8qIEhlYWRlciA0IC0gYW5kIHRoZSBhdXRob3IgYW5kIGRhdGEgaGVhZGVycyB1c2UgdGhpcyB0b28gICovDQogIGZvbnQtc2l6ZTogMThweDsNCiAgZm9udC1mYW1pbHk6ICJUaW1lcyBOZXcgUm9tYW4iLCBUaW1lcywgc2VyaWY7DQogIGNvbG9yOiBEYXJrQmx1ZTsNCiAgdGV4dC1hbGlnbjogY2VudGVyOw0KfQ0KaDEgeyAvKiBIZWFkZXIgMyAtIGFuZCB0aGUgYXV0aG9yIGFuZCBkYXRhIGhlYWRlcnMgdXNlIHRoaXMgdG9vICAqLw0KICAgIGZvbnQtc2l6ZTogMjJweDsNCiAgICBmb250LWZhbWlseTogIlRpbWVzIE5ldyBSb21hbiIsIFRpbWVzLCBzZXJpZjsNCiAgICBjb2xvcjogZGFya3JlZDsNCiAgICB0ZXh0LWFsaWduOiBjZW50ZXI7DQp9DQpoMiB7IC8qIEhlYWRlciAzIC0gYW5kIHRoZSBhdXRob3IgYW5kIGRhdGEgaGVhZGVycyB1c2UgdGhpcyB0b28gICovDQogICAgZm9udC1zaXplOiAxOHB4Ow0KICAgIGZvbnQtZmFtaWx5OiAiVGltZXMgTmV3IFJvbWFuIiwgVGltZXMsIHNlcmlmOw0KICAgIGNvbG9yOiBuYXZ5Ow0KICAgIHRleHQtYWxpZ246IGxlZnQ7DQp9DQoNCmgzIHsgLyogSGVhZGVyIDMgLSBhbmQgdGhlIGF1dGhvciBhbmQgZGF0YSBoZWFkZXJzIHVzZSB0aGlzIHRvbyAgKi8NCiAgICBmb250LXNpemU6IDE1cHg7DQogICAgZm9udC1mYW1pbHk6ICJUaW1lcyBOZXcgUm9tYW4iLCBUaW1lcywgc2VyaWY7DQogICAgY29sb3I6IG5hdnk7DQogICAgdGV4dC1hbGlnbjogbGVmdDsNCn0NCg0KaDQgeyAvKiBIZWFkZXIgNCAtIGFuZCB0aGUgYXV0aG9yIGFuZCBkYXRhIGhlYWRlcnMgdXNlIHRoaXMgdG9vICAqLw0KICAgIGZvbnQtc2l6ZTogMThweDsNCiAgICBmb250LWZhbWlseTogIlRpbWVzIE5ldyBSb21hbiIsIFRpbWVzLCBzZXJpZjsNCiAgICBjb2xvcjogZGFya3JlZDsNCiAgICB0ZXh0LWFsaWduOiBsZWZ0Ow0KfQ0KPC9zdHlsZT4NCg0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCiMgY29kZSBjaHVuayBzcGVjaWZpZXMgd2hldGhlciB0aGUgUiBjb2RlLCB3YXJuaW5ncywgYW5kIG91dHB1dCANCiMgd2lsbCBiZSBpbmNsdWRlZCBpbiB0aGUgb3V0cHV0IGZpbGVzLg0Kb3B0aW9ucyhyZXBvcyA9IGxpc3QoQ1JBTj0iaHR0cDovL2NyYW4ucnN0dWRpby5jb20vIikpDQppZiAoIXJlcXVpcmUoInRpZHl2ZXJzZSIpKSB7DQogICBpbnN0YWxsLnBhY2thZ2VzKCJ0aWR5dmVyc2UiKQ0KICAgbGlicmFyeSh0aWR5dmVyc2UpDQp9DQppZiAoIXJlcXVpcmUoImtuaXRyIikpIHsNCiAgIGluc3RhbGwucGFja2FnZXMoImtuaXRyIikNCiAgIGxpYnJhcnkoa25pdHIpDQp9DQppZiAoIXJlcXVpcmUoInN0cmluZ3IiKSkgew0KICAgaW5zdGFsbC5wYWNrYWdlcygic3RyaW5nciIsIGRlcGVuZGVuY2llcyA9IFRSVUUpDQogICBsaWJyYXJ5KHN0cmluZ3IpDQp9DQoNCmlmICghcmVxdWlyZSgidG0iKSkgew0KICAgaW5zdGFsbC5wYWNrYWdlcygidG0iLCBkZXBlbmRlbmNpZXMgPSBUUlVFKQ0KICAgbGlicmFyeSh0bSkNCn0NCg0KaWYgKCFyZXF1aXJlKCJ3b3JkY2xvdWQiKSkgew0KICAgaW5zdGFsbC5wYWNrYWdlcygid29yZGNsb3VkIiwgZGVwZW5kZW5jaWVzID0gVFJVRSkNCiAgIGxpYnJhcnkod29yZGNsb3VkKQ0KfQ0KDQppZiAoIXJlcXVpcmUoIlJDdXJsIikpIHsNCiAgIGluc3RhbGwucGFja2FnZXMoIlJDdXJsIiwgZGVwZW5kZW5jaWVzID0gVFJVRSkNCiAgIGxpYnJhcnkoUkN1cmwpDQp9DQoNCmlmICghcmVxdWlyZSgiWE1MIikpIHsNCiAgIGluc3RhbGwucGFja2FnZXMoIlhNTCIsIGRlcGVuZGVuY2llcyA9IFRSVUUpDQogICBsaWJyYXJ5KFhNTCkNCn0NCg0KDQppZiAoIXJlcXVpcmUoIlNub3diYWxsQyIpKSB7DQogICBpbnN0YWxsLnBhY2thZ2VzKCJTbm93YmFsbEMiLCBkZXBlbmRlbmNpZXMgPSBUUlVFKQ0KICAgbGlicmFyeShTbm93YmFsbEMpDQp9DQoNCmlmICghcmVxdWlyZSgiUkNvbG9yQnJld2VyIikpIHsNCiAgIGluc3RhbGwucGFja2FnZXMoIlJDb2xvckJyZXdlciIsIGRlcGVuZGVuY2llcyA9IFRSVUUpDQogICBsaWJyYXJ5KFJDb2xvckJyZXdlcikNCn0NCg0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCAgICAgICANCiAgICAgICAgICAgICAgICAgICAgICB3YXJuaW5nID0gRkFMU0UsICAgDQogICAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0gVFJVRSwgICANCiAgICAgICAgICAgICAgICAgICAgICBtZXNzYWdlID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgY29tbWVudCA9IE5BKQ0KYGBgDQoNCg0KXA0KDQojIEludHJvZHVjdGlvbg0KDQpUaGlzIHNob3J0IG5vdGUgaW50cm9kdWNlcyB0aGUgc3RlcHMgZm9yIHByb2Nlc3Npbmcgc2ltcGxlIHRleHQgZm9ybWF0IGRhdGEgZm9yIHZpc3VhbGl6YXRpb24gYW5kIHNpbXBsZSBkZXNjcmlwdGl2ZSB2aXN1YWwgYW5hbHlzaXMgdXNpbmcgUi4NCg0KSW4gYWRkaXRpb24gdG8gdGhlIGJhc2ljIFIgc3RyaW5nIGZ1bmN0aW9ucywgd2Ugd2lsbCB1c2UgYSBwb3B1bGFyIFIgbGlicmFyeSBmb3IgdGV4dCBtaW5pbmcgZm9yIHNvbWUgdGV4dCBmb3JtYXQgZGF0YSBwcm9jZXNzaW5nLg0KDQoNCiMgQmFzaWMgU3RyaW5nIEZ1bmN0aW9ucyBpbiBSDQoNClNldmVyYWwgUiBzdHJpbmcgZnVuY3Rpb25zIGFyZSBjb21tb25seSB1c2VkIGluIG1hbmlwdWxhdGluZyBzdHJpbmdzLg0KDQojIyBDaGFyYWN0ZXIgVHJhbnNsYXRpb24NCg0KVGhlIGJhc2UgUiBoYXMgc2V2ZXJhbCBjaGFyYWN0ZXIgdHJhbnNsYXRpb24gZnVuY3Rpb25zLiBUaGUgbW9zdCBmcmVxdWVudGx5IHVzZWQgZnVuY3Rpb25zIGFyZSBgdG9sb3dlcmAgYW5kIGB0b3VwcGVyYC4gVGhlIGZvbGxvd2luZyBhcmUgYSBmZXcgaWxsdXN0cmF0aXZlIGV4YW1wbGVzLg0KDQoNCmBgYHtyfQ0KbXlfc3RyaW5nIDwtICJFeGFtcGxlIFNUUklORywgd2l0aCBleGFtcGxlIG51bWJlcnMgKDEyLCAxNSBhbmQgYWxzbyAxMC4yKT8hIg0KdG9sb3dlcihteV9zdHJpbmcpDQp0b3VwcGVyKG15X3N0cmluZykNCmNhc2Vmb2xkKG15X3N0cmluZywgdXBwZXIgPSBUUlVFKQ0KYGBgDQoNCiMjIENvbmNhdGVuYXRpbmcgYW5kIFNwbGl0dGluZyBTdHJpbmdzDQoNCndoZW4gcHJvY2Vzc2luZyB0ZXh0IGRhdGEsIHNvbWV0aW1lcyB3ZSBuZWVkIHRvIGNvbWJpbmUgbXVsdGlwbGUgc3RyaW5ncyB0byBtYWtlIGEgc2luZ2xlIHN0cmluZy4gV2hpbGUgb24gc29tZSBvdGhlciBvY2Nhc2lvbnMsIHdlIG5lZWQgdG8gc3BsaXQgYSBzdHJpbmcgaW50byBtdWx0aXBsZSBzdWJzdHJpbmdzLiBUaGUgZm9sbG93aW5nIGlzIGEgc2ltcGxlIGV4YW1wbGUuDQoNCg0KYGBge3J9DQpzdHJpbmcxID0gIkl0IGlzIGVhc3kgdG8gZ2V0IHN0YXJ0ZWQgd2l0aCBSIg0Kc3RyaW5nMiA9ICJIb3dldmVyLCBpdCB0YWtlcyB0aW1lIHRvIGJlIGEgZ29vZCBSIHByb2dyYW1tZXIiDQojIyMNCnN0cl9zcGxpdChzdHJpbmcyLCAiLCIpDQpgYGANCg0KVG8gb2J0YWluIHRoZSBhY3R1cmFsIHN0cmluZywgdGhlIGBbW11dYCBzdHJpbmcgb3BlcmF0b3IgbmVlZHMgdG8gYmUgdXNlZCwNCg0KYGBge3J9DQpzdHJfc3BsaXQoc3RyaW5nMiwgIiwiKVtbMV1dDQpgYGANCg0KYHBhc3RlKClgIGlzIHdpZGVseSB1c2VkIHRvIGNvbmNhdGVuYXRlIHN0cmluZ3MuIEl0IGlzIHZlcnkgdXNlZnVsIHRvIHBhc3MgdmFsdWVzIG9mIHBhcmFtZXRlcnMgdG8gdGhlIHRleHQgb2YgZ3JhcGhpYyBhbm5vdGF0aW9ucy4NCg0KYGBge3J9DQpzdHJpbmcxMiA9IHBhc3RlKHN0cmluZzEsICIuIiwgc3RyaW5nMiwgIiEiICkNCnN0cmluZzEyICAgICMgVGhlcmUgaXMgd2hpdGUgc3BhY2UgYmVmb3JlIGFkZGVkIHB1bmN0dWF0aW9uLg0KYGBgDQoNCmBgYHtyfQ0KIyMgVG8gcmVtb3ZlIHRoZSB3aGl0ZSBzcGFjZSwgc2V0IGFyZ3VtZW50IHNlcCA9ICIiDQpzdHJpbmcxMjAgPSBwYXN0ZShzdHJpbmcxLCAiLiIsIHN0cmluZzIsICIhIiwgc2VwID0gIiIgKQ0Kc3RyaW5nMTIwDQpgYGANCg0KQW4gZXhhbXBsZSBvZiBncmFwaGljYWwgYW5ub3RhdGlvbiB0aGF0IHBhc3NlcyB2YWx1ZXMgb2YgYSBwYXJhbWV0ZXIuDQoNCmBgYHtyIGZpZy5hbGlnbj0nY2VudGVyJywgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9M30NCm15RGF0ID0gZGF0YS5mcmFtZShub3JtYWwgPSBybm9ybSgxMDApLA0KICAgICAgICAgICAgICAgICAgIGV4cG9uZW50aWFsID0gcmV4cCgxMDAsIHJhdGU9MSksDQogICAgICAgICAgICAgICAgICAgZ2FtbWEgPSByZ2FtbWEoMTAwLCBzaGFwZT0wLjUsIHJhdGUgPSAxKSkNCmRpc3ROYW1lID0gYygiTm9ybWFsIiwgIkV4cG9uZW50aWFsIiwgIkdhbW1hIikNCg0KcGFyKG1mcm93PWMoMSwzKSkNCmZvciAoaSBpbiAxOjMpew0KICBwbG90KGRlbnNpdHkobXlEYXRbLGldKSwgeGxhYj1wYXN0ZShkaXN0TmFtZVtpXSwgInNjb3JlIiksIG1haW4gPSAiIikNCiAgbGVnZW5kKCJ0b3ByaWdodCIsIHBhc3RlKGRpc3ROYW1lW2ldLCAiZGlzdHJpYnV0aW9uIiksIGNleCA9IDAuNywgYnR5PSJuIikNCn0NCg0KYGBgDQoNCg0KDQojIyBTZWFyY2hpbmcgXCYgUmVwbGFjaW5nIFN5bWJvbHMgDQoNClR3byBSIGZ1bmN0aW9ucyBgZ3JlcCgpYCBhbmQgYGdyZXBsKClgIGNhbiBiZSB1c2VkIHRvIGRldGVjdCBzcGVjaWFsIHN5bWJvbHMgaW4gYSB2ZWN0b3Igb2Ygc3RyaW5ncy4gYGdyZXAoKWAgcmV0dXJucyB0aGUgaW5kZXggb2YgdGhlIHN0cmluZyB2ZWN0b3IgdGhhdCBjb250YWlucyB0aGUgc3BlY2lhbCBzeW1ib2wgd2hpbGUgYGdyZXBsKClgIHJldHVybnMgdGhlIGxvZ2ljYWwgdmFsdWVzIGluZGljYXRpbmcgd2hldGhlciBlYWNoIGNvbXBvbmVudCBoYXMgdGhlIHNwZWNpYWwgc3ltYm9sLg0KDQoNClNpbmNlIHNwZWNpYWwgY2hhcmFjdGVycyAoc3ltYm9scykgbmVlZCB0byBiZSBlc2NhcGVkIHRvIG1ha2UgdGhlbSAqbGl0ZXJhbCosIGBcXGAgbmVlZHMgdG8gYmUgYWRkZWQgaW4gZnJvbnQgb2Ygc3BlY2lhbCBzeW1ib2xzLiBUaGUgZm9sbG93aW5nIGV4YW1wbGVzIHNob3cgaG93IHVzZSBgZ3JlcCgpYCBhbmQgYGdyZXBsKClgIHRvIGRldGVjdCBzcGVjaWFsIGNoYXJhY3RlcnMgaW4gYSBzdHJpbmcuDQoNCg0KYGBge3J9DQpteV9zdHJpbmcgPC0gIkV4YW1wbGUgU1RSSU5HLCB3aXRoIGV4YW1wbGUgbnVtYmVycyAoMTIsIDE1IGFuZCBhbHNvIDEwLjIpPyEiDQpzdHJfdmVjID0gc3RyX3NwbGl0KG15X3N0cmluZywgIiwiKVtbMV1dDQpzdHJfdmVjDQpgYGANCg0KYGBge3J9DQpncmVwKCJcXD8iLHN0cl92ZWMpICAjIHJldHVybnMgdGhlIGluZGV4KGVzKQ0KZ3JlcGwoIlxcPyIsc3RyX3ZlYykgICMgcmV0dXJucyB0aGUgbG9naWNhbCB2ZWN0b3INCmBgYA0KDQpUaGUgb3RoZXIgdHdvIHVzZWZ1bCBmdW5jdGlvbnMgaW4gcHJvY2Vzc2luZyBzdHJpbmdzIGFyZSBgc3RyX3JlcGxhY2VfYWxsKClgIGFuZCBgc3RyX2V4dHJhY3RfYWxsKClgLg0KDQpCZWZvcmUgcHJlc2VudGluZyBpbGx1c3RyYXRpdmUgZXhhbXBsZXMsIHdlIGludHJvZHVjZSB0aGUgdGVybSBgcmVnZXhgIChyZWd1bGFyIGV4cHJlc3Npb24pIC0gYSBzZXF1ZW5jZSBvZiBjaGFyYWN0ZXJzIHRoYXQgZm9ybXMgYSBzZWFyY2ggcGF0dGVybi4gYHJlZ2V4YCBjYW4gYmUgdXNlZCB0byBjaGVjayBpZiBhIHN0cmluZyBjb250YWlucyB0aGUgc3BlY2lmaWVkIHNlYXJjaCBwYXR0ZXJuLiBmb3IgZXhhbXBsZSwgYFswLTldK2AgKm1hdGNoIGFueSBzdWJzdHJpbmcgdGhhdCBpcyBvbmUgb3IgbW9yZSBjb250aWd1b3VzIG51bWJlcnMqLiBgW2EtekEtWlxzXWAgaXMgKlRoZSBsZXR0ZXJzIEEtWiwgdXBwZXIgYW5kIGxvd2VyIGNhc2UsIHBsdXMgd2hpdGVzcGFjZSouIGBbXkEtWmEtelxzXWAgaXMgdGhlIG5lZ2F0aW9uIG9mIHRoZSBwcmV2aW91cyBgcmVnZXhgLiAgYFxcc2AgaXMgYSB3aGl0ZSBzcGFjZS4NCg0KDQpgYGB7cn0NCm15X3N0cmluZyA8LSAiRXhhbXBsZSBTVFJJTkcsIHdpdGggZXhhbXBsZSBudW1iZXJzICgxMiwgMTUgYW5kIGFsc28gMTAuMik/ISINCnN0cl9yZXBsYWNlX2FsbChteV9zdHJpbmcsICJlIiwiX19fIikgICAjIHJlcGxhY2VtZW50DQojIyMNCnN0cl9leHRyYWN0X2FsbChteV9zdHJpbmcsIlswLTldKyIpW1sxXV0NCg0KYGBgDQoNCiMjIEEgV3JhcC11cCBGdW5jdGlvbg0KDQpUaGUgZm9sbG93aW5nIGlzIGEgZnVuY3Rpb24gdGhhdCBjYWxscyB0aGUgYWZvcmVtZW50aW9uZWQgZnVuY3Rpb25zIHRvIGNsZWFuIGEgc3RyaW5nIChzZW50ZW5jZXMpIGFuZCByZXR1cm4gYSB2ZWN0b3Igb2Ygd29yZHMuDQoNCmBgYHtyfQ0KU3RyQ2xlYW5pbmcgPC0gZnVuY3Rpb24oc3RyaW5nKXsNCiAgICAjIExvd2VyY2FzZQ0KICAgIHRlbXAgPC0gdG9sb3dlcihzdHJpbmcpDQogICAgIyBSZW1vdmUgZXZlcnl0aGluZyB0aGF0IGlzIG5vdCBhIG51bWJlciBvciBsZXR0ZXIgKG1heSB3YW50IHRvIGtlZXAgbW9yZSANCiAgICAjIHN0dWZmIGluIHlvdXIgYWN0dWFsIGFuYWx5c2VzKS4gDQogICAgdGVtcCA8LSBzdHJpbmdyOjpzdHJfcmVwbGFjZV9hbGwodGVtcCwiW15hLXpBLVpcXHNdIiwgIiAiKQ0KICAgICMgU2hyaW5rIGRvd24gdG8ganVzdCBvbmUgd2hpdGUgc3BhY2UNCiAgICB0ZW1wIDwtIHN0cmluZ3I6OnN0cl9yZXBsYWNlX2FsbCh0ZW1wLCJbXFxzXSsiLCAiICIpDQogICAgIyBTcGxpdCBpdA0KICAgIHRlbXAgPC0gc3RyaW5ncjo6c3RyX3NwbGl0KHRlbXAsICIgIilbWzFdXQ0KICAgICMgR2V0IHJpZCBvZiB0cmFpbGluZyAiIiBpZiBuZWNlc3NhcnkNCiAgICBpbmRleGVzIDwtIHdoaWNoKHRlbXAgPT0gIiIpDQogICAgaWYobGVuZ3RoKGluZGV4ZXMpID4gMCl7DQogICAgICB0ZW1wIDwtIHRlbXBbLWluZGV4ZXNdDQogICAgfSANCiAgICByZXR1cm4odGVtcCkNCn0NCmBgYA0KDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSBjYWxscyB0aGUgYWJvdmUgZnVuY3Rpb24gdG8gY2xlYW4gYSBzZW50ZW5jZSBhbmQgbWFrZSBhIHZlY3RvciBvZiB3b3Jkcy4NCg0KYGBge3J9DQpzZW50ZW5jZSA9ICJUaGUgdGVybSAnZGF0YSBzY2llbmNlJyAob3JpZ2luYWxseSB1c2VkIGludGVyY2hhbmdlYWJseSB3aXRoICdkYXRhbG9neScpIGhhcyBleGlzdGVkIGZvciBvdmVyIHRoaXJ0eSB5ZWFycyBhbmQgd2FzIHVzZWQgaW5pdGlhbGx5IGFzIGEgc3Vic3RpdHV0ZSBmb3IgY29tcHV0ZXIgc2NpZW5jZSBieSBQZXRlciBOYXVyIGluIDE5NjAuIg0KQ2xlYW5TZW50ZW5jZSA8LSBTdHJDbGVhbmluZyhzZW50ZW5jZSkNCkNsZWFuU2VudGVuY2UgDQpgYGANCg0KVGhlIGZvbGxvd2luZyBSIGZ1bmN0aW9uIGNsZWFucyBhIHRleHQgYmxvY2tzLg0KDQpgYGB7cn0NCiMgZnVuY3Rpb24gdG8gY2xlYW4gdGV4dA0KQ2xlYW5UZXh0RG9jcyA8LSBmdW5jdGlvbih0ZXh0KXsNCiAgICAjIEdldCByaWQgb2YgYmxhbmsgbGluZXMNCiAgICBpbmRleGVzIDwtIHdoaWNoKHRleHQgPT0gIiIpDQogICAgaWYgKGxlbmd0aChpbmRleGVzKSA+IDApIHsNCiAgICAgICAgdGV4dCA8LSB0ZXh0Wy1pbmRleGVzXQ0KICAgIH0NCgkjIFNlZSBpZiB3ZSBhcmUgbGVmdCB3aXRoIGFueSB2YWxpZCB0ZXh0Og0KICAgIGlmIChsZW5ndGgodGV4dCkgPT0gMCkgew0KICAgICAgICBjYXQoIlRoZXJlIHdhcyBubyB0ZXh0IGluIHRoaXMgZG9jdW1lbnQhIFxuIikNCiAgICAgICAgdG9fcmV0dXJuIDwtIGxpc3QobnVtX3Rva2VucyA9IDAsIA0KCQkgICAgICAgICAgICAgICAgICAgICB1bmlxdWVfdG9rZW5zID0gMCwgDQoJCQkJCQkJIHRleHQgPSAiIikNCiAgICB9IGVsc2Ugew0KICAgICAgICAjIElmIHRoZXJlIGlzIHZhbGlkIHRleHQsIHByb2Nlc3MgaXQuDQogICAgICAgICMgTG9vcCB0aHJvdWdoIHRoZSBsaW5lcyBpbiB0aGUgdGV4dCBhbmQgY29tYmluZSB0aGVtOg0KICAgICAgICBjbGVhbl90ZXh0IDwtIE5VTEwNCiAgICAgICAgZm9yIChpIGluIDE6bGVuZ3RoKHRleHQpKSB7DQogICAgICAgICAgICAjIGFkZCB0aGVtIHRvIGEgdmVjdG9yIA0KICAgICAgICAgICAgY2xlYW5fdGV4dCA8LSBjKGNsZWFuX3RleHQsIFN0ckNsZWFuaW5nKHRleHRbaV0pKQ0KICAgICAgICB9DQogICAgICAgICMgQ2FsY3VsYXRlIHRoZSBudW1iZXIgb2YgdG9rZW5zIGFuZCB1bmlxdWUgdG9rZW5zIGFuZCByZXR1cm4gdGhlbSBpbiBhIA0KICAgICAgICAjIG5hbWVkIGxpc3Qgb2JqZWN0Lg0KICAgICAgICBudW1fdG9rIDwtIGxlbmd0aChjbGVhbl90ZXh0KQ0KICAgICAgICBudW1fdW5pcSA8LSBsZW5ndGgodW5pcXVlKGNsZWFuX3RleHQpKQ0KICAgICAgICB0b19yZXR1cm4gPC0gbGlzdChudW1fdG9rZW5zID0gbnVtX3RvaywgDQoJCSAgICAgICAgICAgICAgICAgICAgIHVuaXF1ZV90b2tlbnMgPSBudW1fdW5pcSwgDQoJCQkJCQkJIHRleHQgPSBjbGVhbl90ZXh0KQ0KICAgIH0NCgkNCiAgICByZXR1cm4odG9fcmV0dXJuKQ0KfQ0KYGBgDQoNCmBgYHtyfQ0KdGV4dCA8LSByZWFkTGluZXMoImh0dHBzOi8vcGVuZ2RzY2kuZ2l0aHViLmlvL1NUQTU1M1ZJWi93MDQvT2JhbWFfU3BlZWNoXzItMjQtMDkudHh0IikNClRleHRWZWMgPSBDbGVhblRleHREb2NzKHRleHQpDQpzb3J0KHRhYmxlKFRleHRWZWMkdGV4dCksIGRlY3JlYXNpbmcgPSBUUlVFKVsxOjMwXQ0KYGBgDQoNClRoZSBhYm92ZSBmcmVxdWVuY3kgdGFibGUgY29udGFpbnMgbWFueSB3b3JkcyB0aGF0IGhhdmUgbGVzcyBpbmZvcm1hdGlvbi4gSW4gdGV4dCBtaW5pbmcsIHRoZXNlIHR5cGVzIG9mIHdvcmRzIGFyZSBjYWxsZWQgYHN0b3B3b3Jkc2AgdGhhdCBuZWVkIHRvIGJlIHJlbW92ZWQgZnJvbSB0aGUgdGV4dCBhbmFseXNpcy4gSW4gUiB0ZXh0IG1pbmluZyBwYWNrYWdlIGB0bWAgbGlzdHMgYWJvdXQgMTc1IGBzdG9wd29yZHNgLiBUaGUgZGV0YWlsZWQgbGlzdCBjYW4gYmUgZm91bmQgdXNpbmcgdGhlIGNvZGUNCg0KYGBgDQpzdG9wd29yZHMoa2luZCA9ICJlbiIpDQpgYGANCg0KTmV4dCwgd2UgcmVtb3ZlIGFsbCBgc3RvcHdvcmRzYCBhbmQgbWVhbmluZ2xlc3Mgc3RyaW5ncyBmcm9tIHRoZSBhYm92ZSBwYXJ0aWFsbHkgcHJvY2Vzc2VkIHRleHQgYW5kIG1ha2UgYSBmcmVxdWVuY3kgdGFibGUgb2Ygd29yZHMgaW4gdGhlIHNwZWVjaC4NCg0KDQpgYGB7cn0NCnJlbW92ZWRUZXh0ID0gcmVtb3ZlV29yZHMoVGV4dFZlYyR0ZXh0LCBjKHN0b3B3b3JkcygiZW5nbGlzaCIpLCJzIiwgInQiLCAidmUiKSkNCiMjIHJlbW92ZSBlbXB0eSBzdHJpbmcgIiINCnJlbW92ZWRUZXh0ID1yZW1vdmVkVGV4dFtyZW1vdmVkVGV4dCE9IiJdDQojIyBNYWtlIGEgZnJlcXVlbmN5IHRhYmxlDQpmcmVxVmVjID0gc29ydCh0YWJsZShyZW1vdmVkVGV4dCksIGRlY3JlYXNpbmcgPSBUUlVFKQ0KZnJlcVZlY1sxOjIwXQ0KYGBgDQoNCmBgYHtyfQ0KeHRpY2sgPSBuYW1lcyhmcmVxVmVjWzE6MjBdKQ0KeCA9IGJhcnBsb3QoZnJlcVZlY1sxOjIwXSwgY29sID0gaGVhdC5jb2xvcnMoMjApLCB4YXh0PSJuIikNCiMjIHhwZCA9IFRSVUUgb3IgTkEgYWxsb3dzIHRleHQgYW5ub3RhdGlvbiBwbG90dGVkIG91dHNpZGUgdGhlIHBsb3QgcmVnaW9uDQp0ZXh0KGNleD0wLjc1LCB4ID0geCAtLjI1LCB5ID0gLTEwLCBsYWJlbHMgPSB4dGljaywgeHBkPVRSVUUsIHNydD03NSwgY29sID0gIm5hdnkiKQ0KDQpgYGANCg0KVGhlIGFib3ZlIGJhciBjaGFydCBzaG93cyB0aGUgZnJlcXVlbmNpZXMgb2YgdGhlIGZpcnN0IDIwIHdvcmRzIHdpdGggdGhlIGhpZ2hlc3QgZnJlcXVlbmN5LiBXZSBjYW4gc2VlIHRoZSAqKnJvdWdoIGlkZWEqKiBvZiB0aGUgc3BlZWNoOiBhY3Rpb25zIGZvciB0aGUgVVMgZWNvbm9teSwgaGVhbHRoIGNhcmUsIGVkdWNhdGlvbiwgZXRjLg0KDQoNCiMgVGV4dCBGaWxlIFByb2Nlc3Npbmcgd2l0aCBMaWJyYXJ5IGB0bWANCg0KUiBoYXMgYSBwb3dlcmZ1bCBsaWJyYXJ5IGZvciB0ZXh0IG1pbmluZy4gVGhlIGZvbGxvd2luZyB0d28gZnVuY3Rpb25zIGNhbiBiZSB1c2VkIHRvIHNjcmFwZSBpbnRlcm5ldCBmaWxlcyBhbmQgY2xlYW4gdGhlbSBmb3IgYmFzaWMgdGV4dCBhbmFseXRpY3MgaW5jbHVkaW5nIHZpc3VhbCByZXByZXNlbnRhdGlvbiB1c2luZyB3b3JkY2xvdWQuDQoNCmBgYHtyfQ0KIysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysNCiMgcnF1ZXJ5LndvcmRjbG91ZCgpIDogV29yZCBjbG91ZCBnZW5lcmF0b3INCiMgLSBodHRwOi8vd3d3LnN0aGRhLmNvbQ0KIysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrDQojIHggOiBjaGFyYWN0ZXIgc3RyaW5nIChwbGFpbiB0ZXh0LCB3ZWIgdXJsLCB0eHQgZmlsZSBwYXRoKQ0KIyB0eXBlIDogc3BlY2lmeSB3aGV0aGVyIHggaXMgYSBwbGFpbiB0ZXh0LCBhIHdlYiBwYWdlIHVybCBvciBhIGZpbGUgcGF0aA0KIyBsYW5nIDogdGhlIGxhbmd1YWdlIG9mIHRoZSB0ZXh0DQojIGV4Y2x1ZGVXb3JkcyA6IGEgdmVjdG9yIG9mIHdvcmRzIHRvIGV4Y2x1ZGUgZnJvbSB0aGUgdGV4dA0KIyB0ZXh0U3RlbW1pbmcgOiByZWR1Y2VzIHdvcmRzIHRvIHRoZWlyIHJvb3QgZm9ybQ0KIyBjb2xvclBhbGV0dGUgOiB0aGUgbmFtZSBvZiBjb2xvciBwYWxldHRlIHRha2VuIGZyb20gUkNvbG9yQnJld2VyIHBhY2thZ2UsIA0KICAjIG9yIGEgY29sb3IgbmFtZSwgb3IgYSBjb2xvciBjb2RlDQojIG1pbi5mcmVxIDogd29yZHMgd2l0aCBmcmVxdWVuY3kgYmVsb3cgbWluLmZyZXEgd2lsbCBub3QgYmUgcGxvdHRlZA0KIyBtYXgud29yZHMgOiBNYXhpbXVtIG51bWJlciBvZiB3b3JkcyB0byBiZSBwbG90dGVkLiBsZWFzdCBmcmVxdWVudCB0ZXJtcyBkcm9wcGVkDQoNCiMgdmFsdWUgcmV0dXJuZWQgYnkgdGhlIGZ1bmN0aW9uIDogYSBsaXN0KHRkbSwgZnJlcVRhYmxlKQ0KcnF1ZXJ5LndvcmRjbG91ZCA8LSBmdW5jdGlvbih4LCB0eXBlPWMoInRleHQiLCAidXJsIiwgImZpbGUiKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIGxhbmc9ImVuZ2xpc2giLCBleGNsdWRlV29yZHM9TlVMTCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRleHRTdGVtbWluZz1GQUxTRSwgIGNvbG9yUGFsZXR0ZT0iRGFyazIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBtaW4uZnJlcT0zLCBtYXgud29yZHM9MjAwMCkNCnsgDQogIGlmKHR5cGVbMV09PSJmaWxlIikgdGV4dCA8LSByZWFkTGluZXMoeCkNCiAgZWxzZSBpZih0eXBlWzFdPT0idXJsIikgdGV4dCA8LSBodG1sX3RvX3RleHQoeCkNCiAgZWxzZSBpZih0eXBlWzFdPT0idGV4dCIpIHRleHQgPC0geA0KICANCiAgIyBMb2FkIHRoZSB0ZXh0IGFzIGEgY29ycHVzDQogIGRvY3MgPC0gQ29ycHVzKFZlY3RvclNvdXJjZSh0ZXh0KSkNCiAgIyBDb252ZXJ0IHRoZSB0ZXh0IHRvIGxvd2VyIGNhc2UNCiAgZG9jcyA8LSB0bV9tYXAoZG9jcywgY29udGVudF90cmFuc2Zvcm1lcih0b2xvd2VyKSkNCiAgIyBSZW1vdmUgbnVtYmVycw0KICBkb2NzIDwtIHRtX21hcChkb2NzLCByZW1vdmVOdW1iZXJzKQ0KICAjIFJlbW92ZSBzdG9wd29yZHMgZm9yIHRoZSBsYW5ndWFnZSANCiAgZG9jcyA8LSB0bV9tYXAoZG9jcywgcmVtb3ZlV29yZHMsIHN0b3B3b3JkcyhsYW5nKSkNCiAgIyBSZW1vdmUgcHVuY3R1YXRpb25zDQogIGRvY3MgPC0gdG1fbWFwKGRvY3MsIHJlbW92ZVB1bmN0dWF0aW9uKQ0KICAjIEVsaW1pbmF0ZSBleHRyYSB3aGl0ZSBzcGFjZXMNCiAgZG9jcyA8LSB0bV9tYXAoZG9jcywgc3RyaXBXaGl0ZXNwYWNlKQ0KICAjIFJlbW92ZSB5b3VyIG93biBzdG9wd29yZHMNCiAgaWYoIWlzLm51bGwoZXhjbHVkZVdvcmRzKSkgDQogICAgZG9jcyA8LSB0bV9tYXAoZG9jcywgcmVtb3ZlV29yZHMsIGV4Y2x1ZGVXb3JkcykgDQogICMgVGV4dCBzdGVtbWluZw0KICBpZih0ZXh0U3RlbW1pbmcpIGRvY3MgPC0gdG1fbWFwKGRvY3MsIHN0ZW1Eb2N1bWVudCkNCiAgIyBDcmVhdGUgdGVybS1kb2N1bWVudCBtYXRyaXgNCiAgdGRtIDwtIFRlcm1Eb2N1bWVudE1hdHJpeChkb2NzKQ0KICBtIDwtIGFzLm1hdHJpeCh0ZG0pDQogIHYgPC0gc29ydChyb3dTdW1zKG0pLGRlY3JlYXNpbmc9VFJVRSkNCiAgZCA8LSBkYXRhLmZyYW1lKHdvcmQgPSBuYW1lcyh2KSxmcmVxPXYpDQogICMgY2hlY2sgdGhlIGNvbG9yIHBhbGV0dGUgbmFtZSANCiAgaWYoIWNvbG9yUGFsZXR0ZSAlaW4lIHJvd25hbWVzKGJyZXdlci5wYWwuaW5mbykpIGNvbG9ycyA9IGNvbG9yUGFsZXR0ZQ0KICBlbHNlIGNvbG9ycyA9IGJyZXdlci5wYWwoOCwgY29sb3JQYWxldHRlKSANCiAgIyBQbG90IHRoZSB3b3JkIGNsb3VkDQogIHNldC5zZWVkKDEyMzQpDQogIHdvcmRjbG91ZChkJHdvcmQsZCRmcmVxLCBtaW4uZnJlcT1taW4uZnJlcSwgbWF4LndvcmRzPW1heC53b3JkcywNCiAgICAgICAgICAgIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICAgIHVzZS5yLmxheW91dD1GQUxTRSwgY29sb3JzPWNvbG9ycykNCiAgDQogIGludmlzaWJsZShsaXN0KHRkbT10ZG0sIGZyZXFUYWJsZSA9IGQpKQ0KfQ0KDQojKysrKysrKysrKysrKysrKysrKysrKw0KIyBIZWxwZXIgZnVuY3Rpb24NCiMrKysrKysrKysrKysrKysrKysrKysrDQojIERvd25sb2FkIGFuZCBwYXJzZSB3ZWJwYWdlDQpodG1sX3RvX3RleHQ8LWZ1bmN0aW9uKHVybCl7DQogICMgZG93bmxvYWQgaHRtbA0KICBodG1sLmRvYyA8LSBnZXRVUkwodXJsKSAgDQogICNjb252ZXJ0IHRvIHBsYWluIHRleHQNCiAgZG9jID0gaHRtbFBhcnNlKGh0bWwuZG9jLCBhc1RleHQ9VFJVRSkNCiAjICIvL3RleHQoKSIgcmV0dXJucyBhbGwgdGV4dCBvdXRzaWRlIG9mIEhUTUwgdGFncy4NCiAjIFdlIGFsc28gZG9uJ3Qgd2FudCB0ZXh0IHN1Y2ggYXMgc3R5bGUgYW5kIHNjcmlwdCBjb2Rlcw0KICB0ZXh0IDwtIHhwYXRoU0FwcGx5KGRvYywgIi8vdGV4dCgpW25vdChhbmNlc3Rvcjo6c2NyaXB0KV1bbm90KGFuY2VzdG9yOjpzdHlsZSldW25vdChhbmNlc3Rvcjo6bm9zY3JpcHQpXVtub3QoYW5jZXN0b3I6OmZvcm0pXSIsIHhtbFZhbHVlKQ0KICAjIEZvcm1hdCB0ZXh0IHZlY3RvciBpbnRvIG9uZSBjaGFyYWN0ZXIgc3RyaW5nDQogIHJldHVybihwYXN0ZSh0ZXh0LCBjb2xsYXBzZSA9ICIgIikpDQp9DQpgYGANCg0KDQpgYGB7cn0NCnJxdWVyeS53b3JkY2xvdWQoImh0dHBzOi8vcGVuZ2RzY2kuZ2l0aHViLmlvL1NUQTU1M1ZJWi93MDQvT2JhbWFfU3BlZWNoXzItMjQtMDkudHh0IiwgdHlwZT0idXJsIikNCmBgYA0K