“Definice”

Text mining je sada lingvistických a statistických technik, které dovolují dolovat cenné, doposud neznámé informace z obsáhlých nestrukturovaných, textových databází.

Srovnání s dalšími úkoly datové analýzy:

Příklady využití text miningu ve skutečném světě:

Analýza přirozeného jazyka

Obrázek: They ate the pizza with anchovies

Obrázek: They ate the pizza with anchovies

Uložení textu

V R máme k dispozici několik datových struktur k uložení a zpracování textu. Klasické textové řetězec jsou vhodé například při práci s popisky grafů, faktory použijeme při modelování kategoriálních proměnných v regresních modelech a při využití pokročilejších technik analýzy textu použijeme také speciální datové struktury. Nejprve se tedy podívejme na krátký přehled datových struktur, které lze použít v R při práci s textem.

Textové řetězce

Nejobecnější a nejuniverzálnější datová struktura, která může nést text.

text <- c("I don’t like sand. It’s coarse and rough and irritating, and it gets everywhere.",
          "No. I am your father.",
          "Use the force, Luke.",
          "These are not the droids your looking for.",
          "Do. Or do not. There is no try.")
text
[1] "I don’t like sand. It’s coarse and rough and irritating, and it gets everywhere."
[2] "No. I am your father."                                                           
[3] "Use the force, Luke."                                                            
[4] "These are not the droids your looking for."                                      
[5] "Do. Or do not. There is no try."                                                 

Faktory

Faktory si můžeme představit jako čísla s popisem. Každá kategorie (level) faktoru je reprezentován číslem (např. 1 = “Male”, 2 = “Female”), avšak s faktorem nelze provádět klasické matematické operace. Faktory najdou své využití především v regresních modelech, protože text nemůžeme analyzovat kvantitativně a musíme najít způsob, jak převést slova na čísla.

states <- factor(state.region)
attributes(states)
$levels
[1] "Northeast"     "South"         "North Central"
[4] "West"         

$class
[1] "factor"
as.numeric(states)
 [1] 2 4 4 2 4 4 1 2 2 2 4 4 3 3 3 3 2 2 1 2 1 3 3 2 3 4 3 4 1 1
[31] 4 1 2 3 3 2 4 1 1 2 3 2 2 4 1 2 4 2 3 4

Tidytext

Balíček tidytext nabízí další možnosti práce s textem a je plně integrován do ekosystému tidyverse.

# install.packages("tidytext")
library(tidytext)
library(tidyverse)

Nejprve si převeďme původní textový vektor na tibble (původně data_frame), který uloží každý kaýdý prvek vektoru do samostatného řádku a přidejme si navíc i proměnnou, která označí pořadí daného prvku.

text_tbl <- tibble(line=1:5, text=text)
text_tbl

Pomocí funkce unnest_tokens můžeme nechat R, ať nám daný text rozdělí na jednotlivé elementy (tzv. tokenizace). Těmi mohou být jednotlivá slova.

text_tt <- text_tbl %>%
  unnest_tokens(word, text)
text_tt

Různě n-tice slov, třeba tzv. bigramy.

text_tt_bigrams <- text_tbl %>%
  unnest_tokens(ngram, text, token="ngrams", n=2)
text_tt_bigrams

Nebo dokonce i celé věty.

text_tt_sentences <- text_tbl %>%
  unnest_tokens(sentence, text, token="sentences")
text_tt_sentences

Document-term matice

Jedná se o matici, která popisuje četnost jednotlivých slov (sloupce) v analyzovaných dokumentech (řádky). Jedná se tedy o matici, která má zpravidla několik stovek až tisíc sloupců. Na druhou stranu je většina prvků této matice nulová, takže zabírá méně místa než originální dokumenty.

Document-term matici již můžeme použít jako vstup v některém z algoritmu strojového učení (rozhodovací stromy, neuronové sítě apod.).

Podívejme se, jak vypadá document-term matice pro náš text.

# install.packages("tm")
library(tm)
text_dtm <- text_tt %>%
  group_by(line) %>%
  count(word) %>%
  cast_dtm(line, word, n)
inspect(text_dtm)
<<DocumentTermMatrix (documents: 5, terms: 31)>>
Non-/sparse entries: 36/119
Sparsity           : 77%
Maximal term length: 10
Weighting          : term frequency (tf)
Sample             :
    Terms
Docs and coarse do don’t everywhere i no not the your
   1   3      1  0     1          1 1  0   0   0    0
   2   0      0  0     0          0 1  1   0   0    1
   3   0      0  0     0          0 0  0   0   1    0
   4   0      0  0     0          0 0  0   1   1    1
   5   0      0  2     0          0 0  1   1   0    0

Pro konkrétní matici si můžeme vypsat i jednotlivá slova (sloupce).

Terms(text_dtm)
 [1] "and"        "coarse"     "don’t"      "everywhere"
 [5] "gets"       "i"          "irritating" "it"        
 [9] "it’s"       "like"       "rough"      "sand"      
[13] "am"         "father"     "no"         "your"      
[17] "force"      "luke"       "the"        "use"       
[21] "are"        "droids"     "for"        "looking"   
[25] "not"        "these"      "do"         "is"        
[29] "or"         "there"      "try"       

Jazykové korpusy

Jazykový korpus je rozsáhlý soubor textů určitého jazyka. Používají se jednak pro lingvistický výzkum, jednak jako datová základna pro tvorbu slovníků, korektorů, překladačů atd.

text_corpus <- VCorpus(VectorSource(text))
summary(text_corpus)
  Length Class             Mode
1 2      PlainTextDocument list
2 2      PlainTextDocument list
3 2      PlainTextDocument list
4 2      PlainTextDocument list
5 2      PlainTextDocument list
text_corpus[[1]][1]
$content
[1] "I don’t like sand. It’s coarse and rough and irritating, and it gets everywhere."

Příklady některých jazykových korpusů:

Práce s textem

Regulární výrazy

Pomocí jednoduchého řetězce lze nadefinovat celou množinu řetězcům, čehož se využívá nejčastěji při vyhledávání v textu nebo při manipulaci s textem (nahrazování hledaného výrazu za jiný apod.). Znalost regulárních výrazů by měla patřit do výbavy každého datového analytika. Na internetu existuje mnoho nástrojů, které mohou být nápomocné při práci s regulárními výrazy (např. regexone.com pro interaktivní výuku regulárních výrazů, regex101.com pro testování vlastních regulárních výrazů).

Pro práci s textem a s regulárními výrazy lze používat základní R funkce. Nejčastěji se můžete setkat s funkcemi:

  • paste: vytváření textových řetězců kombinací textových a numerických proměnných
paste0(1:12, c("st", "nd", "rd", rep("th", 9)))
 [1] "1st"  "2nd"  "3rd"  "4th"  "5th"  "6th"  "7th"  "8th" 
 [9] "9th"  "10th" "11th" "12th"
  • substr: vyjmutí nebo nahrazení podřetezce v textovém řetězci
substr("abcdef", 2, 4)
[1] "bcd"
  • grep: hledání v textovém řetězci pomocí regulárních výrazů
x <- c("https://www.muni.cz/", "Text.s.teckami", "ftp://ftp.xyz.com/")
grep("^(([^:]+)://)?([^:/]+)(:([0-9]+))?(/.*)", x, value=TRUE)
[1] "https://www.muni.cz/" "ftp://ftp.xyz.com/"  
  • gsub: nahrazuje v textu hledaný řetězec za jiný specifikovaný řetezec, lze použít regulární výrazy
x <- "Tato  veta ma    moc        mezer."
gsub("\\s{2,}", " ", x)
[1] "Tato veta ma moc mezer."
  • strsplit: rozdělení textového řetězce podle daného oddělovače
unlist(strsplit("oddelovac-muze-byt-treba-i-pomlcka", "-"))
[1] "oddelovac" "muze"      "byt"       "treba"     "i"        
[6] "pomlcka"  

Novější balíčky nabízejí příjemnější manipulaci s textem a také lepší integraci do hojně využívaného prostředí tidyverse. Jedním z nich je stringr, který poskytuje mnoho užitečných funkcí, které využijeme v druhé lekci. Pro rychlý přehled funkcí v tomto balíčku doporučuji projít stringr cheat sheet.

Stop words

Stop words jsou taková slova, která nenesou žádnou informační hodnotu. Většinou se je snažíme v průběhu analýzy odstranit, jelikož mají velkou četnost a nenesou žádnou cennou informaci. V angličtině se může jednat například o členy (the, a, an), spojky (and, or, because) nebo zájmena (I, you, he, she).

Můžete si definovat vlastní slovník stop words nebo využít nějaký cizí slovník volně dostupný na internetu. My se podíváme na slovník anglických stop words z balíčku tidytext, který je dostupný v tibble stop_words.

stop_words

Můžeme se podívat, které slova z našeho původního textu zůstanou po odstranění stop words.

text_tt %>%
  anti_join(stop_words, by="word")

Slovník stop words z balíčku tidytext můžeme obohatit i o vlastní slova jednoduchým způsobem.

custom_stop_words <- bind_rows(tibble(word = c("don’t", "it’s"), lexicon = c("custom")), stop_words)
text_tt %>%
  anti_join(custom_stop_words, by="word")

TF-IDF

Určitě vás napadlo, jak správně určit, která slova ještě přidat do stop words a která už ne? Defaultní slovníky navíc mohou obsahovat slova, která ve vašem kontextu mají platný význam (např. is - anglické sloveso vs IS - informační systém).

K zodpovězení těchto otázek nám může pomoci Inverse document frequency, která označuje, jak je dané slovo důležité v kolekci dokumentů. Ta se definuje následujícím vztahem:

\[\text{idf(slovo)} = \ln \left( \frac{\#\text{dokumenty}}{\#\text{dokumenty obsahující dané slovo}} \right)\]

Pokud se slovo nachází ve všech dokumentech, potom je váha takového slova 0 (\(=\ln (1)\)).

Naopak pokud se dané slovo nachází pouze v jednom dokuemntu z mnoha, má toto slovo velkou váhu (např. \(\ln (10/1) = 2,3\)).

Takto vypočítanou váhu můžeme vynásobit četností daného slova a dostaneme tzv. term frequency-inverse document frequency. Ta se definuje následujícím vztahem:

\[\text{tf-idf(slovo)} = \frac{\#\text{slovo v dokumentu}}{\#\text{všechna slova v dokumentu}} \cdot \ln \left( \frac{\#\text{dokumenty}}{\#\text{dokumenty obsahující dané slovo}} \right)\]

Pro ilustraci si můžeme ukázat výpočet tf-idf na našem původním textu. Nejprve začneme výpočtem četností pro každé slovo.

# vyskyt slov v jednotlivych dokumentech
text_words <- text_tt %>%
  count(line, word, sort=TRUE)
# celkovy vyskyt slov
total_words <- text_words %>% 
  group_by(word) %>% 
  summarise(total=sum(n))
text_words <- left_join(text_words, total_words, by="word")
text_words

Pro samotný výpočet tf-idf můžeme použít funkci bind_tf_idf z balíčku tidytext.

text_words <- text_words %>%
  bind_tf_idf(word, line, n) %>% 
  arrange(desc(tf_idf))
text_words

První slovo do se nachází pouze v jednom dokumentu z pěti, dostáváme tedy \(\text{idf("do")} = \ln (5/1)= 1,6094379\).

Tento dokument obsahuje celkem 8 slov a slovo do se opakuje dvakrát, takže dostáváme \(\text{tf("do", D5)} = 1/4\).

Vynásobením těchto dvou čísel dostáváme \(\text{tf-idf("do", D5)} = 0,40235948\).

text[5]
[1] "Do. Or do not. There is no try."

Stemizace a lemmatizace

Stemizace (anglicky stemming) je proces, při kterém se slova nahrazují pouze svým základem. Smyslem stemizace je sjednocení slov se stejným význam, ale odlišným tvarem. Výsledný základ nemusí být platným slovem.

Lemmatizace (anglicky lemmatization) je proces podobný stemizaci, avšak cílem lemmatizace je získat platné slovo. Je náročnější na výpočetní výkon, protože musí procházet velké korpusy a slovníky. Při stemizaci naopak postupujeme podle relativně jednoduchých pravidel.

Například slovo studies je při stemizaci normalizováno na základ studi (odstranění koncovky -es) a slovo studying na základ study (odstranění koncovky -ing). Při lemmatizaci jsou oba tvary převedeny na společné lemma study.

Pro ilustraci ještě uveďme příklad věty the boy’s cars are different colors, která může být normalizovaná na: the boy car be differ color.

Obrázek: Normalizace textu

Obrázek: Normalizace textu

Ukažme si, jak provést stemizaci v R na našem původním textu. Nejprve je potřeba doinstalovat balíček SnowballC. Potom můžeme použít funkci wordStem.

#install.packages("SnowballC")
library(SnowballC)
text_tt %>% 
  mutate(stem=wordStem(word, "english"))

Lemmatizace je náročnější proces, který i vyžaduje pokročilejší techniky. My použijeme externí software TreeTagger, který má podporu několika jazyků včetně češtiny a slovenštiny. Pro integraci s R je potřeba doinstalovat balíček koRpus.

# install.packages("koRpus")
library(koRpus)
# install the language support package
# install.koRpus.lang("en")
# load the package
library(koRpus.lang.en)

Samotné nastavení je trochu složitější a pravděpodobně budete muset projít více internetových návodů. Podívejme se, jak dopadne lemmatizace našeho původního textu.

set.kRp.env(TT.cmd="manual", TT.options=list(path="C:\\TreeTagger", preset="en"), lang="en")
lemmatization <- treetag(text_tt$word, treetagger="manual", format="obj",
                      TT.tknz=FALSE , lang="en",
                      TT.options=list(path="C:/treeTagger", preset="en"))
lemmatization@tokens

Se stemizací se můžete setkat:

  • tagovací systémy
  • indexování
  • SEO (search engine optimization)
  • fulltextové vyhledávače

Analýza sentimentu

Jednou z častých úloh text miningu je analýza sentimentu (nebo také dolování názorů), jejímž cílem je identifikace a extrakce subjektivních informací ze zdrojových textů, nejčastěji různých komentářů a recenzí.

Nejprve je potřeba doinstalovat balíček textdata, který obsahuje všechny důležité slovníky.

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

K dispozici máme několik slovníků se sentimenty:

get_sentiments("afinn")
get_sentiments("bing")
get_sentiments("nrc")
get_sentiments("loughran")

Zkusíme se podívat, jak by byl ohodnocen těmito slovníky nás původní text.

text_tt %>% 
  inner_join(get_sentiments("afinn"), by="word")
text_tt %>% 
  inner_join(get_sentiments("bing"), by="word")
text_tt %>% 
  inner_join(get_sentiments("nrc"), by="word")
text_tt %>% 
  inner_join(get_sentiments("loughran"), by="word")
LS0tDQp0aXRsZTogIlrDoWtsYWR5IHRleHQgbWluaW5ndSINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCiMjIyAiRGVmaW5pY2UiDQoNCioqVGV4dCBtaW5pbmcqKiBqZSBzYWRhIGxpbmd2aXN0aWNrw71jaCBhIHN0YXRpc3RpY2vDvWNoIHRlY2huaWssIGt0ZXLDqSBkb3ZvbHVqw60gZG9sb3ZhdCBjZW5uw6ksIGRvcG9zdWQgbmV6bsOhbcOpIGluZm9ybWFjZSB6IG9ic8OhaGzDvWNoIG5lc3RydWt0dXJvdmFuw71jaCwgdGV4dG92w71jaCBkYXRhYsOhesOtLg0KDQpTcm92bsOhbsOtIHMgZGFsxaHDrW1pIMO6a29seSBkYXRvdsOpIGFuYWzDvXp5Og0KDQoqICoqdnlobGVkw6F2w6Fuw60gdmUgc3RydWt0dXJvdmFuw71jaCBkYXRlY2gqKiAodnlobGVkw6F2w6Fuw60gZGF0KTogbmFqZMSbdGUgdiBCcm7EmyB2xaFlY2hueSBqYXBvbnNrw6kgcmVzdGF1cmFjZSwga3RlcsOpIHNlcnbDrXJ1asOtIHZlZ2V0YXJpw6Fuc2vDoSBqw61kbGENCmBgYA0KU0VMRUNUICogRlJPTSByZXN0YXVyYW50cyBXSEVSRSBjaXR5ID0gIkJybm8iIEFORCB0eXBlID0gIkphcGFuZXNlIiBBTkQgaGFzX3ZlZyA9IHRydWU7DQpgYGANCiogKip2eWhsZWTDoXbDoW7DrSB2IG5lc3RydWt0dXJvdmFuw71jaCBkYXRlY2gqKiAodnlobGVkw6F2w6Fuw60gaW5mb3JtYWPDrSk6IG5hamTEm3RlIHYgQnJuxJsgamFwb25za291IHJlc3RhdXJhY2kNCmBgYA0KRG8gZnVsbHRleHRvdsOpaG8gdnlobGVkw6F2YcSNZSAoR29vZ2xlKSB6YWTDoW1lOiAiamFwb25za8OhIHJlc3RhdXJhY2UgQnJubyINCmBgYA0KKiAqKmRvbG92w6Fuw60gdmUgc3RydWt0dXJvdmFuw71jaCBkYXRlY2gqKiAoZGF0YSBtaW5pbmcpOiB1a2HFvnRlIMSNYXNvdsO9IHRyZW5kIHYgcG/EjXR1IG7DoXbFoXTEm3YgamFwb25za8O9Y2ggcmVzdGF1cmFjw60gdiBCcm7Emw0KYGBgDQpTRUxFQ1QgU1VNKHZpc2l0cykgRlJPTSByZXN0YXVyYW50cyBXSEVSRSBjaXR5ID0gIkJybm8iCUFORCB0eXBlID0gIkphcGFuZXNlIiBHUk9VUCBCWSBkYXRlIE9SREVSIEJZIGRhdGU7DQpgYGANCiogKipkb2xvdsOhbsOtIHYgbmVzdHJ1a3R1cm92YW7DvWNoIGRhdGVjaCoqICh0ZXh0IG1pbmluZyk6IG5hamTEm3RlIHLFr3puw6kgdHlweSBvdHJhdiBqw61kbGVtLCBrdGVyw6kganNvdSBzcG9qb3bDoW55IHMgamFwb25za8O9bWkgcmVzdGF1cmFjZW1pDQpgYGANClByb2pkZW1lIHbFoWVjaG55IHbEm2RlY2vDqSDEjWzDoW5reSwga3RlcsOpIHNlIHphYsO9dmFqw60gb3RyYXZhbWkgasOtZGxlbSBhIHZ5YmVyZW1lIHR5LCBrdGVyw6kganNvdSBzcG9qb3bDoW55DQpzIGphcG9uc2tvdSBrdWNoeW7DrS4NCmBgYA0KDQpQxZnDrWtsYWR5IHZ5dcW+aXTDrSB0ZXh0IG1pbmluZ3UgdmUgc2t1dGXEjW7DqW0gc3bEm3TEmzoNCg0KKiBhbmFsw716YSBwb8WhdHkgYSB2eWhvZG5vY2Vuw60sIGplc3RsaSBzZSBqZWRuw6EgbyBzcGFtDQoqIGFuYWzDvXphIHrDoWthem7DrWthIGEgcGVyc29uYWxpem92YW7DoSByZWtsYW1hDQoqIG1vbml0b3JvdsOhbsOtIHZlxZllam7DqWhvIG3DrW7Em27DrQ0KKiBhdXRvbWF0aWNrw6kgcm96ZXpuw6F2w6Fuw60gZG9rdW1lbnTFryBhIGplamljaCB6YcWZYXpvdsOhbsOtDQoNCiMjIyBBbmFsw716YSBwxZlpcm96ZW7DqWhvIGphenlrYQ0KDQoqICoqUMWZaXJvemVuw70gamF6eWsqKiBqZSB6w6FrbGFkbsOtIHByb3N0xZllZGVrIGxpZHNrw6kga29tdW5pa2FjZSwgdnpuaWtsIHDFmWlyb3plbsSbIGEgZHluYW1pY2t5IHNlIHZ5dsOtasOtLiBKZSBzdWJqZWt0aXZuw60gYSBlbW9jacOhbG7DrSwgbcSbbsOtY8OtIHNlIG9kIMSNbG92xJtrYSBrIMSNbG92xJtrdSBhIHUga2HFvmTDqWhvIHogbmljaCB2IMSNYXNlLiBQxZnDrWtsYWRlbSBqZSDEjWXFoXRpbmEsIHNsb3ZlbsWhdGluYSwgYW5nbGnEjXRpbmEsIHpuYWtvdsO9IGphenlrLg0KKiAqKkZvcm3DoWxuw60gamF6eWsgKiogamUgdW3Em2xlIHZ5dHZvxZllbsO9IHN5c3TDqW0sIGt0ZXLDvSBtw6EgbnVsb3ZvdSB2bml0xZluw60gbmV1csSNaXRvc3QgKGpzb3UgZXhha3Ruw60pLiBUai4ga2HFvmTDvSBwxZllc27EmyB2w60sIGpha8O9IG1hasOtIHbDvXpuYW0uIFDFmcOta2xhZGVtIGplIG1hdGVtYXRpa2EsIHByb2dyYW1vdmFjw60gamF6eWt5Lg0KDQohW09icsOhemVrOiBUaGV5IGF0ZSB0aGUgcGl6emEgd2l0aCBhbmNob3ZpZXNdKGFuY2hvdmllcy5qcGcpDQoNCiMgVWxvxb5lbsOtIHRleHR1DQoNClYgUiBtw6FtZSBrIGRpc3BvemljaSBuxJtrb2xpayBkYXRvdsO9Y2ggc3RydWt0dXIgayB1bG/FvmVuw60gYSB6cHJhY292w6Fuw60gdGV4dHUuIEtsYXNpY2vDqSB0ZXh0b3bDqSDFmWV0xJt6ZWMganNvdSB2aG9kw6kgbmFwxZnDrWtsYWQgcMWZaSBwcsOhY2kgcyBwb3Bpc2t5IGdyYWbFrywgZmFrdG9yeSBwb3XFvmlqZW1lIHDFmWkgbW9kZWxvdsOhbsOtIGthdGVnb3Jpw6FsbsOtY2ggcHJvbcSbbm7DvWNoIHYgcmVncmVzbsOtY2ggbW9kZWxlY2ggYSBwxZlpIHZ5dcW+aXTDrSBwb2tyb8SNaWxlasWhw61jaCB0ZWNobmlrIGFuYWzDvXp5IHRleHR1IHBvdcW+aWplbWUgdGFrw6kgc3BlY2nDoWxuw60gZGF0b3bDqSBzdHJ1a3R1cnkuIE5lanBydmUgc2UgdGVkeSBwb2TDrXZlam1lIG5hIGtyw6F0a8O9IHDFmWVobGVkIGRhdG92w71jaCBzdHJ1a3R1ciwga3RlcsOpIGx6ZSBwb3XFvsOtdCB2IFIgcMWZaSBwcsOhY2kgcyB0ZXh0ZW0uDQoNCiMjIyBUZXh0b3bDqSDFmWV0xJt6Y2UNCg0KTmVqb2JlY27Em2rFocOtIGEgbmVqdW5pdmVyesOhbG7Em2rFocOtIGRhdG92w6Egc3RydWt0dXJhLCBrdGVyw6EgbcWvxb5lIG7DqXN0IHRleHQuDQoNCmBgYHtyfQ0KdGV4dCA8LSBjKCJJIGRvbuKAmXQgbGlrZSBzYW5kLiBJdOKAmXMgY29hcnNlIGFuZCByb3VnaCBhbmQgaXJyaXRhdGluZywgYW5kIGl0IGdldHMgZXZlcnl3aGVyZS4iLA0KICAgICAgICAgICJOby4gSSBhbSB5b3VyIGZhdGhlci4iLA0KICAgICAgICAgICJVc2UgdGhlIGZvcmNlLCBMdWtlLiIsDQogICAgICAgICAgIlRoZXNlIGFyZSBub3QgdGhlIGRyb2lkcyB5b3VyIGxvb2tpbmcgZm9yLiIsDQogICAgICAgICAgIkRvLiBPciBkbyBub3QuIFRoZXJlIGlzIG5vIHRyeS4iKQ0KDQp0ZXh0DQpgYGANCg0KIyMjIEZha3RvcnkNCg0KRmFrdG9yeSBzaSBtxa/FvmVtZSBwxZllZHN0YXZpdCBqYWtvIMSNw61zbGEgcyBwb3Bpc2VtLiBLYcW+ZMOhIGthdGVnb3JpZSAobGV2ZWwpIGZha3RvcnUgamUgcmVwcmV6ZW50b3bDoW4gxI3DrXNsZW0gKG5hcMWZLiAxID0gIk1hbGUiLCAyID0gIkZlbWFsZSIpLCBhdsWhYWsgcyBmYWt0b3JlbSBuZWx6ZSBwcm92w6FkxJt0IGtsYXNpY2vDqSBtYXRlbWF0aWNrw6kgb3BlcmFjZS4gRmFrdG9yeSBuYWpkb3Ugc3bDqSB2eXXFvml0w60gcMWZZWRldsWhw61tIHYgcmVncmVzbsOtY2ggbW9kZWxlY2gsIHByb3Rvxb5lIHRleHQgbmVtxa/FvmVtZSBhbmFseXpvdmF0IGt2YW50aXRhdGl2bsSbIGEgbXVzw61tZSBuYWrDrXQgenDFr3NvYiwgamFrIHDFmWV2w6lzdCBzbG92YSBuYSDEjcOtc2xhLg0KDQpgYGB7cn0NCnN0YXRlcyA8LSBmYWN0b3Ioc3RhdGUucmVnaW9uKQ0KDQphdHRyaWJ1dGVzKHN0YXRlcykNCg0KYXMubnVtZXJpYyhzdGF0ZXMpDQpgYGANCg0KIyMjIFRpZHl0ZXh0DQoNCkJhbMOtxI1layAqKnRpZHl0ZXh0KiogbmFiw616w60gZGFsxaHDrSBtb8W+bm9zdGkgcHLDoWNlIHMgdGV4dGVtIGEgamUgcGxuxJsgaW50ZWdyb3bDoW4gZG8gZWtvc3lzdMOpbXUgKip0aWR5dmVyc2UqKi4NCg0KYGBge3J9DQojIGluc3RhbGwucGFja2FnZXMoInRpZHl0ZXh0IikNCmxpYnJhcnkodGlkeXRleHQpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmBgYA0KDQpOZWpwcnZlIHNpIHDFmWV2ZcSPbWUgcMWvdm9kbsOtIHRleHRvdsO9IHZla3RvciBuYSAqdGliYmxlKiAocMWvdm9kbsSbICpkYXRhX2ZyYW1lKiksIGt0ZXLDvSB1bG/FvsOtIGthxb5kw70ga2HDvWTDvSBwcnZlayB2ZWt0b3J1IGRvIHNhbW9zdGF0bsOpaG8gxZnDoWRrdSBhIHDFmWlkZWptZSBzaSBuYXbDrWMgaSBwcm9txJtubm91LCBrdGVyw6Egb3puYcSNw60gcG/FmWFkw60gZGFuw6lobyBwcnZrdS4NCg0KYGBge3J9DQp0ZXh0X3RibCA8LSB0aWJibGUobGluZT0xOjUsIHRleHQ9dGV4dCkNCg0KdGV4dF90YmwNCmBgYA0KDQpQb21vY8OtIGZ1bmtjZSAqdW5uZXN0X3Rva2VucyogbcWvxb5lbWUgbmVjaGF0IFIsIGHFpSBuw6FtIGRhbsO9IHRleHQgcm96ZMSbbMOtIG5hIGplZG5vdGxpdsOpIGVsZW1lbnR5ICh0enYuICp0b2tlbml6YWNlKikuIFTEm21pIG1vaG91IGLDvXQgamVkbm90bGl2w6Egc2xvdmEuDQoNCmBgYHtyfQ0KdGV4dF90dCA8LSB0ZXh0X3RibCAlPiUNCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0KQ0KDQp0ZXh0X3R0DQpgYGANCg0KUsWvem7EmyBuLXRpY2Ugc2xvdiwgdMWZZWJhIHR6di4gYmlncmFteS4NCg0KYGBge3J9DQp0ZXh0X3R0X2JpZ3JhbXMgPC0gdGV4dF90YmwgJT4lDQogIHVubmVzdF90b2tlbnMobmdyYW0sIHRleHQsIHRva2VuPSJuZ3JhbXMiLCBuPTIpDQoNCnRleHRfdHRfYmlncmFtcw0KYGBgDQoNCk5lYm8gZG9rb25jZSBpIGNlbMOpIHbEm3R5Lg0KDQpgYGB7cn0NCnRleHRfdHRfc2VudGVuY2VzIDwtIHRleHRfdGJsICU+JQ0KICB1bm5lc3RfdG9rZW5zKHNlbnRlbmNlLCB0ZXh0LCB0b2tlbj0ic2VudGVuY2VzIikNCg0KdGV4dF90dF9zZW50ZW5jZXMNCmBgYA0KDQojIyMgRG9jdW1lbnQtdGVybSBtYXRpY2UNCg0KSmVkbsOhIHNlIG8gbWF0aWNpLCBrdGVyw6EgcG9waXN1amUgxI1ldG5vc3QgamVkbm90bGl2w71jaCBzbG92IChzbG91cGNlKSB2IGFuYWx5em92YW7DvWNoIGRva3VtZW50ZWNoICjFmcOhZGt5KS4gSmVkbsOhIHNlIHRlZHkgbyBtYXRpY2ksIGt0ZXLDoSBtw6EgenByYXZpZGxhIG7Em2tvbGlrIHN0b3ZlayBhxb4gdGlzw61jIHNsb3VwY8WvLiBOYSBkcnVob3Ugc3RyYW51IGplIHbEm3TFoWluYSBwcnZrxa8gdMOpdG8gbWF0aWNlIG51bG92w6EsIHRha8W+ZSB6YWLDrXLDoSBtw6luxJsgbcOtc3RhIG5lxb4gb3JpZ2luw6FsbsOtIGRva3VtZW50eS4NCg0KRG9jdW1lbnQtdGVybSBtYXRpY2kgamnFviBtxa/FvmVtZSBwb3XFvsOtdCBqYWtvIHZzdHVwIHYgbsSba3RlcsOpbSB6IGFsZ29yaXRtdSBzdHJvam92w6lobyB1xI1lbsOtIChyb3pob2RvdmFjw60gc3Ryb215LCBuZXVyb25vdsOpIHPDrXTEmyBhcG9kLikuDQoNClBvZMOtdmVqbWUgc2UsIGphayB2eXBhZMOhIGRvY3VtZW50LXRlcm0gbWF0aWNlIHBybyBuw6HFoSB0ZXh0Lg0KDQpgYGB7cn0NCiMgaW5zdGFsbC5wYWNrYWdlcygidG0iKQ0KbGlicmFyeSh0bSkNCg0KdGV4dF9kdG0gPC0gdGV4dF90dCAlPiUNCiAgZ3JvdXBfYnkobGluZSkgJT4lDQogIGNvdW50KHdvcmQpICU+JQ0KICBjYXN0X2R0bShsaW5lLCB3b3JkLCBuKQ0KDQppbnNwZWN0KHRleHRfZHRtKQ0KYGBgDQoNClBybyBrb25rcsOpdG7DrSBtYXRpY2kgc2kgbcWvxb5lbWUgdnlwc2F0IGkgamVkbm90bGl2w6Egc2xvdmEgKHNsb3VwY2UpLg0KDQpgYGB7cn0NClRlcm1zKHRleHRfZHRtKQ0KYGBgDQoNCiMjIyBKYXp5a292w6kga29ycHVzeQ0KDQpKYXp5a292w70ga29ycHVzIGplIHJvenPDoWhsw70gc291Ym9yIHRleHTFryB1csSNaXTDqWhvIGphenlrYS4gUG91xb7DrXZhasOtIHNlIGplZG5hayBwcm8gbGluZ3Zpc3RpY2vDvSB2w716a3VtLCBqZWRuYWsgamFrbyBkYXRvdsOhIHrDoWtsYWRuYSBwcm8gdHZvcmJ1IHNsb3Zuw61rxa8sIGtvcmVrdG9yxa8sIHDFmWVrbGFkYcSNxa8gYXRkLg0KDQpgYGB7cn0NCnRleHRfY29ycHVzIDwtIFZDb3JwdXMoVmVjdG9yU291cmNlKHRleHQpKQ0KDQpzdW1tYXJ5KHRleHRfY29ycHVzKQ0KDQp0ZXh0X2NvcnB1c1tbMV1dWzFdDQpgYGANCg0KUMWZw61rbGFkeSBuxJtrdGVyw71jaCBqYXp5a292w71jaCBrb3JwdXPFrzoNCg0KKiBbxIxlc2vDvSBrb3JwdXNdKGh0dHBzOi8vd3d3LmtvcnB1cy5jei8pDQoqIFtCcml0aXNoIE5hdGlvbmFsIENvcnB1c10oaHR0cDovL3d3dy5uYXRjb3JwLm94LmFjLnVrKQ0KKiBbQ29tbW9uIENyYXdsXShodHRwOi8vY29tbW9uY3Jhd2wub3JnL3RoZS1kYXRhL2dldC1zdGFydGVkLykNCiogW1dpa2lwZWRpYV0oaHR0cHM6Ly9kdW1wcy53aWtpbWVkaWEub3JnLykNCg0KIyBQcsOhY2UgcyB0ZXh0ZW0NCg0KIyMjIFJlZ3Vsw6FybsOtIHbDvXJhenkNCg0KUG9tb2PDrSBqZWRub2R1Y2jDqWhvIMWZZXTEm3pjZSBsemUgbmFkZWZpbm92YXQgY2Vsb3UgbW5vxb5pbnUgxZlldMSbemPFr20sIMSNZWhvxb4gc2Ugdnl1xb7DrXbDoSBuZWrEjWFzdMSbamkgcMWZaSB2eWhsZWTDoXbDoW7DrSB2IHRleHR1IG5lYm8gcMWZaSBtYW5pcHVsYWNpIHMgdGV4dGVtIChuYWhyYXpvdsOhbsOtIGhsZWRhbsOpaG8gdsO9cmF6dSB6YSBqaW7DvSBhcG9kLikuIFpuYWxvc3QgcmVndWzDoXJuw61jaCB2w71yYXrFryBieSBtxJtsYSBwYXTFmWl0IGRvIHbDvWJhdnkga2HFvmTDqWhvIGRhdG92w6lobyBhbmFseXRpa2EuIE5hIGludGVybmV0dSBleGlzdHVqZSBtbm9obyBuw6FzdHJvasWvLCBrdGVyw6kgbW9ob3UgYsO9dCBuw6Fwb21vY27DqSBwxZlpIHByw6FjaSBzIHJlZ3Vsw6FybsOtbWkgdsO9cmF6eSAobmFwxZkuIFtyZWdleG9uZS5jb21dKGh0dHBzOi8vcmVnZXhvbmUuY29tLykgcHJvIGludGVyYWt0aXZuw60gdsO9dWt1IHJlZ3Vsw6FybsOtY2ggdsO9cmF6xa8sIFtyZWdleDEwMS5jb21dKGh0dHBzOi8vcmVnZXgxMDEuY29tLykgcHJvIHRlc3RvdsOhbsOtIHZsYXN0bsOtY2ggcmVndWzDoXJuw61jaCB2w71yYXrFrykuDQoNClBybyBwcsOhY2kgcyB0ZXh0ZW0gYSBzIHJlZ3Vsw6FybsOtbWkgdsO9cmF6eSBsemUgcG91xb7DrXZhdCB6w6FrbGFkbsOtIFIgZnVua2NlLiBOZWrEjWFzdMSbamkgc2UgbcWvxb5ldGUgc2V0a2F0IHMgZnVua2NlbWk6DQoNCiogKipwYXN0ZSoqOiB2eXR2w6HFmWVuw60gdGV4dG92w71jaCDFmWV0xJt6Y8WvIGtvbWJpbmFjw60gdGV4dG92w71jaCBhIG51bWVyaWNrw71jaCBwcm9txJtubsO9Y2gNCmBgYHtyfQ0KcGFzdGUwKDE6MTIsIGMoInN0IiwgIm5kIiwgInJkIiwgcmVwKCJ0aCIsIDkpKSkNCmBgYA0KKiAqKnN1YnN0cioqOiB2eWptdXTDrSBuZWJvIG5haHJhemVuw60gcG9kxZlldGV6Y2UgdiB0ZXh0b3bDqW0gxZlldMSbemNpDQpgYGB7cn0NCnN1YnN0cigiYWJjZGVmIiwgMiwgNCkNCmBgYA0KKiAqKmdyZXAqKjogaGxlZMOhbsOtIHYgdGV4dG92w6ltIMWZZXTEm3pjaSBwb21vY8OtIHJlZ3Vsw6FybsOtY2ggdsO9cmF6xa8NCmBgYHtyfQ0KeCA8LSBjKCJodHRwczovL3d3dy5tdW5pLmN6LyIsICJUZXh0LnMudGVja2FtaSIsICJmdHA6Ly9mdHAueHl6LmNvbS8iKQ0KDQpncmVwKCJeKChbXjpdKyk6Ly8pPyhbXjovXSspKDooWzAtOV0rKSk/KC8uKikiLCB4LCB2YWx1ZT1UUlVFKQ0KYGBgDQoqICoqZ3N1YioqOiBuYWhyYXp1amUgdiB0ZXh0dSBobGVkYW7DvSDFmWV0xJt6ZWMgemEgamluw70gc3BlY2lmaWtvdmFuw70gxZlldGV6ZWMsIGx6ZSBwb3XFvsOtdCByZWd1bMOhcm7DrSB2w71yYXp5DQpgYGB7cn0NCnggPC0gIlRhdG8gIHZldGEgbWEgICAgbW9jICAgICAgICBtZXplci4iDQpnc3ViKCJcXHN7Mix9IiwgIiAiLCB4KQ0KYGBgDQoqICoqc3Ryc3BsaXQqKjogcm96ZMSbbGVuw60gdGV4dG92w6lobyDFmWV0xJt6Y2UgcG9kbGUgZGFuw6lobyBvZGTEm2xvdmHEjWUNCmBgYHtyfQ0KdW5saXN0KHN0cnNwbGl0KCJvZGRlbG92YWMtbXV6ZS1ieXQtdHJlYmEtaS1wb21sY2thIiwgIi0iKSkNCmBgYA0KDQpOb3bEm2rFocOtIGJhbMOtxI1reSBuYWLDrXplasOtIHDFmcOtamVtbsSbasWhw60gbWFuaXB1bGFjaSBzIHRleHRlbSBhIHRha8OpIGxlcMWhw60gaW50ZWdyYWNpIGRvIGhvam7EmyB2eXXFvsOtdmFuw6lobyBwcm9zdMWZZWTDrSAqdGlkeXZlcnNlKi4gSmVkbsOtbSB6IG5pY2ggamUgKnN0cmluZ3IqLCBrdGVyw70gcG9za3l0dWplIG1ub2hvIHXFvml0ZcSNbsO9Y2ggZnVua2PDrSwga3RlcsOpIHZ5dcW+aWplbWUgdiBkcnVow6kgbGVrY2kuIFBybyByeWNobMO9IHDFmWVobGVkIGZ1bmtjw60gdiB0b210byBiYWzDrcSNa3UgZG9wb3J1xI11amkgcHJvasOtdCBbc3RyaW5nciBjaGVhdCBzaGVldF0oaHR0cHM6Ly9naXRodWIuY29tL3JzdHVkaW8vY2hlYXRzaGVldHMvYmxvYi9tYXN0ZXIvc3RyaW5ncy5wZGYpLg0KDQojIyMgU3RvcCB3b3Jkcw0KDQpTdG9wIHdvcmRzIGpzb3UgdGFrb3bDoSBzbG92YSwga3RlcsOhIG5lbmVzb3Ugxb7DoWRub3UgaW5mb3JtYcSNbsOtIGhvZG5vdHUuIFbEm3TFoWlub3Ugc2UgamUgc25hxb7DrW1lIHYgcHLFr2LEm2h1IGFuYWzDvXp5IG9kc3RyYW5pdCwgamVsaWtvxb4gbWFqw60gdmVsa291IMSNZXRub3N0IGEgbmVuZXNvdSDFvsOhZG5vdSBjZW5ub3UgaW5mb3JtYWNpLiBWIGFuZ2xpxI10aW7EmyBzZSBtxa/FvmUgamVkbmF0IG5hcMWZw61rbGFkIG8gxI1sZW55ICh0aGUsIGEsIGFuKSwgc3Bvamt5IChhbmQsIG9yLCBiZWNhdXNlKSBuZWJvIHrDoWptZW5hIChJLCB5b3UsIGhlLCBzaGUpLg0KDQpNxa/FvmV0ZSBzaSBkZWZpbm92YXQgdmxhc3Ruw60gc2xvdm7DrWsgc3RvcCB3b3JkcyBuZWJvIHZ5dcW+w610IG7Em2pha8O9IGNpesOtIHNsb3Zuw61rIHZvbG7EmyBkb3N0dXBuw70gbmEgaW50ZXJuZXR1LiBNeSBzZSBwb2TDrXbDoW1lIG5hIHNsb3Zuw61rIGFuZ2xpY2vDvWNoIHN0b3Agd29yZHMgeiBiYWzDrcSNa3UgKnRpZHl0ZXh0Kiwga3RlcsO9IGplIGRvc3R1cG7DvSB2IHRpYmJsZSAqc3RvcF93b3JkcyouDQoNCg0KYGBge3J9DQpzdG9wX3dvcmRzDQpgYGANCg0KTcWvxb5lbWUgc2UgcG9kw612YXQsIGt0ZXLDqSBzbG92YSB6IG5hxaFlaG8gcMWvdm9kbsOtaG8gdGV4dHUgesWvc3Rhbm91IHBvIG9kc3RyYW7Em27DrSBzdG9wIHdvcmRzLg0KDQpgYGB7cn0NCnRleHRfdHQgJT4lDQogIGFudGlfam9pbihzdG9wX3dvcmRzLCBieT0id29yZCIpDQpgYGANCg0KU2xvdm7DrWsgc3RvcCB3b3JkcyB6IGJhbMOtxI1rdSAqdGlkeXRleHQqIG3Fr8W+ZW1lIG9ib2hhdGl0IGkgbyB2bGFzdG7DrSBzbG92YSBqZWRub2R1Y2jDvW0genDFr3NvYmVtLg0KDQpgYGB7cn0NCmN1c3RvbV9zdG9wX3dvcmRzIDwtIGJpbmRfcm93cyh0aWJibGUod29yZCA9IGMoImRvbuKAmXQiLCAiaXTigJlzIiksIGxleGljb24gPSBjKCJjdXN0b20iKSksIHN0b3Bfd29yZHMpDQoNCnRleHRfdHQgJT4lDQogIGFudGlfam9pbihjdXN0b21fc3RvcF93b3JkcywgYnk9IndvcmQiKQ0KYGBgDQoNCiMjIyBURi1JREYNCg0KVXLEjWl0xJsgdsOhcyBuYXBhZGxvLCBqYWsgc3Byw6F2bsSbIHVyxI1pdCwga3RlcsOhIHNsb3ZhIGplxaF0xJsgcMWZaWRhdCBkbyBzdG9wIHdvcmRzIGEga3RlcsOhIHXFviBuZT8gRGVmYXVsdG7DrSBzbG92bsOta3kgbmF2w61jIG1vaG91IG9ic2Fob3ZhdCBzbG92YSwga3RlcsOhIHZlIHZhxaFlbSBrb250ZXh0dSBtYWrDrSBwbGF0bsO9IHbDvXpuYW0gKG5hcMWZLiBpcyAtIGFuZ2xpY2vDqSBzbG92ZXNvIHZzIElTIC0gaW5mb3JtYcSNbsOtIHN5c3TDqW0pLg0KDQpLIHpvZHBvdsSbemVuw60gdMSbY2h0byBvdMOhemVrIG7DoW0gbcWvxb5lIHBvbW9jaSAqKkludmVyc2UgZG9jdW1lbnQgZnJlcXVlbmN5KiosIGt0ZXLDoSBvem5hxI11amUsIGphayBqZSBkYW7DqSBzbG92byBkxa9sZcW+aXTDqSB2IGtvbGVrY2kgZG9rdW1lbnTFry4gVGEgc2UgZGVmaW51amUgbsOhc2xlZHVqw61jw61tIHZ6dGFoZW06DQoNCiQkXHRleHR7aWRmKHNsb3ZvKX0gPSBcbG4gXGxlZnQoIFxmcmFje1wjXHRleHR7ZG9rdW1lbnR5fX17XCNcdGV4dHtkb2t1bWVudHkgb2JzYWh1asOtY8OtIGRhbsOpIHNsb3ZvfX0gXHJpZ2h0KSQkDQoNCg0KUG9rdWQgc2Ugc2xvdm8gbmFjaMOhesOtIHZlIHbFoWVjaCBkb2t1bWVudGVjaCwgcG90b20gamUgdsOhaGEgdGFrb3bDqWhvIHNsb3ZhIDAgKCQ9XGxuICgxKSQpLg0KDQpOYW9wYWsgcG9rdWQgc2UgZGFuw6kgc2xvdm8gbmFjaMOhesOtIHBvdXplIHYgamVkbm9tIGRva3VlbW50dSB6IG1ub2hhLCBtw6EgdG90byBzbG92byB2ZWxrb3UgdsOhaHUgKG5hcMWZLiAkXGxuICgxMC8xKSA9IDIsMyQpLg0KDQpUYWt0byB2eXBvxI3DrXRhbm91IHbDoWh1IG3Fr8W+ZW1lIHZ5bsOhc29iaXQgxI1ldG5vc3TDrSBkYW7DqWhvIHNsb3ZhIGEgZG9zdGFuZW1lIHR6di4gKip0ZXJtIGZyZXF1ZW5jeS1pbnZlcnNlIGRvY3VtZW50IGZyZXF1ZW5jeSoqLiBUYSBzZSBkZWZpbnVqZSBuw6FzbGVkdWrDrWPDrW0gdnp0YWhlbToNCg0KJCRcdGV4dHt0Zi1pZGYoc2xvdm8pfSA9IFxmcmFje1wjXHRleHR7c2xvdm8gdiBkb2t1bWVudHV9fXtcI1x0ZXh0e3bFoWVjaG5hIHNsb3ZhIHYgZG9rdW1lbnR1fX0gXGNkb3QgXGxuIFxsZWZ0KCBcZnJhY3tcI1x0ZXh0e2Rva3VtZW50eX19e1wjXHRleHR7ZG9rdW1lbnR5IG9ic2FodWrDrWPDrSBkYW7DqSBzbG92b319IFxyaWdodCkkJA0KDQpQcm8gaWx1c3RyYWNpIHNpIG3Fr8W+ZW1lIHVrw6F6YXQgdsO9cG/EjWV0ICp0Zi1pZGYqIG5hIG5hxaFlbSBwxa92b2Ruw61tIHRleHR1LiBOZWpwcnZlIHphxI1uZW1lIHbDvXBvxI10ZW0gxI1ldG5vc3TDrSBwcm8ga2HFvmTDqSBzbG92by4NCg0KYGBge3J9DQojIHZ5c2t5dCBzbG92IHYgamVkbm90bGl2eWNoIGRva3VtZW50ZWNoDQp0ZXh0X3dvcmRzIDwtIHRleHRfdHQgJT4lDQogIGNvdW50KGxpbmUsIHdvcmQsIHNvcnQ9VFJVRSkNCg0KIyBjZWxrb3Z5IHZ5c2t5dCBzbG92DQp0b3RhbF93b3JkcyA8LSB0ZXh0X3dvcmRzICU+JSANCiAgZ3JvdXBfYnkod29yZCkgJT4lIA0KICBzdW1tYXJpc2UodG90YWw9c3VtKG4pKQ0KDQp0ZXh0X3dvcmRzIDwtIGxlZnRfam9pbih0ZXh0X3dvcmRzLCB0b3RhbF93b3JkcywgYnk9IndvcmQiKQ0KDQp0ZXh0X3dvcmRzDQpgYGANCg0KUHJvIHNhbW90bsO9IHbDvXBvxI1ldCAqdGYtaWRmKiBtxa/FvmVtZSBwb3XFvsOtdCBmdW5rY2kgKmJpbmRfdGZfaWRmKiB6IGJhbMOtxI1rdSAqdGlkeXRleHQqLg0KDQpgYGB7cn0NCnRleHRfd29yZHMgPC0gdGV4dF93b3JkcyAlPiUNCiAgYmluZF90Zl9pZGYod29yZCwgbGluZSwgbikgJT4lIA0KICBhcnJhbmdlKGRlc2ModGZfaWRmKSkNCg0KdGV4dF93b3Jkcw0KYGBgDQoNClBydm7DrSBzbG92byAqZG8qIHNlIG5hY2jDoXrDrSBwb3V6ZSB2IGplZG5vbSBkb2t1bWVudHUgeiBwxJt0aSwgZG9zdMOhdsOhbWUgdGVkeSAkXHRleHR7aWRmKCJkbyIpfSA9IFxsbiAoNS8xKT0gMSw2MDk0Mzc5JC4NCg0KVGVudG8gZG9rdW1lbnQgb2JzYWh1amUgY2Vsa2VtIDggc2xvdiBhIHNsb3ZvICpkbyogc2Ugb3Bha3VqZSBkdmFrcsOhdCwgdGFrxb5lIGRvc3TDoXbDoW1lICRcdGV4dHt0ZigiZG8iLCBENSl9ID0gMS80JC4NCg0KVnluw6Fzb2JlbsOtbSB0xJtjaHRvIGR2b3UgxI3DrXNlbCBkb3N0w6F2w6FtZSAkXHRleHR7dGYtaWRmKCJkbyIsIEQ1KX0gPSAwLDQwMjM1OTQ4JC4NCg0KYGBge3J9DQp0ZXh0WzVdDQpgYGANCg0KIyMjIFN0ZW1pemFjZSBhIGxlbW1hdGl6YWNlDQoNCioqU3RlbWl6YWNlKiogKGFuZ2xpY2t5ICpzdGVtbWluZyopIGplIHByb2NlcywgcMWZaSBrdGVyw6ltIHNlIHNsb3ZhIG5haHJhenVqw60gcG91emUgc3bDvW0gesOha2xhZGVtLiBTbXlzbGVtIHN0ZW1pemFjZSBqZSBzamVkbm9jZW7DrSBzbG92IHNlIHN0ZWpuw71tIHbDvXpuYW0sIGFsZSBvZGxpxaFuw71tIHR2YXJlbS4gVsO9c2xlZG7DvSB6w6FrbGFkIG5lbXVzw60gYsO9dCBwbGF0bsO9bSBzbG92ZW0uDQoNCioqTGVtbWF0aXphY2UqKiAoYW5nbGlja3kgKmxlbW1hdGl6YXRpb24qKSBqZSBwcm9jZXMgcG9kb2Juw70gc3RlbWl6YWNpLCBhdsWhYWsgY8OtbGVtIGxlbW1hdGl6YWNlIGplIHrDrXNrYXQgcGxhdG7DqSBzbG92by4gSmUgbsOhcm/EjW7Em2rFocOtIG5hIHbDvXBvxI1ldG7DrSB2w71rb24sIHByb3Rvxb5lIG11c8OtIHByb2Now6F6ZXQgdmVsa8OpIGtvcnB1c3kgYSBzbG92bsOta3kuIFDFmWkgc3RlbWl6YWNpIG5hb3BhayBwb3N0dXB1amVtZSBwb2RsZSByZWxhdGl2bsSbIGplZG5vZHVjaMO9Y2ggcHJhdmlkZWwuDQoNCk5hcMWZw61rbGFkIHNsb3ZvICpzdHVkaWVzKiBqZSBwxZlpIHN0ZW1pemFjaSBub3JtYWxpem92w6FubyBuYSB6w6FrbGFkICpzdHVkaSogKG9kc3RyYW7Em27DrSBrb25jb3ZreSAqLWVzKikgYSBzbG92byAqc3R1ZHlpbmcqIG5hIHrDoWtsYWQgKnN0dWR5KiAob2RzdHJhbsSbbsOtIGtvbmNvdmt5ICotaW5nKikuIFDFmWkgbGVtbWF0aXphY2kganNvdSBvYmEgdHZhcnkgcMWZZXZlZGVueSBuYSBzcG9sZcSNbsOpIGxlbW1hICpzdHVkeSouDQoNClBybyBpbHVzdHJhY2kgamXFoXTEmyB1dmXEj21lIHDFmcOta2xhZCB2xJt0eSAqdGhlIGJveSdzIGNhcnMgYXJlIGRpZmZlcmVudCBjb2xvcnMqLCBrdGVyw6EgbcWvxb5lIGLDvXQgbm9ybWFsaXpvdmFuw6EgbmE6ICp0aGUgYm95IGNhciBiZSBkaWZmZXIgY29sb3IqLg0KDQohW09icsOhemVrOiBOb3JtYWxpemFjZSB0ZXh0dV0oemVudS5wbmcpDQoNClVrYcW+bWUgc2ksIGphayBwcm92w6lzdCBzdGVtaXphY2kgdiBSIG5hIG5hxaFlbSBwxa92b2Ruw61tIHRleHR1LiBOZWpwcnZlIGplIHBvdMWZZWJhIGRvaW5zdGFsb3ZhdCBiYWzDrcSNZWsgKlNub3diYWxsQyouIFBvdG9tIG3Fr8W+ZW1lIHBvdcW+w610IGZ1bmtjaSAqd29yZFN0ZW0qLg0KDQpgYGB7cn0NCiNpbnN0YWxsLnBhY2thZ2VzKCJTbm93YmFsbEMiKQ0KbGlicmFyeShTbm93YmFsbEMpDQoNCnRleHRfdHQgJT4lIA0KICBtdXRhdGUoc3RlbT13b3JkU3RlbSh3b3JkLCAiZW5nbGlzaCIpKQ0KYGBgDQoNCkxlbW1hdGl6YWNlIGplIG7DoXJvxI1uxJtqxaHDrSBwcm9jZXMsIGt0ZXLDvSBpIHZ5xb5hZHVqZSBwb2tyb8SNaWxlasWhw60gdGVjaG5pa3kuIE15IHBvdcW+aWplbWUgZXh0ZXJuw60gc29mdHdhcmUgW1RyZWVUYWdnZXJdKGh0dHBzOi8vd3d3LmNpcy51bmktbXVlbmNoZW4uZGUvfnNjaG1pZC90b29scy9UcmVlVGFnZ2VyLyksIGt0ZXLDvSBtw6EgcG9kcG9ydSBuxJtrb2xpa2EgamF6eWvFryB2xI1ldG7EmyDEjWXFoXRpbnkgYSBzbG92ZW7FoXRpbnkuIFBybyBpbnRlZ3JhY2kgcyBSIGplIHBvdMWZZWJhIGRvaW5zdGFsb3ZhdCBiYWzDrcSNZWsgKmtvUnB1cyouDQoNCmBgYHtyfQ0KIyBpbnN0YWxsLnBhY2thZ2VzKCJrb1JwdXMiKQ0KbGlicmFyeShrb1JwdXMpDQoNCiMgaW5zdGFsbCB0aGUgbGFuZ3VhZ2Ugc3VwcG9ydCBwYWNrYWdlDQojIGluc3RhbGwua29ScHVzLmxhbmcoImVuIikNCmxpYnJhcnkoa29ScHVzLmxhbmcuZW4pDQpgYGANCg0KU2Ftb3Ruw6kgbmFzdGF2ZW7DrSBqZSB0cm9jaHUgc2xvxb5pdMSbasWhw60gYSBwcmF2ZMSbcG9kb2JuxJsgYnVkZXRlIG11c2V0IHByb2rDrXQgdsOtY2UgaW50ZXJuZXRvdsO9Y2ggbsOhdm9kxa8uIFBvZMOtdmVqbWUgc2UsIGphayBkb3BhZG5lIGxlbW1hdGl6YWNlIG5hxaFlaG8gcMWvdm9kbsOtaG8gdGV4dHUuDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0V9DQoNCg0Kc2V0LmtScC5lbnYoVFQuY21kPSJtYW51YWwiLCBUVC5vcHRpb25zPWxpc3QocGF0aD0iQzpcXFRyZWVUYWdnZXIiLCBwcmVzZXQ9ImVuIiksIGxhbmc9ImVuIikNCg0KbGVtbWF0aXphdGlvbiA8LSB0cmVldGFnKHRleHRfdHQkd29yZCwgdHJlZXRhZ2dlcj0ibWFudWFsIiwgZm9ybWF0PSJvYmoiLA0KICAgICAgICAgICAgICAgICAgICAgIFRULnRrbno9RkFMU0UgLCBsYW5nPSJlbiIsDQogICAgICAgICAgICAgICAgICAgICAgVFQub3B0aW9ucz1saXN0KHBhdGg9IkM6L3RyZWVUYWdnZXIiLCBwcmVzZXQ9ImVuIikpDQoNCmxlbW1hdGl6YXRpb25AdG9rZW5zDQpgYGANCg0KU2Ugc3RlbWl6YWPDrSBzZSBtxa/FvmV0ZSBzZXRrYXQ6DQoNCiogdGFnb3ZhY8OtIHN5c3TDqW15DQoqIGluZGV4b3bDoW7DrQ0KKiBTRU8gKHNlYXJjaCBlbmdpbmUgb3B0aW1pemF0aW9uKQ0KKiBmdWxsdGV4dG92w6kgdnlobGVkw6F2YcSNZQ0KDQoNCiMgQW5hbMO9emEgc2VudGltZW50dQ0KDQpKZWRub3UgeiDEjWFzdMO9Y2ggw7psb2ggdGV4dCBtaW5pbmd1IGplIGFuYWzDvXphIHNlbnRpbWVudHUgKG5lYm8gdGFrw6kgZG9sb3bDoW7DrSBuw6F6b3LFryksIGplasOtbcW+IGPDrWxlbSBqZSBpZGVudGlmaWthY2UgYSBleHRyYWtjZSBzdWJqZWt0aXZuw61jaCBpbmZvcm1hY8OtIHplIHpkcm9qb3bDvWNoIHRleHTFrywgbmVqxI1hc3TEm2ppIHLFr3puw71jaCBrb21lbnTDocWZxa8gYSByZWNlbnrDrS4NCg0KTmVqcHJ2ZSBqZSBwb3TFmWViYSBkb2luc3RhbG92YXQgYmFsw63EjWVrICp0ZXh0ZGF0YSosIGt0ZXLDvSBvYnNhaHVqZSB2xaFlY2hueSBkxa9sZcW+aXTDqSBzbG92bsOta3kuDQoNCmBgYHtyfQ0KI2luc3RhbGwucGFja2FnZXMoInRleHRkYXRhIikNCmxpYnJhcnkodGV4dGRhdGEpDQpgYGANCg0KSyBkaXNwb3ppY2kgbcOhbWUgbsSba29saWsgc2xvdm7DrWvFryBzZSBzZW50aW1lbnR5Og0KDQoqICoqQUZJTk4qKiAtIG9ic2FodWplIHNsb3ZhIGEgayBuaW0gcMWZacWZYXplbsOpIHNrw7NyZSBvZCAtNSBkbyA1LCDEjcOtbSB2xJt0xaHDrSDEjcOtc2xvLCB0w61tIHBveml0aXZuxJtqxaHDrSBzbG92bw0KYGBge3J9DQpnZXRfc2VudGltZW50cygiYWZpbm4iKQ0KYGBgDQoNCiogKipiaW5nKiogLSByb3pkxJtsdWplIHNsb3ZhIGRvIGR2b3Uga2F0ZWdvcmnDrTogcG9zaXRpdmUsIG5lZ2F0aXZlDQpgYGB7cn0NCmdldF9zZW50aW1lbnRzKCJiaW5nIikNCmBgYA0KDQoqICoqbnJjKiogLSByb3pkxJtsdWplIHNsb3ZhIGRvIGthdGVnb3Jpw606IHBvc2l0aXZlLCBuZWdhdGl2ZSwgYW5nZXIgKGhuxJt2KSwgYW50aWNpcGF0aW9uIChvxI1la8OhdsOhbsOtKSwgZGlzZ3VzdCAob2Rwb3IpLCBmZWFyIChzdHJhY2gpLCBqb3kgKHJhZG9zdCksIHNhZG5lc3MgKHNtdXRlayksIHN1cnByaXNlIChwxZlla3ZhcGVuw60pLCB0cnVzdCAoZMWvdsSbcmEpDQpgYGB7cn0NCmdldF9zZW50aW1lbnRzKCJucmMiKQ0KYGBgDQoNCiogKipsb3VnaHJhbioqIC0gcm96ZMSbbHVqZSBzbG92YSBkbyBrYXRlZ29yacOtOiBjb25zdHJhaW5pbmcgKG9tZXp1asOtY8OtKSwgbGl0aWdpb3VzIChzcG9ybsO9KSwgbmVnYXRpdmUsIHBvc2l0aXZlLCBzdXBlcmZsdW91cyAobmFkYnl0ZcSNbsO9KSwgdW5jZXJ0YWludHkgKG5lamlzdG90YSkNCmBgYHtyfQ0KZ2V0X3NlbnRpbWVudHMoImxvdWdocmFuIikNCmBgYA0KDQpaa3Vzw61tZSBzZSBwb2TDrXZhdCwgamFrIGJ5IGJ5bCBvaG9kbm9jZW4gdMSbbWl0byBzbG92bsOta3kgbsOhcyBwxa92b2Ruw60gdGV4dC4NCg0KYGBge3J9DQp0ZXh0X3R0ICU+JSANCiAgaW5uZXJfam9pbihnZXRfc2VudGltZW50cygiYWZpbm4iKSwgYnk9IndvcmQiKQ0KYGBgDQoNCmBgYHtyfQ0KdGV4dF90dCAlPiUgDQogIGlubmVyX2pvaW4oZ2V0X3NlbnRpbWVudHMoImJpbmciKSwgYnk9IndvcmQiKQ0KYGBgDQoNCmBgYHtyfQ0KdGV4dF90dCAlPiUgDQogIGlubmVyX2pvaW4oZ2V0X3NlbnRpbWVudHMoIm5yYyIpLCBieT0id29yZCIpDQpgYGANCg0KYGBge3J9DQp0ZXh0X3R0ICU+JSANCiAgaW5uZXJfam9pbihnZXRfc2VudGltZW50cygibG91Z2hyYW4iKSwgYnk9IndvcmQiKQ0KYGBg