Dieses erste Kapitel liefert einen Überblick über zahlreiche Grundfunktionen des Pakets Quanteda, die gleichzeitig die Basis der automatisierten Inhaltsanalyse mit R bilden. Über Quanteda hinaus werden im Verlauf dieser neunteiligen Einführung noch eine Reihe weiterer R-Bibliotheken verwendet, etwa für das überwachte maschinelle Lernen (Kapitel 5) und das Berechnen von Themenmodellen (Kapitel 6). In praktisch jeder Einheit relevant sind dabei die Pakete des tidyverse (vor allem ggplot, dplyr und stringr), durch die zahlreiche Funktionen wie Plotten, Textverarbeitung und Datenmanagement gegenüber den R-Basisfunktionen stark verbessert werden. Pakete für einzelne Teilbereiche, die erst später eine Rolle spielen werden, sind u.a. topicmodels und stm (Themenmodelle), RTextTools und keras (überwachtes maschinelles Lernen), spacyr und udpipe (POS-Tagging und Named-Entity-Erkennung) sowie googlenlp bzw. googleLanguageR (weitere Annotations- und Übersetzungfunktionen).

Die Basis der Analyse in diesem ersten Kapitel bildet einerseits eine Sammlung einfacher Beispielsätze, anhand derer sich die Grundfunktionen von Quanteda gut erläutern lassen, und andererseits die beliebten Detektivgeschichten von Sherlock Holmes. Das Sherlock Holmes-Korpus besteht aus zwölf Erzählungen, die in dem 1892 erschienenem Band The Adventures of Sherlock Holmes zusammengefasst sind, und die man gemeinfrei unter anderem durch das Internet Archive herunterladen kann. Die für diese Einführung verwendete Fassung wurde zunächst dem Internet Archive entnommen und dann in zwölf Einzeldateien aufgeteilt. Natürlich können die vorgestellten Methoden auf die anderen hier behandelten Korpora angewandt werden – das Beispiel dient nur dazu, sich langsam an quanteda und die Grundlagen der computergestützen Inhaltsanalyse zu gewöhnen ohne das dabei bestimmte Vorkenntnisse vorausgesetzt werden.

Sämtliche in dieser Einführung verwendeter Codebeispiele, Korpora und Lexika können hier heruntergeladen werden.

Installation und Laden der benötigten R-Bibliotheken

Zunächst werden die notwendigen Bibliotheken installiert (sofern noch nicht vorhanden) und anschließend geladen. Zudem wird vorbereitend die Theme-Einstellung für das Paket ggplot gesetzt (dies sorgt für hübschere Plots). Diesen Schritt wiederholen wir zu Beginn jedes Kapitels, daher wird auf ihn später nicht mehr weiter eingegangen. In einigen Kapiteln werden noch weiteren Pakete gelanden, etwa für eine erweiterte Farbpalette (RColorBrewer), Wortwolken (wordcloud) oder um URLs zu parsen (urltools).

if(!require("quanteda")) {install.packages("quanteda"); library("quanteda")}
if(!require("readtext")) {install.packages("readtext"); library("readtext")}
if(!require("tidyverse")) {install.packages("tidyverse"); library("tidyverse")}
if(!require("RColorBrewer")) {install.packages("RColorBrewer"); library("RColorBrewer")}
theme_set(theme_minimal())

Erste Gehversuche mit Quanteda

Wenn alle notwendigen Pakete erfolgreich geladen wurden, können wir einen ersten Gehversuch mit Quanteda unternehmen. Typischerweise wird ein Korpus (also eine Sammlung von Texten und zugehörigen Metadaten) erstellt, indem Dateien von der Festplatte oder aus dem Internet eingelesen werden, die dann den Korpusinhalt bilden. Im folgenden ersten Schritt halten wir es noch etwas einfacher und arbeiten mit einem synthethischen Beispiel. Dazu lesen wir lediglich drei Beispielsätze ein in R ein und erstellen daraus einen Korpus.

beispielsaetze <- c("Ein Hund kam in die Küche",
                    "In der Küche gibt es noch Kaffee",
                    "Im Kaffee fehlt noch die Milch")

Das Objekt beispielsaetze enthält aus Sicht von Quanteda drei Texte, aus denen sich mit der gleichnamigen Funktion problemlos ein Objekt vom Typ corpus erstellen lässt.

beispielkorpus <- corpus(beispielsaetze)
beispielkorpus
Corpus consisting of 3 documents.
text1 :
"Ein Hund kam in die Küche"

text2 :
"In der Küche gibt es noch Kaffee"

text3 :
"Im Kaffee fehlt noch die Milch"

Die Meldung bestätigt uns genau das – wir haben erfolgreich einen Korpus aus drei Texten erstellt. Derzeit enthält unser Korpus noch keinerlei Metadaten, also Angaben zu den im Korpus enthaltenen Texten. In der Terminologie von Quanteda werden Metadaten auch als Dokument-Variablen (docvars) bezeichnet.

Die in einem Korpus enthaltenen Texte lassen sich jedenzeit mit dem Befehl texts ausgeben.

texts(beispielkorpus)
                             text1                              text2 
       "Ein Hund kam in die Küche" "In der Küche gibt es noch Kaffee" 
                             text3 
  "Im Kaffee fehlt noch die Milch" 

Nun fügen wir unserem Korpus mit Hilfe des Befehls docvars eine Dokument-Variable hinzu. Sinn und Zweck dieser Variablen ist die (später noch wichtige) Zuordnung von Metadaten zu dem erfassten Textmaterial.

docvars(beispielkorpus, "Text_Autor") <- c("Paul", "Marie", "Paul")

Wieso ist die Erstellung eines Korpus und das Anlegen von Dokument-Variablen sinnvoll? Eine Zusammenfassung unseres Beispielkorpus gibt einen ersten Aufschluss.

summary(beispielkorpus)
Corpus consisting of 3 documents, showing 3 documents:

  Text Types Tokens Sentences Text_Autor
 text1     6      6         1       Paul
 text2     7      7         1      Marie
 text3     6      6         1       Paul

Die Zusammenfassung zeigt die im Korpus enthaltenden Texte, die jeweils einen einmaligen Bezeichner erhalten (text1, text2, text3). Weiterhin gibt sie Aufschluss über die Anzahl der Tokens (laufende Wörter), die Anzahl der Types (einmalige Wörter), und die Anzahl der Sätze pro Text. Schließlich wird noch die gerade von uns eingeführte Dokument-Variable Text_Autor widergegeben.

Auch wenn das zunächst nicht weiter wichtig erscheinen mag: Die Möglichkeit, eine große Anzahl von Texten aus unterschiedlichen Quellen zu einem Korpus-Objekt zusammenzufassen und dieses anschließend mit Metadaten zu versehen, ist ein ganz entscheidender Vorteil von Quanteda. Wir werden folgend mit deutlich größeren Korpora arbeiten, die alle nach diesem Prinzip erstellt wurden.

Einlesen der Daten und Anlegen des Sherlock Holmes-Korpus

Nachdem wir einen ersten Einblick in die Erstellung eines sehr simplen Quanteda-Korpus erhalten haben, können wir nun die Sherlock Holmes-Romane einlesen und daraus ebenfalls einen Korpus erstellen, nun aber in einer realistischen Größe.

Für das Einlesen der Plaintext-Dateien wird die Funktion readtext aus dem gleichnamigen Paket verwendet, durch die sich eine Reihe von Dateiformaten erfolgreich importieren lassen (u.a. TXT, PDF und Word). Grundsätzlich sich Plaintext–Daten (i.d.R. mit der Endung “.txt”) und Daten in Tabellenform (etwa im Format CSV oder auch als Excel–Datei) für readtext ohne größere Probleme lesbar, allerdings muss man beim Einlesen erklären, wie genau die einzelnen Datensätze von einander getrennt sind (bei Plaintext–Dateien wo nicht 1 Datei == 1 Text, was etwa bei Exporten aus Lexis Nexis der Fall sein kann), bzw. welche Felder die Primär– und welche Metadaten beinhalten (bei Tabellen). Eine gute Einführung zum Paket readtext findet sich hier.

daten.sherlock <- readtext("daten/sherlock/romane/*.txt") 
daten.sherlock$doc_id <- str_sub(daten.sherlock$doc_id, start = 4, end = -5)

In diesem Fall entspricht jede Datei einem Text (nicht wie zuvor, einen einzigen Satz), wodurch der Import sehr umkompliziert ausfällt. Zunächst laden wir nur Dateien mit der Endung “.txt” aus dem Verzeichnis daten/sherlock/romane. Dann ziehen wir die Namen der Romane aus den Dateinamen, um diese später in Plot–Beschriftungen verwenden zu können.

korpus <- corpus(daten.sherlock, docid_field = "doc_id") 
docvars(korpus, "Textnummer") <- 1:12
korpus
Corpus consisting of 12 documents and 1 docvar.
A Scandal in Bohemia :
"A Scandal in Bohemia   To Sherlock Holmes she is always the ..."

The Red-headed League :
"The Red-headed League   I had called upon my friend, Mr. She..."

A Case of Identity :
"A Case of Identity   "My dear fellow." said Sherlock Holmes ..."

The Boscombe Valley Mystery :
"The Boscombe Valley Mystery   We were seated at breakfast on..."

The Five Orange Pips :
"The Five Orange Pips   When I glance over my notes and recor..."

The Man with the Twisted Lip :
"The Man with the Twisted Lip   Isa Whitney, brother of the l..."

[ reached max_ndoc ... 6 more documents ]

Nun generieren wir wieder mit dem Befehl corpus ein Quanteda-Korpus-Objekt. Im Gegensatz zu unserem Beispielkorpus bilden in diesem Fall die mittels readtext zuvor eingelesenen Datein die Grundlage. Die Funktion corpus versteht mehrere Datenformate, also Character-Vektoren (unser erstes Beispiel) ebenso wie Data Frames (wie mittels readtext erstellt) oder Objekte aus dem Paket tm. Zweitens wird eine Dokument-Variable Textnummer generiert, die wir ebenfalls später noch gebrauchen können. Zum Schluss wird die Variable korpus aufgerufen, was uns die wichtigen Eckdaten zum Korpus sowie Metadaten zu den enthaltenden Texten zurückliefert.

In den folgenden Abschnitten werden häufig bereits vorbereitete Korpora geladen, d.h. der Befehl corpus wird hier nicht mehr explizit ausgeführt. Er ist aber im Vorfeld ausgeführt worden, um aus Textdatein auf der Festplatte oder Twitter-Daten in einem R-Data Frame einen Quanteda-Korpus zu erstellen.

Die Funktionen ndoc, ntoken, ntype und nsentence geben die Anzahl der Dokumente, Tokens, Types und Sätze aus. Diese Statistiken können bequem gemeinsam mit Metadaten auf Dokumentebene durch die Funktion summary erstellt werden. Bei den meisten Korpora, die hier verwendet werden, liegt ein solcher Data Frame mit Statistiken zu jedem Text bereits bei. Notwendig ist dies allerdings nicht. Will man auf Korpus–Metadaten zurückgreifen oder diese verändern, kann man dies jederzeit über den Befehl docvars tun.

korpus.stats <- summary(korpus)
korpus.stats$Text <- reorder(korpus.stats$Text, 1:12, order = T)
korpus.stats
Corpus consisting of 12 documents, showing 12 documents:

                                  Text Types Tokens Sentences Textnummer
                  A Scandal in Bohemia  2153  10475       674          1
                 The Red-headed League  2091  11073       577          2
                    A Case of Identity  1758   8456       399          3
           The Boscombe Valley Mystery  2103  11453       636          4
                  The Five Orange Pips  1929   8833       476          5
          The Man with the Twisted Lip  2178  11109       616          6
   The Adventure of the Blue Carbuncle  1930   9611       553          7
    The Adventure of the Speckled Band  2243  11737       634          8
 The Adventure of the Engineer's Thumb  1976   9960       512          9
   The Adventure of the Noble Bachelor  1950   9930       580         10
    The Adventure of the Beryl Coronet  1997  11620       626         11
   The Adventure of the Copper Beeches  2119  11962       621         12

Manchmal ist es bei der Erstellung von Korpus-Zusammenfassungen sinnvoll, das optionale Funktionsargument n = 1000000 zu verwenden, weil die Funktion summary ansonsten nur maximal 100 Texte zusammenfasst. In diesem Fall reicht das zwar aus, aber bei größeren Datensätzen ist diese Beschränkung eher unpraktisch. Technisch gesehen heißt diese Funktion summary.corpus und ist eine an Korpus-Objekte angepasste Variante der Basisfunktion summary, die auch sonst in R verwendet wird. Der Befehl reorder wird verwendet, um die Texte auch in Plots nach ihrer Reihenfolge in The Adventures of Sherlock Holmes zu sortieren, statt alphabetisch nach Titel.

Der Inhalt der Variable korpus.stats kann natürlich auch geplottet werden, um einen anschaulichen Eindruck von der Korpusbeschaffenheit zu geben. Die folgenden Zeilen liefern die Anzahl der Tokens (laufende Wörter), die Anzahl der Types (einmalige Wörter), und Sätze pro Roman zurück (vgl. dazu diese Einführung). Schließlich wird noch das Verhältnis von Typen zu Tokens (oder die sog. Typ-Token-Relation) geplottet.

Grundlage solcher Plots sind praktisch immer Data Frame-Objekte (also Tabellen), die Informationen über Korpora, Texte, Wörter, Themen usw. enthalten, welche sich visuell darstellen lassen. Im Rest dieser Einführung gehe ich nicht im Detail darauf ein, wie die jeweiligen Plots genau konstruiert werden, allerdings lassen sich die meisten Daten auch (etwas weniger ansprechend) mit der R-internen Funktion plot darstellen. Eine hilfreiche deutschsprachige Einführung in das Plotten mit ggplot2 findet sich hier. Viele der hier vorgestellten Plots stammen zudem direkt aus quanteda (beginnend mit textplot_).

ggplot(korpus.stats, aes(Text, Tokens, group = 1)) + 
  geom_line() + 
  geom_point() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) +
  ggtitle("Tokens pro Roman")

ggplot(korpus.stats, aes(Text, Types, group = 1)) + 
  geom_line() + geom_point() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) +
  ggtitle("Types pro Roman")

ggplot(korpus.stats, aes(Text, Sentences, group = 1)) + 
  geom_line() + 
  geom_point() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) +
  ggtitle("Sätze pro Roman")

ggplot(korpus.stats, aes(Tokens, Types, group = 1, label = Textnummer)) +
  geom_smooth(method = "lm", formula = "y ~ x", se = FALSE) + 
  geom_text(check_overlap = T) + 
  ggtitle("Typ-Token-Relation pro Roman")

Diese Grafiken sind zunächst einmal nicht umwerfend informativ. Sie belegen lediglich, dass die Erzählungen ‘A Case of Identity’ und (in geringerem Maße) ‘The Five Orangen Pips’ deutlich kürzer sind als die anderen Texte, was sich auf allen drei Ebenen (Tokens, Types, Sätze) niederschlägt. Etwas interessanter wird es allerdings bei der Typ-Token-Relation: während drei Romane (mit den Nummern 3, 11 und 12) jeweils einen eher unterdurchschnittlichen TTR aufweisen, liegen weitere vier oberhalb der linearen Relation (1, 5, 6 und 8), während die verbleibenden sechs ziemlich genau dem Durchschnitt entsprechen. Über den TTR lassen sich Rückschlüssen über die Informationsdichte ziehen – dazu später noch mehr.

Mit Korpora arbeiten

Korpora lassen sich in quanteda sehr leicht samplen, umformen und mit zusätzlichen Metadaten versehen. Metadaten können wiederum genutzt werden, um das Korpus nach bestimmten Kriterien zu filtern. Der folgenden Aufruf zeit die ersten 1.000 Wörter des ersten Romans.

str_sub(korpus[2], start = 1, end = 1000)
[1] "The Red-headed League\n\n  I had called upon my friend, Mr. Sherlock Holmes, one day in\nthe autumn of last year and found him in deep conversation with\na very stout, florid-faced, elderly gentleman with fiery red hair.\nWith an apology for my intrusion, I was about to withdraw when\nHolmes pulled me abruptly into the room and closed the door\nbehind me.\n  \"You could not possibly have come at a better time, my dear\nWatson,\" he said cordially.\n  \"I was afraid that you were engaged.\"\n  \"So I am. Very much so.\"\n  \"Then I can wait in the next room.\"\n  \"Not at all. This gentleman, Mr. Wilson, has been my partner\nand helper in many of my most successful cases, and I have no\ndoubt that he will be of the utmost use to me in yours also.\"\n  The stout gentleman half rose from his chair and gave a bob of\ngreeting, with a quick little questioning glance from his small\nfat-encircled eyes.\n  \"Try the settee,\" said Holmes, relapsing into his armchair\nand putting his fingertips together, as was his custom wh"

Jeder Text lässt sich also auch direkt anhand seiner Indizierung aufrufen (etwa korpus[1] für den ersten Text), neben dem Aufruf über die Funktion texts. Texte könne auf diesem Weg auch ersetzt werden, ganz so, wie man die Elemente eines Vektors oder Data Frames überschreiben kann.

Mittels corpus_reshape lässt sich ein Korpus so umformen, dass jeder Satz ein eigenes Dokument ergibt. Alternative Argumente sind “paragraphs” und “documents” (so lässt sich ein Satz-Korpus wieder in seinen Anfangszustand zurückversetzen). Die Erstellung von Satz-Korpora ist für die Sentimentanalyse und das überwachte maschinelle Lernen von Interesse.

Die Beschriftung des Beispiels besteht hier aus der Variable docname und einer angehängten Zahl (eine 1 für den ersten Satz).

korpus.saetze <- corpus_reshape(korpus, to = "sentences")
korpus.saetze[100]
Corpus consisting of 1 document and 1 docvar.
A Scandal in Bohemia :
"Let us glance at our Continental Gazetteer.""

Mit corpus_sample kann weiterhin ein zufälliges Sample aus einem Korpus gezogen werden. Wir wenden die Funktion hier auf das Satz-Korpus an.

zufallssatz <- corpus_sample(korpus.saetze, size = 1)
zufallssatz[1]
Corpus consisting of 1 document and 1 docvar.
The Red-headed League :
"It is most refreshingly unusual."

Anhand von corpus_subset kann ein Korpus schließlich nach Metadaten gefiltert werden.

Um dies tun zu können, erstellen wir im nächsten Schritt eine binäre Dokument-Variable mit dem Namen LangerSatz, die dann TRUE ist, wenn ein Satz >= 25 Tokens enthält. So lässt sich mit corpus_subset ein Teilkorpus zu bilden, in dem nur längere Sätze enthalten sind. Das Beispiel soll verdeutlichen, dass mithilfe der von Quanteda bereitgestellten Funktionen zahlreiche Schritte für die Bereinigung von Korpora möglich sind.

docvars(korpus.saetze, "LangerSatz") <- ntoken(korpus.saetze)>=25
korpus.saetze_lang <- corpus_subset(korpus.saetze, LangerSatz == TRUE)
korpus.saetze_lang[1:3]
Corpus consisting of 3 documents and 2 docvars.
A Scandal in Bohemia.1 :
"He was, I take it, the most perfect reasoning and observing ..."

A Scandal in Bohemia.2 :
"But for the trained teasoner to admit such intrusions into h..."

A Scandal in Bohemia.3 :
"Grit in a sensitive instrument, or a crack in one of his own..."

Die Möglichkeit, mit bestehenden (bspw. Autor, Quelle, Rubrik, Zeitstempel) und eigens erstellten Dokument-Variablen (bspw. Thema, Sentiment) zu arbeiten, gehört wie schon erwähnt zu den größten Stärken von Quanteda, weil diese Angaben zu jedem Zeitpunkt in der Analyse zur Verfügung stehen. Folgend filtern oder gruppieren wir Korpora häufig auf der Grundlage von Metadaten.

Schließlich lassen sich Korpora mit Hilfe von corpus_segment auch nach bestimmten Kriterien aufspalten und mittels corpus_trim nach festen Bedingungen reduzieren, um etwa sehr kurze Texte auszuschließen. Im folgenden Beispiel wird eine ganze Kette von Operationen (Segmentierung, Trimmen, Zufallssample ziehen) so durchgeführt. Dabei werden auch erstmals die für das tidyverse typischen pipes verwendet. Bei pipes handelt es sich um eine Art spezielle Art der R-Syntax, die das schrittweise Umformen von Daten stark erleichtert. Im Verlauf dieser Einführung werden pipes stark genutzt, vor allem, um Ergebnisse für die Darstellung mittels ggplot vorzubereiten.

zufallsabsatz <- corpus_segment(korpus, "  ") %>% 
  corpus_trim(what = "documents", min_ntoken = 3) %>% 
  corpus_sample(size = 1)
zufallsabsatz[1]
Corpus consisting of 1 document and 2 docvars.
The Adventure of the Noble Bachelor :
"The Lord St. Simon marriage, and its curious termination, ha..."

Tokenisierung

Unter Tokensierung versteht man die Aufspaltung eines Textes in laufende Wörter oder sog. N-Gramme, also Sequenzen mehrerer Wörter in Folge. Die Funktion tokens realisiert die Tokenisierung eines Korpus in quanteda. Zusätzlich versteht tokens auch unzählige Argumente für die Entfernung bestimmter Features.

meine.tokens <- tokens(korpus, verbose = TRUE)
Creating a tokens object from a corpus input...
 ...starting tokenization
 ...A Scandal in Bohemia to The Adventure of the Copper Beeches
 ...preserving hyphens
 ...preserving social media tags (#, @)
 ...segmenting into words
 ...9,397 unique types
 ...removing separators 
 ...complete, elapsed time: 0.29 seconds.
Finished constructing tokens from 12 documents.
head(as.list(meine.tokens)$`A Scandal in Bohemia`, 20)
 [1] "A"        "Scandal"  "in"       "Bohemia"  "To"       "Sherlock" "Holmes"  
 [8] "she"      "is"       "always"   "the"      "woman"    "."        "I"       
[15] "have"     "seldom"   "heard"    "him"      "mention"  "her"     

Mittels der Funktion tokens lässt sich der Text über das Argument ngrams auch gleich in N-Gramme (Mehrwortsequenzen) aufspalten. Im folgenden Beispiel werden erst Bigramme vom Anfang des ersten Textes angezeigt, und dann alle Sequenzen von einem, zwei oder drei Begriffen extrahiert (durch die Anwendung von head sehen wir nur Trigramme, es sind aber auch kürzere Sequenzen vorhanden).

meine.ngrams <- tokens_ngrams(meine.tokens, n = 2)
sample(as.list(meine.ngrams)$`A Scandal in Bohemia`, 5)
[1] "there_was"    "eyes_of"      "man_."        "am_lost"      "understand_,"
meine.ngrams <- tokens_ngrams(meine.tokens, n = 1:3)
sample(as.list(meine.ngrams)$`A Scandal in Bohemia`, 5)
[1] "on_the_other"           "anything_which_you"     "to"                    
[4] "conduct"                "bridegroom_from_having"

Hilfreich ist auch die Möglichkeit, bei der Tokenisierung bestimmte Begriffe zu entfernen oder zurückzubehalten.

meine.tokens <- tokens(korpus)
begriffe.behalten <- tokens_select(meine.tokens, c("holmes", "watson")) 
head(as.list(begriffe.behalten)$`A Scandal in Bohemia`)
[1] "Holmes" "Holmes" "Holmes" "Holmes" "Watson" "Watson"
begriffe.entfernen <- tokens_remove(meine.tokens, c("sherlock", "holmes", "in", "is", "the", "a"))
head(as.list(begriffe.entfernen)$`A Scandal in Bohemia`)
[1] "Scandal" "Bohemia" "To"      "she"     "always"  "woman"  

Wie bereits angedeutet akzeptiert die Funktion tokens eine Reihe von Argumenten, mit denen ganze Klassen von Zeichenketten (Zahlen, Interpunktion, Symbole usw.) gezielt ausgeschlossen oder zurückbehalten werden können. Folgend werden zunächst Zahlen, Interpunktion und Symbole entfernt, dann mittels tokens_tolower alle Wörter in Kleinschreibung umgewandelt und dann dann noch die Wörter “sherlock” und “holmes”, sowie eine Reihe englischer Stoppwörter entfernt. Wir verwenden hier dplyr-Syntax, wenden also nacheinander eine Reihe von Umformungen auf das gleich Objekt an.

meine.tokens <- korpus %>% 
  tokens(remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE) %>% 
  tokens_tolower() %>% 
  tokens_remove(c(stopwords("english"), "sherlock", "holmes"))
head(as.list(meine.tokens)$`A Scandal in Bohemia`)
[1] "scandal" "bohemia" "always"  "woman"   "seldom"  "heard"  

Das Resultat ist der Art von Daten, mit denen man bei Verfahren wie der Anwendung von Lexika (Kapitel 2-3), dem überwachten maschinellen Lernen (Kapitel 4) und der Berechnung von Themenmodellen (Kapitel 5), häufig arbeitet sehr ähnlich. Durch die Stoppwortentfernung und andere Schritte gehen syntaktische Informationen verloren, d.h. man kann nicht mehr nachvollziehen, wer was mit wem tut, oder wie der Text insgesamt argumentativ oder erzählerisch aufgebaut ist. Diese Informationen sind allerdings im “Bag-of-Words-Ansatz”, der in der automatisierten Inhaltsanalyse nahezu immer verwendet wird, nicht unbedingt relevant.

Die in diesem Abschnitt beschriebenen Schritte sind zwar im Einzelfall nützlich, werden aber in den folgenden Kapitel praktisch nicht angewandt, weil die Daten dort schon als quanteda–Korpora vorliegen, und weil zudem häufig auch bis auf die Anwendung der Funktion corpus keine weiteren Schritte notwendig sind. Die Tokenisierung wird zudem implizit angewandt, sobald eine Dokument-Feature-Matrize (DFM, s.u.) erstellt wird.

Dokument-Feature-Matrizen (DFMs) erstellen

Wir kommen nun zu einer zentralen Datenstruktur von quanteda, die im Gegensatz zu den zuvor vorgestellten Funktionen praktisch in jedem Projekt vorkommt: die Document Feature-Matrize (DFM). Üblicherweise wird direkt nachdem ein Korpus angelegt wurde eine DFM berechnet, zuweilen auch mehrere. Eine DFM ist eine Tabelle, deren Zeilen Texte und deren Spalten Wortfrequenzen enhalten. Dabei gehen Informationen darüber, wo in einem Text ein Wort vorkommt verloren (man spricht auch vom ‘Bag-of-Words-Ansatz’ und davon, dass DFMs im Gegensatz zum eigentlichen Korpus nicht-positional sind). Immer dann, wenn wir uns für die Beziehung von Wörtern zu Texten (und umgekehrt) interessieren, berechnen wir eine DFM.

Im DFMs zu verstehen hilft es, noch einmal zu unserem künstlichen Beispielkorpus vom Anfang zurückzukehren. Eine DFM wird surprise, surprise mit Hilfe der Funktion dfm erstellt. Grundlage sollte immer ein Quanteda-Korpus-Objekt sein, auch wenn theoretisch auch Tokens-Objekte akzeptiert werden. Das Beispiel zeigt, was mit den Texten im Korpus geschieht, wenn diese in eine DFM überführt werden.

beispielsaetze <- c("Ein Hund kam in die Küche",
                    "In der Küche gibt es noch Kaffee",
                    "Im Kaffee fehlt noch die Milch")
beispielkorpus <- corpus(beispielsaetze)
beispieldfm <- dfm(beispielkorpus)
beispieldfm
Document-feature matrix of: 3 documents, 14 features (54.8% sparse).
       features
docs    ein hund kam in die küche der gibt es noch
  text1   1    1   1  1   1     1   0    0  0    0
  text2   0    0   0  1   0     1   1    1  1    1
  text3   0    0   0  0   1     0   0    0  0    1
[ reached max_nfeat ... 4 more features ]

Die Funktion liefert uns eine Tabelle (oder genauer: ein R-Objekt vom Typ Matrix), in der die Zeilen die im Korpus enthaltenen Dokumente und die Spalten die im Korpus enthaltenen Wörter widergeben. Die Zahlen in den Zellen geben die Worthhäufigkeit an – in unserem simplen Beispiel kommt jedes Wort pro Text nur maximal einmal vor, das ist aber in einem richtigen Korpus durchweg anders. Die Anordung der Spalten folgt zwar dem Vorkommen der Wörter im Korpus, allerdings taucht jedes Feature nur einmal auf, da es sich ja um Summenangaben handelt. Bei der sog. Sparsity (“54.8% sparse”) handelt es sich um den Prozentsatz der Zellen, in denen eine Null steht – dazu später noch etwas mehr.

Wieso heißt es bei Quanteda DFM, wird also von Features statt von Wörtern oder Termen gesprochen? Weil die Features auch Zahlen, Satzzeichen oder Emjois sein können. Auch Sequenzen aus mehreren Tokens (sog. N-Gramme) sind Features.

Was hat sich gegenüber dem Korpus (in dem wir ja auch schon die Typ- und Tokenanzahl pro Text berechnen konnten) überhaupt verändert? Eine DFM ist nicht-positional, d.h. die Information darüber, wo im Text ein Wort vorkommt, ist im Gegensatz zum Korpus nicht mehr vorhanden. Dafür haben DFMs jedoch einen entscheinden Vorteil: Sie erlauben es uns, die Dokumente (anhand eines Dokumentenvektors) und die Features (anhand eines Featurevektors) miteinander zu vergleichen. Im nächsten Kapitel werden wir uns diesem Umstand zunutze machen, um Dokumente und Wörter miteinander zu vergleichen.

Wir wenden das, was wir gerade anhand des Beispielkorpus getestet haben, nun auf das Sherlock Holmes-Korpus an. Dabei nutzen wir realistischere Einstellungen für die Tokenisierung, entfernen also Zahlen, Interpunktion, Symbole und Stoppwörter.

meine.dfm <- dfm(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, remove = stopwords("english"))
meine.dfm
Document-feature matrix of: 12 documents, 8,588 features (79.3% sparse) and 1 docvar.
                              features
docs                           scandal bohemia sherlock holmes always woman seldom heard
  A Scandal in Bohemia               4       8       11     47      5    12      3     8
  The Red-headed League              0       0       10     51      5     0      0    15
  A Case of Identity                 0       2        7     46      7    10      0     5
  The Boscombe Valley Mystery        1       0       10     43      5     1      0    10
  The Five Orange Pips               1       0       10     25      5     1      0     5
  The Man with the Twisted Lip       0       0       10     28      4     5      0     8
                              features
docs                           mention name
  A Scandal in Bohemia               1    6
  The Red-headed League              0    6
  A Case of Identity                 0    1
  The Boscombe Valley Mystery        0    3
  The Five Orange Pips               1    5
  The Man with the Twisted Lip       0    4
[ reached max_ndoc ... 6 more documents, reached max_nfeat ... 8,578 more features ]

Wer sich fragt, wo diese Argumente für dfm eigentlich herkommen: Hier wird implizit der uns schon vertraute Befehl tokens angewandt, um bestimmte Features zu entfernen (oder ggf. auch zurückzubehalten). Ergebnis ist eine deutlich größere Matrix als zuvor, die auch eine deutlich höhere Sparsity aufweist. Folgend tokenisieren wir praktisch nie, ohne auch gleich eine DFM zu berechnen.

Vieles funktioniert bei DFMs analog zur Erstellung eines Korpus. So zählen die Funktionen ndoc und nfeat Dokumente und Features (Wörter).

ndoc(meine.dfm)
[1] 12
nfeat(meine.dfm)
[1] 8588

Mittels der Funktionen docnames und featnames lassen sich die Namen der Dokumente und Features ausgeben.

message("Docnames")
Docnames
head(docnames(meine.dfm)) 
[1] "A Scandal in Bohemia"         "The Red-headed League"       
[3] "A Case of Identity"           "The Boscombe Valley Mystery" 
[5] "The Five Orange Pips"         "The Man with the Twisted Lip"
message("Featnames")
Featnames
head(featnames(meine.dfm))
[1] "scandal"  "bohemia"  "sherlock" "holmes"   "always"   "woman"   

Die tabellarische Ansicht illustriert den Inhalt der DFM als Text-Wort-Matrix am besten. Wie bereits angedeutet, beschreibt die Sparsity (“Spärlichkeit”) einer DFM dabei den Anteil der leeren Zellen, also Wörter, die nur in sehr wenigen Texten vorkommen. Wie sich leicht ableiten lässt, werden DFMs sehr schnell sehr groß. Zum Glück macht sich Quanteda eine Reihe von für den Nutzer unsichtbaren Funktionen aus anderen Paketen zunutze, um diesem Problem zu begegnen.

head(meine.dfm, n = 12, nf = 5)
Document-feature matrix of: 12 documents, 5 features (28.3% sparse) and 1 docvar.
                              features
docs                           scandal bohemia sherlock holmes always
  A Scandal in Bohemia               4       8       11     47      5
  The Red-headed League              0       0       10     51      5
  A Case of Identity                 0       2        7     46      7
  The Boscombe Valley Mystery        1       0       10     43      5
  The Five Orange Pips               1       0       10     25      5
  The Man with the Twisted Lip       0       0       10     28      4
[ reached max_ndoc ... 6 more documents ]

Gleich an den ersten Blick fällt auf, das die Wörter ‘sherlock’ und ‘holmes’ in allen Romanen vorkommen, also sehr wenig distinktiv sind, weshalb wir sie unter Umständen zu den Stoppwörtern für dieses Korpus hinzufügen sollten.

Die Funktion topfeatures, die wie bereits im Zusammenhang mit Korpora kennengelernt haben, zählt auch Features innerhalb einer DFM aus.

topfeatures(meine.dfm)
  said   upon holmes    one    man     mr little    now    see    may 
   485    465    443    372    290    275    269    234    229    197 

Mit DFMs arbeiten

DFMs lassen sich mit dfm_sort leicht nach Dokument- und Feature-Frequenzen sortieren. Das ist in der Praxis unter anderem dann hilfreich, wenn man einen besseren Einblick in den Inhalt einer großen DFM bekommen möchte. Hier lassen wir uns zwölf Dokumente (n = 12) und (nur) fünf Features (nf = 5) ausgeben und sortieren dabei absteigend nach der Feature-Frequenz.

head(dfm_sort(meine.dfm, decreasing = TRUE, margin = "features"), n = 12, nf = 5) 
Document-feature matrix of: 12 documents, 5 features (0.0% sparse) and 1 docvar.
                              features
docs                           said upon holmes one man
  A Scandal in Bohemia           33   25     47  27  23
  The Red-headed League          51   50     51  29  25
  A Case of Identity             45   35     46  17  16
  The Boscombe Valley Mystery    37   42     43  31  41
  The Five Orange Pips           32   47     25  29  19
  The Man with the Twisted Lip   28   54     28  36  30
[ reached max_ndoc ... 6 more documents ]

Weiterhin lassen sich bestimmte Features einer DFM gezielt mittels dfm_select auswählen.

dfm_select(meine.dfm, pattern = "lov*")
Document-feature matrix of: 12 documents, 7 features (67.9% sparse) and 1 docvar.
                              features
docs                           love lover lovely loves loved lovers loving
  A Scandal in Bohemia            5     1      1     1     1      0      0
  The Red-headed League           1     0      0     0     0      0      0
  A Case of Identity              2     0      0     0     0      1      0
  The Boscombe Valley Mystery     1     0      1     0     1      0      0
  The Five Orange Pips            1     0      0     0     0      0      0
  The Man with the Twisted Lip    0     0      0     0     0      0      0
[ reached max_ndoc ... 6 more documents ]

Die Funktion dfm_wordstem reduziert Wörter auf ihre Stammform. Diese Funktion existiert in quanteda derzeit nur für Englisch und ist auch dort nur begrenzt zuverlässig, was die folgende Ausgabe gut illustriert (‘holm’ ist natürlich kein Wortstamm).

meine.dfm.stemmed <- dfm_wordstem(meine.dfm)
topfeatures(meine.dfm.stemmed)
 said  upon  holm   one   man    mr littl   see   now  come 
  485   465   460   383   304   275   269   253   234   207 

Ebenso wie bei Wortfrequenzen in Korpora ist die Gewichtung einer DFM nach relativen Wortfrequenzen und Verfahren wie TF-IDF oftmals sinnvoll. Praktischerweise beherrscht die Quanteda-Funktion dfm_weight eine ganze Reihe von Gewichtungsansätzen um relative Wortfrequenzen zu berechnen.

Zunächst schauen wir uns noch einmal die absoluten Wortfrequenzen für das gesamte Korpus an.

topfeatures(meine.dfm)
  said   upon holmes    one    man     mr little    now    see    may 
   485    465    443    372    290    275    269    234    229    197 

Dann verwenden wir dfm_weight um diese proportional zur Gesamtwortanzahl des Romans zu gewichten.

meine.dfm.proportional <- dfm_weight(meine.dfm, scheme = "prop")
topfeatures(meine.dfm.proportional)
      said       upon     holmes        one        man         mr     little        now 
0.12555092 0.12033928 0.11379305 0.09552194 0.07420396 0.07121706 0.06911393 0.06020163 
       see        may 
0.05895969 0.05060945 

Das ist aber noch nicht alles. Die Gewichtung einer DFM basiert bei bestimmten Gewichtungsmechanismen (propmax, augmented, logave sowie TF-IDF) auf der Wort-Dokument-Relation, weshalb topfeatures() in Kombination mit diesen Gewichtungen merkwürdige Resultate produziert.

Die Gewichtungsansätze Propmax und TF-IDF liefern relevante Wortmetriken, zum Beispiel für die Bestimmung von Stoppwörtern. Propmax skaliert die Worthäufigkeit relativ zum frequentesten Wort (hier ‘holmes’ in ‘A Scandal in Bohemia’).

meine.dfm.propmax <- dfm_weight(meine.dfm, scheme = "propmax")
topfeatures(meine.dfm.propmax[1,])
    holmes       said        one       upon        man        may photograph     street 
 1.0000000  0.7021277  0.5744681  0.5319149  0.4893617  0.4468085  0.4042553  0.3829787 
      know        now 
 0.3829787  0.3617021 

Funktional ähneln sich TF-IDF und der später vorgestellte Keyness-Ansatz – beide finden besonders distinktive Terme. Beim Vergleich von Propmax und TF-IDF wird deutlich, dass die wörter ‘holmes’ und ‘said’ zwar besonders häufig in ‘A Scandal in Bohemia’ vorkommen, aber nicht unbedingt besonders distinktiv sind, weil sie logischerweise im gesamten Korpus frequent sind, während etwa das Wort ‘photograph’ sowohl oft vorkommt, als auch für diesen Roman kennzeichnend ist.

meine.dfm.tfidf <- dfm_tfidf(meine.dfm)
topfeatures(meine.dfm.tfidf[1,])
   majesty     briony photograph      irene      adler     norton      lodge       mask 
 17.266900  11.870994   9.065304   7.826780   7.826780   7.554269   6.622660   5.395906 
   godfrey       king 
  5.395906   5.117510 

Schließlich lässt sich mit dfm_trim noch eine reduzierten Dokument-Feature-Matrix erstellen. Das ist dann sinnvoll, wenn man davon ausgeht, dass beispielsweise nur solche Begriffe eine Rolle spielen, die mindestes X mal im Gesamtkorpus vorkommen. Auch eine Mindestzahl oder ein Maximum an Dokumenten, in denen ein Begriff vorkommen muss oder darf, kann bestimmt werden. Schließlich lassen sich beide Filteroptionen auch proportional anwenden (vgl. Beispiel).

Die erste ‘getrimmte’ DFM enthält lediglich solche Features, die mindestens in 11 Romanen vorkommen, die zweite hingegen solche Features im 95. Häufigkeitsperzentil (=Top 5% aller Features).

message("Getrimmte DFM mit docfreq = 11")
Getrimmte DFM mit docfreq = 11
meine.dfm.trim <- dfm_trim(meine.dfm, min_docfreq = 11)
meine.dfm.trim
Document-feature matrix of: 12 documents, 288 features (3.18% sparse) and 1 docvar.
                              features
docs                           sherlock holmes always heard name eyes whole felt one
  A Scandal in Bohemia               11     47      5     8    6    9     4    2  27
  The Red-headed League              10     51      5    15    6   10     9    4  29
  A Case of Identity                  7     46      7     5    1    6     3    3  17
  The Boscombe Valley Mystery        10     43      5    10    3    6     2    0  31
  The Five Orange Pips               10     25      5     5    5    5     2    2  29
  The Man with the Twisted Lip       10     28      4     8    4   11     3    2  36
                              features
docs                           cold
  A Scandal in Bohemia            2
  The Red-headed League           1
  A Case of Identity              1
  The Boscombe Valley Mystery     1
  The Five Orange Pips            1
  The Man with the Twisted Lip    2
[ reached max_ndoc ... 6 more documents, reached max_nfeat ... 278 more features ]
message("Getrimmte DFM mit min_termfreq = 0.95")
Getrimmte DFM mit min_termfreq = 0.95
meine.dfm.trim <- dfm_trim(meine.dfm, min_termfreq = 0.95, termfreq_type = "quantile")
meine.dfm.trim
Document-feature matrix of: 12 documents, 444 features (14.6% sparse) and 1 docvar.
                              features
docs                           sherlock holmes always woman heard name eyes whole felt
  A Scandal in Bohemia               11     47      5    12     8    6    9     4    2
  The Red-headed League              10     51      5     0    15    6   10     9    4
  A Case of Identity                  7     46      7    10     5    1    6     3    3
  The Boscombe Valley Mystery        10     43      5     1    10    3    6     2    0
  The Five Orange Pips               10     25      5     1     5    5    5     2    2
  The Man with the Twisted Lip       10     28      4     5     8    4   11     3    2
                              features
docs                           one
  A Scandal in Bohemia          27
  The Red-headed League         29
  A Case of Identity            17
  The Boscombe Valley Mystery   31
  The Five Orange Pips          29
  The Man with the Twisted Lip  36
[ reached max_ndoc ... 6 more documents, reached max_nfeat ... 434 more features ]

DFMs visualisieren

DFMs lassen sich schließlich auch grafisch darstellen, etwa als Wortwolke der häufigsten Begriffe.

textplot_wordcloud(meine.dfm, min_size = 1, max_size = 5, max_words = 100)

Interessanter als die Darstellung des Gesamtkorpus ist auch hier der Vergleich. Das folgende Plot zeigt die distinktivsten Begriffe für vier Romane, wobei die Farbe den jeweiligen Roman kennzeichnet. Dass im Plot die Wortgröße nicht die absolute Frequenz anzeigt, sondern die jeweils distinktivsten Begriffe, macht ein solches Plot für den unmittelbaren Vergleich nützlich.

textplot_wordcloud(meine.dfm[3:6,], color = brewer.pal(4, "Set1"), min_size = 0.2, max_size = 4, max_words = 50, comparison = TRUE)

Weiterführende Lektüre

Grundsätzlich muss in diesen Kapitel zwischen den geschildertern grundlegenden Verfahren aus der Computerlinguistik und Informatik einerseits und den konkreten Funktionsumfang von quanteda unterschieden werden.

Techniken wie die Tokenisierung von Korpora, die Normalisierung von Wortfrequenzen, und die Erstellung und Gewichtung von DFMs (oder klassisch “document feature matrices”) ist buchstäblicher Kern der Computerlinguistik, daher sollte es nicht verwundern, dass hier auflagenstarke Standardwerke existieren.

Hauptquelle zu quanteda ist die Projekwebsite, die einerseits eine Reihe von Tutorials zu einzelnen Einsatzbereichen enthält (etwa Sentimentanalyse…), denen die hier aufgeführten Beispiele z.T. sehr ähnlich sind, zum anderen aber auch eine Funktionsregerenz, welche die einzeln Befehle systematisch gruppiert und erklärt. Neben der quanteda github-Seite, über die man die aktuelle Emtwicklerversion des Pakets beziehen kann, ist vor allem das quanteda-Forum zu empfehlen, wo konkrete Fragen vom Programmiererteam beantwortet werden.

LS0tCnRpdGxlOiAiQXV0b21hdGlzaWVydGUgSW5oYWx0c2FuYWx5c2UgbWl0IFIiCmF1dGhvcjogIkNvcm5lbGl1cyBQdXNjaG1hbm4iCnN1YnRpdGxlOiBHcnVuZGxhZ2VuCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCjwhLS0tClRvZG9zCiogWmVpdC1TTUw6IFRpdGVsIGR1cmNoIFZvbGx0ZXh0ZSBlcnNldHplbj8KKiBWZXJ3ZW5kdW5nIGVpbmVzIHNlaHIgZWluZmFjaGVuIHN5bnRoZXRoaXNjaGVuIERhdGVuc2F0emVzCiogTWRCLVNNTDogU3BsaXQgaW4gVHJhaW5ncy0gdW5kIFRlc3RzZXQ/CiogCi0tPgoKRGllc2VzIGVyc3RlIEthcGl0ZWwgbGllZmVydCBlaW5lbiDDnGJlcmJsaWNrIMO8YmVyIHphaGxyZWljaGUgR3J1bmRmdW5rdGlvbmVuIGRlcyBQYWtldHMgW1F1YW50ZWRhXShodHRwczovL3F1YW50ZWRhLmlvLyksIGRpZSBnbGVpY2h6ZWl0aWcgZGllIEJhc2lzIGRlciBhdXRvbWF0aXNpZXJ0ZW4gSW5oYWx0c2FuYWx5c2UgbWl0IFtSXShodHRwczovL3d3dy5yLXByb2plY3Qub3JnLykgYmlsZGVuLiDDnGJlciBRdWFudGVkYSBoaW5hdXMgd2VyZGVuIGltIFZlcmxhdWYgZGllc2VyIG5ldW50ZWlsaWdlbiBFaW5mw7xocnVuZyBub2NoIGVpbmUgUmVpaGUgd2VpdGVyZXIgUi1CaWJsaW90aGVrZW4gdmVyd2VuZGV0LCBldHdhIGbDvHIgZGFzIFvDvGJlcndhY2h0ZSBtYXNjaGluZWxsZSBMZXJuZW5dKG1hc2NoaW5lbGxlc19sZXJuZW4uaHRtbCkgKEthcGl0ZWwgNSkgdW5kIGRhcyBCZXJlY2huZW4gdm9uIFtUaGVtZW5tb2RlbGxlbl0odGhlbWVubW9kZWxsZS5odG1sKSAoS2FwaXRlbCA2KS4gSW4gcHJha3Rpc2NoIGplZGVyIEVpbmhlaXQgcmVsZXZhbnQgc2luZCBkYWJlaSBkaWUgUGFrZXRlIGRlcyBbdGlkeXZlcnNlXShodHRwczovL3d3dy50aWR5dmVyc2Uub3JnLykgKHZvciBhbGxlbSBbZ2dwbG90XShodHRwczovL2dncGxvdDIudGlkeXZlcnNlLm9yZy8pLCBbZHBseXJdKGh0dHBzOi8vZHBseXIudGlkeXZlcnNlLm9yZy8pIHVuZCBbc3RyaW5ncl0oaHR0cHM6Ly9zdHJpbmdyLnRpZHl2ZXJzZS5vcmcvKSksIGR1cmNoIGRpZSB6YWhscmVpY2hlIEZ1bmt0aW9uZW4gd2llIFBsb3R0ZW4sIFRleHR2ZXJhcmJlaXR1bmcgdW5kIERhdGVubWFuYWdlbWVudCBnZWdlbsO8YmVyIGRlbiBSLUJhc2lzZnVua3Rpb25lbiBzdGFyayB2ZXJiZXNzZXJ0IHdlcmRlbi4gUGFrZXRlIGbDvHIgZWluemVsbmUgVGVpbGJlcmVpY2hlLCBkaWUgZXJzdCBzcMOkdGVyIGVpbmUgUm9sbGUgc3BpZWxlbiB3ZXJkZW4sIHNpbmQgdS5hLiBbdG9waWNtb2RlbHNdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3BhY2thZ2U9dG9waWNtb2RlbHMpIHVuZCBbc3RtXShodHRwczovL3d3dy5zdHJ1Y3R1cmFsdG9waWNtb2RlbC5jb20vKSAoVGhlbWVubW9kZWxsZSksIFtSVGV4dFRvb2xzXShodHRwOi8vd3d3LnJ0ZXh0dG9vbHMuY29tLykgdW5kIFtrZXJhc10oaHR0cHM6Ly9rZXJhcy5yc3R1ZGlvLmNvbS8pICjDvGJlcndhY2h0ZXMgbWFzY2hpbmVsbGVzIExlcm5lbiksIFtzcGFjeXJdKGh0dHBzOi8vZ2l0aHViLmNvbS9xdWFudGVkYS9zcGFjeXIpIHVuZCBbdWRwaXBlXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPXVkcGlwZSkgKFBPUy1UYWdnaW5nIHVuZCBOYW1lZC1FbnRpdHktRXJrZW5udW5nKSBzb3dpZSBbZ29vZ2xlbmxwXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPWdvb2dsZW5scCkgYnp3LiBbZ29vZ2xlTGFuZ3VhZ2VSXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPWdvb2dsZUxhbmd1YWdlUikgKHdlaXRlcmUgQW5ub3RhdGlvbnMtIHVuZCDDnGJlcnNldHp1bmdmdW5rdGlvbmVuKS4KCkRpZSBCYXNpcyBkZXIgQW5hbHlzZSBpbiBkaWVzZW0gZXJzdGVuIEthcGl0ZWwgYmlsZGV0IGVpbmVyc2VpdHMgZWluZSBTYW1tbHVuZyBlaW5mYWNoZXIgQmVpc3BpZWxzw6R0emUsIGFuaGFuZCBkZXJlciBzaWNoIGRpZSBHcnVuZGZ1bmt0aW9uZW4gdm9uIFF1YW50ZWRhIGd1dCBlcmzDpHV0ZXJuIGxhc3NlbiwgdW5kIGFuZGVyZXJzZWl0cyBkaWUgYmVsaWVidGVuIERldGVrdGl2Z2VzY2hpY2h0ZW4gdm9uIFNoZXJsb2NrIEhvbG1lcy4gRGFzIFNoZXJsb2NrIEhvbG1lcy1Lb3JwdXMgYmVzdGVodCBhdXMgenfDtmxmIEVyesOkaGx1bmdlbiwgZGllIGluIGRlbSAxODkyIGVyc2NoaWVuZW5lbSBCYW5kICpUaGUgQWR2ZW50dXJlcyBvZiBTaGVybG9jayBIb2xtZXMqIHp1c2FtbWVuZ2VmYXNzdCBzaW5kLCB1bmQgZGllIG1hbiBnZW1laW5mcmVpIHVudGVyIGFuZGVyZW0gZHVyY2ggZGFzIFtJbnRlcm5ldCBBcmNoaXZlXShodHRwczovL2FyY2hpdmUub3JnLykgaGVydW50ZXJsYWRlbiBrYW5uLiBEaWUgZsO8ciBkaWVzZSBFaW5mw7xocnVuZyB2ZXJ3ZW5kZXRlIEZhc3N1bmcgd3VyZGUgenVuw6RjaHN0IGRlbSBJbnRlcm5ldCBBcmNoaXZlIGVudG5vbW1lbiB1bmQgZGFubiBpbiB6d8O2bGYgRWluemVsZGF0ZWllbiBhdWZnZXRlaWx0LiBOYXTDvHJsaWNoIGvDtm5uZW4gZGllIHZvcmdlc3RlbGx0ZW4gTWV0aG9kZW4gYXVmIGRpZSBhbmRlcmVuIGhpZXIgYmVoYW5kZWx0ZW4gS29ycG9yYSBhbmdld2FuZHQgd2VyZGVuIC0tIGRhcyBCZWlzcGllbCBkaWVudCBudXIgZGF6dSwgc2ljaCBsYW5nc2FtIGFuIHF1YW50ZWRhIHVuZCBkaWUgR3J1bmRsYWdlbiBkZXIgY29tcHV0ZXJnZXN0w7x0emVuIEluaGFsdHNhbmFseXNlIHp1IGdld8O2aG5lbiBvaG5lIGRhcyBkYWJlaSBiZXN0aW1tdGUgVm9ya2VubnRuaXNzZSB2b3JhdXNnZXNldHp0IHdlcmRlbi4KCioqU8OkbXRsaWNoZSBpbiBkaWVzZXIgRWluZsO8aHJ1bmcgdmVyd2VuZGV0ZXIgQ29kZWJlaXNwaWVsZSwgS29ycG9yYSB1bmQgTGV4aWthIGvDtm5uZW4gW2hpZXJdKGluaGFsdHNhbmFseXNlX21pdF9yLnppcCkgaGVydW50ZXJnZWxhZGVuIHdlcmRlbi4qKgoKCiMjIyMgSW5zdGFsbGF0aW9uIHVuZCBMYWRlbiBkZXIgYmVuw7Z0aWd0ZW4gUi1CaWJsaW90aGVrZW4KClp1bsOkY2hzdCB3ZXJkZW4gZGllIG5vdHdlbmRpZ2VuIEJpYmxpb3RoZWtlbiBpbnN0YWxsaWVydCAoc29mZXJuIG5vY2ggbmljaHQgdm9yaGFuZGVuKSB1bmQgYW5zY2hsaWXDn2VuZCBnZWxhZGVuLiBadWRlbSB3aXJkIHZvcmJlcmVpdGVuZCBkaWUgVGhlbWUtRWluc3RlbGx1bmcgZsO8ciBkYXMgUGFrZXQgZ2dwbG90IGdlc2V0enQgKGRpZXMgc29yZ3QgZsO8ciBow7xic2NoZXJlIFBsb3RzKS4gRGllc2VuIFNjaHJpdHQgd2llZGVyaG9sZW4gd2lyIHp1IEJlZ2lubiBqZWRlcyBLYXBpdGVscywgZGFoZXIgd2lyZCBhdWYgaWhuIHNww6R0ZXIgbmljaHQgbWVociB3ZWl0ZXIgZWluZ2VnYW5nZW4uIEluIGVpbmlnZW4gS2FwaXRlbG4gd2VyZGVuIG5vY2ggd2VpdGVyZW4gUGFrZXRlIGdlbGFuZGVuLCBldHdhIGbDvHIgZWluZSBlcndlaXRlcnRlIEZhcmJwYWxldHRlIChbUkNvbG9yQnJld2VyXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPVJDb2xvckJyZXdlcikpLCBXb3J0d29sa2VuIChbd29yZGNsb3VkXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPXdvcmRjbG91ZCkpIG9kZXIgdW0gVVJMcyB6dSBwYXJzZW4gKFt1cmx0b29sc10oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvcGFja2FnZT11cmx0b29scykpLiAgCgpgYGB7ciBJbnN0YWxsYXRpb24gdW5kIExhZGVuIGRlciBiZW7DtnRpZ3RlbiBSLUJpYmxpb3RoZWtlbiwgbWVzc2FnZSA9IEZBTFNFfQppZighcmVxdWlyZSgicXVhbnRlZGEiKSkge2luc3RhbGwucGFja2FnZXMoInF1YW50ZWRhIik7IGxpYnJhcnkoInF1YW50ZWRhIil9CmlmKCFyZXF1aXJlKCJyZWFkdGV4dCIpKSB7aW5zdGFsbC5wYWNrYWdlcygicmVhZHRleHQiKTsgbGlicmFyeSgicmVhZHRleHQiKX0KaWYoIXJlcXVpcmUoInRpZHl2ZXJzZSIpKSB7aW5zdGFsbC5wYWNrYWdlcygidGlkeXZlcnNlIik7IGxpYnJhcnkoInRpZHl2ZXJzZSIpfQppZighcmVxdWlyZSgiUkNvbG9yQnJld2VyIikpIHtpbnN0YWxsLnBhY2thZ2VzKCJSQ29sb3JCcmV3ZXIiKTsgbGlicmFyeSgiUkNvbG9yQnJld2VyIil9CnRoZW1lX3NldCh0aGVtZV9taW5pbWFsKCkpCmBgYAoKCiMjIyMgRXJzdGUgR2VodmVyc3VjaGUgbWl0IFF1YW50ZWRhCgpXZW5uIGFsbGUgbm90d2VuZGlnZW4gUGFrZXRlIGVyZm9sZ3JlaWNoIGdlbGFkZW4gd3VyZGVuLCBrw7ZubmVuIHdpciBlaW5lbiBlcnN0ZW4gR2VodmVyc3VjaCBtaXQgUXVhbnRlZGEgdW50ZXJuZWhtZW4uIFR5cGlzY2hlcndlaXNlIHdpcmQgZWluIEtvcnB1cyAoYWxzbyBlaW5lIFNhbW1sdW5nIHZvbiBUZXh0ZW4gdW5kIHp1Z2Vow7ZyaWdlbiBNZXRhZGF0ZW4pIGVyc3RlbGx0LCBpbmRlbSBEYXRlaWVuIHZvbiBkZXIgRmVzdHBsYXR0ZSBvZGVyIGF1cyBkZW0gSW50ZXJuZXQgZWluZ2VsZXNlbiB3ZXJkZW4sIGRpZSBkYW5uIGRlbiBLb3JwdXNpbmhhbHQgYmlsZGVuLiBJbSBmb2xnZW5kZW4gZXJzdGVuIFNjaHJpdHQgaGFsdGVuIHdpciBlcyBub2NoIGV0d2FzIGVpbmZhY2hlciB1bmQgYXJiZWl0ZW4gbWl0IGVpbmVtIHN5bnRoZXRoaXNjaGVuIEJlaXNwaWVsLiBEYXp1IGxlc2VuIHdpciBsZWRpZ2xpY2ggZHJlaSBCZWlzcGllbHPDpHR6ZSBlaW4gaW4gUiBlaW4gdW5kIGVyc3RlbGxlbiBkYXJhdXMgZWluZW4gS29ycHVzLgoKYGBge3IgQmVpc3BpZWxzw6R0emUgZXJzdGVsbGVufQpiZWlzcGllbHNhZXR6ZSA8LSBjKCJFaW4gSHVuZCBrYW0gaW4gZGllIEvDvGNoZSIsCiAgICAgICAgICAgICAgICAgICAgIkluIGRlciBLw7xjaGUgZ2lidCBlcyBub2NoIEthZmZlZSIsCiAgICAgICAgICAgICAgICAgICAgIkltIEthZmZlZSBmZWhsdCBub2NoIGRpZSBNaWxjaCIpCmBgYAoKRGFzIE9iamVrdCAqYmVpc3BpZWxzYWV0emUqIGVudGjDpGx0IGF1cyBTaWNodCB2b24gUXVhbnRlZGEgZHJlaSBUZXh0ZSwgYXVzIGRlbmVuIHNpY2ggbWl0IGRlciBnbGVpY2huYW1pZ2VuIEZ1bmt0aW9uIHByb2JsZW1sb3MgZWluIE9iamVrdCB2b20gVHlwIFtjb3JwdXNdKGh0dHBzOi8vcXVhbnRlZGEuaW8vcmVmZXJlbmNlL2NvcnB1cy5odG1sKSBlcnN0ZWxsZW4gbMOkc3N0LgoKYGBge3IgRWluZW4gc2VociBlaW5mYWNoZW4gS29ycHVzIGVyc3RlbGxlbn0KYmVpc3BpZWxrb3JwdXMgPC0gY29ycHVzKGJlaXNwaWVsc2FldHplKQpiZWlzcGllbGtvcnB1cwpgYGAKCkRpZSBNZWxkdW5nIGJlc3TDpHRpZ3QgdW5zIGdlbmF1IGRhcyAtLSB3aXIgaGFiZW4gZXJmb2xncmVpY2ggZWluZW4gS29ycHVzIGF1cyBkcmVpIFRleHRlbiBlcnN0ZWxsdC4gRGVyemVpdCBlbnRow6RsdCB1bnNlciBLb3JwdXMgbm9jaCBrZWluZXJsZWkgTWV0YWRhdGVuLCBhbHNvIEFuZ2FiZW4genUgZGVuIGltIEtvcnB1cyBlbnRoYWx0ZW5lbiBUZXh0ZW4uIEluIGRlciBUZXJtaW5vbG9naWUgdm9uIFF1YW50ZWRhIHdlcmRlbiBNZXRhZGF0ZW4gYXVjaCBhbHMgRG9rdW1lbnQtVmFyaWFibGVuICgqZG9jdmFycyopIGJlemVpY2huZXQuIAoKRGllIGluIGVpbmVtIEtvcnB1cyBlbnRoYWx0ZW5lbiBUZXh0ZSBsYXNzZW4gc2ljaCBqZWRlbnplaXQgbWl0IGRlbSBCZWZlaGwgW3RleHRzXShodHRwczovL3F1YW50ZWRhLmlvL3JlZmVyZW5jZS90ZXh0cy5odG1sKSBhdXNnZWJlbi4KCmBgYHtyIFRleHRlIGF1cyBlaW5lbSBLb3JwdXMgYXVzZ2ViZW59CnRleHRzKGJlaXNwaWVsa29ycHVzKQpgYGAKCk51biBmw7xnZW4gd2lyIHVuc2VyZW0gS29ycHVzIG1pdCBIaWxmZSBkZXMgQmVmZWhscyBbZG9jdmFyc10oaHR0cDovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2RvY3ZhcnMuaHRtbCkgZWluZSBEb2t1bWVudC1WYXJpYWJsZSBoaW56dS4gU2lubiB1bmQgWndlY2sgZGllc2VyIFZhcmlhYmxlbiBpc3QgZGllIChzcMOkdGVyIG5vY2ggd2ljaHRpZ2UpIFp1b3JkbnVuZyB2b24gTWV0YWRhdGVuIHp1IGRlbSBlcmZhc3N0ZW4gVGV4dG1hdGVyaWFsLiAKCmBgYHtyIERva3VtZW50LVZhcmlhYmxlbiBhbmxlZ2VufQpkb2N2YXJzKGJlaXNwaWVsa29ycHVzLCAiVGV4dF9BdXRvciIpIDwtIGMoIlBhdWwiLCAiTWFyaWUiLCAiUGF1bCIpCmBgYAoKV2llc28gaXN0IGRpZSBFcnN0ZWxsdW5nIGVpbmVzIEtvcnB1cyB1bmQgZGFzIEFubGVnZW4gdm9uIERva3VtZW50LVZhcmlhYmxlbiBzaW5udm9sbD8gRWluZSBadXNhbW1lbmZhc3N1bmcgdW5zZXJlcyBCZWlzcGllbGtvcnB1cyBnaWJ0IGVpbmVuIGVyc3RlbiBBdWZzY2hsdXNzLiAKCmBgYHtyIEVpbmUgS29ycHVzLVp1c2FtbWVuZmFzc3VuZyBlcnN0ZWxsZW59CnN1bW1hcnkoYmVpc3BpZWxrb3JwdXMpCmBgYAoKRGllIFp1c2FtbWVuZmFzc3VuZyB6ZWlndCBkaWUgaW0gS29ycHVzIGVudGhhbHRlbmRlbiBUZXh0ZSwgZGllIGpld2VpbHMgZWluZW4gZWlubWFsaWdlbiBCZXplaWNobmVyIGVyaGFsdGVuICgqdGV4dDEqLCAqdGV4dDIqLCAqdGV4dDMqKS4gV2VpdGVyaGluIGdpYnQgc2llIEF1ZnNjaGx1c3Mgw7xiZXIgZGllIEFuemFobCBkZXIgW1Rva2Vuc10oaHR0cHM6Ly93d3cuc2tldGNoZW5naW5lLmV1L215X2tleXdvcmRzL3Rva2VuLykgKGxhdWZlbmRlIFfDtnJ0ZXIpLCBkaWUgQW56YWhsIGRlciBUeXBlcyAoZWlubWFsaWdlIFfDtnJ0ZXIpLCB1bmQgZGllIEFuemFobCBkZXIgU8OkdHplIHBybyBUZXh0LiBTY2hsaWXDn2xpY2ggd2lyZCBub2NoIGRpZSBnZXJhZGUgdm9uIHVucyBlaW5nZWbDvGhydGUgRG9rdW1lbnQtVmFyaWFibGUgKlRleHRfQXV0b3IqIHdpZGVyZ2VnZWJlbi4gCgpBdWNoIHdlbm4gZGFzIHp1bsOkY2hzdCBuaWNodCB3ZWl0ZXIgd2ljaHRpZyBlcnNjaGVpbmVuIG1hZzogRGllIE3DtmdsaWNoa2VpdCwgZWluZSBncm/Dn2UgQW56YWhsIHZvbiBUZXh0ZW4gYXVzIHVudGVyc2NoaWVkbGljaGVuIFF1ZWxsZW4genUgZWluZW0gS29ycHVzLU9iamVrdCB6dXNhbW1lbnp1ZmFzc2VuIHVuZCBkaWVzZXMgYW5zY2hsaWXDn2VuZCBtaXQgTWV0YWRhdGVuIHp1IHZlcnNlaGVuLCBpc3QgZWluIGdhbnogIGVudHNjaGVpZGVuZGVyIFZvcnRlaWwgdm9uIFF1YW50ZWRhLiBXaXIgd2VyZGVuIGZvbGdlbmQgbWl0IGRldXRsaWNoIGdyw7bDn2VyZW4gS29ycG9yYSBhcmJlaXRlbiwgZGllIGFsbGUgbmFjaCBkaWVzZW0gUHJpbnppcCBlcnN0ZWxsdCB3dXJkZW4uIAoKIyMjIyBFaW5sZXNlbiBkZXIgRGF0ZW4gdW5kIEFubGVnZW4gZGVzIFNoZXJsb2NrIEhvbG1lcy1Lb3JwdXMKCk5hY2hkZW0gd2lyIGVpbmVuIGVyc3RlbiBFaW5ibGljayBpbiBkaWUgRXJzdGVsbHVuZyBlaW5lcyBzZWhyIHNpbXBsZW4gUXVhbnRlZGEtS29ycHVzIGVyaGFsdGVuIGhhYmVuLCBrw7ZubmVuIHdpciBudW4gZGllIFNoZXJsb2NrIEhvbG1lcy1Sb21hbmUgZWlubGVzZW4gdW5kIGRhcmF1cyBlYmVuZmFsbHMgZWluZW4gS29ycHVzIGVyc3RlbGxlbiwgbnVuIGFiZXIgaW4gZWluZXIgcmVhbGlzdGlzY2hlbiBHcsO2w59lLiAKCkbDvHIgZGFzIEVpbmxlc2VuIGRlciBQbGFpbnRleHQtRGF0ZWllbiB3aXJkIGRpZSBGdW5rdGlvbiBbcmVhZHRleHRdKGh0dHBzOi8vd3d3LnJkb2N1bWVudGF0aW9uLm9yZy9wYWNrYWdlcy9yZWFkdGV4dCkgYXVzIGRlbSBnbGVpY2huYW1pZ2VuIFBha2V0IHZlcndlbmRldCwgZHVyY2ggZGllIHNpY2ggZWluZSBSZWloZSB2b24gRGF0ZWlmb3JtYXRlbiBlcmZvbGdyZWljaCBpbXBvcnRpZXJlbiBsYXNzZW4gKHUuYS4gVFhULCBQREYgdW5kIFdvcmQpLiBHcnVuZHPDpHR6bGljaCBzaWNoIFBsYWludGV4dOKAk0RhdGVuIChpLmQuUi4gbWl0IGRlciBFbmR1bmcgIi50eHQiKSB1bmQgRGF0ZW4gaW4gVGFiZWxsZW5mb3JtIChldHdhIGltIEZvcm1hdCBDU1Ygb2RlciBhdWNoIGFscyBFeGNlbOKAk0RhdGVpKSBmw7xyIHJlYWR0ZXh0IG9obmUgZ3LDtsOfZXJlIFByb2JsZW1lIGxlc2JhciwgYWxsZXJkaW5ncyBtdXNzIG1hbiBiZWltIEVpbmxlc2VuIGVya2zDpHJlbiwgd2llIGdlbmF1IGRpZSBlaW56ZWxuZW4gRGF0ZW5zw6R0emUgdm9uIGVpbmFuZGVyIGdldHJlbm50IHNpbmQgKGJlaSBQbGFpbnRleHTigJNEYXRlaWVuIHdvIG5pY2h0IDEgRGF0ZWkgPT0gMSBUZXh0LCB3YXMgZXR3YSBiZWkgRXhwb3J0ZW4gYXVzIExleGlzIE5leGlzIGRlciBGYWxsIHNlaW4ga2FubiksIGJ6dy4gd2VsY2hlIEZlbGRlciBkaWUgUHJpbcOkcuKAkyB1bmQgd2VsY2hlIE1ldGFkYXRlbiBiZWluaGFsdGVuIChiZWkgVGFiZWxsZW4pLiBFaW5lIGd1dGUgRWluZsO8aHJ1bmcgenVtIFBha2V0IHJlYWR0ZXh0IGZpbmRldCBzaWNoIFtoaWVyXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvcmVhZHRleHQvdmlnbmV0dGVzL3JlYWR0ZXh0X3ZpZ25ldHRlLmh0bWwpLgoKYGBge3IgVGV4dGRhdGVuIGVpbmxlc2VufQpkYXRlbi5zaGVybG9jayA8LSByZWFkdGV4dCgiZGF0ZW4vc2hlcmxvY2svcm9tYW5lLyoudHh0IikgCmRhdGVuLnNoZXJsb2NrJGRvY19pZCA8LSBzdHJfc3ViKGRhdGVuLnNoZXJsb2NrJGRvY19pZCwgc3RhcnQgPSA0LCBlbmQgPSAtNSkKYGBgCgpJbiBkaWVzZW0gRmFsbCBlbnRzcHJpY2h0IGplZGUgRGF0ZWkgZWluZW0gVGV4dCAobmljaHQgd2llIHp1dm9yLCBlaW5lbiBlaW56aWdlbiBTYXR6KSwgd29kdXJjaCBkZXIgSW1wb3J0IHNlaHIgdW1rb21wbGl6aWVydCBhdXNmw6RsbHQuIFp1bsOkY2hzdCBsYWRlbiB3aXIgbnVyIERhdGVpZW4gbWl0IGRlciBFbmR1bmcgIi50eHQiIGF1cyBkZW0gVmVyemVpY2huaXMgZGF0ZW4vc2hlcmxvY2svcm9tYW5lLiBEYW5uIHppZWhlbiB3aXIgZGllIE5hbWVuIGRlciBSb21hbmUgYXVzIGRlbiBEYXRlaW5hbWVuLCB1bSBkaWVzZSBzcMOkdGVyIGluIFBsb3TigJNCZXNjaHJpZnR1bmdlbiB2ZXJ3ZW5kZW4genUga8O2bm5lbi4KCmBgYHtyIERhcyBTaGVybG9jayBIb2xtZXMtS29ycHVzIGFubGVnZW59CmtvcnB1cyA8LSBjb3JwdXMoZGF0ZW4uc2hlcmxvY2ssIGRvY2lkX2ZpZWxkID0gImRvY19pZCIpIApkb2N2YXJzKGtvcnB1cywgIlRleHRudW1tZXIiKSA8LSAxOjEyCmtvcnB1cwpgYGAKCk51biBnZW5lcmllcmVuIHdpciB3aWVkZXIgbWl0IGRlbSBCZWZlaGwgW2NvcnB1c10oaHR0cHM6Ly9xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzLmh0bWwpIGVpbiBRdWFudGVkYS1Lb3JwdXMtT2JqZWt0LiBJbSBHZWdlbnNhdHogenUgdW5zZXJlbSBCZWlzcGllbGtvcnB1cyBiaWxkZW4gaW4gZGllc2VtIEZhbGwgZGllIG1pdHRlbHMgW3JlYWR0ZXh0XShodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvcGFja2FnZXMvcmVhZHRleHQpIHp1dm9yIGVpbmdlbGVzZW5lbiBEYXRlaW4gZGllIEdydW5kbGFnZS4gRGllIEZ1bmt0aW9uIFtjb3JwdXNdKGh0dHBzOi8vcXVhbnRlZGEuaW8vcmVmZXJlbmNlL2NvcnB1cy5odG1sKSB2ZXJzdGVodCBtZWhyZXJlIERhdGVuZm9ybWF0ZSwgYWxzbyBDaGFyYWN0ZXItVmVrdG9yZW4gKHVuc2VyIGVyc3RlcyBCZWlzcGllbCkgZWJlbnNvIHdpZSBEYXRhIEZyYW1lcyAod2llIG1pdHRlbHMgcmVhZHRleHQgZXJzdGVsbHQpIG9kZXIgT2JqZWt0ZSBhdXMgZGVtIFBha2V0IFt0bV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvcGFja2FnZT10bSkuIFp3ZWl0ZW5zIHdpcmQgZWluZSBEb2t1bWVudC1WYXJpYWJsZSAqVGV4dG51bW1lciogZ2VuZXJpZXJ0LCBkaWUgd2lyIGViZW5mYWxscyBzcMOkdGVyIG5vY2ggZ2VicmF1Y2hlbiBrw7ZubmVuLiBadW0gU2NobHVzcyB3aXJkIGRpZSBWYXJpYWJsZSAqa29ycHVzKiBhdWZnZXJ1ZmVuLCB3YXMgdW5zIGRpZSB3aWNodGlnZW4gRWNrZGF0ZW4genVtIEtvcnB1cyBzb3dpZSBNZXRhZGF0ZW4genUgZGVuIGVudGhhbHRlbmRlbiBUZXh0ZW4genVyw7xja2xpZWZlcnQuCgpJbiBkZW4gZm9sZ2VuZGVuIEFic2Nobml0dGVuIHdlcmRlbiBow6R1ZmlnIGJlcmVpdHMgdm9yYmVyZWl0ZXRlIEtvcnBvcmEgZ2VsYWRlbiwgZC5oLiBkZXIgQmVmZWhsIFtjb3JwdXNdKGh0dHBzOi8vcXVhbnRlZGEuaW8vcmVmZXJlbmNlL2NvcnB1cy5odG1sKSB3aXJkIGhpZXIgbmljaHQgbWVociBleHBsaXppdCBhdXNnZWbDvGhydC4gRXIgaXN0IGFiZXIgaW0gVm9yZmVsZCBhdXNnZWbDvGhydCB3b3JkZW4sIHVtIGF1cyBUZXh0ZGF0ZWluIGF1ZiBkZXIgRmVzdHBsYXR0ZSBvZGVyIFR3aXR0ZXItRGF0ZW4gaW4gZWluZW0gUi1EYXRhIEZyYW1lIGVpbmVuIFF1YW50ZWRhLUtvcnB1cyB6dSBlcnN0ZWxsZW4uIAoKRGllIEZ1bmt0aW9uZW4gW25kb2NdKGh0dHA6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9uZG9jLmh0bWwpLCBbbnRva2VuXShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvbnRva2VuLmh0bWwpLCBbbnR5cGVdKGh0dHA6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9udG9rZW4uaHRtbCkgdW5kIFtuc2VudGVuY2VdKGh0dHA6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9uc2VudGVuY2UuaHRtbCkgZ2ViZW4gZGllIEFuemFobCBkZXIgRG9rdW1lbnRlLCBUb2tlbnMsIFR5cGVzIHVuZCBTw6R0emUgYXVzLiBEaWVzZSBTdGF0aXN0aWtlbiBrw7ZubmVuIGJlcXVlbSBnZW1laW5zYW0gbWl0IE1ldGFkYXRlbiBhdWYgRG9rdW1lbnRlYmVuZSBkdXJjaCBkaWUgRnVua3Rpb24gW3N1bW1hcnldKGh0dHBzOi8vd3d3LnJkb2N1bWVudGF0aW9uLm9yZy9wYWNrYWdlcy9xdWFudGVkYS92ZXJzaW9ucy8xLjUuMC90b3BpY3Mvc3VtbWFyeS5jb3JwdXMpIGVyc3RlbGx0IHdlcmRlbi4gQmVpIGRlbiBtZWlzdGVuIEtvcnBvcmEsIGRpZSBoaWVyIHZlcndlbmRldCB3ZXJkZW4sIGxpZWd0IGVpbiBzb2xjaGVyIERhdGEgRnJhbWUgbWl0IFN0YXRpc3Rpa2VuIHp1IGplZGVtIFRleHQgYmVyZWl0cyBiZWkuIE5vdHdlbmRpZyBpc3QgZGllcyBhbGxlcmRpbmdzIG5pY2h0LiBXaWxsIG1hbiBhdWYgS29ycHVz4oCTTWV0YWRhdGVuIHp1csO8Y2tncmVpZmVuIG9kZXIgZGllc2UgdmVyw6RuZGVybiwga2FubiBtYW4gZGllcyBqZWRlcnplaXQgw7xiZXIgZGVuIEJlZmVobCBbZG9jdmFyc10oaHR0cDovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2RvY3ZhcnMuaHRtbCkgdHVuLgoKYGBge3IgS29ycHVzc3RhdGlzdGlrZW4gYmVyZWNobmVufQprb3JwdXMuc3RhdHMgPC0gc3VtbWFyeShrb3JwdXMpCmtvcnB1cy5zdGF0cyRUZXh0IDwtIHJlb3JkZXIoa29ycHVzLnN0YXRzJFRleHQsIDE6MTIsIG9yZGVyID0gVCkKa29ycHVzLnN0YXRzCmBgYAoKTWFuY2htYWwgaXN0IGVzIGJlaSBkZXIgRXJzdGVsbHVuZyB2b24gS29ycHVzLVp1c2FtbWVuZmFzc3VuZ2VuIHNpbm52b2xsLCBkYXMgb3B0aW9uYWxlIEZ1bmt0aW9uc2FyZ3VtZW50IG4gPSAxMDAwMDAwIHp1IHZlcndlbmRlbiwgd2VpbCBkaWUgRnVua3Rpb24gW3N1bW1hcnldKGh0dHBzOi8vd3d3LnJkb2N1bWVudGF0aW9uLm9yZy9wYWNrYWdlcy9xdWFudGVkYS92ZXJzaW9ucy8xLjUuMC90b3BpY3Mvc3VtbWFyeS5jb3JwdXMpIGFuc29uc3RlbiBudXIgbWF4aW1hbCAxMDAgVGV4dGUgenVzYW1tZW5mYXNzdC4gSW4gZGllc2VtIEZhbGwgcmVpY2h0IGRhcyB6d2FyIGF1cywgYWJlciBiZWkgZ3LDtsOfZXJlbiBEYXRlbnPDpHR6ZW4gaXN0IGRpZXNlIEJlc2NocsOkbmt1bmcgZWhlciB1bnByYWt0aXNjaC4gVGVjaG5pc2NoIGdlc2VoZW4gaGVpw590IGRpZXNlIEZ1bmt0aW9uIFtzdW1tYXJ5LmNvcnB1c10oaHR0cHM6Ly93d3cucmRvY3VtZW50YXRpb24ub3JnL3BhY2thZ2VzL3F1YW50ZWRhL3ZlcnNpb25zLzEuNS4wL3RvcGljcy9zdW1tYXJ5LmNvcnB1cykgdW5kIGlzdCBlaW5lIGFuIEtvcnB1cy1PYmpla3RlIGFuZ2VwYXNzdGUgVmFyaWFudGUgZGVyIEJhc2lzZnVua3Rpb24gW3N1bW1hcnldKGh0dHBzOi8vd3d3LnJkb2N1bWVudGF0aW9uLm9yZy9wYWNrYWdlcy9iYXNlL3ZlcnNpb25zLzMuNi4xL3RvcGljcy9zdW1tYXJ5KSwgZGllIGF1Y2ggc29uc3QgaW4gUiB2ZXJ3ZW5kZXQgd2lyZC4gRGVyIEJlZmVobCBbcmVvcmRlcl0oaHR0cHM6Ly93d3cucmRvY3VtZW50YXRpb24ub3JnL3BhY2thZ2VzL3N0YXRzL3ZlcnNpb25zLzMuNi4xL3RvcGljcy9yZW9yZGVyLmRlZmF1bHQpIHdpcmQgdmVyd2VuZGV0LCB1bSBkaWUgVGV4dGUgYXVjaCBpbiBQbG90cyBuYWNoIGlocmVyIFJlaWhlbmZvbGdlIGluICpUaGUgQWR2ZW50dXJlcyBvZiBTaGVybG9jayBIb2xtZXMqIHp1IHNvcnRpZXJlbiwgc3RhdHQgYWxwaGFiZXRpc2NoIG5hY2ggVGl0ZWwuCgpEZXIgSW5oYWx0IGRlciBWYXJpYWJsZSAqa29ycHVzLnN0YXRzKiBrYW5uIG5hdMO8cmxpY2ggYXVjaCBnZXBsb3R0ZXQgd2VyZGVuLCB1bSBlaW5lbiBhbnNjaGF1bGljaGVuIEVpbmRydWNrIHZvbiBkZXIgS29ycHVzYmVzY2hhZmZlbmhlaXQgenUgZ2ViZW4uIERpZSBmb2xnZW5kZW4gWmVpbGVuIGxpZWZlcm4gZGllIEFuemFobCBkZXIgVG9rZW5zIChsYXVmZW5kZSBXw7ZydGVyKSwgZGllIEFuemFobCBkZXIgVHlwZXMgKGVpbm1hbGlnZSBXw7ZydGVyKSwgdW5kIFPDpHR6ZSBwcm8gUm9tYW4genVyw7xjayAodmdsLiBkYXp1IFtkaWVzZSBFaW5mw7xocnVuZ10oaHR0cHM6Ly93d3cuYnViZW5ob2Zlci5jb20va29ycHVzbGluZ3Vpc3Rpay9rdXJzL2luZGV4LnBocD9pZD1lcnN0ZWxsdW5nX2tvcnBvcmEuaHRtbCkpLiBTY2hsaWXDn2xpY2ggd2lyZCBub2NoIGRhcyBWZXJow6RsdG5pcyB2b24gVHlwZW4genUgVG9rZW5zIChvZGVyIGRpZSBzb2cuIFtUeXAtVG9rZW4tUmVsYXRpb25dKGh0dHBzOi8vZGUud2lraXBlZGlhLm9yZy93aWtpL1R5cGUtVG9rZW4tUmVsYXRpb24pKSBnZXBsb3R0ZXQuIAoKR3J1bmRsYWdlIHNvbGNoZXIgUGxvdHMgc2luZCBwcmFrdGlzY2ggaW1tZXIgRGF0YSBGcmFtZS1PYmpla3RlIChhbHNvIFRhYmVsbGVuKSwgZGllIEluZm9ybWF0aW9uZW4gw7xiZXIgS29ycG9yYSwgVGV4dGUsIFfDtnJ0ZXIsIFRoZW1lbiB1c3cuIGVudGhhbHRlbiwgd2VsY2hlIHNpY2ggdmlzdWVsbCBkYXJzdGVsbGVuIGxhc3Nlbi4gSW0gUmVzdCBkaWVzZXIgRWluZsO8aHJ1bmcgZ2VoZSBpY2ggbmljaHQgaW0gRGV0YWlsIGRhcmF1ZiBlaW4sIHdpZSBkaWUgamV3ZWlsaWdlbiBQbG90cyBnZW5hdSBrb25zdHJ1aWVydCB3ZXJkZW4sIGFsbGVyZGluZ3MgbGFzc2VuIHNpY2ggZGllIG1laXN0ZW4gRGF0ZW4gYXVjaCAoZXR3YXMgd2VuaWdlciBhbnNwcmVjaGVuZCkgbWl0IGRlciBSLWludGVybmVuIEZ1bmt0aW9uIFtwbG90XShodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvcGFja2FnZXMvZ3JhcGhpY3MvdmVyc2lvbnMvMy41LjAvdG9waWNzL3Bsb3QpIGRhcnN0ZWxsZW4uIEVpbmUgaGlsZnJlaWNoZSBkZXV0c2Noc3ByYWNoaWdlIEVpbmbDvGhydW5nIGluIGRhcyBQbG90dGVuIG1pdCBnZ3Bsb3QyIGZpbmRldCBzaWNoIFtoaWVyXShodHRwOi8vbWQucHN5Y2guYmlvLnVuaS1nb2V0dGluZ2VuLmRlL212L3VuaXQvZ2dwbG90Mi9nZ3Bsb3QyLmh0bWwpLiBWaWVsZSBkZXIgaGllciB2b3JnZXN0ZWxsdGVuIFBsb3RzIHN0YW1tZW4genVkZW0gZGlyZWt0IGF1cyBxdWFudGVkYSAoYmVnaW5uZW5kIG1pdCB0ZXh0cGxvdF8pLgoKYGBge3IgVG9rZW5zL1R5cGVzL1PDpHR6ZSB1bmQgVFRSIHBybyBSb21hbiBwbG90dGVufQpnZ3Bsb3Qoa29ycHVzLnN0YXRzLCBhZXMoVGV4dCwgVG9rZW5zLCBncm91cCA9IDEpKSArIAogIGdlb21fbGluZSgpICsgCiAgZ2VvbV9wb2ludCgpICsgCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgdmp1c3QgPSAxLCBoanVzdCA9IDEpKSArCiAgZ2d0aXRsZSgiVG9rZW5zIHBybyBSb21hbiIpCmdncGxvdChrb3JwdXMuc3RhdHMsIGFlcyhUZXh0LCBUeXBlcywgZ3JvdXAgPSAxKSkgKyAKICBnZW9tX2xpbmUoKSArIGdlb21fcG9pbnQoKSArIAogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNDUsIHZqdXN0ID0gMSwgaGp1c3QgPSAxKSkgKwogIGdndGl0bGUoIlR5cGVzIHBybyBSb21hbiIpCmdncGxvdChrb3JwdXMuc3RhdHMsIGFlcyhUZXh0LCBTZW50ZW5jZXMsIGdyb3VwID0gMSkpICsgCiAgZ2VvbV9saW5lKCkgKyAKICBnZW9tX3BvaW50KCkgKyAKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDQ1LCB2anVzdCA9IDEsIGhqdXN0ID0gMSkpICsKICBnZ3RpdGxlKCJTw6R0emUgcHJvIFJvbWFuIikKZ2dwbG90KGtvcnB1cy5zdGF0cywgYWVzKFRva2VucywgVHlwZXMsIGdyb3VwID0gMSwgbGFiZWwgPSBUZXh0bnVtbWVyKSkgKwogIGdlb21fc21vb3RoKG1ldGhvZCA9ICJsbSIsIGZvcm11bGEgPSAieSB+IHgiLCBzZSA9IEZBTFNFKSArIAogIGdlb21fdGV4dChjaGVja19vdmVybGFwID0gVCkgKyAKICBnZ3RpdGxlKCJUeXAtVG9rZW4tUmVsYXRpb24gcHJvIFJvbWFuIikKYGBgCgpEaWVzZSBHcmFmaWtlbiBzaW5kIHp1bsOkY2hzdCBlaW5tYWwgbmljaHQgdW13ZXJmZW5kIGluZm9ybWF0aXYuIFNpZSBiZWxlZ2VuIGxlZGlnbGljaCwgZGFzcyBkaWUgRXJ6w6RobHVuZ2VuIOKAmEEgQ2FzZSBvZiBJZGVudGl0eeKAmSB1bmQgKGluIGdlcmluZ2VyZW0gTWHDn2UpIOKAmFRoZSBGaXZlIE9yYW5nZW4gUGlwc+KAmSBkZXV0bGljaCBrw7xyemVyIHNpbmQgYWxzIGRpZSBhbmRlcmVuIFRleHRlLCB3YXMgc2ljaCBhdWYgYWxsZW4gZHJlaSBFYmVuZW4gKFRva2VucywgVHlwZXMsIFPDpHR6ZSkgbmllZGVyc2NobMOkZ3QuIEV0d2FzIGludGVyZXNzYW50ZXIgd2lyZCBlcyBhbGxlcmRpbmdzIGJlaSBkZXIgVHlwLVRva2VuLVJlbGF0aW9uOiB3w6RocmVuZCBkcmVpIFJvbWFuZSAobWl0IGRlbiBOdW1tZXJuIDMsIDExIHVuZCAxMikgamV3ZWlscyBlaW5lbiBlaGVyIHVudGVyZHVyY2hzY2huaXR0bGljaGVuIFRUUiBhdWZ3ZWlzZW4sIGxpZWdlbiB3ZWl0ZXJlIHZpZXIgb2JlcmhhbGIgZGVyIGxpbmVhcmVuIFJlbGF0aW9uICgxLCA1LCA2ICB1bmQgOCksIHfDpGhyZW5kIGRpZSB2ZXJibGVpYmVuZGVuIHNlY2hzIHppZW1saWNoIGdlbmF1IGRlbSBEdXJjaHNjaG5pdHQgZW50c3ByZWNoZW4uIMOcYmVyIGRlbiBUVFIgbGFzc2VuIHNpY2ggUsO8Y2tzY2hsw7xzc2VuIMO8YmVyIGRpZSBJbmZvcm1hdGlvbnNkaWNodGUgemllaGVuIC0tIGRhenUgc3DDpHRlciBub2NoIG1laHIuIAoKCiMjIyMgTWl0IEtvcnBvcmEgYXJiZWl0ZW4KCktvcnBvcmEgbGFzc2VuIHNpY2ggaW4gcXVhbnRlZGEgc2VociBsZWljaHQgc2FtcGxlbiwgdW1mb3JtZW4gdW5kIG1pdCB6dXPDpHR6bGljaGVuIE1ldGFkYXRlbiB2ZXJzZWhlbi4gTWV0YWRhdGVuIGvDtm5uZW4gd2llZGVydW0gZ2VudXR6dCB3ZXJkZW4sIHVtIGRhcyBLb3JwdXMgbmFjaCBiZXN0aW1tdGVuIEtyaXRlcmllbiB6dSBmaWx0ZXJuLiBEZXIgZm9sZ2VuZGVuIEF1ZnJ1ZiB6ZWl0IGRpZSBlcnN0ZW4gMS4wMDAgV8O2cnRlciBkZXMgZXJzdGVuIFJvbWFucy4KCmBgYHtyIFN1YnN0cmluZ3MgZXh0cmFoaWVyZW59CnN0cl9zdWIoa29ycHVzWzJdLCBzdGFydCA9IDEsIGVuZCA9IDEwMDApCmBgYAoKSmVkZXIgVGV4dCBsw6Rzc3Qgc2ljaCBhbHNvIGF1Y2ggZGlyZWt0IGFuaGFuZCBzZWluZXIgSW5kaXppZXJ1bmcgYXVmcnVmZW4gKGV0d2Ega29ycHVzWzFdIGbDvHIgZGVuIGVyc3RlbiBUZXh0KSwgbmViZW4gZGVtIEF1ZnJ1ZiDDvGJlciBkaWUgRnVua3Rpb24gW3RleHRzXShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvdGV4dHMuaHRtbCkuIFRleHRlIGvDtm5uZSBhdWYgZGllc2VtIFdlZyBhdWNoIGVyc2V0enQgd2VyZGVuLCBnYW56IHNvLCB3aWUgbWFuIGRpZSBFbGVtZW50ZSBlaW5lcyBWZWt0b3JzIG9kZXIgRGF0YSBGcmFtZXMgw7xiZXJzY2hyZWliZW4ga2Fubi4KCk1pdHRlbHMgW2NvcnB1c19yZXNoYXBlXShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzX3Jlc2hhcGUuaHRtbCkgbMOkc3N0IHNpY2ggZWluIEtvcnB1cyBzbyB1bWZvcm1lbiwgZGFzcyBqZWRlciBTYXR6IGVpbiBlaWdlbmVzIERva3VtZW50IGVyZ2lidC4gQWx0ZXJuYXRpdmUgQXJndW1lbnRlIHNpbmQgInBhcmFncmFwaHMiIHVuZCAiZG9jdW1lbnRzIiAoc28gbMOkc3N0IHNpY2ggZWluIFNhdHotS29ycHVzIHdpZWRlciBpbiBzZWluZW4gQW5mYW5nc3p1c3RhbmQgenVyw7xja3ZlcnNldHplbikuIERpZSBFcnN0ZWxsdW5nIHZvbiBTYXR6LUtvcnBvcmEgaXN0IGbDvHIgZGllIFNlbnRpbWVudGFuYWx5c2UgdW5kIGRhcyDDvGJlcndhY2h0ZSBtYXNjaGluZWxsZSBMZXJuZW4gdm9uIEludGVyZXNzZS4gCgpEaWUgQmVzY2hyaWZ0dW5nIGRlcyBCZWlzcGllbHMgYmVzdGVodCBoaWVyIGF1cyBkZXIgVmFyaWFibGUgKmRvY25hbWUqIHVuZCBlaW5lciBhbmdlaMOkbmd0ZW4gWmFobCAoZWluZSAxIGbDvHIgZGVuIGVyc3RlbiBTYXR6KS4gCgpgYGB7ciBFaW4gS29ycHVzIHVtZm9ybWVufQprb3JwdXMuc2FldHplIDwtIGNvcnB1c19yZXNoYXBlKGtvcnB1cywgdG8gPSAic2VudGVuY2VzIikKa29ycHVzLnNhZXR6ZVsxMDBdCmBgYAoKTWl0IFtjb3JwdXNfc2FtcGxlXShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzX3NhbXBsZS5odG1sKSBrYW5uIHdlaXRlcmhpbiBlaW4genVmw6RsbGlnZXMgU2FtcGxlIGF1cyBlaW5lbSBLb3JwdXMgZ2V6b2dlbiB3ZXJkZW4uIFdpciB3ZW5kZW4gZGllIEZ1bmt0aW9uIGhpZXIgYXVmIGRhcyBTYXR6LUtvcnB1cyBhbi4gCgpgYGB7ciBFaW4gWnVmYWxsc2FtcGxlIHppZWhlbn0KenVmYWxsc3NhdHogPC0gY29ycHVzX3NhbXBsZShrb3JwdXMuc2FldHplLCBzaXplID0gMSkKenVmYWxsc3NhdHpbMV0KYGBgCgpBbmhhbmQgdm9uIFtjb3JwdXNfc3Vic2V0XShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzX3N1YnNldC5odG1sKSBrYW5uIGVpbiBLb3JwdXMgc2NobGllw59saWNoIG5hY2ggTWV0YWRhdGVuIGdlZmlsdGVydCB3ZXJkZW4uCgpVbSBkaWVzIHR1biB6dSBrw7ZubmVuLCBlcnN0ZWxsZW4gd2lyIGltIG7DpGNoc3RlbiBTY2hyaXR0IGVpbmUgYmluw6RyZSAgRG9rdW1lbnQtVmFyaWFibGUgbWl0IGRlbSBOYW1lbiAqTGFuZ2VyU2F0eiosIGRpZSBkYW5uIFRSVUUgaXN0LCB3ZW5uIGVpbiBTYXR6ID49IDI1IFRva2VucyBlbnRow6RsdC4gU28gbMOkc3N0IHNpY2ggbWl0IFtjb3JwdXNfc3Vic2V0XShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzX3N1YnNldC5odG1sKSBlaW4gVGVpbGtvcnB1cyB6dSBiaWxkZW4sIGluIGRlbSBudXIgbMOkbmdlcmUgU8OkdHplIGVudGhhbHRlbiBzaW5kLiBEYXMgQmVpc3BpZWwgc29sbCB2ZXJkZXV0bGljaGVuLCBkYXNzIG1pdGhpbGZlIGRlciB2b24gUXVhbnRlZGEgYmVyZWl0Z2VzdGVsbHRlbiBGdW5rdGlvbmVuIHphaGxyZWljaGUgU2Nocml0dGUgZsO8ciBkaWUgQmVyZWluaWd1bmcgdm9uIEtvcnBvcmEgbcO2Z2xpY2ggc2luZC4gCgpgYGB7ciBOZXVlIERvY3ZhcnMgaGluenVmw7xnZW59CmRvY3ZhcnMoa29ycHVzLnNhZXR6ZSwgIkxhbmdlclNhdHoiKSA8LSBudG9rZW4oa29ycHVzLnNhZXR6ZSk+PTI1CmtvcnB1cy5zYWV0emVfbGFuZyA8LSBjb3JwdXNfc3Vic2V0KGtvcnB1cy5zYWV0emUsIExhbmdlclNhdHogPT0gVFJVRSkKa29ycHVzLnNhZXR6ZV9sYW5nWzE6M10KYGBgCgpEaWUgTcO2Z2xpY2hrZWl0LCBtaXQgYmVzdGVoZW5kZW4gKGJzcHcuIEF1dG9yLCBRdWVsbGUsIFJ1YnJpaywgWmVpdHN0ZW1wZWwpIHVuZCBlaWdlbnMgZXJzdGVsbHRlbiBEb2t1bWVudC1WYXJpYWJsZW4gKGJzcHcuIFRoZW1hLCBTZW50aW1lbnQpIHp1IGFyYmVpdGVuLCBnZWjDtnJ0IHdpZSBzY2hvbiBlcnfDpGhudCB6dSBkZW4gZ3LDtsOfdGVuIFN0w6Rya2VuIHZvbiBRdWFudGVkYSwgd2VpbCBkaWVzZSBBbmdhYmVuIHp1IGplZGVtIFplaXRwdW5rdCBpbiBkZXIgQW5hbHlzZSB6dXIgVmVyZsO8Z3VuZyBzdGVoZW4uIEZvbGdlbmQgZmlsdGVybiBvZGVyIGdydXBwaWVyZW4gd2lyIEtvcnBvcmEgaMOkdWZpZyBhdWYgZGVyIEdydW5kbGFnZSB2b24gTWV0YWRhdGVuLiAKClNjaGxpZcOfbGljaCBsYXNzZW4gc2ljaCBLb3Jwb3JhIG1pdCBIaWxmZSB2b24gW2NvcnB1c19zZWdtZW50XShodHRwOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvY29ycHVzX3NlZ21lbnQuaHRtbCkgYXVjaCBuYWNoIGJlc3RpbW10ZW4gS3JpdGVyaWVuIGF1ZnNwYWx0ZW4gdW5kIG1pdHRlbHMgW2NvcnB1c190cmltXShodHRwczovL3F1YW50ZWRhLmlvL3JlZmVyZW5jZS9jb3JwdXNfdHJpbS5odG1sKSBuYWNoIGZlc3RlbiBCZWRpbmd1bmdlbiByZWR1emllcmVuLCB1bSBldHdhIHNlaHIga3VyemUgVGV4dGUgYXVzenVzY2hsaWXDn2VuLiBJbSBmb2xnZW5kZW4gQmVpc3BpZWwgd2lyZCBlaW5lIGdhbnplIEtldHRlIHZvbiBPcGVyYXRpb25lbiAoU2VnbWVudGllcnVuZywgVHJpbW1lbiwgWnVmYWxsc3NhbXBsZSB6aWVoZW4pIHNvIGR1cmNoZ2Vmw7xocnQuIERhYmVpIHdlcmRlbiBhdWNoIGVyc3RtYWxzIGRpZSBmw7xyIGRhcyBbdGlkeXZlcnNlXShodHRwczovL3d3dy50aWR5dmVyc2Uub3JnLykgdHlwaXNjaGVuIFsqcGlwZXMqXShodHRwczovL3N0eWxlLnRpZHl2ZXJzZS5vcmcvcGlwZXMuaHRtbCkgdmVyd2VuZGV0LiBCZWkgcGlwZXMgaGFuZGVsdCBlcyBzaWNoIHVtIGVpbmUgQXJ0IHNwZXppZWxsZSBBcnQgZGVyIFItU3ludGF4LCBkaWUgZGFzIHNjaHJpdHR3ZWlzZSBVbWZvcm1lbiB2b24gRGF0ZW4gc3RhcmsgZXJsZWljaHRlcnQuIEltIFZlcmxhdWYgZGllc2VyIEVpbmbDvGhydW5nIHdlcmRlbiBwaXBlcyBzdGFyayBnZW51dHp0LCB2b3IgYWxsZW0sIHVtIEVyZ2Vibmlzc2UgZsO8ciBkaWUgRGFyc3RlbGx1bmcgbWl0dGVscyBbZ2dwbG90XShodHRwczovL2dncGxvdDIudGlkeXZlcnNlLm9yZy8pIHZvcnp1YmVyZWl0ZW4uCgpgYGB7ciBLb3Jwb3JhIHNlZ21lbnRpZXJlbiB1bmQgdHJpbW1lbn0KenVmYWxsc2Fic2F0eiA8LSBjb3JwdXNfc2VnbWVudChrb3JwdXMsICIgICIpICU+JSAKICBjb3JwdXNfdHJpbSh3aGF0ID0gImRvY3VtZW50cyIsIG1pbl9udG9rZW4gPSAzKSAlPiUgCiAgY29ycHVzX3NhbXBsZShzaXplID0gMSkKenVmYWxsc2Fic2F0elsxXQpgYGAKCiMjIyMgVG9rZW5pc2llcnVuZyAKClVudGVyIFRva2Vuc2llcnVuZyB2ZXJzdGVodCBtYW4gZGllIEF1ZnNwYWx0dW5nIGVpbmVzIFRleHRlcyBpbiBsYXVmZW5kZSBXw7ZydGVyIG9kZXIgc29nLiBOLUdyYW1tZSwgYWxzbyBTZXF1ZW56ZW4gbWVocmVyZXIgV8O2cnRlciBpbiBGb2xnZS4gRGllIEZ1bmt0aW9uIFt0b2tlbnNdKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvdG9rZW5zLmh0bWwpIHJlYWxpc2llcnQgZGllIFRva2VuaXNpZXJ1bmcgZWluZXMgS29ycHVzIGluIHF1YW50ZWRhLiBadXPDpHR6bGljaCB2ZXJzdGVodCAqdG9rZW5zKiBhdWNoIHVuesOkaGxpZ2UgQXJndW1lbnRlIGbDvHIgZGllIEVudGZlcm51bmcgYmVzdGltbXRlciBGZWF0dXJlcy4KCmBgYHtyIEVpbmZhY2hlIFRva2VuaXNpZXJ1bmd9Cm1laW5lLnRva2VucyA8LSB0b2tlbnMoa29ycHVzLCB2ZXJib3NlID0gVFJVRSkKaGVhZChhcy5saXN0KG1laW5lLnRva2VucykkYEEgU2NhbmRhbCBpbiBCb2hlbWlhYCwgMjApCmBgYAoKTWl0dGVscyBkZXIgRnVua3Rpb24gW3Rva2Vuc10oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS90b2tlbnMuaHRtbCkgbMOkc3N0IHNpY2ggZGVyIFRleHQgw7xiZXIgZGFzIEFyZ3VtZW50ICpuZ3JhbXMqIGF1Y2ggZ2xlaWNoIGluIE4tR3JhbW1lIChNZWhyd29ydHNlcXVlbnplbikgYXVmc3BhbHRlbi4gSW0gZm9sZ2VuZGVuIEJlaXNwaWVsIHdlcmRlbiBlcnN0IEJpZ3JhbW1lIHZvbSBBbmZhbmcgZGVzIGVyc3RlbiBUZXh0ZXMgYW5nZXplaWd0LCB1bmQgZGFubiBhbGxlIFNlcXVlbnplbiB2b24gZWluZW0sIHp3ZWkgb2RlciBkcmVpIEJlZ3JpZmZlbiBleHRyYWhpZXJ0IChkdXJjaCBkaWUgQW53ZW5kdW5nIHZvbiBbaGVhZF0oaHR0cHM6Ly93d3cucmRvY3VtZW50YXRpb24ub3JnL3BhY2thZ2VzL3V0aWxzL3ZlcnNpb25zLzMuNS4xL3RvcGljcy9oZWFkKSBzZWhlbiB3aXIgbnVyIFRyaWdyYW1tZSwgZXMgc2luZCBhYmVyIGF1Y2gga8O8cnplcmUgU2VxdWVuemVuIHZvcmhhbmRlbikuIAoKYGBge3IgTi1HcmFtbWUgZXh0cmFoaWVyZW59Cm1laW5lLm5ncmFtcyA8LSB0b2tlbnNfbmdyYW1zKG1laW5lLnRva2VucywgbiA9IDIpCnNhbXBsZShhcy5saXN0KG1laW5lLm5ncmFtcykkYEEgU2NhbmRhbCBpbiBCb2hlbWlhYCwgNSkKbWVpbmUubmdyYW1zIDwtIHRva2Vuc19uZ3JhbXMobWVpbmUudG9rZW5zLCBuID0gMTozKQpzYW1wbGUoYXMubGlzdChtZWluZS5uZ3JhbXMpJGBBIFNjYW5kYWwgaW4gQm9oZW1pYWAsIDUpCmBgYAoKSGlsZnJlaWNoIGlzdCBhdWNoIGRpZSBNw7ZnbGljaGtlaXQsIGJlaSBkZXIgVG9rZW5pc2llcnVuZyBiZXN0aW1tdGUgQmVncmlmZmUgenUgZW50ZmVybmVuIG9kZXIgenVyw7xja3p1YmVoYWx0ZW4uIAoKYGBge3IgVG9rZW5zIGVudGZlcm5lbiBvZGVyIGJlaGFsdGVufQptZWluZS50b2tlbnMgPC0gdG9rZW5zKGtvcnB1cykKYmVncmlmZmUuYmVoYWx0ZW4gPC0gdG9rZW5zX3NlbGVjdChtZWluZS50b2tlbnMsIGMoImhvbG1lcyIsICJ3YXRzb24iKSkgCmhlYWQoYXMubGlzdChiZWdyaWZmZS5iZWhhbHRlbikkYEEgU2NhbmRhbCBpbiBCb2hlbWlhYCkKYmVncmlmZmUuZW50ZmVybmVuIDwtIHRva2Vuc19yZW1vdmUobWVpbmUudG9rZW5zLCBjKCJzaGVybG9jayIsICJob2xtZXMiLCAiaW4iLCAiaXMiLCAidGhlIiwgImEiKSkKaGVhZChhcy5saXN0KGJlZ3JpZmZlLmVudGZlcm5lbikkYEEgU2NhbmRhbCBpbiBCb2hlbWlhYCkKYGBgCgpXaWUgYmVyZWl0cyBhbmdlZGV1dGV0IGFremVwdGllcnQgZGllIEZ1bmt0aW9uICp0b2tlbnMqIGVpbmUgUmVpaGUgdm9uIEFyZ3VtZW50ZW4sIG1pdCBkZW5lbiBnYW56ZSBLbGFzc2VuIHZvbiBaZWljaGVua2V0dGVuIChaYWhsZW4sIEludGVycHVua3Rpb24sIFN5bWJvbGUgdXN3LikgZ2V6aWVsdCBhdXNnZXNjaGxvc3NlbiBvZGVyIHp1csO8Y2tiZWhhbHRlbiB3ZXJkZW4ga8O2bm5lbi4gRm9sZ2VuZCB3ZXJkZW4genVuw6RjaHN0IFphaGxlbiwgSW50ZXJwdW5rdGlvbiB1bmQgU3ltYm9sZSBlbnRmZXJudCwgZGFubiBtaXR0ZWxzIFt0b2tlbnNfdG9sb3dlcl0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS90b2tlbnNfdG9sb3dlci5odG1sKSBhbGxlIFfDtnJ0ZXIgaW4gS2xlaW5zY2hyZWlidW5nIHVtZ2V3YW5kZWx0IHVuZCBkYW5uIGRhbm4gbm9jaCBkaWUgV8O2cnRlciAic2hlcmxvY2siIHVuZCAiaG9sbWVzIiwgc293aWUgZWluZSBSZWloZSBlbmdsaXNjaGVyIFtTdG9wcHfDtnJ0ZXJdKGh0dHBzOi8vZGUud2lraXBlZGlhLm9yZy93aWtpL1N0b3Bwd29ydCkgZW50ZmVybnQuIFdpciB2ZXJ3ZW5kZW4gaGllciBkcGx5ci1TeW50YXgsIHdlbmRlbiBhbHNvIG5hY2hlaW5hbmRlciBlaW5lIFJlaWhlIHZvbiBVbWZvcm11bmdlbiBhdWYgZGFzIGdsZWljaCBPYmpla3QgYW4uCgpgYGB7ciBUb2tlbmlzaWVydW5nIG1pdCB3ZWl0ZXJlbiBBcmd1bWVudGV9Cm1laW5lLnRva2VucyA8LSBrb3JwdXMgJT4lIAogIHRva2VucyhyZW1vdmVfbnVtYmVycyA9IFRSVUUsIHJlbW92ZV9wdW5jdCA9IFRSVUUsIHJlbW92ZV9zeW1ib2xzID0gVFJVRSkgJT4lIAogIHRva2Vuc190b2xvd2VyKCkgJT4lIAogIHRva2Vuc19yZW1vdmUoYyhzdG9wd29yZHMoImVuZ2xpc2giKSwgInNoZXJsb2NrIiwgImhvbG1lcyIpKQpoZWFkKGFzLmxpc3QobWVpbmUudG9rZW5zKSRgQSBTY2FuZGFsIGluIEJvaGVtaWFgKQpgYGAKCkRhcyBSZXN1bHRhdCBpc3QgZGVyIEFydCB2b24gRGF0ZW4sIG1pdCBkZW5lbiBtYW4gYmVpIFZlcmZhaHJlbiB3aWUgZGVyIEFud2VuZHVuZyB2b24gTGV4aWthIChLYXBpdGVsIDItMyksIGRlbSDDvGJlcndhY2h0ZW4gbWFzY2hpbmVsbGVuIExlcm5lbiAoS2FwaXRlbCA0KSB1bmQgZGVyIEJlcmVjaG51bmcgdm9uIFRoZW1lbm1vZGVsbGVuIChLYXBpdGVsIDUpLCBow6R1ZmlnIGFyYmVpdGV0IHNlaHIgw6RobmxpY2guIER1cmNoIGRpZSBTdG9wcHdvcnRlbnRmZXJudW5nIHVuZCBhbmRlcmUgU2Nocml0dGUgZ2VoZW4gc3ludGFrdGlzY2hlIEluZm9ybWF0aW9uZW4gdmVybG9yZW4sIGQuaC4gbWFuIGthbm4gbmljaHQgbWVociBuYWNodm9sbHppZWhlbiwgd2VyIHdhcyBtaXQgd2VtIHR1dCwgb2RlciB3aWUgZGVyIFRleHQgaW5zZ2VzYW10IGFyZ3VtZW50YXRpdiBvZGVyIGVyesOkaGxlcmlzY2ggYXVmZ2ViYXV0IGlzdC4gRGllc2UgSW5mb3JtYXRpb25lbiBzaW5kIGFsbGVyZGluZ3MgaW0gIltCYWctb2YtV29yZHMtQW5zYXR6XShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9CYWctb2Ytd29yZHNfbW9kZWwpIiwgZGVyIGluIGRlciBhdXRvbWF0aXNpZXJ0ZW4gSW5oYWx0c2FuYWx5c2UgbmFoZXp1IGltbWVyIHZlcndlbmRldCB3aXJkLCBuaWNodCB1bmJlZGluZ3QgcmVsZXZhbnQuCgpEaWUgaW4gZGllc2VtIEFic2Nobml0dCBiZXNjaHJpZWJlbmVuIFNjaHJpdHRlIHNpbmQgendhciBpbSBFaW56ZWxmYWxsIG7DvHR6bGljaCwgd2VyZGVuIGFiZXIgaW4gZGVuIGZvbGdlbmRlbiBLYXBpdGVsIHByYWt0aXNjaCBuaWNodCBhbmdld2FuZHQsIHdlaWwgZGllIERhdGVuIGRvcnQgc2Nob24gYWxzIHF1YW50ZWRh4oCTS29ycG9yYSB2b3JsaWVnZW4sIHVuZCB3ZWlsIHp1ZGVtIGjDpHVmaWcgYXVjaCBiaXMgYXVmIGRpZSBBbndlbmR1bmcgZGVyIEZ1bmt0aW9uICpjb3JwdXMqIGtlaW5lIHdlaXRlcmVuIFNjaHJpdHRlIG5vdHdlbmRpZyBzaW5kLiBEaWUgVG9rZW5pc2llcnVuZyB3aXJkIHp1ZGVtIGltcGxpeml0IGFuZ2V3YW5kdCwgc29iYWxkIGVpbmUgRG9rdW1lbnQtRmVhdHVyZS1NYXRyaXplIChERk0sIHMudS4pIGVyc3RlbGx0IHdpcmQuCgoKIyMjIyBEb2t1bWVudC1GZWF0dXJlLU1hdHJpemVuIChERk1zKSBlcnN0ZWxsZW4KCldpciBrb21tZW4gbnVuIHp1IGVpbmVyIHplbnRyYWxlbiBEYXRlbnN0cnVrdHVyIHZvbiBxdWFudGVkYSwgZGllIGltIEdlZ2Vuc2F0eiB6dSBkZW4genV2b3Igdm9yZ2VzdGVsbHRlbiBGdW5rdGlvbmVuIHByYWt0aXNjaCBpbiBqZWRlbSBQcm9qZWt0IHZvcmtvbW10OiBkaWUgRG9jdW1lbnQgRmVhdHVyZS1NYXRyaXplIChERk0pLiDDnGJsaWNoZXJ3ZWlzZSB3aXJkIGRpcmVrdCBuYWNoZGVtIGVpbiBLb3JwdXMgYW5nZWxlZ3Qgd3VyZGUgZWluZSBERk0gYmVyZWNobmV0LCB6dXdlaWxlbiBhdWNoIG1laHJlcmUuIEVpbmUgREZNIGlzdCBlaW5lIFRhYmVsbGUsIGRlcmVuIFplaWxlbiBUZXh0ZSB1bmQgZGVyZW4gU3BhbHRlbiBXb3J0ZnJlcXVlbnplbiBlbmhhbHRlbi4gRGFiZWkgZ2VoZW4gSW5mb3JtYXRpb25lbiBkYXLDvGJlciwgd28gaW4gZWluZW0gVGV4dCBlaW4gV29ydCB2b3Jrb21tdCB2ZXJsb3JlbiAobWFuIHNwcmljaHQgYXVjaCB2b20gJ1tCYWctb2YtV29yZHMtQW5zYXR6XShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9CYWctb2Ytd29yZHNfbW9kZWwpJyB1bmQgZGF2b24sIGRhc3MgREZNcyBpbSBHZWdlbnNhdHogenVtIGVpZ2VudGxpY2hlbiBLb3JwdXMgKm5pY2h0LXBvc2l0aW9uYWwqIHNpbmQpLiBJbW1lciBkYW5uLCB3ZW5uIHdpciB1bnMgZsO8ciBkaWUgQmV6aWVodW5nIHZvbiBXw7ZydGVybiB6dSBUZXh0ZW4gKHVuZCB1bWdla2VocnQpIGludGVyZXNzaWVyZW4sIGJlcmVjaG5lbiB3aXIgZWluZSBERk0uCgpJbSBERk1zIHp1IHZlcnN0ZWhlbiBoaWxmdCBlcywgbm9jaCBlaW5tYWwgenUgdW5zZXJlbSBrw7xuc3RsaWNoZW4gQmVpc3BpZWxrb3JwdXMgdm9tIEFuZmFuZyB6dXLDvGNrenVrZWhyZW4uIEVpbmUgREZNIHdpcmQgKnN1cnByaXNlLCBzdXJwcmlzZSogbWl0IEhpbGZlIGRlciBGdW5rdGlvbiBbZGZtXShodHRwczovL3F1YW50ZWRhLmlvL3JlZmVyZW5jZS9kZm0uaHRtbCkgZXJzdGVsbHQuIEdydW5kbGFnZSBzb2xsdGUgaW1tZXIgZWluIFF1YW50ZWRhLUtvcnB1cy1PYmpla3Qgc2VpbiwgYXVjaCB3ZW5uIHRoZW9yZXRpc2NoIGF1Y2ggVG9rZW5zLU9iamVrdGUgYWt6ZXB0aWVydCB3ZXJkZW4uIERhcyBCZWlzcGllbCB6ZWlndCwgd2FzIG1pdCBkZW4gVGV4dGVuIGltIEtvcnB1cyBnZXNjaGllaHQsIHdlbm4gZGllc2UgaW4gZWluZSBERk0gw7xiZXJmw7xocnQgd2VyZGVuLiAKCmBgYHtyIEVpbmUgRWluZmFjaGUgREZNIGVyc3RlbGxlbn0KYmVpc3BpZWxzYWV0emUgPC0gYygiRWluIEh1bmQga2FtIGluIGRpZSBLw7xjaGUiLAogICAgICAgICAgICAgICAgICAgICJJbiBkZXIgS8O8Y2hlIGdpYnQgZXMgbm9jaCBLYWZmZWUiLAogICAgICAgICAgICAgICAgICAgICJJbSBLYWZmZWUgZmVobHQgbm9jaCBkaWUgTWlsY2giKQpiZWlzcGllbGtvcnB1cyA8LSBjb3JwdXMoYmVpc3BpZWxzYWV0emUpCmJlaXNwaWVsZGZtIDwtIGRmbShiZWlzcGllbGtvcnB1cykKYmVpc3BpZWxkZm0KYGBgCgpEaWUgRnVua3Rpb24gbGllZmVydCB1bnMgZWluZSBUYWJlbGxlIChvZGVyIGdlbmF1ZXI6IGVpbiBSLU9iamVrdCB2b20gVHlwIE1hdHJpeCksIGluIGRlciBkaWUgWmVpbGVuIGRpZSBpbSBLb3JwdXMgZW50aGFsdGVuZW4gRG9rdW1lbnRlIHVuZCBkaWUgU3BhbHRlbiBkaWUgaW0gS29ycHVzIGVudGhhbHRlbmVuIFfDtnJ0ZXIgd2lkZXJnZWJlbi4gRGllIFphaGxlbiBpbiBkZW4gWmVsbGVuIGdlYmVuIGRpZSBXb3J0aGjDpHVmaWdrZWl0IGFuIC0tIGluIHVuc2VyZW0gc2ltcGxlbiBCZWlzcGllbCBrb21tdCBqZWRlcyBXb3J0IHBybyBUZXh0IG51ciBtYXhpbWFsIGVpbm1hbCB2b3IsIGRhcyBpc3QgYWJlciBpbiBlaW5lbSByaWNodGlnZW4gS29ycHVzIGR1cmNod2VnIGFuZGVycy4gRGllIEFub3JkdW5nIGRlciBTcGFsdGVuIGZvbGd0IHp3YXIgZGVtIFZvcmtvbW1lbiBkZXIgV8O2cnRlciBpbSBLb3JwdXMsIGFsbGVyZGluZ3MgdGF1Y2h0IGplZGVzIEZlYXR1cmUgbnVyIGVpbm1hbCBhdWYsIGRhIGVzIHNpY2ggamEgdW0gU3VtbWVuYW5nYWJlbiBoYW5kZWx0LiBCZWkgZGVyIHNvZy4gKlNwYXJzaXR5KiAoIjU0LjglIHNwYXJzZSIpIGhhbmRlbHQgZXMgc2ljaCB1bSBkZW4gUHJvemVudHNhdHogZGVyIFplbGxlbiwgaW4gZGVuZW4gZWluZSBOdWxsIHN0ZWh0IC0tIGRhenUgc3DDpHRlciBub2NoIGV0d2FzIG1laHIuICAKCldpZXNvIGhlacOfdCBlcyBiZWkgUXVhbnRlZGEgRCpGKk0sIHdpcmQgYWxzbyB2b24gKkZlYXR1cmVzKiBzdGF0dCB2b24gV8O2cnRlcm4gb2RlciBUZXJtZW4gZ2VzcHJvY2hlbj8gV2VpbCBkaWUgRmVhdHVyZXMgYXVjaCBaYWhsZW4sIFNhdHp6ZWljaGVuIG9kZXIgRW1qb2lzIHNlaW4ga8O2bm5lbi4gQXVjaCBTZXF1ZW56ZW4gYXVzIG1laHJlcmVuIFRva2VucyAoc29nLiBbTi1HcmFtbWVdKGh0dHBzOi8vZGUud2lraXBlZGlhLm9yZy93aWtpL04tR3JhbW0pKSBzaW5kIEZlYXR1cmVzLiAKCldhcyBoYXQgc2ljaCBnZWdlbsO8YmVyIGRlbSBLb3JwdXMgKGluIGRlbSB3aXIgamEgYXVjaCBzY2hvbiBkaWUgVHlwLSB1bmQgVG9rZW5hbnphaGwgcHJvIFRleHQgYmVyZWNobmVuIGtvbm50ZW4pIMO8YmVyaGF1cHQgdmVyw6RuZGVydD8gRWluZSBERk0gaXN0IG5pY2h0LXBvc2l0aW9uYWwsIGQuaC4gZGllIEluZm9ybWF0aW9uIGRhcsO8YmVyLCB3byBpbSBUZXh0IGVpbiBXb3J0IHZvcmtvbW10LCBpc3QgaW0gR2VnZW5zYXR6IHp1bSBLb3JwdXMgbmljaHQgbWVociB2b3JoYW5kZW4uIERhZsO8ciBoYWJlbiBERk1zIGplZG9jaCBlaW5lbiBlbnRzY2hlaW5kZW4gVm9ydGVpbDogU2llIGVybGF1YmVuIGVzIHVucywgZGllIERva3VtZW50ZSAoYW5oYW5kIGVpbmVzIERva3VtZW50ZW52ZWt0b3JzKSB1bmQgZGllIEZlYXR1cmVzIChhbmhhbmQgZWluZXMgRmVhdHVyZXZla3RvcnMpIG1pdGVpbmFuZGVyIHp1IHZlcmdsZWljaGVuLiBJbSBuw6RjaHN0ZW4gS2FwaXRlbCB3ZXJkZW4gd2lyIHVucyBkaWVzZW0gVW1zdGFuZCB6dW51dHplIG1hY2hlbiwgdW0gRG9rdW1lbnRlIHVuZCBXw7ZydGVyIG1pdGVpbmFuZGVyIHp1IHZlcmdsZWljaGVuLgoKV2lyIHdlbmRlbiBkYXMsIHdhcyB3aXIgZ2VyYWRlIGFuaGFuZCBkZXMgQmVpc3BpZWxrb3JwdXMgZ2V0ZXN0ZXQgaGFiZW4sIG51biBhdWYgZGFzIFNoZXJsb2NrIEhvbG1lcy1Lb3JwdXMgYW4uIERhYmVpIG51dHplbiB3aXIgcmVhbGlzdGlzY2hlcmUgRWluc3RlbGx1bmdlbiBmw7xyIGRpZSBUb2tlbmlzaWVydW5nLCBlbnRmZXJuZW4gYWxzbyBaYWhsZW4sIEludGVycHVua3Rpb24sIFN5bWJvbGUgdW5kIFN0b3Bwd8O2cnRlci4gCgpgYGB7ciBFaW5lIERGTSBmw7xyIGRhcyBTaGVybG9jayBIb2xtZXMtS29ycHVzIGVyc3RlbGxlbn0KbWVpbmUuZGZtIDwtIGRmbShrb3JwdXMsIHJlbW92ZV9udW1iZXJzID0gVFJVRSwgcmVtb3ZlX3B1bmN0ID0gVFJVRSwgcmVtb3ZlX3N5bWJvbHMgPSBUUlVFLCByZW1vdmUgPSBzdG9wd29yZHMoImVuZ2xpc2giKSkKbWVpbmUuZGZtCmBgYAoKV2VyIHNpY2ggZnJhZ3QsIHdvIGRpZXNlIEFyZ3VtZW50ZSBmw7xyIFtkZm1dKGh0dHBzOi8vcXVhbnRlZGEuaW8vcmVmZXJlbmNlL2RmbS5odG1sKSBlaWdlbnRsaWNoIGhlcmtvbW1lbjogSGllciB3aXJkIGltcGxpeml0IGRlciB1bnMgc2Nob24gdmVydHJhdXRlIEJlZmVobCBbdG9rZW5zXShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL3Rva2Vucy5odG1sKSBhbmdld2FuZHQsIHVtIGJlc3RpbW10ZSBGZWF0dXJlcyB6dSBlbnRmZXJuZW4gKG9kZXIgZ2dmLiBhdWNoIHp1csO8Y2t6dWJlaGFsdGVuKS4gRXJnZWJuaXMgaXN0IGVpbmUgZGV1dGxpY2ggZ3LDtsOfZXJlIE1hdHJpeCBhbHMgenV2b3IsIGRpZSBhdWNoIGVpbmUgZGV1dGxpY2ggaMO2aGVyZSBTcGFyc2l0eSBhdWZ3ZWlzdC4gRm9sZ2VuZCB0b2tlbmlzaWVyZW4gd2lyIHByYWt0aXNjaCBuaWUsIG9obmUgYXVjaCBnbGVpY2ggZWluZSBERk0genUgYmVyZWNobmVuLiAgCgpWaWVsZXMgZnVua3Rpb25pZXJ0IGJlaSBERk1zIGFuYWxvZyB6dXIgRXJzdGVsbHVuZyBlaW5lcyBLb3JwdXMuIFNvIHrDpGhsZW4gZGllIEZ1bmt0aW9uZW4gW25kb2NdKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvbmRvYy5odG1sKSB1bmQgW25mZWF0XShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL25kb2MuaHRtbCkgRG9rdW1lbnRlIHVuZCBGZWF0dXJlcyAoV8O2cnRlcikuCgpgYGB7ciBEb2t1bWVudGUgdW5kIEZlYXR1cmVzIHrDpGhsZW59Cm5kb2MobWVpbmUuZGZtKQpuZmVhdChtZWluZS5kZm0pCmBgYAoKTWl0dGVscyBkZXIgRnVua3Rpb25lbiBbZG9jbmFtZXNdKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvZG9jbmFtZXMuaHRtbCkgdW5kIApbZmVhdG5hbWVzXShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2ZlYXRuYW1lcy5odG1sKSBsYXNzZW4gc2ljaCBkaWUgTmFtZW4gZGVyIERva3VtZW50ZSB1bmQgRmVhdHVyZXMgYXVzZ2ViZW4uCgpgYGB7ciBEb2t1bWVudGUgdW5kIEZlYXR1cmVzIGFuemVpZ2VufQptZXNzYWdlKCJEb2NuYW1lcyIpCmhlYWQoZG9jbmFtZXMobWVpbmUuZGZtKSkgCm1lc3NhZ2UoIkZlYXRuYW1lcyIpCmhlYWQoZmVhdG5hbWVzKG1laW5lLmRmbSkpCmBgYAoKRGllIHRhYmVsbGFyaXNjaGUgQW5zaWNodCBpbGx1c3RyaWVydCBkZW4gSW5oYWx0IGRlciBERk0gYWxzIFRleHQtV29ydC1NYXRyaXggYW0gYmVzdGVuLiBXaWUgYmVyZWl0cyBhbmdlZGV1dGV0LCBiZXNjaHJlaWJ0IGRpZSAqU3BhcnNpdHkqICgiU3DDpHJsaWNoa2VpdCIpIGVpbmVyIERGTSBkYWJlaSBkZW4gQW50ZWlsIGRlciBsZWVyZW4gWmVsbGVuLCBhbHNvIFfDtnJ0ZXIsIGRpZSBudXIgaW4gc2VociB3ZW5pZ2VuIFRleHRlbiB2b3Jrb21tZW4uIFdpZSBzaWNoIGxlaWNodCBhYmxlaXRlbiBsw6Rzc3QsIHdlcmRlbiBERk1zIHNlaHIgc2NobmVsbCBzZWhyIGdyb8OfLiBadW0gR2zDvGNrIG1hY2h0IHNpY2ggUXVhbnRlZGEgZWluZSBSZWloZSB2b24gZsO8ciBkZW4gTnV0emVyIHVuc2ljaHRiYXJlbiBGdW5rdGlvbmVuIGF1cyBhbmRlcmVuIFBha2V0ZW4genVudXR6ZSwgdW0gZGllc2VtIFByb2JsZW0genUgYmVnZWduZW4uIAoKYGBge3IgRmVhdHVyZXMvVGV4dGUgYWxzIE1hdHJpeH0KaGVhZChtZWluZS5kZm0sIG4gPSAxMiwgbmYgPSA1KQpgYGAKCkdsZWljaCBhbiBkZW4gZXJzdGVuIEJsaWNrIGbDpGxsdCBhdWYsIGRhcyBkaWUgV8O2cnRlciAnc2hlcmxvY2snIHVuZCAnaG9sbWVzJyBpbiBhbGxlbiBSb21hbmVuIHZvcmtvbW1lbiwgYWxzbyBzZWhyIHdlbmlnIGRpc3Rpbmt0aXYgc2luZCwgd2VzaGFsYiB3aXIgc2llIHVudGVyIFVtc3TDpG5kZW4genUgZGVuIFN0b3Bwd8O2cnRlcm4gZsO8ciBkaWVzZXMgS29ycHVzIGhpbnp1ZsO8Z2VuIHNvbGx0ZW4uIAoKRGllIEZ1bmt0aW9uIFt0b3BmZWF0dXJlc10oaHR0cHM6Ly9xdWFudGVkYS5pby9yZWZlcmVuY2UvdG9wZmVhdHVyZXMuaHRtbCksIGRpZSB3aWUgYmVyZWl0cyBpbSBadXNhbW1lbmhhbmcgbWl0IEtvcnBvcmEga2VubmVuZ2VsZXJudCBoYWJlbiwgesOkaGx0IGF1Y2ggRmVhdHVyZXMgaW5uZXJoYWxiIGVpbmVyIERGTSBhdXMuIAoKYGBge3IgRWluZmFjaGUgV29ydGjDpHVmaWdrZWl0ZW4gYmVyZWNobmVufQp0b3BmZWF0dXJlcyhtZWluZS5kZm0pCmBgYAoKCiMjIyMgTWl0IERGTXMgYXJiZWl0ZW4KCkRGTXMgbGFzc2VuIHNpY2ggbWl0IFtkZm1fc29ydF0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9kZm1fc29ydC5odG1sKSBsZWljaHQgbmFjaCBEb2t1bWVudC0gdW5kIEZlYXR1cmUtRnJlcXVlbnplbiBzb3J0aWVyZW4uIERhcyBpc3QgaW4gZGVyIFByYXhpcyB1bnRlciBhbmRlcmVtIGRhbm4gaGlsZnJlaWNoLCB3ZW5uIG1hbiBlaW5lbiBiZXNzZXJlbiBFaW5ibGljayBpbiBkZW4gSW5oYWx0IGVpbmVyIGdyb8OfZW4gREZNIGJla29tbWVuIG3DtmNodGUuIEhpZXIgbGFzc2VuIHdpciB1bnMgenfDtmxmIERva3VtZW50ZSAobiA9IDEyKSB1bmQgKG51cikgZsO8bmYgRmVhdHVyZXMgKG5mID0gNSkgYXVzZ2ViZW4gdW5kIHNvcnRpZXJlbiBkYWJlaSBhYnN0ZWlnZW5kIG5hY2ggZGVyIEZlYXR1cmUtRnJlcXVlbnouCgpgYGB7ciBERk1zIHNvcnRpZXJlbn0KaGVhZChkZm1fc29ydChtZWluZS5kZm0sIGRlY3JlYXNpbmcgPSBUUlVFLCBtYXJnaW4gPSAiZmVhdHVyZXMiKSwgbiA9IDEyLCBuZiA9IDUpIApgYGAKCldlaXRlcmhpbiBsYXNzZW4gc2ljaCBiZXN0aW1tdGUgRmVhdHVyZXMgZWluZXIgREZNIGdlemllbHQgbWl0dGVscyBbZGZtX3NlbGVjdF0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9kZm1fc2VsZWN0Lmh0bWwpIGF1c3fDpGhsZW4uCgpgYGB7ciBERk1zIGZpbHRlcm59CmRmbV9zZWxlY3QobWVpbmUuZGZtLCBwYXR0ZXJuID0gImxvdioiKQpgYGAKCkRpZSBGdW5rdGlvbiBbZGZtX3dvcmRzdGVtXShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2RmbV93b3Jkc3RlbS5odG1sKSByZWR1emllcnQgV8O2cnRlciBhdWYgaWhyZSBTdGFtbWZvcm0uIERpZXNlIEZ1bmt0aW9uIGV4aXN0aWVydCBpbiBxdWFudGVkYSBkZXJ6ZWl0IG51ciBmw7xyIEVuZ2xpc2NoIHVuZCBpc3QgYXVjaCBkb3J0IG51ciBiZWdyZW56dCB6dXZlcmzDpHNzaWcsIHdhcyBkaWUgZm9sZ2VuZGUgQXVzZ2FiZSBndXQgaWxsdXN0cmllcnQgKCdob2xtJyBpc3QgbmF0w7xybGljaCBrZWluIFdvcnRzdGFtbSkuIAoKYGBge3IgV29ydHN0YW1tcmVkdWt0aW9ufQptZWluZS5kZm0uc3RlbW1lZCA8LSBkZm1fd29yZHN0ZW0obWVpbmUuZGZtKQp0b3BmZWF0dXJlcyhtZWluZS5kZm0uc3RlbW1lZCkKYGBgCgpFYmVuc28gd2llIGJlaSBXb3J0ZnJlcXVlbnplbiBpbiBLb3Jwb3JhIGlzdCBkaWUgR2V3aWNodHVuZyBlaW5lciBERk0gbmFjaCByZWxhdGl2ZW4gV29ydGZyZXF1ZW56ZW4gdW5kIFZlcmZhaHJlbiB3aWUgW1RGLUlERl0oaHR0cHM6Ly9kZS53aWtpcGVkaWEub3JnL3dpa2kvVGYtaWRmLU1hJUMzJTlGKSBvZnRtYWxzIHNpbm52b2xsLiBQcmFrdGlzY2hlcndlaXNlIGJlaGVycnNjaHQgZGllIFF1YW50ZWRhLUZ1bmt0aW9uIFtkZm1fd2VpZ2h0XShodHRwczovL2RvY3MucXVhbnRlZGEuaW8vcmVmZXJlbmNlL2RmbV93ZWlnaHQuaHRtbCkgZWluZSBnYW56ZSBSZWloZSB2b24gR2V3aWNodHVuZ3NhbnPDpHR6ZW4gdW0gcmVsYXRpdmUgV29ydGZyZXF1ZW56ZW4genUgYmVyZWNobmVuLiAKClp1bsOkY2hzdCBzY2hhdWVuIHdpciB1bnMgbm9jaCBlaW5tYWwgZGllIGFic29sdXRlbiBXb3J0ZnJlcXVlbnplbiBmw7xyIGRhcyBnZXNhbXRlIEtvcnB1cyBhbi4KCmBgYHtyIEFic29sdXRlIFdvcnRmcmVxdWVuemVuIGbDvHIgZGFzIEdlc2FtdGtvcnB1c30KdG9wZmVhdHVyZXMobWVpbmUuZGZtKQpgYGAKCkRhbm4gdmVyd2VuZGVuIHdpciBbZGZtX3dlaWdodF0oaHR0cHM6Ly9kb2NzLnF1YW50ZWRhLmlvL3JlZmVyZW5jZS9kZm1fd2VpZ2h0Lmh0bWwpIHVtIGRpZXNlIHByb3BvcnRpb25hbCB6dXIgR2VzYW10d29ydGFuemFobCBkZXMgUm9tYW5zIHp1IGdld2ljaHRlbi4gCgpgYGB7ciBSZWxhdGl2ZSBXb3J0ZnJlcXVlbnplbiBmw7xyIGRhcyBHZXNhbXRrb3JwdXN9Cm1laW5lLmRmbS5wcm9wb3J0aW9uYWwgPC0gZGZtX3dlaWdodChtZWluZS5kZm0sIHNjaGVtZSA9ICJwcm9wIikKdG9wZmVhdHVyZXMobWVpbmUuZGZtLnByb3BvcnRpb25hbCkKYGBgCgpEYXMgaXN0IGFiZXIgbm9jaCBuaWNodCBhbGxlcy4gRGllIEdld2ljaHR1bmcgZWluZXIgREZNIGJhc2llcnQgYmVpIGJlc3RpbW10ZW4gR2V3aWNodHVuZ3NtZWNoYW5pc21lbiAocHJvcG1heCwgYXVnbWVudGVkLCBsb2dhdmUgc293aWUgVEYtSURGKSBhdWYgZGVyIFdvcnQtRG9rdW1lbnQtUmVsYXRpb24sIHdlc2hhbGIgdG9wZmVhdHVyZXMoKSBpbiBLb21iaW5hdGlvbiBtaXQgZGllc2VuIEdld2ljaHR1bmdlbiBtZXJrd8O8cmRpZ2UgUmVzdWx0YXRlIHByb2R1emllcnQuCgpEaWUgR2V3aWNodHVuZ3NhbnPDpHR6ZSBQcm9wbWF4IHVuZCBURi1JREYgbGllZmVybiByZWxldmFudGUgV29ydG1ldHJpa2VuLCB6dW0gQmVpc3BpZWwgZsO8ciBkaWUgQmVzdGltbXVuZyB2b24gU3RvcHB3w7ZydGVybi4gUHJvcG1heCBza2FsaWVydCBkaWUgV29ydGjDpHVmaWdrZWl0IHJlbGF0aXYgenVtIGZyZXF1ZW50ZXN0ZW4gV29ydCAoaGllciAnaG9sbWVzJyBpbiAnQSBTY2FuZGFsIGluIEJvaGVtaWEnKS4gCgpgYGB7ciBHZXdpY2h0dW5nIGVpbmUgREZNIG5hY2ggUHJvcG1heCB9Cm1laW5lLmRmbS5wcm9wbWF4IDwtIGRmbV93ZWlnaHQobWVpbmUuZGZtLCBzY2hlbWUgPSAicHJvcG1heCIpCnRvcGZlYXR1cmVzKG1laW5lLmRmbS5wcm9wbWF4WzEsXSkKYGBgCgpGdW5rdGlvbmFsIMOkaG5lbG4gc2ljaCBURi1JREYgdW5kIGRlciBzcMOkdGVyIHZvcmdlc3RlbGx0ZSBLZXluZXNzLUFuc2F0eiAtLSBiZWlkZSBmaW5kZW4gYmVzb25kZXJzIGRpc3Rpbmt0aXZlIFRlcm1lLiBCZWltIFZlcmdsZWljaCB2b24gUHJvcG1heCB1bmQgVEYtSURGIHdpcmQgZGV1dGxpY2gsIGRhc3MgZGllIHfDtnJ0ZXIgJ2hvbG1lcycgdW5kICdzYWlkJyB6d2FyIGJlc29uZGVycyBow6R1ZmlnIGluICdBIFNjYW5kYWwgaW4gQm9oZW1pYScgdm9ya29tbWVuLCBhYmVyIG5pY2h0IHVuYmVkaW5ndCBiZXNvbmRlcnMgZGlzdGlua3RpdiBzaW5kLCB3ZWlsIHNpZSBsb2dpc2NoZXJ3ZWlzZSBpbSBnZXNhbXRlbiBLb3JwdXMgZnJlcXVlbnQgc2luZCwgd8OkaHJlbmQgZXR3YSBkYXMgV29ydCAncGhvdG9ncmFwaCcgc293b2hsIG9mdCB2b3Jrb21tdCwgYWxzIGF1Y2ggZsO8ciBkaWVzZW4gUm9tYW4ga2VubnplaWNobmVuZCBpc3QuCgpgYGB7ciBHZXdpY2h0dW5nIGVpbmVyIERGTSBuYWNoIFRGLUlERn0KbWVpbmUuZGZtLnRmaWRmIDwtIGRmbV90ZmlkZihtZWluZS5kZm0pCnRvcGZlYXR1cmVzKG1laW5lLmRmbS50ZmlkZlsxLF0pCmBgYAoKU2NobGllw59saWNoIGzDpHNzdCBzaWNoIG1pdCBbZGZtX3RyaW1dKGh0dHBzOi8vZG9jcy5xdWFudGVkYS5pby9yZWZlcmVuY2UvZGZtX3RyaW0uaHRtbCkgbm9jaCBlaW5lIHJlZHV6aWVydGVuIERva3VtZW50LUZlYXR1cmUtTWF0cml4IGVyc3RlbGxlbi4gRGFzIGlzdCBkYW5uIHNpbm52b2xsLCB3ZW5uIG1hbiBkYXZvbiBhdXNnZWh0LCBkYXNzIGJlaXNwaWVsc3dlaXNlIG51ciBzb2xjaGUgQmVncmlmZmUgZWluZSBSb2xsZSBzcGllbGVuLCBkaWUgbWluZGVzdGVzIFggbWFsIGltIEdlc2FtdGtvcnB1cyB2b3Jrb21tZW4uIEF1Y2ggZWluZSBNaW5kZXN0emFobCBvZGVyIGVpbiBNYXhpbXVtIGFuIERva3VtZW50ZW4sIGluIGRlbmVuIGVpbiBCZWdyaWZmIHZvcmtvbW1lbiBtdXNzIG9kZXIgZGFyZiwga2FubiBiZXN0aW1tdCB3ZXJkZW4uIFNjaGxpZcOfbGljaCBsYXNzZW4gc2ljaCBiZWlkZSBGaWx0ZXJvcHRpb25lbiBhdWNoIHByb3BvcnRpb25hbCBhbndlbmRlbiAodmdsLiBCZWlzcGllbCkuCgpEaWUgZXJzdGUgJ2dldHJpbW10ZScgREZNIGVudGjDpGx0IGxlZGlnbGljaCBzb2xjaGUgRmVhdHVyZXMsIGRpZSBtaW5kZXN0ZW5zIGluIDExIFJvbWFuZW4gdm9ya29tbWVuLCBkaWUgendlaXRlIGhpbmdlZ2VuIHNvbGNoZSBGZWF0dXJlcyBpbSA5NS4gSMOkdWZpZ2tlaXRzcGVyemVudGlsICg9VG9wIDUlIGFsbGVyIEZlYXR1cmVzKS4KCmBgYHtyIERGTXMgdHJpbW1lbn0KbWVzc2FnZSgiR2V0cmltbXRlIERGTSBtaXQgZG9jZnJlcSA9IDExIikKbWVpbmUuZGZtLnRyaW0gPC0gZGZtX3RyaW0obWVpbmUuZGZtLCBtaW5fZG9jZnJlcSA9IDExKQptZWluZS5kZm0udHJpbQoKbWVzc2FnZSgiR2V0cmltbXRlIERGTSBtaXQgbWluX3Rlcm1mcmVxID0gMC45NSIpCm1laW5lLmRmbS50cmltIDwtIGRmbV90cmltKG1laW5lLmRmbSwgbWluX3Rlcm1mcmVxID0gMC45NSwgdGVybWZyZXFfdHlwZSA9ICJxdWFudGlsZSIpCm1laW5lLmRmbS50cmltCmBgYAoKCiMjIyMgREZNcyB2aXN1YWxpc2llcmVuCgpERk1zIGxhc3NlbiBzaWNoIHNjaGxpZcOfbGljaCBhdWNoIGdyYWZpc2NoIGRhcnN0ZWxsZW4sIGV0d2EgYWxzIFdvcnR3b2xrZSBkZXIgaMOkdWZpZ3N0ZW4gQmVncmlmZmUuCgpgYGB7ciBXb3J0d29sa2UgbmFjaCBIw6R1Zmlna2VpdH0KdGV4dHBsb3Rfd29yZGNsb3VkKG1laW5lLmRmbSwgbWluX3NpemUgPSAxLCBtYXhfc2l6ZSA9IDUsIG1heF93b3JkcyA9IDEwMCkKYGBgCgpJbnRlcmVzc2FudGVyIGFscyBkaWUgRGFyc3RlbGx1bmcgZGVzIEdlc2FtdGtvcnB1cyBpc3QgYXVjaCBoaWVyIGRlciBWZXJnbGVpY2guIERhcyBmb2xnZW5kZSBQbG90IHplaWd0IGRpZSBkaXN0aW5rdGl2c3RlbiBCZWdyaWZmZSBmw7xyIHZpZXIgUm9tYW5lLCB3b2JlaSBkaWUgRmFyYmUgZGVuIGpld2VpbGlnZW4gUm9tYW4ga2VubnplaWNobmV0LiBEYXNzIGltIFBsb3QgZGllIFdvcnRncsO2w59lIG5pY2h0IGRpZSBhYnNvbHV0ZSBGcmVxdWVueiBhbnplaWd0LCBzb25kZXJuIGRpZSBqZXdlaWxzIGRpc3Rpbmt0aXZzdGVuIEJlZ3JpZmZlLCBtYWNodCBlaW4gc29sY2hlcyBQbG90IGbDvHIgZGVuIHVubWl0dGVsYmFyZW4gVmVyZ2xlaWNoIG7DvHR6bGljaC4KCmBgYHtyIFZlcmdsZWljaGVuZGUgV29ydHdvbGtlIGbDvHIgdmllciBSb21hbmV9CnRleHRwbG90X3dvcmRjbG91ZChtZWluZS5kZm1bMzo2LF0sIGNvbG9yID0gYnJld2VyLnBhbCg0LCAiU2V0MSIpLCBtaW5fc2l6ZSA9IDAuMiwgbWF4X3NpemUgPSA0LCBtYXhfd29yZHMgPSA1MCwgY29tcGFyaXNvbiA9IFRSVUUpCmBgYAoKIyMjIFdlaXRlcmbDvGhyZW5kZSBMZWt0w7xyZQoKR3J1bmRzw6R0emxpY2ggbXVzcyBpbiBkaWVzZW4gS2FwaXRlbCB6d2lzY2hlbiBkZW4gZ2VzY2hpbGRlcnRlcm4gZ3J1bmRsZWdlbmRlbiBWZXJmYWhyZW4gYXVzIGRlciBDb21wdXRlcmxpbmd1aXN0aWsgdW5kIEluZm9ybWF0aWsgZWluZXJzZWl0cyB1bmQgZGVuIGtvbmtyZXRlbiBGdW5rdGlvbnN1bWZhbmcgdm9uIHF1YW50ZWRhIHVudGVyc2NoaWVkZW4gd2VyZGVuLgoKVGVjaG5pa2VuIHdpZSBkaWUgVG9rZW5pc2llcnVuZyB2b24gS29ycG9yYSwgZGllIE5vcm1hbGlzaWVydW5nIHZvbiBXb3J0ZnJlcXVlbnplbiwgdW5kIGRpZSBFcnN0ZWxsdW5nIHVuZCBHZXdpY2h0dW5nIHZvbiBERk1zIChvZGVyIGtsYXNzaXNjaCAiZG9jdW1lbnQgZmVhdHVyZSBtYXRyaWNlcyIpIGlzdCBidWNoc3TDpGJsaWNoZXIgS2VybiBkZXIgQ29tcHV0ZXJsaW5ndWlzdGlrLCBkYWhlciBzb2xsdGUgZXMgbmljaHQgdmVyd3VuZGVybiwgZGFzcyBoaWVyIGF1ZmxhZ2Vuc3RhcmtlIFN0YW5kYXJkd2Vya2UgZXhpc3RpZXJlbi4gCgpIYXVwdHF1ZWxsZSB6dSBxdWFudGVkYSBpc3QgZGllIFByb2pla3dlYnNpdGUsIGRpZSBlaW5lcnNlaXRzIGVpbmUgUmVpaGUgdm9uIFR1dG9yaWFscyB6dSBlaW56ZWxuZW4gRWluc2F0emJlcmVpY2hlbiBlbnRow6RsdCAoZXR3YSBTZW50aW1lbnRhbmFseXNlLi4uKSwgZGVuZW4gZGllIGhpZXIgYXVmZ2Vmw7xocnRlbiBCZWlzcGllbGUgei5ULiBzZWhyIMOkaG5saWNoIHNpbmQsIHp1bSBhbmRlcmVuIGFiZXIgYXVjaCBlaW5lIEZ1bmt0aW9uc3JlZ2VyZW56LCB3ZWxjaGUgZGllIGVpbnplbG4gQmVmZWhsZSBzeXN0ZW1hdGlzY2ggZ3J1cHBpZXJ0IHVuZCBlcmtsw6RydC4gTmViZW4gZGVyIHF1YW50ZWRhIGdpdGh1Yi1TZWl0ZSwgw7xiZXIgZGllIG1hbiBkaWUgYWt0dWVsbGUgRW10d2lja2xlcnZlcnNpb24gZGVzIFBha2V0cyBiZXppZWhlbiBrYW5uLCBpc3Qgdm9yIGFsbGVtIGRhcyBxdWFudGVkYS1Gb3J1bSB6dSBlbXBmZWhsZW4sIHdvIGtvbmtyZXRlIEZyYWdlbiB2b20gUHJvZ3JhbW1pZXJlcnRlYW0gYmVhbnR3b3J0ZXQgd2VyZGVuLgoKCg==