1. CoreNLP

1.1 環境建置與套件安裝

packages = c("dplyr","ggplot2","rtweet" ,"xml2", "httr", "jsonlite", "gutenbergr", "data.tree", "NLP", "igraph","sentimentr","tidytext","wordcloud2")
existing = as.character(installed.packages()[,1])
for(pkg in packages[!(packages %in% existing)]) install.packages(pkg)
library(wordcloud2)
library(ggplot2)
library(scales)
library(rtweet)
library(dplyr)
library(xml2)
library(httr)
library(jsonlite)
library(magrittr)
library(data.tree)
library(tidytext)
library(stringr)

載入已經跑完的資料

load("coreNLP2.RData")

1.2 資料收集:透過rtweet抓取tweets

1.2.1 Twitter API Authentication設定

app = 'social_media_31lab'
consumer_key = 'GODaKYgABKACWnCC8oaQyrDz8'
consumer_secret = 'dIlhRbYkgjiCJzVrBqD4lKyQuekc6caKJmFKZXC3de2LaqQWMZ'
access_token = '1108983131132956672-DvMTaH6VsiQgOCN4BMRp7UJOe4mZsQ'
access_secret = 'gEICepruCbQHrMk8OWBAYRRHxEepIH1JsFkrzdrQVRPSH'
create_token(app,consumer_key, consumer_secret,
                    access_token, access_secret)
<Token>
<oauth_endpoint>
 request:   https://api.twitter.com/oauth/request_token
 authorize: https://api.twitter.com/oauth/authenticate
 access:    https://api.twitter.com/oauth/access_token
<oauth_app> social_media_31lab
  key:    GODaKYgABKACWnCC8oaQyrDz8
  secret: <hidden>
<credentials> oauth_token, oauth_token_secret
---

1.2.2 設定關鍵字中美貿易戰與tweets內容清理

# 關鍵字
Keys = c("#tradewars","trade war")
# 除關鍵字外還需在tweets裡出現context才會抓取
context = "China"
df = data.frame()
clean = function(txt,key,context) {
  txt = iconv(txt, "latin1", "ASCII", sub="") #轉換編碼
  txt = gsub("(@|#)\\w+", "", txt) #去除@或#後有數字,字母,底線 (標記人名或hashtag)
  txt = gsub("(http|https)://.*", "", txt) #去除網址
  txt = gsub("[\t]{2,}", "", txt) #去除兩個以上的tab
  txt = gsub("\\n"," ",txt) #去除換行
  txt = gsub("&.*;","",txt) #去除html特殊字元編碼
  txt = gsub("xi jinping","Xi",txt,ignore.case = T) #先將全部的xi jinping(不論大小寫)換成Xi)
  txt = gsub(" Xi "," Xi Jinping ",txt,ignore.case = T) 
  #再將全部的Xi 換成Xi Jinping(正確大小寫 比較能被正確辨識出NER為PERSON)
  #要記得是" Xi "前後有空格,否則沒有空格的話 遇到如Brexit這個字 會變成BreXi Jinping 
  #因為有些原本只有寫Xi 所以做兩次的轉換
  
  #最後再整理空格
  txt = gsub("\\s+"," ",txt) #去除一個以上的空格
  txt = gsub("^\\s+|\\s+$","",txt) #去除前後一個以上的空格
  
  #只留下我們想看的字元
  txt = gsub("[^a-zA-Z0-9?!. ']","",txt) #除了字母,數字 ?!.' ,空白的都去掉
  txt } 
for(key in Keys) {
  q = paste(c(key,context),collapse=" AND ")   
  # 查詢字詞 "#tradewars AND China","trade war AND China""
  # 為了避免只下#tradewars 會找到非中美貿易戰的tweets,加入China要同時出現的條件
  tweets = search_tweets(q,lang="en",n=3000,include_rts = FALSE,retryonratelimit = T) 
  #抓3000筆 不抓轉推
  tweets$text = clean(tweets$text,key,context)
  
  df = rbind(df,tweets)  # transfer to data frame
}
Searching for tweets...
This may take a few seconds...
Finished collecting tweets!
Searching for tweets...
This may take a few seconds...
Finished collecting tweets!
df = df[!duplicated(df[,"status_id"]),]  #去除重複的tweets

因為tweets是自由發揮,沒有固定格式的文章,因此在資料前處理會較繁雜,且要多次嘗試

查看df內容與欄位,了解rtweet抓回了什麼資料

head(df)

df共有88個欄位,但我們在這裡僅會使用幾個欄位:

  • user_id: 用戶id
  • status_id : 推文id
  • created_at : 發文時間
  • screen_name : 用戶暱稱
  • text : 推文內容
  • source : 發文來源

rtweet最多只能抓到距今10天的資料,因此即使我們設定一個query要抓3000筆,照理說我們下了2個query,應該要抓到6000,但因為rtweet最遠只能抓到10天前的資料,在這10天內也無法抓到那麼多

nrow(df)
[1] 3737

created_at已經是一個date類型的欄位,因此可以直接用min,max來看最遠或最近的日期

min(df$created_at)
[1] "2019-03-15 11:28:54 UTC"
max(df$created_at)
[1] "2019-03-25 05:02:41 UTC"

1.3 串接CoreNLP API

1.3.1 API呼叫的設定

server端 : + 需先在terminal開啟corenlp server + 在corenlp的路徑下開啟terminal輸入 java -mx4g -cp "*" edu.stanford.nlp.pipeline.StanfordCoreNLPServer -port 9000 -timeout 15000

# 生產core-nlp的api url,可以設定斷詞依據、以及要標註的任務
generate_API_url <- function(host, port="9000",
                    tokenize.whitespace="false", annotators=""){ #斷詞依據不是空格
    url <- sprintf('http://%s:%s/?properties={"tokenize.whitespace":"%s","annotators":"%s"}',
                     host, port, tokenize.whitespace, annotators)
    url <- URLencode(url)
}
generate_API_url("127.0.0.1")

指定服務的位置

host = "127.0.0.1"
# 呼叫core-nlp api
call_coreNLP <- function(server_host, text, host="localhost", language="eng",
                    tokenize.whitespace="true", ssplit.eolonly="true", annotators=c("tokenize","ssplit","pos","lemma","ner","parse","sentiment")){
  # 假設有兩個core-nlp server、一個負責英文(使用9000 port)、另一個則負責中文(使用9001 port)
  port <- ifelse(language=="eng", 9000, 9001);
  # 產生api網址
  url <- generate_API_url(server_host, port=port,
                    tokenize.whitespace=tokenize.whitespace, annotators=paste0(annotators, collapse = ','))
  
  result <- POST(url, body = text, encode = "json")
  doc <- httr::content(result, "parsed","application/json",encoding = "UTF-8")
  return (doc)
}
coreNLP <- function(data,host){
  # 依序將每個文件丟進core-nlp進行處理,每份文件的回傳結果為json格式
  # 在R中使用objects來儲存處理結果
  result <- apply(data, 1 , function(x){
    object <- call_coreNLP(host, x['text'])
    list(doc=object, data=x)
  })
  
  return(result)
}

1.3.2 資料整理function (code by CEO Chin)

從回傳的object中整理斷詞出結果,輸出為 tidydata 格式

coreNLP_tokens_parser <- function(coreNLP_objects){
  
  result <- do.call(rbind, lapply(coreNLP_objects, function(obj){
    original_data <- obj$data
    doc <- obj$doc
    # for a sentences
    sentences <- doc$sentences
   
    sen <- sentences[[1]]
    
    tokens <- do.call(rbind, lapply(sen$tokens, function(x){
      result <- data.frame(word=x$word, lemma=x$lemma, pos=x$pos, ner=x$ner)
      result
    }))
    
    tokens <- original_data %>%
      t() %>% 
      data.frame() %>% 
      select(-text) %>% 
      slice(rep(1:n(), each = nrow(tokens))) %>% 
      bind_cols(tokens)
    
    tokens
  }))
  return(result)
}

從回傳的core-nlp object中整理出詞彙依存關係,輸出為 tidydata 格式

coreNLP_dependency_parser <- function(coreNLP_objects){
  result <- do.call(rbind, lapply(coreNLP_objects, function(obj){
    original_data <- obj$data
    doc <- obj$doc
    # for a sentences
    sentences <- doc$sentences
    sen <- sentences[[1]]
    dependencies <- do.call(rbind, lapply(sen$basicDependencies, function(x){
      result <- data.frame(dep=x$dep, governor=x$governor, governorGloss=x$governorGloss, dependent=x$dependent, dependentGloss=x$dependentGloss)
      result
    }))
  
    dependencies <- original_data %>%
      t() %>% 
      data.frame() %>% 
      select(-text) %>% 
      slice(rep(1:n(), each = nrow(dependencies))) %>% 
      bind_cols(dependencies)
    dependencies
  }))
  return(result)
}

從回傳的core-nlp object中整理出語句情緒,輸出為 tidydata 格式

coreNLP_sentiment_parser <- function(coreNLP_objects){
  result <- do.call(rbind, lapply(coreNLP_objects, function(obj){
    original_data <- obj$data
    doc <- obj$doc
    # for a sentences
    sentences <- doc$sentences
    sen <- sentences[[1]]
    
    sentiment <- original_data %>%
      t() %>% 
      data.frame() %>% 
      bind_cols(data.frame(sentiment=sen$sentiment, sentimentValue=sen$sentimentValue))
  
    sentiment
  }))
  return(result)
}

1.3.3 圖形化 Dependency tree

程式參考來源:https://stackoverflow.com/questions/35496560/how-to-convert-corenlp-generated-parse-tree-into-data-tree-r-package

# 圖形化顯示dependency結果
parse2tree <- function(ptext) {
  stopifnot(require(NLP) && require(igraph))
  
  # this step modifies coreNLP parse tree to mimic openNLP parse tree
  ptext <- gsub("[\r\n]", "", ptext)
  ptext <- gsub("ROOT", "TOP", ptext)
  ## Replace words with unique versions
  ms <- gregexpr("[^() ]+", ptext)                                      # just ignoring spaces and brackets?
  words <- regmatches(ptext, ms)[[1]]                                   # just words
  regmatches(ptext, ms) <- list(paste0(words, seq.int(length(words))))  # add id to words
  
  ## Going to construct an edgelist and pass that to igraph
  ## allocate here since we know the size (number of nodes - 1) and -1 more to exclude 'TOP'
  edgelist <- matrix('', nrow=length(words)-2, ncol=2)
  
  ## Function to fill in edgelist in place
  edgemaker <- (function() {
    i <- 0                                       # row counter
    g <- function(node) {                        # the recursive function
      if (inherits(node, "Tree")) {            # only recurse subtrees
        if ((val <- node$value) != 'TOP1') { # skip 'TOP' node (added '1' above)
          for (child in node$children) {
            childval <- if(inherits(child, "Tree")) child$value else child
            i <<- i+1
            edgelist[i,1:2] <<- c(val, childval)
          }
        }
        invisible(lapply(node$children, g))
      }
    }
  })()
  
  ## Create the edgelist from the parse tree
  edgemaker(Tree_parse(ptext))
  tree <- FromDataFrameNetwork(as.data.frame(edgelist))
  return (tree)
}

1.4 呼叫 coreNLP 服務分析句子

1.4.1 將句子丟入服務

取得coreNLP回傳的物件
先不要跑這段,會花大概18分鐘(如果你記憶體只有4G可能會當掉…)

# gc() #釋放不使用的記憶體
# 
# t0 = Sys.time()
# obj = df[,c(2,5)]  %>% filter(text != "") %>% coreNLP(host)
# #先過濾掉沒有內容的的tweet
# #丟入coreNLP的物件 必須符合: 是一個data.frame 且有一個text欄位
# 
# Sys.time() - t0 #執行時間
# #Time difference of 17.89611 mins
# 
# save.image("coreNLP_0325.RData")

1.4.2 從回傳的物件中提取斷詞、詞彙還原、詞性標註、命名實體標註等結果

tokens
tokens =  coreNLP_tokens_parser(obj)
tokens

coreNLP_tokens_parser欄位:

  • status_id : 對應原本df裡的status_id,為一則tweets的唯一id
  • word: 原始斷詞
  • lemma : 對斷詞做詞形還原
  • pos : part-of-speech,詞性
  • ner: 命名實體
從NER查看特定類型的實體

辨識出哪幾種類型的實體

levels(tokens$ner)
 [1] "O"                 "COUNTRY"           "NATIONALITY"       "DATE"             
 [5] "MISC"              "NUMBER"            "PERSON"            "TITLE"            
 [9] "ORDINAL"           "CITY"              "CAUSE_OF_DEATH"    "LOCATION"         
[13] "ORGANIZATION"      "RELIGION"          "PERCENT"           "DURATION"         
[17] "TIME"              "IDEOLOGY"          "MONEY"             "SET"              
[21] "STATE_OR_PROVINCE" "CRIMINAL_CHARGE"   "URL"              
length(unique(tokens$word[tokens$ner != "O"])) #除去entity為Other,有多少種word有被標註entity
[1] 1752

因為大小寫也會影響corenlp對NER的判斷,因此我們一開始給的推文內容是沒有處理大小寫的,但在跑完anotator後,為了正確計算詞頻,創建新欄位lower_word與lower_lemma,存放轉換小寫的word與lemma
轉成小寫的目的是要將不同大小寫的同一字詞(如Trump與trump)都換成小寫,再來計算詞頻。

tokens$lower_word = tolower(tokens$word)
tokens$lower_lemma = tolower(tokens$lemma)

對於初步不了解中美貿易戰的人來說,可能不知道相關人物是誰,無法設立完整的人物字典。透過coreNLP解析出NER,篩選出PERSON並計算實體的詞頻,能讓我們知道相關人物的在話題中的重要性。

tokens %>%
  filter(ner == "PERSON") %>%  #篩選NER為PERSION
  group_by(lower_word) %>% #根據word分組
  summarize(count = n()) %>% #計算每組
  top_n(n = 15, count) %>%
  ungroup() %>% 
  mutate(lower_word = reorder(lower_word, count)) %>%
  ggplot(aes(lower_word, count)) + 
  geom_col()+
  ggtitle("Word Frequency (NER is PERSON)") +
  theme(text=element_text(size=14))+
  coord_flip()

tokens %>%
  filter(ner == "COUNTRY") %>%  #篩選NER為COUNTRY
  group_by(lower_word) %>% #根據word分組
  summarize(count = n()) %>% #計算每組
  top_n(n = 10, count) %>%
  ungroup() %>% 
  mutate(lower_word = reorder(lower_word, count)) %>%
  ggplot(aes(lower_word, count)) + 
  geom_col()+
  ggtitle("Word Frequency (NER is COUNTRY)") +
  theme(text=element_text(size=14))+
  coord_flip()

tokens %>%
  filter(ner == "IDEOLOGY") %>%  #篩選NER為IDEOLOGY
  group_by(lower_word) %>% #根據word分組
  summarize(count = n()) %>% #計算每組
  top_n(n = 10, count) %>%
  ungroup() %>% 
  mutate(lower_word = reorder(lower_word, count)) %>%
  ggplot(aes(lower_word, count)) + 
  geom_col()+
  ggtitle("Word Frequency (NER is IDEOLOGY)") +
  theme(text=element_text(size=14))+
  coord_flip()

練習(15 min):
  1. 對word去除掉stop words(hint: 在tidytext第一章)
  2. 篩選去除trade,war,china,u.s.
  3. 根據word分組計算count
  4. 依據count做遞減排序
  5. 根據lemma做分組會有什麼不同嗎?

參考解答

data("stop_words") #載入存在tidytext套件中的stop_words資料
names(stop_words)[1] = "lower_word" #將stop_words的第一個欄位名稱改做lower_word
tokens %>%
  anti_join(stop_words) %>%  #tokens和stop_words anti_join by 相同名稱的欄位lower_word 
  filter(!( lower_word %in% c("trade","war","china","us","u.s.","united","states","america"))) %>% 
  group_by(lower_word) %>% 
  summarise(count = n()) %>% 
  arrange(desc(count))
Joining, by = "lower_word"
tokens %>% 
  anti_join(stop_words) %>% 
  filter(!( lower_word %in% c("trade","war","china","us","u.s.","united","states","america"))) %>% 
  group_by(lower_lemma) %>% 
  summarise(count = n()) %>% 
  arrange(desc(count))
Joining, by = "lower_word"
不同用戶設備端的常用字會不同嗎?
table(df$source) %>% sort(decreasing = T) %>% head

 Twitter Web Client  Twitter for iPhone Twitter for Android               IFTTT 
                976                 581                 450                 246 
            dlvr.it    Twitter for iPad 
                149                 127 
tokens %>% 
  anti_join(stop_words) %>%
  merge(df[,c(2,6)]) %>% 
  filter(!(lower_word %in% c("trade","war","china","u.s.","united","states","america")),
         source %in% c("Twitter Web Client","Twitter for iPhone","Twitter for Android")) %>% 
  group_by(source,lower_lemma) %>%
  summarize(count = n()) %>% #計算每組
  top_n(15,count) %>%
  ungroup() %>% 
  mutate(lower_lemma = reorder(lower_lemma, count)) %>% 
  #arrange(desc(count),.by_group = TRUE) %>% 
  ggplot(aes(lower_lemma, count)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~source, scales = "free_y") +
  labs(y = "tweets from difference source",
       x = NULL) +
  theme(text=element_text(size=14))+
  coord_flip()
Joining, by = "lower_word"

語句依存關係結果
dependencies = coreNLP_dependency_parser(obj)
dependencies

查看原句

df$text[df$status_id == "1109868276069343233"]
[1] "Paper Dragon. Laws of economics can only be manipulated for so long. Trade Wars are easy. China goes bust Chinese companies had record amount of corporate bond defaults in 2018"
視覺化 Dependency tree
parse_tree <- obj[[1]]$doc[[1]][[1]]$parse
tree <- parse2tree(parse_tree)
Loading required package: NLP
package 㤼㸱NLP㤼㸲 was built under R version 3.5.2
Attaching package: 㤼㸱NLP㤼㸲

The following object is masked from 㤼㸱package:httr㤼㸲:

    content

The following object is masked from 㤼㸱package:ggplot2㤼㸲:

    annotate

Loading required package: igraph
package 㤼㸱igraph㤼㸲 was built under R version 3.5.3
Attaching package: 㤼㸱igraph㤼㸲

The following objects are masked from 㤼㸱package:dplyr㤼㸲:

    as_data_frame, groups, union

The following objects are masked from 㤼㸱package:stats㤼㸲:

    decompose, spectrum

The following object is masked from 㤼㸱package:base㤼㸲:

    union
SetNodeStyle(tree, style = "filled,rounded", shape = "box")
plot(tree)
語句情緒值
sentiment = coreNLP_sentiment_parser(obj)
sentiment

在這個資料集中,情緒label有幾種

levels(sentiment$sentiment)
[1] "Negative"     "Neutral"      "Positive"     "Verynegative"

**注意**

先讓我們查看sentimentValue的類型,是factor

class(sentiment$sentimentValue)
[1] "factor"

但是我們必須轉成numeric才能對其做數值運算,例如計算平均情緒
要特別注意,若直接將factor轉成numeric,結果的數值不會是原本factor的數值,而會是factor level的順序,範例:

A = c(1,4,3,0,4,3) %>% as.factor
levels(A)
[1] "0" "1" "3" "4"

level順序是0 > 1 > 3 > 4

若將A直接轉成numeric,可以發現結果並不會是原本的(1,4,3,0,4,3),而是轉換成元素原本的level順序
(1,4,3,0,4,3) => (level順序: 0是1,1是2,3是3,4是4 ) => (2,4,3,1,4,3)

A %>% as.numeric
[1] 2 4 3 1 4 3

因此,我們不能直接將sentimentValue直接由factor類型轉換成numeric
而是先將factor轉character,再轉numeric

sentiment$sentimentValue = sentiment$sentimentValue %>% as.character %>% as.numeric

用table看情緒label對應的sentimentValue

table(sentiment$sentiment,sentiment$sentimentValue)
              
                  0    1    2    3
  Negative        0 2680    0    0
  Neutral         0    0  813    0
  Positive        0    0    0  181
  Verynegative   26    0    0    0

sentimentValue就會是原本的:
0,1 : Verynegative,negative
2 : neutral
3,4 : positive,Verypositive(在本次資料集沒有出現)

平均情緒分數時間趨勢

df$date = as.Date(df$created_at)
sentiment %>% 
  merge(df[,c("status_id","date")]) %>%
  group_by(date) %>% 
  summarise(avg_sentiment = mean(sentimentValue,na.rm=T)) %>% 
  ggplot(aes(x=date,y=avg_sentiment)) + 
  geom_line()

不同用戶端情緒時間趨勢

sentiment$sentimentValue = as.numeric(sentiment$sentimentValue) 
sentiment %>% 
  merge(df[,c("status_id","source","date")]) %>%
  filter(source %in% c("Twitter Web Client","Twitter for iPhone","Twitter for Android")) %>% 
  group_by(date,source) %>% 
  summarise(avg_sentiment = mean(sentimentValue,na.rm=T)) %>% 
  ggplot(aes(x=date,y=avg_sentiment,color=source)) + 
  geom_line()

Verynegative和negative的tweets常出現的字

X = sentiment %>% 
  merge(tokens[,c("status_id","lower_word","lower_lemma","ner")]) %>% 
  anti_join(stop_words)
Joining, by = "lower_word"
X %>% filter(
  !(lower_word %in% c("trade","war","china","china.","u.s.","united","states","trump","america")),
         sentiment %in% c("Verynegative","negative") ,
         ner != "NUMBER") %>% 
  group_by(lower_word) %>% #根據word分組
  summarize(count = n()) %>%
  wordcloud2()
X %>% filter(
  !(lower_word %in% c("trade","war","china","china.","u.s.","united","states","trump","america")),
         sentiment %in% c("Verynegative","negative") ,
         ner != "NUMBER") %>% 
  group_by(lower_word) %>% #根據word分組
  summarize(count = n()) %>% 
  arrange(desc(count))

positive的tweets常出現的字

sentiment %>% 
  merge(tokens[,c("status_id","lower_word","lower_lemma","ner")]) %>% 
  anti_join(stop_words) %>%
  filter(!(lower_word %in% c("trade","war","china","china.","u.s.","united","states","trump","america")),
         sentiment == "positive",
         ner != "NUMBER") %>% 
  group_by(lower_lemma) %>% #根據word分組
  summarize(count = n()) %>%
  top_n(40,count) %>% 
  wordcloud2()
Joining, by = "lower_word"
no non-missing arguments to max; returning -Inf

2.Sentimentr 英文情緒分析


library(sentimentr)

mytext <- c(
    'do you like it?  But I hate really bad dogs',
    'I am the best friend.',
    'Do you really like it?  I\'m not a fan'
)

mytext <- get_sentences(mytext) #將character向量轉成list,list裡放著character向量(斷句)
mytext[[1]][1] #取出mytext(list)裡的第一個向量的第一個元素
[1] "do you like it?"
sentiment_by(mytext) #sentiment_by()  給定文本的平均情感分數
sentiment(mytext) # sentiment() 在sentence的級別評分

轉換Emoji代碼為語意文字

replace_emoji("\U0001f4aa")

補充: 在R使用coreNLP的另一種方式- cleanNLP

cleanNLP

套件與環境參數設定

packages = c("cleanNLP","dplyr", "magrittr","twitterR","stringi", "udpipe", "reticulate", "rJava",
                   "RCurl", "knitr", "rmarkdown", "testthat", "covr",
                   "roxygen2")
existing = as.character(installed.packages()[,1])
for(pkg in packages[!(packages %in% existing)]) install.packages(pkg)
# library(rJava)
# library(cleanNLP)
# library(udpipe)
# library(reticulate)

# gc()
# 
# use_python("C:\\Users\\konir\\Anaconda3\\python") #指定自己的python路徑(版本3.6+)
# cnlp_init_udpipe()
# cnlp_init_corenlp("en",anno_level=2,lib_location = "C:\\Users\\konir\\Documents\\R\\win-library\\3.5\\cleanNLP\\extdata") #lib_locatio改成corenlp模組ㄜ
# 
# 
# #如果出現java.lang.OutOfMemoryError: GC overhead limit exceeded代表QQ你的記憶體就算gc()過了還是不夠,請重開R看看
# t0  = Sys.time()
# obj <- cnlp_annotate(df$text, as_strings = TRUE,backend = "coreNLP")
# Sys.time() - t0
# cnlp_get_document(obj)
# cnlp_get_dependency(obj)
# cnlp_get_token(obj)
# cnlp_get_entity(obj)
# cnlp_get_sentence(obj) #a score from 0 (most negative) to 4 (most positive) 
LS0tDQp0aXRsZTogIuekvue+pOWqkumrlOWIhuaekC1Db3JlTkxQ6IiHc2VudGltZW50ciINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KYWJzdHJhY3Q6ICfkvb/nlKhjb3JlTkxQ6IiHc2VudGltZW50cuWIhuaekHR3aXR0ZXLkuIrpl5zmlrzkuK3nvo7osr/mmJPmiLDnmoTmloflrZfos4fmlpknDQotLS0NCg0KIyMgMS4gQ29yZU5MUA0KDQojIyMgMS4xIOeSsOWig+W7uue9ruiIh+Wll+S7tuWuieijnQ0KDQorIOWuieijnUpBVkEgSlJFIDEuOCsgaHR0cHM6Ly93d3cuamF2YS5jb20vemhfVFcvDQorIOS4i+i8iVN0YW5mb3JkIGNvcmVOTFAgZnVsbOaooee1hCANCg0KDQpgYGB7cn0NCnBhY2thZ2VzID0gYygiZHBseXIiLCJnZ3Bsb3QyIiwicnR3ZWV0IiAsInhtbDIiLCAiaHR0ciIsICJqc29ubGl0ZSIsICJndXRlbmJlcmdyIiwgImRhdGEudHJlZSIsICJOTFAiLCAiaWdyYXBoIiwic2VudGltZW50ciIsInRpZHl0ZXh0Iiwid29yZGNsb3VkMiIpDQpleGlzdGluZyA9IGFzLmNoYXJhY3RlcihpbnN0YWxsZWQucGFja2FnZXMoKVssMV0pDQpmb3IocGtnIGluIHBhY2thZ2VzWyEocGFja2FnZXMgJWluJSBleGlzdGluZyldKSBpbnN0YWxsLnBhY2thZ2VzKHBrZykNCmBgYA0KDQoNCmBgYHtyfQ0KDQpsaWJyYXJ5KHdvcmRjbG91ZDIpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KHNjYWxlcykNCmxpYnJhcnkocnR3ZWV0KQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoeG1sMikNCmxpYnJhcnkoaHR0cikNCmxpYnJhcnkoanNvbmxpdGUpDQpsaWJyYXJ5KG1hZ3JpdHRyKQ0KbGlicmFyeShkYXRhLnRyZWUpDQpsaWJyYXJ5KHRpZHl0ZXh0KQ0KbGlicmFyeShzdHJpbmdyKQ0KDQpgYGANCg0K6LyJ5YWl5bey57aT6LeR5a6M55qE6LOH5paZDQpgYGB7cn0NCmxvYWQoImNvcmVOTFAwMzI1LlJEYXRhIikNCmBgYA0KDQoNCiMjIyAxLjIg6LOH5paZ5pS26ZuG77ya6YCP6YGOcnR3ZWV05oqT5Y+WdHdlZXRzDQoNCiMjIyMgMS4yLjEgVHdpdHRlciBBUEkgQXV0aGVudGljYXRpb27oqK3lrpoNCg0KYGBge3J9DQoNCmFwcCA9ICdzb2NpYWxfbWVkaWFfMzFsYWInDQpjb25zdW1lcl9rZXkgPSAnR09EYUtZZ0FCS0FDV25DQzhvYVF5ckR6OCcNCmNvbnN1bWVyX3NlY3JldCA9ICdkSWxoUmJZa2dqaUNKelZyQnFENGxLeVF1ZWtjNmNhS0ptRktaWEMzZGUyTGFxUVdNWicNCmFjY2Vzc190b2tlbiA9ICcxMTA4OTgzMTMxMTMyOTU2NjcyLUR2TVRhSDZWc2lRZ09DTjRCTVJwN1VKT2U0bVpzUScNCmFjY2Vzc19zZWNyZXQgPSAnZ0VJQ2VwcnVDYlFIck1rOE9XQkFZUlJIeEVlcElIMUpzRmtyemRyUVZSUFNIJw0KY3JlYXRlX3Rva2VuKGFwcCxjb25zdW1lcl9rZXksIGNvbnN1bWVyX3NlY3JldCwNCiAgICAgICAgICAgICAgICAgICAgYWNjZXNzX3Rva2VuLCBhY2Nlc3Nfc2VjcmV0KQ0KDQoNCmBgYA0KDQoNCiMjIyMgMS4yLjIg6Kit5a6a6Zec6Y215a2X5Lit576O6LK/5piT5oiw6IiHdHdlZXRz5YWn5a655riF55CGDQoNCmBgYHtyfQ0KDQojIOmXnOmNteWtlw0KS2V5cyA9IGMoIiN0cmFkZXdhcnMiLCJ0cmFkZSB3YXIiKQ0KDQojIOmZpOmXnOmNteWtl+WklumChOmcgOWcqHR3ZWV0c+ijoeWHuuePvmNvbnRleHTmiY3mnIPmipPlj5YNCmNvbnRleHQgPSAiQ2hpbmEiDQpkZiA9IGRhdGEuZnJhbWUoKQ0KDQoNCmNsZWFuID0gZnVuY3Rpb24odHh0LGtleSxjb250ZXh0KSB7DQogIHR4dCA9IGljb252KHR4dCwgImxhdGluMSIsICJBU0NJSSIsIHN1Yj0iIikgI+i9ieaPm+e3qOeivA0KICB0eHQgPSBnc3ViKCIoQHwjKVxcdysiLCAiIiwgdHh0KSAj5Y676ZmkQOaIliPlvozmnInmlbjlrZcs5a2X5q+NLOW6lee3miAo5qiZ6KiY5Lq65ZCN5oiWaGFzaHRhZykNCiAgdHh0ID0gZ3N1YigiKGh0dHB8aHR0cHMpOi8vLioiLCAiIiwgdHh0KSAj5Y676Zmk57ay5Z2ADQogIHR4dCA9IGdzdWIoIltcdF17Mix9IiwgIiIsIHR4dCkgI+WOu+mZpOWFqeWAi+S7peS4iueahHRhYg0KICB0eHQgPSBnc3ViKCJcXG4iLCIgIix0eHQpICPljrvpmaTmj5vooYwNCiAgdHh0ID0gZ3N1YigiJi4qOyIsIiIsdHh0KSAj5Y676ZmkaHRtbOeJueauiuWtl+WFg+e3qOeivA0KDQogIHR4dCA9IGdzdWIoInhpIGppbnBpbmciLCJYaSIsdHh0LGlnbm9yZS5jYXNlID0gVCkgI+WFiOWwh+WFqOmDqOeahHhpIGppbnBpbmco5LiN6KuW5aSn5bCP5a+rKeaPm+aIkFhpKQ0KICB0eHQgPSBnc3ViKCIgWGkgIiwiIFhpIEppbnBpbmcgIix0eHQsaWdub3JlLmNhc2UgPSBUKSANCiAgI+WGjeWwh+WFqOmDqOeahFhpIOaPm+aIkFhpIEppbnBpbmco5q2j56K65aSn5bCP5a+rIOavlOi8g+iDveiiq+ato+eiuui+qOitmOWHuk5FUueCulBFUlNPTikNCiAgI+imgeiomOW+l+aYryIgWGkgIuWJjeW+jOacieepuuagvCzlkKbliYfmspLmnInnqbrmoLznmoToqbEg6YGH5Yiw5aaCQnJleGl06YCZ5YCL5a2XIOacg+iuiuaIkEJyZVhpIEppbnBpbmd0DQogICPlm6DngrrmnInkupvljp/mnKzlj6rmnInlr6tYaSDmiYDku6XlgZrlhanmrKHnmoTovYnmj5sNCiAgDQogICPmnIDlvozlho3mlbTnkIbnqbrmoLwNCiAgdHh0ID0gZ3N1YigiXFxzKyIsIiAiLHR4dCkgI+WOu+mZpOS4gOWAi+S7peS4iueahOepuuagvA0KICB0eHQgPSBnc3ViKCJeXFxzK3xcXHMrJCIsIiIsdHh0KSAj5Y676Zmk5YmN5b6M5LiA5YCL5Lul5LiK55qE56m65qC8DQogIA0KICAj5Y+q55WZ5LiL5oiR5YCR5oOz55yL55qE5a2X5YWDDQogIHR4dCA9IGdzdWIoIlteYS16QS1aMC05PyEuICddIiwiIix0eHQpICPpmaTkuoblrZfmr40s5pW45a2XID8hLicgLOepuueZveeahOmDveWOu+aOiQ0KICB0eHQgfSANCg0KDQoNCmZvcihrZXkgaW4gS2V5cykgew0KICBxID0gcGFzdGUoYyhrZXksY29udGV4dCksY29sbGFwc2U9IiBBTkQgIikgICANCiAgIyDmn6XoqaLlrZfoqZ4gIiN0cmFkZXdhcnMgQU5EIENoaW5hIiwidHJhZGUgd2FyIEFORCBDaGluYSIiDQogICMg54K65LqG6YG/5YWN5Y+q5LiLI3RyYWRld2FycyDmnIPmib7liLDpnZ7kuK3nvo7osr/mmJPmiLDnmoR0d2VldHPvvIzliqDlhaVDaGluYeimgeWQjOaZguWHuuePvueahOaineS7tg0KDQogIHR3ZWV0cyA9IHNlYXJjaF90d2VldHMocSxsYW5nPSJlbiIsbj0zMDAwLGluY2x1ZGVfcnRzID0gRkFMU0UscmV0cnlvbnJhdGVsaW1pdCA9IFQpIA0KICAj5oqTMzAwMOethiDkuI3mipPovYnmjqgNCiAgdHdlZXRzJHRleHQgPSBjbGVhbih0d2VldHMkdGV4dCxrZXksY29udGV4dCkNCiAgDQogIGRmID0gcmJpbmQoZGYsdHdlZXRzKSAgIyB0cmFuc2ZlciB0byBkYXRhIGZyYW1lDQp9DQoNCmRmID0gZGZbIWR1cGxpY2F0ZWQoZGZbLCJzdGF0dXNfaWQiXSksXSAgI+WOu+mZpOmHjeikh+eahHR3ZWV0cw0KDQogIA0KYGBgDQoNCuWboOeCunR3ZWV0c+aYr+iHqueUseeZvOaPru+8jOaykuacieWbuuWumuagvOW8j+eahOaWh+eroO+8jOWboOatpOWcqOizh+aWmeWJjeiZleeQhuacg+i8g+e5gembnO+8jOS4lOimgeWkmuasoeWYl+ippjxicj48YnI+DQoNCg0K5p+l55yLZGblhaflrrnoiIfmrITkvY3vvIzkuobop6NydHdlZXTmipPlm57kuobku4Dpurzos4fmlpkNCmBgYHtyfQ0KaGVhZChkZikNCmBgYA0KDQpkZuWFseaciTg45YCL5qyE5L2N77yM5L2G5oiR5YCR5Zyo6YCZ6KOh5YOF5pyD5L2/55So5bm+5YCL5qyE5L2NOg0KDQorIHVzZXJfaWQ6IOeUqOaItmlkDQorIHN0YXR1c19pZCA6IOaOqOaWh2lkDQorIGNyZWF0ZWRfYXQgOiDnmbzmlofmmYLplpMNCisgc2NyZWVuX25hbWUgOiDnlKjmiLbmmrHnqLENCisgdGV4dCA6IOaOqOaWh+WFp+WuuQ0KKyBzb3VyY2UgOiDnmbzmlofkvobmupANCg0KDQpydHdlZXTmnIDlpJrlj6rog73mipPliLDot53ku4oxMOWkqeeahOizh+aWme+8jOWboOatpOWNs+S9v+aIkeWAkeioreWumuS4gOWAi3F1ZXJ56KaB5oqTMzAwMOethu+8jOeFp+eQhuiqquaIkeWAkeS4i+S6hjLlgItxdWVyee+8jOaHieipsuimgeaKk+WIsDYwMDDvvIzkvYblm6DngrpydHdlZXTmnIDpgaDlj6rog73mipPliLAxMOWkqeWJjeeahOizh+aWme+8jOWcqOmAmTEw5aSp5YWn5Lmf54Sh5rOV5oqT5Yiw6YKj6bq85aSaDQpgYGB7cn0NCm5yb3coZGYpDQpgYGANCg0KY3JlYXRlZF9hdOW3sue2k+aYr+S4gOWAi2RhdGXpoZ7lnovnmoTmrITkvY3vvIzlm6DmraTlj6/ku6Xnm7TmjqXnlKhtaW4sbWF45L6G55yL5pyA6YGg5oiW5pyA6L+R55qE5pel5pyfDQpgYGB7cn0NCm1pbihkZiRjcmVhdGVkX2F0KQ0KbWF4KGRmJGNyZWF0ZWRfYXQpDQpgYGANCg0KDQoNCiMjIyAxLjMg5Liy5o6lQ29yZU5MUCBBUEkNCg0KDQojIyMjIDEuMy4xIEFQSeWRvOWPq+eahOioreWumg0KDQpzZXJ2ZXLnq68gOg0KKyDpnIDlhYjlnKh0ZXJtaW5hbOmWi+WVn2NvcmVubHAgc2VydmVyDQorIOWcqGNvcmVubHDnmoTot6/lvpHkuIvplovllZ90ZXJtaW5hbOi8uOWFpSBgamF2YSAtbXg0ZyAtY3AgIioiIGVkdS5zdGFuZm9yZC5ubHAucGlwZWxpbmUuU3RhbmZvcmRDb3JlTkxQU2VydmVyIC1wb3J0IDkwMDAgLXRpbWVvdXQgMTUwMDBgDQoNCmBgYHtyfQ0KIyDnlJ/nlKJjb3JlLW5scOeahGFwaSB1cmzvvIzlj6/ku6XoqK3lrprmlrfoqZ7kvp3mk5rjgIHku6Xlj4ropoHmqJnoqLvnmoTku7vli5kNCmdlbmVyYXRlX0FQSV91cmwgPC0gZnVuY3Rpb24oaG9zdCwgcG9ydD0iOTAwMCIsDQogICAgICAgICAgICAgICAgICAgIHRva2VuaXplLndoaXRlc3BhY2U9ImZhbHNlIiwgYW5ub3RhdG9ycz0iIil7ICPmlrfoqZ7kvp3mk5rkuI3mmK/nqbrmoLwNCiAgICB1cmwgPC0gc3ByaW50ZignaHR0cDovLyVzOiVzLz9wcm9wZXJ0aWVzPXsidG9rZW5pemUud2hpdGVzcGFjZSI6IiVzIiwiYW5ub3RhdG9ycyI6IiVzIn0nLA0KICAgICAgICAgICAgICAgICAgICAgaG9zdCwgcG9ydCwgdG9rZW5pemUud2hpdGVzcGFjZSwgYW5ub3RhdG9ycykNCiAgICB1cmwgPC0gVVJMZW5jb2RlKHVybCkNCn0NCmdlbmVyYXRlX0FQSV91cmwoIjEyNy4wLjAuMSIpDQpgYGANCg0KDQrmjIflrprmnI3li5nnmoTkvY3nva4NCmBgYHtyfQ0KaG9zdCA9ICIxMjcuMC4wLjEiICPmnKzmqZ8NCmBgYA0KDQoNCmBgYHtyfQ0KIyDlkbzlj6tjb3JlLW5scCBhcGkNCmNhbGxfY29yZU5MUCA8LSBmdW5jdGlvbihzZXJ2ZXJfaG9zdCwgdGV4dCwgaG9zdD0ibG9jYWxob3N0IiwgbGFuZ3VhZ2U9ImVuZyIsDQogICAgICAgICAgICAgICAgICAgIHRva2VuaXplLndoaXRlc3BhY2U9InRydWUiLCBzc3BsaXQuZW9sb25seT0idHJ1ZSIsIGFubm90YXRvcnM9YygidG9rZW5pemUiLCJzc3BsaXQiLCJwb3MiLCJsZW1tYSIsIm5lciIsInBhcnNlIiwic2VudGltZW50Iikpew0KICAjIOWBh+ioreacieWFqeWAi2NvcmUtbmxwIHNlcnZlcuOAgeS4gOWAi+iyoOiyrOiLseaWh++8iOS9v+eUqDkwMDAgcG9ydO+8ieOAgeWPpuS4gOWAi+WJh+iyoOiyrOS4reaWh++8iOS9v+eUqDkwMDEgcG9ydO+8iQ0KICBwb3J0IDwtIGlmZWxzZShsYW5ndWFnZT09ImVuZyIsIDkwMDAsIDkwMDEpOw0KICAjIOeUoueUn2Fwaee2suWdgA0KICB1cmwgPC0gZ2VuZXJhdGVfQVBJX3VybChzZXJ2ZXJfaG9zdCwgcG9ydD1wb3J0LA0KICAgICAgICAgICAgICAgICAgICB0b2tlbml6ZS53aGl0ZXNwYWNlPXRva2VuaXplLndoaXRlc3BhY2UsIGFubm90YXRvcnM9cGFzdGUwKGFubm90YXRvcnMsIGNvbGxhcHNlID0gJywnKSkNCiAgDQogIHJlc3VsdCA8LSBQT1NUKHVybCwgYm9keSA9IHRleHQsIGVuY29kZSA9ICJqc29uIikNCiAgZG9jIDwtIGh0dHI6OmNvbnRlbnQocmVzdWx0LCAicGFyc2VkIiwiYXBwbGljYXRpb24vanNvbiIsZW5jb2RpbmcgPSAiVVRGLTgiKQ0KICByZXR1cm4gKGRvYykNCn0NCmBgYA0KDQoNCmBgYHtyfQ0KDQpjb3JlTkxQIDwtIGZ1bmN0aW9uKGRhdGEsaG9zdCl7DQogICMg5L6d5bqP5bCH5q+P5YCL5paH5Lu25Lif6YCyY29yZS1ubHDpgLLooYzomZXnkIbvvIzmr4/ku73mlofku7bnmoTlm57lgrPntZDmnpzngrpqc29u5qC85byPDQogICMg5ZyoUuS4reS9v+eUqG9iamVjdHPkvoblhLLlrZjomZXnkIbntZDmnpwNCiAgcmVzdWx0IDwtIGFwcGx5KGRhdGEsIDEgLCBmdW5jdGlvbih4KXsNCiAgICBvYmplY3QgPC0gY2FsbF9jb3JlTkxQKGhvc3QsIHhbJ3RleHQnXSkNCiAgICBsaXN0KGRvYz1vYmplY3QsIGRhdGE9eCkNCiAgfSkNCiAgDQogIHJldHVybihyZXN1bHQpDQp9DQpgYGANCg0KIyMjIyAxLjMuMiDos4fmlpnmlbTnkIZmdW5jdGlvbiAoY29kZSBieSBDRU8gQ2hpbikNCg0K5b6e5Zue5YKz55qEb2JqZWN05Lit5pW055CG5pa36Kme5Ye657WQ5p6c77yM6Ly45Ye654K6IHRpZHlkYXRhIOagvOW8jw0KYGBge3J9DQpjb3JlTkxQX3Rva2Vuc19wYXJzZXIgPC0gZnVuY3Rpb24oY29yZU5MUF9vYmplY3RzKXsNCiAgDQogIHJlc3VsdCA8LSBkby5jYWxsKHJiaW5kLCBsYXBwbHkoY29yZU5MUF9vYmplY3RzLCBmdW5jdGlvbihvYmopew0KICAgIG9yaWdpbmFsX2RhdGEgPC0gb2JqJGRhdGENCiAgICBkb2MgPC0gb2JqJGRvYw0KICAgICMgZm9yIGEgc2VudGVuY2VzDQogICAgc2VudGVuY2VzIDwtIGRvYyRzZW50ZW5jZXMNCiAgIA0KICAgIHNlbiA8LSBzZW50ZW5jZXNbWzFdXQ0KICAgIA0KICAgIHRva2VucyA8LSBkby5jYWxsKHJiaW5kLCBsYXBwbHkoc2VuJHRva2VucywgZnVuY3Rpb24oeCl7DQogICAgICByZXN1bHQgPC0gZGF0YS5mcmFtZSh3b3JkPXgkd29yZCwgbGVtbWE9eCRsZW1tYSwgcG9zPXgkcG9zLCBuZXI9eCRuZXIpDQogICAgICByZXN1bHQNCiAgICB9KSkNCiAgICANCiAgICB0b2tlbnMgPC0gb3JpZ2luYWxfZGF0YSAlPiUNCiAgICAgIHQoKSAlPiUgDQogICAgICBkYXRhLmZyYW1lKCkgJT4lIA0KICAgICAgc2VsZWN0KC10ZXh0KSAlPiUgDQogICAgICBzbGljZShyZXAoMTpuKCksIGVhY2ggPSBucm93KHRva2VucykpKSAlPiUgDQogICAgICBiaW5kX2NvbHModG9rZW5zKQ0KICAgIA0KICAgIHRva2Vucw0KICB9KSkNCiAgcmV0dXJuKHJlc3VsdCkNCn0NCg0KYGBgDQoNCg0K5b6e5Zue5YKz55qEY29yZS1ubHAgb2JqZWN05Lit5pW055CG5Ye66Kme5b2Z5L6d5a2Y6Zec5L+C77yM6Ly45Ye654K6IHRpZHlkYXRhIOagvOW8jw0KDQpgYGB7cn0NCg0KY29yZU5MUF9kZXBlbmRlbmN5X3BhcnNlciA8LSBmdW5jdGlvbihjb3JlTkxQX29iamVjdHMpew0KICByZXN1bHQgPC0gZG8uY2FsbChyYmluZCwgbGFwcGx5KGNvcmVOTFBfb2JqZWN0cywgZnVuY3Rpb24ob2JqKXsNCiAgICBvcmlnaW5hbF9kYXRhIDwtIG9iaiRkYXRhDQogICAgZG9jIDwtIG9iaiRkb2MNCiAgICAjIGZvciBhIHNlbnRlbmNlcw0KICAgIHNlbnRlbmNlcyA8LSBkb2Mkc2VudGVuY2VzDQogICAgc2VuIDwtIHNlbnRlbmNlc1tbMV1dDQogICAgZGVwZW5kZW5jaWVzIDwtIGRvLmNhbGwocmJpbmQsIGxhcHBseShzZW4kYmFzaWNEZXBlbmRlbmNpZXMsIGZ1bmN0aW9uKHgpew0KICAgICAgcmVzdWx0IDwtIGRhdGEuZnJhbWUoZGVwPXgkZGVwLCBnb3Zlcm5vcj14JGdvdmVybm9yLCBnb3Zlcm5vckdsb3NzPXgkZ292ZXJub3JHbG9zcywgZGVwZW5kZW50PXgkZGVwZW5kZW50LCBkZXBlbmRlbnRHbG9zcz14JGRlcGVuZGVudEdsb3NzKQ0KICAgICAgcmVzdWx0DQogICAgfSkpDQogIA0KICAgIGRlcGVuZGVuY2llcyA8LSBvcmlnaW5hbF9kYXRhICU+JQ0KICAgICAgdCgpICU+JSANCiAgICAgIGRhdGEuZnJhbWUoKSAlPiUgDQogICAgICBzZWxlY3QoLXRleHQpICU+JSANCiAgICAgIHNsaWNlKHJlcCgxOm4oKSwgZWFjaCA9IG5yb3coZGVwZW5kZW5jaWVzKSkpICU+JSANCiAgICAgIGJpbmRfY29scyhkZXBlbmRlbmNpZXMpDQogICAgZGVwZW5kZW5jaWVzDQogIH0pKQ0KICByZXR1cm4ocmVzdWx0KQ0KfQ0KYGBgDQoNCg0K5b6e5Zue5YKz55qEY29yZS1ubHAgb2JqZWN05Lit5pW055CG5Ye66Kqe5Y+l5oOF57eS77yM6Ly45Ye654K6IHRpZHlkYXRhIOagvOW8jw0KYGBge3J9DQoNCmNvcmVOTFBfc2VudGltZW50X3BhcnNlciA8LSBmdW5jdGlvbihjb3JlTkxQX29iamVjdHMpew0KICByZXN1bHQgPC0gZG8uY2FsbChyYmluZCwgbGFwcGx5KGNvcmVOTFBfb2JqZWN0cywgZnVuY3Rpb24ob2JqKXsNCiAgICBvcmlnaW5hbF9kYXRhIDwtIG9iaiRkYXRhDQogICAgZG9jIDwtIG9iaiRkb2MNCiAgICAjIGZvciBhIHNlbnRlbmNlcw0KICAgIHNlbnRlbmNlcyA8LSBkb2Mkc2VudGVuY2VzDQogICAgc2VuIDwtIHNlbnRlbmNlc1tbMV1dDQogICAgDQogICAgc2VudGltZW50IDwtIG9yaWdpbmFsX2RhdGEgJT4lDQogICAgICB0KCkgJT4lIA0KICAgICAgZGF0YS5mcmFtZSgpICU+JSANCiAgICAgIGJpbmRfY29scyhkYXRhLmZyYW1lKHNlbnRpbWVudD1zZW4kc2VudGltZW50LCBzZW50aW1lbnRWYWx1ZT1zZW4kc2VudGltZW50VmFsdWUpKQ0KICANCiAgICBzZW50aW1lbnQNCiAgfSkpDQogIHJldHVybihyZXN1bHQpDQp9DQoNCmBgYA0KDQoNCiMjIyMgMS4zLjMg5ZyW5b2i5YyWIERlcGVuZGVuY3kgdHJlZQ0KDQoNCueoi+W8j+WPg+iAg+S+hua6kO+8mmh0dHBzOi8vc3RhY2tvdmVyZmxvdy5jb20vcXVlc3Rpb25zLzM1NDk2NTYwL2hvdy10by1jb252ZXJ0LWNvcmVubHAtZ2VuZXJhdGVkLXBhcnNlLXRyZWUtaW50by1kYXRhLXRyZWUtci1wYWNrYWdlDQoNCmBgYHtyfQ0KIyDlnJblvaLljJbpoa/npLpkZXBlbmRlbmN557WQ5p6cDQpwYXJzZTJ0cmVlIDwtIGZ1bmN0aW9uKHB0ZXh0KSB7DQogIHN0b3BpZm5vdChyZXF1aXJlKE5MUCkgJiYgcmVxdWlyZShpZ3JhcGgpKQ0KICANCiAgIyB0aGlzIHN0ZXAgbW9kaWZpZXMgY29yZU5MUCBwYXJzZSB0cmVlIHRvIG1pbWljIG9wZW5OTFAgcGFyc2UgdHJlZQ0KICBwdGV4dCA8LSBnc3ViKCJbXHJcbl0iLCAiIiwgcHRleHQpDQogIHB0ZXh0IDwtIGdzdWIoIlJPT1QiLCAiVE9QIiwgcHRleHQpDQoNCg0KICAjIyBSZXBsYWNlIHdvcmRzIHdpdGggdW5pcXVlIHZlcnNpb25zDQogIG1zIDwtIGdyZWdleHByKCJbXigpIF0rIiwgcHRleHQpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIGp1c3QgaWdub3Jpbmcgc3BhY2VzIGFuZCBicmFja2V0cz8NCiAgd29yZHMgPC0gcmVnbWF0Y2hlcyhwdGV4dCwgbXMpW1sxXV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMganVzdCB3b3Jkcw0KICByZWdtYXRjaGVzKHB0ZXh0LCBtcykgPC0gbGlzdChwYXN0ZTAod29yZHMsIHNlcS5pbnQobGVuZ3RoKHdvcmRzKSkpKSAgIyBhZGQgaWQgdG8gd29yZHMNCiAgDQogICMjIEdvaW5nIHRvIGNvbnN0cnVjdCBhbiBlZGdlbGlzdCBhbmQgcGFzcyB0aGF0IHRvIGlncmFwaA0KICAjIyBhbGxvY2F0ZSBoZXJlIHNpbmNlIHdlIGtub3cgdGhlIHNpemUgKG51bWJlciBvZiBub2RlcyAtIDEpIGFuZCAtMSBtb3JlIHRvIGV4Y2x1ZGUgJ1RPUCcNCiAgZWRnZWxpc3QgPC0gbWF0cml4KCcnLCBucm93PWxlbmd0aCh3b3JkcyktMiwgbmNvbD0yKQ0KICANCiAgIyMgRnVuY3Rpb24gdG8gZmlsbCBpbiBlZGdlbGlzdCBpbiBwbGFjZQ0KICBlZGdlbWFrZXIgPC0gKGZ1bmN0aW9uKCkgew0KICAgIGkgPC0gMCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgcm93IGNvdW50ZXINCiAgICBnIDwtIGZ1bmN0aW9uKG5vZGUpIHsgICAgICAgICAgICAgICAgICAgICAgICAjIHRoZSByZWN1cnNpdmUgZnVuY3Rpb24NCiAgICAgIGlmIChpbmhlcml0cyhub2RlLCAiVHJlZSIpKSB7ICAgICAgICAgICAgIyBvbmx5IHJlY3Vyc2Ugc3VidHJlZXMNCiAgICAgICAgaWYgKCh2YWwgPC0gbm9kZSR2YWx1ZSkgIT0gJ1RPUDEnKSB7ICMgc2tpcCAnVE9QJyBub2RlIChhZGRlZCAnMScgYWJvdmUpDQogICAgICAgICAgZm9yIChjaGlsZCBpbiBub2RlJGNoaWxkcmVuKSB7DQogICAgICAgICAgICBjaGlsZHZhbCA8LSBpZihpbmhlcml0cyhjaGlsZCwgIlRyZWUiKSkgY2hpbGQkdmFsdWUgZWxzZSBjaGlsZA0KICAgICAgICAgICAgaSA8PC0gaSsxDQogICAgICAgICAgICBlZGdlbGlzdFtpLDE6Ml0gPDwtIGModmFsLCBjaGlsZHZhbCkNCiAgICAgICAgICB9DQogICAgICAgIH0NCiAgICAgICAgaW52aXNpYmxlKGxhcHBseShub2RlJGNoaWxkcmVuLCBnKSkNCiAgICAgIH0NCiAgICB9DQogIH0pKCkNCiAgDQogICMjIENyZWF0ZSB0aGUgZWRnZWxpc3QgZnJvbSB0aGUgcGFyc2UgdHJlZQ0KICBlZGdlbWFrZXIoVHJlZV9wYXJzZShwdGV4dCkpDQogIHRyZWUgPC0gRnJvbURhdGFGcmFtZU5ldHdvcmsoYXMuZGF0YS5mcmFtZShlZGdlbGlzdCkpDQogIHJldHVybiAodHJlZSkNCn0NCg0KYGBgDQoNCg0KIyMjIDEuNCDlkbzlj6sgY29yZU5MUCDmnI3li5nliIbmnpDlj6XlrZANCg0KIyMjIyAxLjQuMSDlsIflj6XlrZDkuJ/lhaXmnI3li5kNCg0K5Y+W5b6XY29yZU5MUOWbnuWCs+eahOeJqeS7tjxicj4NCuWFiOS4jeimgei3kemAmeaute+8jOacg+iKseWkp+amgjE45YiG6ZCY77yI5aaC5p6c5L2g6KiY5oa26auU5Y+q5pyJNEflj6/og73mnIPnlbbmjokuLi7vvIkNCg0KYGBge3J9DQojIGdjKCkgI+mHi+aUvuS4jeS9v+eUqOeahOiomOaGtumrlA0KIyANCiMgdDAgPSBTeXMudGltZSgpDQojIG9iaiA9IGRmWyxjKDIsNSldICAlPiUgZmlsdGVyKHRleHQgIT0gIiIpICU+JSBjb3JlTkxQKGhvc3QpDQojICPlhYjpgY7mv77mjonmspLmnInlhaflrrnnmoTnmoR0d2VldA0KIyAj5Lif5YWlY29yZU5MUOeahOeJqeS7tiDlv4XpoIjnrKblkIg6IOaYr+S4gOWAi2RhdGEuZnJhbWUg5LiU5pyJ5LiA5YCLdGV4dOashOS9jQ0KIyANCiMgU3lzLnRpbWUoKSAtIHQwICPln7fooYzmmYLplpMNCiMgI1RpbWUgZGlmZmVyZW5jZSBvZiAxNy44OTYxMSBtaW5zDQojIA0KIyBzYXZlLmltYWdlKCJjb3JlTkxQXzAzMjUuUkRhdGEiKQ0KYGBgDQoNCg0KIyMjIyAxLjQuMiDlvp7lm57lgrPnmoTnianku7bkuK3mj5Dlj5bmlrfoqZ7jgIHoqZ7lvZnpgoTljp/jgIHoqZ7mgKfmqJnoqLvjgIHlkb3lkI3lr6bpq5TmqJnoqLvnrYnntZDmnpwNCg0KDQojIyMjIyB0b2tlbnMNCg0KDQpgYGB7cn0NCiN0b2tlbnMgPSAgY29yZU5MUF90b2tlbnNfcGFyc2VyKG9iaikNCnRva2Vucw0KYGBgDQoNCmNvcmVOTFBfdG9rZW5zX3BhcnNlcuashOS9jToNCg0KKyBzdGF0dXNfaWQgOiDlsI3mh4nljp/mnKxkZuijoeeahHN0YXR1c19pZO+8jOeCuuS4gOWJh3R3ZWV0c+eahOWUr+S4gGlkDQorIHdvcmQ6IOWOn+Wni+aWt+ipng0KKyBsZW1tYSA6IOWwjeaWt+ipnuWBmuipnuW9oumChOWOnw0KKyBwb3MgOiBwYXJ0LW9mLXNwZWVjaCzoqZ7mgKcNCisgbmVyOiDlkb3lkI3lr6bpq5QNCg0KDQojIyMjIyDlvp5ORVLmn6XnnIvnibnlrprpoZ7lnovnmoTlr6bpq5QNCg0KDQrovqjorZjlh7rlk6rlub7nqK7poZ7lnovnmoTlr6bpq5QNCmBgYHtyfQ0KbGV2ZWxzKHRva2VucyRuZXIpDQpgYGANCg0KYGBge3J9DQpsZW5ndGgodW5pcXVlKHRva2VucyR3b3JkW3Rva2VucyRuZXIgIT0gIk8iXSkpICPpmaTljrtlbnRpdHnngrpPdGhlcu+8jOacieWkmuWwkeeorndvcmTmnInooqvmqJnoqLtlbnRpdHkNCmBgYA0KDQoNCuWboOeCuuWkp+Wwj+Wvq+S5n+acg+W9semfv2NvcmVubHDlsI1ORVLnmoTliKTmlrfvvIzlm6DmraTmiJHlgJHkuIDplovlp4vntabnmoTmjqjmloflhaflrrnmmK/mspLmnInomZXnkIblpKflsI/lr6vnmoTvvIzkvYblnKjot5Hlroxhbm90YXRvcuW+jO+8jOeCuuS6huato+eiuuioiOeul+ipnumgu++8jOWJteW7uuaWsOashOS9jWxvd2VyX3dvcmToiIdsb3dlcl9sZW1tYe+8jOWtmOaUvui9ieaPm+Wwj+Wvq+eahHdvcmToiIdsZW1tYTxicj4NCui9ieaIkOWwj+Wvq+eahOebrueahOaYr+imgeWwh+S4jeWQjOWkp+Wwj+Wvq+eahOWQjOS4gOWtl+ipnu+8iOWmglRydW1w6IiHdHJ1bXDvvInpg73mj5vmiJDlsI/lr6vvvIzlho3kvoboqIjnrpfoqZ7poLvjgIINCmBgYHtyfQ0KdG9rZW5zJGxvd2VyX3dvcmQgPSB0b2xvd2VyKHRva2VucyR3b3JkKQ0KdG9rZW5zJGxvd2VyX2xlbW1hID0gdG9sb3dlcih0b2tlbnMkbGVtbWEpDQpgYGANCg0KDQrlsI3mlrzliJ3mraXkuI3kuobop6PkuK3nvo7osr/mmJPmiLDnmoTkurrkvoboqqrvvIzlj6/og73kuI3nn6XpgZPnm7jpl5zkurrnianmmK/oqrDvvIznhKHms5XoqK3nq4vlrozmlbTnmoTkurrnianlrZflhbjjgILpgI/pgY5jb3JlTkxQ6Kej5p6Q5Ye6TkVS77yM56+p6YG45Ye6UEVSU09O5Lim6KiI566X5a+m6auU55qE6Kme6aC777yM6IO96K6T5oiR5YCR55+l6YGT55u46Zec5Lq654mp55qE5Zyo6Kmx6aGM5Lit55qE6YeN6KaB5oCn44CCDQoNCmBgYHtyfQ0KdG9rZW5zICU+JQ0KICBmaWx0ZXIobmVyID09ICJQRVJTT04iKSAlPiUgICPnr6npgbhORVLngrpQRVJTSU9ODQogIGdyb3VwX2J5KGxvd2VyX3dvcmQpICU+JSAj5qC55pOad29yZOWIhue1hA0KICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JSAj6KiI566X5q+P57WEDQogIHRvcF9uKG4gPSAxNSwgY291bnQpICU+JQ0KICB1bmdyb3VwKCkgJT4lIA0KICBtdXRhdGUobG93ZXJfd29yZCA9IHJlb3JkZXIobG93ZXJfd29yZCwgY291bnQpKSAlPiUNCiAgZ2dwbG90KGFlcyhsb3dlcl93b3JkLCBjb3VudCkpICsgDQogIGdlb21fY29sKCkrDQogIGdndGl0bGUoIldvcmQgRnJlcXVlbmN5IChORVIgaXMgUEVSU09OKSIpICsNCiAgdGhlbWUodGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xNCkpKw0KICBjb29yZF9mbGlwKCkNCmBgYA0KDQoNCg0KDQpgYGB7cn0NCnRva2VucyAlPiUNCiAgZmlsdGVyKG5lciA9PSAiQ09VTlRSWSIpICU+JSAgI+evqemBuE5FUueCukNPVU5UUlkNCiAgZ3JvdXBfYnkobG93ZXJfd29yZCkgJT4lICPmoLnmk5p3b3Jk5YiG57WEDQogIHN1bW1hcml6ZShjb3VudCA9IG4oKSkgJT4lICPoqIjnrpfmr4/ntYQNCiAgdG9wX24obiA9IDEwLCBjb3VudCkgJT4lDQogIHVuZ3JvdXAoKSAlPiUgDQogIG11dGF0ZShsb3dlcl93b3JkID0gcmVvcmRlcihsb3dlcl93b3JkLCBjb3VudCkpICU+JQ0KICBnZ3Bsb3QoYWVzKGxvd2VyX3dvcmQsIGNvdW50KSkgKyANCiAgZ2VvbV9jb2woKSsNCiAgZ2d0aXRsZSgiV29yZCBGcmVxdWVuY3kgKE5FUiBpcyBDT1VOVFJZKSIpICsNCiAgdGhlbWUodGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xNCkpKw0KICBjb29yZF9mbGlwKCkNCmBgYA0KDQoNCg0KYGBge3J9DQp0b2tlbnMgJT4lDQogIGZpbHRlcihuZXIgPT0gIklERU9MT0dZIikgJT4lICAj56+p6YG4TkVS54K6SURFT0xPR1kNCiAgZ3JvdXBfYnkobG93ZXJfd29yZCkgJT4lICPmoLnmk5p3b3Jk5YiG57WEDQogIHN1bW1hcml6ZShjb3VudCA9IG4oKSkgJT4lICPoqIjnrpfmr4/ntYQNCiAgdG9wX24obiA9IDEwLCBjb3VudCkgJT4lDQogIHVuZ3JvdXAoKSAlPiUgDQogIG11dGF0ZShsb3dlcl93b3JkID0gcmVvcmRlcihsb3dlcl93b3JkLCBjb3VudCkpICU+JQ0KICBnZ3Bsb3QoYWVzKGxvd2VyX3dvcmQsIGNvdW50KSkgKyANCiAgZ2VvbV9jb2woKSsNCiAgZ2d0aXRsZSgiV29yZCBGcmVxdWVuY3kgKE5FUiBpcyBJREVPTE9HWSkiKSArDQogIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KHNpemU9MTQpKSsNCiAgY29vcmRfZmxpcCgpDQpgYGANCg0KDQojIyMjIyDnt7Tnv5IoMTUgbWluKe+8mg0KDQoxLiDlsI13b3Jk5Y676Zmk5o6Jc3RvcCB3b3JkcyhoaW50OiDlnKh0aWR5dGV4dOesrOS4gOeroCkgDQoyLiDnr6npgbjljrvpmaR0cmFkZSx3YXIsY2hpbmEsdS5zLg0KMy4g5qC55pOad29yZOWIhue1hOioiOeul2NvdW50DQo0LiDkvp3mk5pjb3VudOWBmumBnua4m+aOkuW6jw0KNS4g5qC55pOabGVtbWHlgZrliIbntYTmnIPmnInku4DpurzkuI3lkIzll47vvJ8NCg0KDQpg5Y+D6ICD6Kej562UYA0KDQoNCmBgYHtyfQ0KDQpkYXRhKCJzdG9wX3dvcmRzIikgI+i8ieWFpeWtmOWcqHRpZHl0ZXh05aWX5Lu25Lit55qEc3RvcF93b3Jkc+izh+aWmQ0KDQpuYW1lcyhzdG9wX3dvcmRzKVsxXSA9ICJsb3dlcl93b3JkIiAj5bCHc3RvcF93b3Jkc+eahOesrOS4gOWAi+ashOS9jeWQjeeoseaUueWBmmxvd2VyX3dvcmQNCg0KdG9rZW5zICU+JQ0KICBhbnRpX2pvaW4oc3RvcF93b3JkcykgJT4lICAjdG9rZW5z5ZKMc3RvcF93b3JkcyBhbnRpX2pvaW4gYnkg55u45ZCM5ZCN56ix55qE5qyE5L2NbG93ZXJfd29yZCANCiAgZmlsdGVyKCEoIGxvd2VyX3dvcmQgJWluJSBjKCJ0cmFkZSIsIndhciIsImNoaW5hIiwidXMiLCJ1LnMuIiwidW5pdGVkIiwic3RhdGVzIiwiYW1lcmljYSIpKSkgJT4lIA0KICBncm91cF9ieShsb3dlcl93b3JkKSAlPiUgDQogIHN1bW1hcmlzZShjb3VudCA9IG4oKSkgJT4lIA0KICBhcnJhbmdlKGRlc2MoY291bnQpKQ0KYGBgDQoNCg0KYGBge3J9DQp0b2tlbnMgJT4lIA0KICBhbnRpX2pvaW4oc3RvcF93b3JkcykgJT4lIA0KICBmaWx0ZXIoISggbG93ZXJfd29yZCAlaW4lIGMoInRyYWRlIiwid2FyIiwiY2hpbmEiLCJ1cyIsInUucy4iLCJ1bml0ZWQiLCJzdGF0ZXMiLCJhbWVyaWNhIikpKSAlPiUgDQogIGdyb3VwX2J5KGxvd2VyX2xlbW1hKSAlPiUgDQogIHN1bW1hcmlzZShjb3VudCA9IG4oKSkgJT4lIA0KICBhcnJhbmdlKGRlc2MoY291bnQpKQ0KYGBgDQoNCg0KIyMjIyMg5LiN5ZCM55So5oi26Kit5YKZ56uv55qE5bi455So5a2X5pyD5LiN5ZCM5ZeO77yfDQoNCmBgYHtyfQ0KdGFibGUoZGYkc291cmNlKSAlPiUgc29ydChkZWNyZWFzaW5nID0gVCkgJT4lIGhlYWQNCmBgYA0KDQoNCg0KYGBge3J9DQp0b2tlbnMgJT4lIA0KICBhbnRpX2pvaW4oc3RvcF93b3JkcykgJT4lDQogIG1lcmdlKGRmWyxjKDIsNildKSAlPiUgDQogIGZpbHRlcighKGxvd2VyX3dvcmQgJWluJSBjKCJ0cmFkZSIsIndhciIsImNoaW5hIiwidS5zLiIsInVuaXRlZCIsInN0YXRlcyIsImFtZXJpY2EiKSksDQogICAgICAgICBzb3VyY2UgJWluJSBjKCJUd2l0dGVyIFdlYiBDbGllbnQiLCJUd2l0dGVyIGZvciBpUGhvbmUiLCJUd2l0dGVyIGZvciBBbmRyb2lkIikpICU+JSANCiAgZ3JvdXBfYnkoc291cmNlLGxvd2VyX2xlbW1hKSAlPiUNCiAgc3VtbWFyaXplKGNvdW50ID0gbigpKSAlPiUgI+ioiOeul+avj+e1hA0KICB0b3BfbigxNSxjb3VudCkgJT4lDQogIHVuZ3JvdXAoKSAlPiUgDQogIG11dGF0ZShsb3dlcl9sZW1tYSA9IHJlb3JkZXIobG93ZXJfbGVtbWEsIGNvdW50KSkgJT4lIA0KICAjYXJyYW5nZShkZXNjKGNvdW50KSwuYnlfZ3JvdXAgPSBUUlVFKSAlPiUgDQogIGdncGxvdChhZXMobG93ZXJfbGVtbWEsIGNvdW50KSkgKw0KICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogIGZhY2V0X3dyYXAofnNvdXJjZSwgc2NhbGVzID0gImZyZWVfeSIpICsNCiAgbGFicyh5ID0gInR3ZWV0cyBmcm9tIGRpZmZlcmVuY2Ugc291cmNlIiwNCiAgICAgICB4ID0gTlVMTCkgKw0KICB0aGVtZSh0ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE0KSkrDQogIGNvb3JkX2ZsaXAoKQ0KYGBgDQoNCg0KDQojIyMjIyDoqp7lj6Xkvp3lrZjpl5zkv4LntZDmnpwNCg0KYGBge3J9DQojZGVwZW5kZW5jaWVzID0gY29yZU5MUF9kZXBlbmRlbmN5X3BhcnNlcihvYmopDQpkZXBlbmRlbmNpZXMNCmBgYA0KDQrmn6XnnIvljp/lj6UNCmBgYHtyfQ0KZGYkdGV4dFtkZiRzdGF0dXNfaWQgPT0gIjExMDk4NjgyNzYwNjkzNDMyMzMiXQ0KYGBgDQoNCg0KIyMjIyMjIOimluimuuWMliBEZXBlbmRlbmN5IHRyZWUNCg0KYGBge3J9DQpwYXJzZV90cmVlIDwtIG9ialtbMV1dJGRvY1tbMV1dW1sxXV0kcGFyc2UNCnRyZWUgPC0gcGFyc2UydHJlZShwYXJzZV90cmVlKQ0KU2V0Tm9kZVN0eWxlKHRyZWUsIHN0eWxlID0gImZpbGxlZCxyb3VuZGVkIiwgc2hhcGUgPSAiYm94IikNCnBsb3QodHJlZSkNCmBgYA0KDQoNCiMjIyMjIOiqnuWPpeaDhee3kuWAvA0KDQoNCmBgYHtyfQ0Kc2VudGltZW50ID0gY29yZU5MUF9zZW50aW1lbnRfcGFyc2VyKG9iaikNCnNlbnRpbWVudA0KYGBgDQoNCuWcqOmAmeWAi+izh+aWmembhuS4re+8jOaDhee3kmxhYmVs5pyJ5bm+56iuDQpgYGB7cn0NCmxldmVscyhzZW50aW1lbnQkc2VudGltZW50KQ0KYGBgDQoNCg0KYCoq5rOo5oSPKipgDQoNCuWFiOiuk+aIkeWAkeafpeeci3NlbnRpbWVudFZhbHVl55qE6aGe5Z6LLOaYr2ZhY3Rvcg0KDQpgYGB7cn0NCmNsYXNzKHNlbnRpbWVudCRzZW50aW1lbnRWYWx1ZSkNCmBgYA0KDQrkvYbmmK/miJHlgJHlv4XpoIjovYnmiJBudW1lcmlj5omN6IO95bCN5YW25YGa5pW45YC86YGL566X77yM5L6L5aaC6KiI566X5bmz5Z2H5oOF57eSPGJyPg0K6KaB54m55Yil5rOo5oSP77yM6Iul55u05o6l5bCHZmFjdG9y6L2J5oiQbnVtZXJpY++8jOe1kOaenOeahOaVuOWAvOS4jeacg+aYr+WOn+acrGZhY3RvcueahOaVuOWAvO+8jOiAjOacg+aYr2ZhY3RvciBsZXZlbOeahOmghuW6j++8jOevhOS+i++8mg0KDQpgYGB7cn0NCkEgPSBjKDEsNCwzLDAsNCwzKSAlPiUgYXMuZmFjdG9yDQpsZXZlbHMoQSkNCg0KYGBgDQpsZXZlbOmghuW6j+aYrzAgPiAxID4gMyA+IDQ8YnI+DQoNCuiLpeWwh0Hnm7TmjqXovYnmiJBudW1lcmlj77yM5Y+v5Lul55m854++57WQ5p6c5Lim5LiN5pyD5piv5Y6f5pys55qEKDEsNCwzLDAsNCwzKe+8jOiAjOaYr+i9ieaPm+aIkOWFg+e0oOWOn+acrOeahGxldmVs6aCG5bqPPGJyPg0KICgxLDQsMywwLDQsMykgPT4gKGxldmVs6aCG5bqPOiAw5pivMSwx5pivMiwz5pivMyw05pivNCApID0+ICgyLDQsMywxLDQsMykNCmBgYHtyfQ0KQSAlPiUgYXMubnVtZXJpYw0KYGBgDQoNCg0K5Zug5q2k77yM5oiR5YCR5LiN6IO955u05o6l5bCHc2VudGltZW50VmFsdWXnm7TmjqXnlLFmYWN0b3LpoZ7lnovovYnmj5vmiJBudW1lcmljPGJyPg0K6ICM5pivYOWFiOWwh2ZhY3Rvcui9iWNoYXJhY3Rlcizlho3ovYludW1lcmljYA0KYGBge3J9DQpzZW50aW1lbnQkc2VudGltZW50VmFsdWUgPSBzZW50aW1lbnQkc2VudGltZW50VmFsdWUgJT4lIGFzLmNoYXJhY3RlciAlPiUgYXMubnVtZXJpYw0KYGBgDQoNCueUqHRhYmxl55yL5oOF57eSbGFiZWzlsI3mh4nnmoRzZW50aW1lbnRWYWx1ZQ0KYGBge3J9DQp0YWJsZShzZW50aW1lbnQkc2VudGltZW50LHNlbnRpbWVudCRzZW50aW1lbnRWYWx1ZSkNCmBgYA0KDQpzZW50aW1lbnRWYWx1ZeWwseacg+aYr+WOn+acrOeahDo8YnI+DQowLDEgOiBWZXJ5bmVnYXRpdmUsbmVnYXRpdmU8YnI+DQoyIDogbmV1dHJhbDxicj4NCjMsNCA6IHBvc2l0aXZlLFZlcnlwb3NpdGl2ZSjlnKjmnKzmrKHos4fmlpnpm4bmspLmnInlh7rnj74pPGJyPjxicj4NCg0KDQoNCiDlubPlnYfmg4Xnt5LliIbmlbjmmYLplpPotqjli6INCmBgYHtyfQ0KDQpkZiRkYXRlID0gYXMuRGF0ZShkZiRjcmVhdGVkX2F0KQ0KDQpzZW50aW1lbnQgJT4lIA0KICBtZXJnZShkZlssYygic3RhdHVzX2lkIiwiZGF0ZSIpXSkgJT4lDQogIGdyb3VwX2J5KGRhdGUpICU+JSANCiAgc3VtbWFyaXNlKGF2Z19zZW50aW1lbnQgPSBtZWFuKHNlbnRpbWVudFZhbHVlLG5hLnJtPVQpKSAlPiUgDQogIGdncGxvdChhZXMoeD1kYXRlLHk9YXZnX3NlbnRpbWVudCkpICsgDQogIGdlb21fbGluZSgpDQoNCmBgYA0KDQoNCuS4jeWQjOeUqOaItuerr+aDhee3kuaZgumWk+i2qOWLog0KDQpgYGB7cn0NCg0Kc2VudGltZW50JHNlbnRpbWVudFZhbHVlID0gYXMubnVtZXJpYyhzZW50aW1lbnQkc2VudGltZW50VmFsdWUpIA0Kc2VudGltZW50ICU+JSANCiAgbWVyZ2UoZGZbLGMoInN0YXR1c19pZCIsInNvdXJjZSIsImRhdGUiKV0pICU+JQ0KICBmaWx0ZXIoc291cmNlICVpbiUgYygiVHdpdHRlciBXZWIgQ2xpZW50IiwiVHdpdHRlciBmb3IgaVBob25lIiwiVHdpdHRlciBmb3IgQW5kcm9pZCIpKSAlPiUgDQogIGdyb3VwX2J5KGRhdGUsc291cmNlKSAlPiUgDQogIHN1bW1hcmlzZShhdmdfc2VudGltZW50ID0gbWVhbihzZW50aW1lbnRWYWx1ZSxuYS5ybT1UKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHg9ZGF0ZSx5PWF2Z19zZW50aW1lbnQsY29sb3I9c291cmNlKSkgKyANCiAgZ2VvbV9saW5lKCkNCg0KYGBgDQoNCg0KDQpWZXJ5bmVnYXRpdmXlkoxuZWdhdGl2ZeeahHR3ZWV0c+W4uOWHuuePvueahOWtlw0KYGBge3J9DQoNClggPSBzZW50aW1lbnQgJT4lIA0KICBtZXJnZSh0b2tlbnNbLGMoInN0YXR1c19pZCIsImxvd2VyX3dvcmQiLCJsb3dlcl9sZW1tYSIsIm5lciIpXSkgJT4lIA0KICBhbnRpX2pvaW4oc3RvcF93b3JkcykNCg0KWCAlPiUgZmlsdGVyKA0KICAhKGxvd2VyX3dvcmQgJWluJSBjKCJ0cmFkZSIsIndhciIsImNoaW5hIiwiY2hpbmEuIiwidS5zLiIsInVuaXRlZCIsInN0YXRlcyIsInRydW1wIiwiYW1lcmljYSIpKSwNCiAgICAgICAgIHNlbnRpbWVudCAlaW4lIGMoIlZlcnluZWdhdGl2ZSIsIm5lZ2F0aXZlIikgLA0KICAgICAgICAgbmVyICE9ICJOVU1CRVIiKSAlPiUgDQogIGdyb3VwX2J5KGxvd2VyX3dvcmQpICU+JSAj5qC55pOad29yZOWIhue1hA0KICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JQ0KICB3b3JkY2xvdWQyKCkNCmBgYA0KDQoNCmBgYHtyfQ0KWCAlPiUgZmlsdGVyKA0KICAhKGxvd2VyX3dvcmQgJWluJSBjKCJ0cmFkZSIsIndhciIsImNoaW5hIiwiY2hpbmEuIiwidS5zLiIsInVuaXRlZCIsInN0YXRlcyIsInRydW1wIiwiYW1lcmljYSIpKSwNCiAgICAgICAgIHNlbnRpbWVudCAlaW4lIGMoIlZlcnluZWdhdGl2ZSIsIm5lZ2F0aXZlIikgLA0KICAgICAgICAgbmVyICE9ICJOVU1CRVIiKSAlPiUgDQogIGdyb3VwX2J5KGxvd2VyX3dvcmQpICU+JSAj5qC55pOad29yZOWIhue1hA0KICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JSANCiAgYXJyYW5nZShkZXNjKGNvdW50KSkNCmBgYA0KDQoNCnBvc2l0aXZl55qEdHdlZXRz5bi45Ye654++55qE5a2XDQpgYGB7cn0NCnNlbnRpbWVudCAlPiUgDQogIG1lcmdlKHRva2Vuc1ssYygic3RhdHVzX2lkIiwibG93ZXJfd29yZCIsImxvd2VyX2xlbW1hIiwibmVyIildKSAlPiUgDQogIGFudGlfam9pbihzdG9wX3dvcmRzKSAlPiUNCiAgZmlsdGVyKCEobG93ZXJfd29yZCAlaW4lIGMoInRyYWRlIiwid2FyIiwiY2hpbmEiLCJjaGluYS4iLCJ1LnMuIiwidW5pdGVkIiwic3RhdGVzIiwidHJ1bXAiLCJhbWVyaWNhIikpLA0KICAgICAgICAgc2VudGltZW50ID09ICJwb3NpdGl2ZSIsDQogICAgICAgICBuZXIgIT0gIk5VTUJFUiIpICU+JSANCiAgZ3JvdXBfYnkobG93ZXJfbGVtbWEpICU+JSAj5qC55pOad29yZOWIhue1hA0KICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JQ0KICB0b3Bfbig0MCxjb3VudCkgJT4lIA0KICB3b3JkY2xvdWQyKCkNCmBgYA0KDQoNCg0KIyMgMi5TZW50aW1lbnRyIOiLseaWh+aDhee3kuWIhuaekA0KDQoNCmBgYHtyfQ0KDQpsaWJyYXJ5KHNlbnRpbWVudHIpDQoNCm15dGV4dCA8LSBjKA0KICAgICdkbyB5b3UgbGlrZSBpdD8gIEJ1dCBJIGhhdGUgcmVhbGx5IGJhZCBkb2dzJywNCiAgICAnSSBhbSB0aGUgYmVzdCBmcmllbmQuJywNCiAgICAnRG8geW91IHJlYWxseSBsaWtlIGl0PyAgSVwnbSBub3QgYSBmYW4nDQopDQoNCm15dGV4dCA8LSBnZXRfc2VudGVuY2VzKG15dGV4dCkgI+Wwh2NoYXJhY3RlcuWQkemHj+i9ieaIkGxpc3QsbGlzdOijoeaUvuiRl2NoYXJhY3RlcuWQkemHjyjmlrflj6UpDQoNCmBgYA0KDQoNCmBgYHtyfQ0KbXl0ZXh0W1sxXV1bMV0gI+WPluWHum15dGV4dChsaXN0KeijoeeahOesrOS4gOWAi+WQkemHj+eahOesrOS4gOWAi+WFg+e0oA0KDQpgYGANCg0KYGBge3J9DQpzZW50aW1lbnRfYnkobXl0ZXh0KSAjc2VudGltZW50X2J5KCkgIOe1puWumuaWh+acrOeahOW5s+Wdh+aDheaEn+WIhuaVuA0KYGBgDQoNCmBgYHtyfQ0Kc2VudGltZW50KG15dGV4dCkgIyBzZW50aW1lbnQoKSDlnKhzZW50ZW5jZeeahOe0muWIpeipleWIhg0KYGBgDQoNCg0K6L2J5o+bRW1vamnku6Pnorzngrroqp7mhI/mloflrZcNCmBgYHtyfQ0KcmVwbGFjZV9lbW9qaSgiXFUwMDAxZjRhYSIpDQpgYGANCg0KDQoNCg0KDQotLS0NCg0KIyMg6KOc5YWFOiDlnKhS5L2/55SoY29yZU5MUOeahOWPpuS4gOeoruaWueW8jy0gY2xlYW5OTFANCg0KIyMgY2xlYW5OTFAgDQoNCg0KIyMjIyDlpZfku7boiIfnkrDlooPlj4PmlbjoqK3lrpoNCg0KYGBge3J9DQpwYWNrYWdlcyA9IGMoImNsZWFuTkxQIiwiZHBseXIiLCAibWFncml0dHIiLCJ0d2l0dGVyUiIsInN0cmluZ2kiLCAidWRwaXBlIiwgInJldGljdWxhdGUiLCAickphdmEiLA0KICAgICAgICAgICAgICAgICAgICJSQ3VybCIsICJrbml0ciIsICJybWFya2Rvd24iLCAidGVzdHRoYXQiLCAiY292ciIsDQogICAgICAgICAgICAgICAgICAgInJveHlnZW4yIikNCmV4aXN0aW5nID0gYXMuY2hhcmFjdGVyKGluc3RhbGxlZC5wYWNrYWdlcygpWywxXSkNCmZvcihwa2cgaW4gcGFja2FnZXNbIShwYWNrYWdlcyAlaW4lIGV4aXN0aW5nKV0pIGluc3RhbGwucGFja2FnZXMocGtnKQ0KYGBgDQoNCmBgYHtyfQ0KIyBsaWJyYXJ5KHJKYXZhKQ0KIyBsaWJyYXJ5KGNsZWFuTkxQKQ0KIyBsaWJyYXJ5KHVkcGlwZSkNCiMgbGlicmFyeShyZXRpY3VsYXRlKQ0KYGBgDQoNCg0KYGBge3J9DQoNCiMgZ2MoKQ0KIyANCiMgdXNlX3B5dGhvbigiQzpcXFVzZXJzXFxrb25pclxcQW5hY29uZGEzXFxweXRob24iKSAj5oyH5a6a6Ieq5bex55qEcHl0aG9u6Lev5b6RKOeJiOacrDMuNispDQojIGNubHBfaW5pdF91ZHBpcGUoKQ0KIyBjbmxwX2luaXRfY29yZW5scCgiZW4iLGFubm9fbGV2ZWw9MixsaWJfbG9jYXRpb24gPSAiQzpcXFVzZXJzXFxrb25pclxcRG9jdW1lbnRzXFxSXFx3aW4tbGlicmFyeVxcMy41XFxjbGVhbk5MUFxcZXh0ZGF0YSIpICNsaWJfbG9jYXRpb+aUueaIkGNvcmVubHDmqKHntYTjhJwNCiMgDQojIA0KIyAj5aaC5p6c5Ye654++amF2YS5sYW5nLk91dE9mTWVtb3J5RXJyb3I6IEdDIG92ZXJoZWFkIGxpbWl0IGV4Y2VlZGVk5Luj6KGoUVHkvaDnmoToqJjmhrbpq5TlsLHnrpdnYygp6YGO5LqG6YKE5piv5LiN5aSg77yM6KuL6YeN6ZaLUueci+eciw0KYGBgDQoNCg0KYGBge3J9DQojIHQwICA9IFN5cy50aW1lKCkNCiMgb2JqIDwtIGNubHBfYW5ub3RhdGUoZGYkdGV4dCwgYXNfc3RyaW5ncyA9IFRSVUUsYmFja2VuZCA9ICJjb3JlTkxQIikNCiMgU3lzLnRpbWUoKSAtIHQwDQpgYGANCg0KYGBge3J9DQojIGNubHBfZ2V0X2RvY3VtZW50KG9iaikNCiMgY25scF9nZXRfZGVwZW5kZW5jeShvYmopDQojIGNubHBfZ2V0X3Rva2VuKG9iaikNCiMgY25scF9nZXRfZW50aXR5KG9iaikNCiMgY25scF9nZXRfc2VudGVuY2Uob2JqKSAjYSBzY29yZSBmcm9tIDAgKG1vc3QgbmVnYXRpdmUpIHRvIDQgKG1vc3QgcG9zaXRpdmUpIA0KYGBg