Kolejnym popularnym sposobem łączenia wektora ze słowem jest zastosowanie gęstych wektorów słów, określanych również mianem osadzeń słów (ang. word embeddings). Wektory uzyskane przy użyciu techniki kodowania z gorącą jedynką są binarne, rzadkie (zawierają głównie zera) i wysokowymiarowe (liczba ich wymiarów jest równa liczbie słów wchodzących w skład słownika). Osadzenia słów charakteryzują się niską liczbą wymiarów. Są to gęste wektory zmiennoprzecinkowe (patrz rysunek 6.2). Osadzenia słów, w przeciwieństwie do wektorów uzyskanych za pomocą techniki kodowania z gorącą jedynką, są uczone z danych. Osadzenia słów mają zwykle 256, 512 wymiarów, a w przypadku pracy z bardzo dużymi słownikami nawet 1024 wymiary. Praca z wektorami słów zakodowanymi metodą gorącej jedynki zwykle prowadzi do wygenerowania wektorów mających 20 000 lub więcej wymiarów (wektor mający 20 000 wymiarów może być użyty do zakodowania 20 000 tokenów). Jak widać, osadzenia słów pozwalają zakodować więcej informacji w mniejszej liczbie wymiarów.

word embeddings vs. one hot encoding

word embeddings vs. one hot encoding

Osadzenia słów można tworzyć na dwa sposoby:

Przyjrzyjmy się obu rozwiązaniom.

Uczenie osadzeń słów przy użyciu warstwy osadzającej

Najprostszym sposobem powiązania gęstego wektora ze słowem jest wybranie losowego wektora. Problemem z takim rozwiązanie jest to, że wynikowa przestrzeń osadzeń nie ma struktury — np. słowa piękny i śliczny mogą skończyć w zupełnie różnych osadzeniach pomimo tego, że w większości zdań są to synonimy. Sieć neuronowa może mieć problem z uporządkowaniem tak zaszumianej i pozbawionej struktury przestrzeni osadzeń.

Przejdźmy na poziom pewnej abstrakcji. Geometryczna zależność między wektorami słów powinna odzwierciedlać semantyczną zależność między tymi słowami. Osadzenia słów mają odzwierciedlać język w przestrzeni geometrycznej. W poprawnej przestrzeni osadzeń synonimy powinny być osadzone w podobnych wektorach słów. Ogólnie rzecz biorąc, odległość geometryczna (np. odległość L2) między dowolnymi dwoma słowami powinna odzwierciedlać różnicę semantyczną między nimi. W przestrzeni osadzeń nie tylko odległość ma znaczenie — również kierunki powinny mieć określone znaczenie. Wyjaśnijmy to na konkretnym przykładzie.

W rzeczywistości przestrzenie osadzania pozwalają na wykonanie transformacji zmieniających płeć lub uzyskanie liczby mnogiej. Dodając wektor „rodzaju żeńskiego” do wektora „król”, uzyskamy wektor „królowa”. Dodając do wektora „król” wektor „liczba mnoga”, uzyskamy wektor „królowie”. Przestrzenie osadzeń słów zwykle zawierają tysiące wektorów, które można zinterpretować i z których można potencjalnie skorzystać.

Czy istnieje doskonała przestrzeń osadzania słów, która idealnie oddałaby zależności między słowami używanymi przez ludzi? Być może, ale nikt jej jeszcze nie uzyskał. Nie ma czegoś takiego jak język ludzki — ludzie posługują się wieloma różnymi językami, które nie są izometryczne (język stanowi odzwierciedlenie kultury i określonego kontekstu). W praktyce to, czy dana przestrzeń osadzeń jest dobra, zależy głównie od problemu, który chcemy rozwiązać. Idealna przestrzeń osadzeń słów języka angielskiego modelu analizy sentymentu recenzji filmów wygląda inaczej od idealnej przestrzeni osadzeń słów języka angielskiego modelu klasyfikującego dokumenty prawne. Wynika to z tego, że w każdym z tych problemów ważne są inne zależności semantyczne.

W związku z tym warto trenować nową przestrzeń osadzeń przy okazji rozwiązywania nowego problemu. Na szczęście proces ten ułatwia algorytm propagacji wstecznej oraz pakiet Keras. Wszystko sprowadza się do wyuczenia wartości wag warstwy osadzania layer_embedding.

rr library(keras)

The embedding layer takes at least two arguments:

the number of possible tokens, here 1000 (1 + maximum word index),

and the dimensionality of the embeddings, here 64.

embedding_layer <- layer_embedding(input_dim = 1000, output_dim = 64)

Warstwę embedding najlepiej jest rozumieć jako słownik mapujący całkowitoliczbowe indeksy oznaczające określone słowa na gęste wektory. Przyjmuje ona na wejściu wartości całkowitoliczbowe i wyszukuje je w wewnętrznym słowniku, zwracając związane z nimi wektory. Na rysunku 6.4 przedstawiono schemat operacji wyszukiwania w słowniku.

Warstwa embedding przyjmuje na wejściu dwuwymiarowy tensor o kształcie (próbki, długość_sekwencji). Każdy element tego tensora jest sekwencją liczb całkowitych. Warstwa ta może osadzać sekwencje o zmiennej długości: do zaprezentowanej w poprzednim przykładzie warstwy Embedding możliwe jest kierowanie wsadów o kształcie (32, 10) (32 sekwencje o długości równej 10) lub wsadów o kształcie (64, 15) (wsad 64 sekwencji o długości 15). Wszystkie sekwencje wchodzące w skład wsadu muszą mieć tę samą długość (wynika to z faktu umieszczania ich w tym samym tensorze), a więc sekwencje, które są krótsze od innych, należy dopełnić zerami, a sekwencje, które są dłuższe, należy uciąć.

Warstwa ta zwraca trójwymiarowy tensor zmiennoprzecinkowy o kształcie (próbki, długość_sekwencji, liczba_wymiarów_osadzenia). Taki trójwymiarowy tensor może zostać przetworzony przez warstwę RNN lub jednowymiarową warstwę konwolucyjną (oba rozwiązania opiszę w kolejnych sekcjach) . Podczas tworzenia instancji warstwy embedding jej wagi (wewnętrzny słownik wektorów tokenów) są inicjowane losowo (tak jak w przypadku każdej innej warstwy). Podczas trenowania wektory słów są stopniowo dostrajane przy użyciu algorytmu propagacji wstecznej, co prowadzi do uzyskania przestrzeni, z której może korzystać model. Po pełnym wytrenowaniu przestrzeń osadzeń będzie miała charakter struktury wyspecjalizowanej pod kątem rozwiązywania problemu, do którego trenowany jest model.

Zastosujmy to rozwiązanie w modelu przewidującym sentyment recenzji filmu wchodzącej w skład zbioru IMDB (z zadaniem tym próbowaliśmy się zmierzyć już wcześniej). Zacznijmy od szybkiego przygotowania danych. Ograniczymy zawartość recenzji do 10 000 najczęściej pojawiających się w nich słów (zabieg taki stosowaliśmy również podczas pierwszego podejścia do tego problemu), a następnie utniemy recenzje po zaledwie 20 słowach. Sieć będzie uczyć się ośmiowymiarowych osadzeń każdego z 10 000 słów, zamieni wejściową sekwencję wartości całkowitoliczbowych (dwuwymiarowy tensor całkowitoliczbowy) na sekwencje osadzone (trójwymiarowy tensor zmiennoprzecinkowy), spłaszczy ten tensor do dwóch wymiarów i wytrenuje pojedynczą warstwę Dense znajdującą się na końcu klasyfikatora.

rr # Number of words to consider as features max_features <- 10000 # Cut texts after this number of words # (among top max_features most common words) maxlen <- 20 # Load the data as lists of integers. imdb <- dataset_imdb(num_words = max_features) c(c(x_train, y_train), c(x_test, y_test)) %<-% imdb # This turns our lists of integers # into a 2D integer tensor of shape (samples, maxlen) x_train <- pad_sequences(x_train, maxlen = maxlen) x_test <- pad_sequences(x_test, maxlen = maxlen)

rr model <- keras_model_sequential() %>% # We specify the maximum input length to our Embedding layer # so we can later flatten the embedded inputs layer_embedding(input_dim = 10000, output_dim = 8, input_length = maxlen) %>% # We flatten the 3D tensor of embeddings # into a 2D tensor of shape (samples, maxlen * 8) layer_flatten() %>% # We add the classifier on top layer_dense(units = 1, activation = ) model %>% compile( optimizer = , loss = _crossentropy, metrics = c() ) history <- model %>% fit( x_train, y_train, epochs = 10, batch_size = 32, validation_split = 0.2 )

Podczas walidacji uzyskujemy dokładność na poziomie ~75%. Jest to wynik dość dobry, biorąc pod uwagę to, że analizujemy tylko 20 pierwszych słów każdej recenzji. Zwróć uwagę na to, że spłaszczenie osadzonych sekwencji i trenowanie jednej górnej warstwy dense prowadzi do uzyskania modelu traktującego niezależnie każde słowo sekwencji wejściowej. Model nie analizuje zależności między słowami i struktury zdania (potraktuje np. zdania this movie is a bomb i this movie is the bomb jako należące do recenzji negatywnych). O wiele lepszym rozwiązaniem jest dodanie do osadzonej sekwencji rekurencyjnych warstw lub jednowymiarowych warstw konwolucyjnych — pozwoli to na uczenie się cech biorących pod uwagę całość sekwencji. Rozwiązania te opiszę w kolejnych sekcjach.

Używanie trenowanych wcześniej osadzeń słów

Czasami dysponuje się tak małą ilością dostępnych danych, że niemożliwe jest korzystanie z samych tych danych w celu wytrenowania osadzenia słów właściwego dla problemu. Co można zrobić w takiej sytuacji?

Zamiast trenować osadzenia słów łącznie z rozwiązywanym problemem, można w takim przypadku załadować osadzające wektory z utworzonej wcześniej przestrzeni osadzania, o której wiadomo, że ma odpowiednią strukturę i właściwości — ujmuje ogólne aspekty struktury języka. Sens używania wytrenowanych wcześniej osadzeń słów podczas przetwarzania języka naturalnego jest taki sam jak sens używania wytrenowanych wcześniej konwolucyjnych sieci neuronowych podczas klasyfikacji obrazów — jeżeli nie dysponujemy wystarczającą ilością danych, aby model mógł wytrenować samodzielnie praktyczne cechy, a cechy, których wytrenowania oczekujemy, mają charakter ogólny, to w przypadku cech wizualnych i semantycznych możemy używać cech wyuczonych przez modele pracujące nad innymi problemami.

Tego typu osadzenia słów tworzy się zwykle, korzystając ze statystyk występowania słów (obserwacji tego, jakie słowa występują obok siebie w zdaniach lub dokumentach). Robi się to za pomocą wielu technik. Niektóre z nich są oparte na sieciach neuronowych. Pomysł tworzenia gęstej niskowymiarowej osadzającej przestrzeni słów tworzonej w sposób nienadzorowany był początkowo analizowany przez Bengio i innych na początku lat dwutysięcznych , ale zyskał popularność w zastosowaniach praktycznych dopiero po opublikowaniu najsłynniejszego algorytmu osadzania słów — algorytmu Word2vec. Algorytm ten został opracowany w 2013 r. przez pracownika firmy Google Tomasa Mikolova. Wymiary algorytmu Word2vec odzwierciedlają określone właściwości semantyczne, takie jak rodzaj (płeć).

Istnieje wiele gotowych baz osadzeń słów, które można pobrać i zaimportować do warstwy Embedding pakietu Keras. Jednym z takich rozwiązań jest właśnie Word2vec. Innym popularnym projektem jest Global Vectors for Word Representation (GloVe). Projekt ten został opracowany w 2014 r. przez badaczy z Uniwersytetu Stanforda. Ta technika osadzania korzysta z faktoryzacji macierzy wartości statystycznych określających wspólne występowanie słów. Jej autorzy udostępnili gotowe osadzenia milionów angielskich słów utworzone na podstawie danych z serwisów Wikipedia i Common Crawl.

Przyjrzyjmy się stosowaniu osadzeń GloVe w modelach Keras. W takim sam sposób można korzystać z osadzeń Word2vec i innych baz osadzeń słów. Pracując nad tym przykładem, przypomnimy również sobie opisane wcześniej techniki tokenizacji tekstu — pracę zaczniemy od surowego tekstu.

Łączenie wszystkich technik: od surowego tekstu do osadzenia słów

Będziemy korzystać z modelu podobnego do tego, który został przed chwilą opisany — zamienimy zdania na sekwencje wektorów, spłaszczymy je i będziemy trenować górną warstwę dense naszego modelu. Tym razem będziemy jednak korzystać z gotowych osadzeń słów. Zamiast wykorzystywać poddane tokenizacji dane zbioru IMDB dołączonego do pakietu Keras, zaczniemy od podstaw — pobierzemy dane w postaci surowego tekstu.

Pobieranie danych zbioru IMDB w postaci surowego tekstu

Na początku musimy wejść na stronę http://ai.stanford.edu/~amaas/data/sentiment pobrać archiwum z surowym zbiorem danych IMDB i je rozpakować.

Następnie należy zebrać poszczególne recenzje tworzące treningowy zbiór danych i przedstawić je w formie listy łańcuchów — każda recenzja powinna tworzyć oddzielny łańcuch. Etykiety recenzji (określenia ich tonu) umieścimy na liście labels.

rr imdb_dir <- ~/Downloads/aclImdb
train_dir <- file.path(imdb_dir, ) labels <- c() texts <- c() for (label_type in c(, )) { label <- switch(label_type, neg = 0, pos = 1) dir_name <- file.path(train_dir, label_type) for (fname in list.files(dir_name, pattern = glob2rx(*.txt), full.names = TRUE)) { texts <- c(texts, readChar(fname, file.info(fname)$size)) labels <- c(labels, label) } }

Tokenizacja danych

Dokonajmy konwersji danych tekstowych na wektory i przygotujmy zbiór treningowy i zbiór walidacyjny, korzystając z przedstawionych wcześniej koncepcji. Trenowane wcześniej osadzenia słów są szczególnie przydatne podczas pracy, gdy ma się dostęp do małej ilości danych treningowych (w przeciwnym razie osadzenia zoptymalizowane pod kątem danego problemu sprawdzą się o wiele lepiej), a więc ograniczmy treningowy zbiór danych do 200 pierwszych próbek. Będziemy trenować klasyfikator recenzji filmów na zaledwie 200 przykładach.

rr library(keras) maxlen <- 100 # We will cut reviews after 100 words training_samples <- 200 # We will be training on 200 samples validation_samples <- 10000 # We will be validating on 10000 samples max_words <- 10000 # We will only consider the top 10,000 words in the dataset tokenizer <- text_tokenizer(num_words = max_words) %>% fit_text_tokenizer(texts) sequences <- texts_to_sequences(tokenizer, texts) word_index = tokenizer$word_index cat(, length(word_index), tokens.)

Found 88584 unique tokens.

rr data <- pad_sequences(sequences, maxlen = maxlen) labels <- as.array(labels) cat(of data tensor:, dim(data), \n)

Shape of data tensor: 25000 100 

rr cat(‘Shape of label tensor:’, dim(labels), \n)

Shape of label tensor: 25000 

rr # Split the data into a training set and a validation set # But first, shuffle the data, since we started from data # where sample are ordered (all negative first, then all positive). indices <- sample(1:nrow(data)) training_indices <- indices[1:training_samples] validation_indices <- indices[(training_samples + 1): (training_samples + validation_samples)] x_train <- data[training_indices,] y_train <- labels[training_indices] x_val <- data[validation_indices,] y_val <- labels[validation_indices]

Pobieranie osadzeń słów GloVe

Ze strony https://nlp.stanford.edu/projects/glove/ możemy pobrać gotowe osadzenia słów wygenerowane w 2014 r. na podstawie artykułów w serwisie Wikipedia. Jest to archiwum ZIP zajmujące 822 MB (plik glove.6B.zip). Zawiera ono stuwymiarowe wektory osadzeń 400 000 słów (można je już określać mianem tokenów). Rozpakujmy to archiwum.

Wstępne przetwarzanie osadzeń

Przeprowadźmy operację parsowania rozpakowanego pliku tekstowego w celu zbudowania indeksu przypisującego słowa (w formie łańcuchów) do reprezentacji wektorowych (wektorów liczb).

rr glove_dir = ‘~/Downloads/glove.6B’ lines <- readLines(file.path(glove_dir, .6B.100d.txt)) embeddings_index <- new.env(hash = TRUE, parent = emptyenv()) for (i in 1:length(lines)) { line <- lines[[i]] values <- strsplit(line,  )[[1]] word <- values[[1]] embeddings_index[[word]] <- as.double(values[-1]) }

Następnie będziemy budować macierz osadzeń, którą można załadować do warstwy embedding. Macierz ta musi mieć kształt (max_words, embedding_dim), a element i ma zawierać wektor embedding_dim-wymiarowy dla słowa o indeksie i zbudowanym podczas tokenizacji. Zwrócimy uwagę na to, że indeks 0 nie ma odpowiadać żadnemu słowu ani tokenowi — jest to po prostu wypełniacz miejsca.

rr embedding_dim <- 100 embedding_matrix <- array(0, c(max_words, embedding_dim)) for (word in names(word_index)) { index <- word_index[[word]] if (index < max_words) { embedding_vector <- embeddings_index[[word]] if (!is.null(embedding_vector)) # Words not found in the embedding index will be all zeros. embedding_matrix[index+1,] <- embedding_vector } }

Definiowanie modelu

Będziemy korzystać z tej samej architektury co we wcześniejszym modelu:

rr model <- keras_model_sequential() %>% layer_embedding(input_dim = max_words, output_dim = embedding_dim, input_length = maxlen) %>% layer_flatten() %>% layer_dense(units = 32, activation = ) %>% layer_dense(units = 1, activation = ) summary(model)

_______________________________________________________________________________________________________________
Layer (type)                                     Output Shape                                 Param #          
===============================================================================================================
embedding_5 (Embedding)                          (None, 100, 100)                             1000000          
_______________________________________________________________________________________________________________
flatten_7 (Flatten)                              (None, 10000)                                0                
_______________________________________________________________________________________________________________
dense_76 (Dense)                                 (None, 32)                                   320032           
_______________________________________________________________________________________________________________
dense_77 (Dense)                                 (None, 1)                                    33               
===============================================================================================================
Total params: 1,320,065
Trainable params: 1,320,065
Non-trainable params: 0
_______________________________________________________________________________________________________________

Ładowanie osadzeń GloVe do modelu

Warstwa embedding ma pojedynczą macierz wag: dwuwymiarową macierz wartości zmiennoprzecinkowych, w której każdy element i jest wektorem słowa skojarzonym z indeksem i. Musimy po prostu załadować przygotowaną macierz GloVe do warstwy embedding, będącej pierwszą warstwą modelu.

rr get_layer(model, index = 1) %>% set_weights(list(embedding_matrix)) %>% freeze_weights()

Dodatkowo zamrozimy warstwę embedding. Sensowność tego rozwiązania wyjaśniałem już przy okazji korzystania z wytrenowanych wcześniej cech konwolucyjnych sieci neuronowych — gdy niektóre elementy modelu (takie jak warstwa embedding) zostały wytrenowane wcześniej, a inne elementy (takie jak klasyfikator) są inicjowane liczbami losowymi, nie powinno dochodzić do modyfikowania wytrenowanych wcześniej elementów modelu, ponieważ doprowadzi to do zniknięcia zapisanej w nich wiedzy. Duże wartości aktualizacji gradientu wywołane losowym charakterem niektórych elementów modelu spowodują wprowadzenie dużych zmian w wytrenowanych cechach.

Trenowanie i ewaluacja modelu

Skompilujmy model i go wytrenujmy.

rr model %>% compile( optimizer = , loss = _crossentropy, metrics = c() ) history <- model %>% fit( x_train, y_train, epochs = 20, batch_size = 32, validation_data = list(x_val, y_val) ) save_model_weights_hdf5(model, _trained_glove_model.h5)

Teraz możemy wygenerować wykresy ilustrujące zmiany wydajności modelu na przestrzeni czasu :

rr plot(history)

Model zaczyna szybko ulegać nadmiernemu dopasowaniu, co nie jest niczym zaskakującym przy tak małej liczbie próbek wchodzących w skład treningowego zbioru danych. Z tego samego powodu dokładność walidacji charakteryzuje się dużą zmiennością, ale wydaje się uzyskiwać szczytową wartość na poziomie przekraczającym 50%.

Przy tak małej liczbie próbek wchodzących w skład treningowego zbioru danych charakterystyka modelu w dużej mierze zależy od tego, które 200 próbek zostanie wybranych do treningowego zbioru danych (próbki te są wybierane w sposób losowy). Jeżeli Twój model uzyskuje wyraźnie gorsze parametry, to spróbuj wylosować inny zestaw 200 próbek (to tylko ćwiczenie, podczas pracy nad prawdziwym problemem nie dysponuje się możliwością wybrania danych treningowych).

Możemy również wytrenować ten sam model bez ładowania wytrenowanych osadzeń słów i bez zamrażania warstwy osadzeń. W takim przypadku model będzie trenować osadzenia tokenów wejściowych właściwe dla naszego problemu. Rozwiązanie to, ogólnie rzecz biorąc, sprawdza się o wiele lepiej od stosowania gotowych osadzeń słów w przypadku problemów, w których dysponuje się obszernym zbiorem danych. Dysponujemy zbiorem tylko 200 próbek treningowych, ale pomimo to wypróbujmy to rozwiązanie (patrz rysunek 6.6).

rr model <- keras_model_sequential() %>% layer_embedding(input_dim = max_words, output_dim = embedding_dim, input_length = maxlen) %>% layer_flatten() %>% layer_dense(units = 32, activation = ) %>% layer_dense(units = 1, activation = ) model %>% compile( optimizer = , loss = _crossentropy, metrics = c() ) history <- model %>% fit( x_train, y_train, epochs = 20, batch_size = 32, validation_data = list(x_val, y_val) )

rr plot(history)

Dokładność walidacji utrzymuje się na poziomie zbliżonym do 50%. W tym przypadku korzystanie z gotowych osadzeń słów pozwoliło na uzyskanie lepszych wyników. Jeżeli zwiększymy liczbę próbek wchodzących w skład treningowego zbioru danych dość szybko uzyskamy lepszą wydajność od tej, którą uzyskaliśmy, korzystając z gotowych osadzeń słów. Spróbuj zrobić to samodzielnie w ramach ćwiczeń.

Na koniec możemy sprawdzić wydajność modelu podczas przetwarzania danych testowych. W tym celu musimy zamienić dane testowe na tokeny:

rr test_dir <- file.path(imdb_dir, ) labels <- c() texts <- c() for (label_type in c(, )) { label <- switch(label_type, neg = 0, pos = 1) dir_name <- file.path(test_dir, label_type) for (fname in list.files(dir_name, pattern = glob2rx(*.txt), full.names = TRUE)) { texts <- c(texts, readChar(fname, file.info(fname)$size)) labels <- c(labels, label) } } sequences <- texts_to_sequences(tokenizer, texts) x_test <- pad_sequences(sequences, maxlen = maxlen) y_test <- as.array(labels)

Teraz możemy załadować pierwszy model i ocenić efekty jego pracy:

rr model %>% load_model_weights_hdf5(_trained_glove_model.h5) %>% evaluate(x_test, y_test, verbose = 0)

$loss
[1] 0.8509479

$acc
[1] 0.5598

Uzyskujemy dokładność testową na poziomie 56%. Praca z małą liczbą próbek treningowych jest naprawdę trudna!

LS0tDQp0aXRsZTogIk9zYWR6YW5pZSBzxYLDs3ciDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOiANCiAgICB0aGVtZTogY2VydWxlYW4NCiAgICBoaWdobGlnaHQ6IHRleHRtYXRlDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQod2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UpDQpgYGANCg0KDQoNCg0KS29sZWpueW0gcG9wdWxhcm55bSBzcG9zb2JlbSDFgsSFY3plbmlhIHdla3RvcmEgemUgc8WCb3dlbSBqZXN0IHphc3Rvc293YW5pZSBnxJlzdHljaCB3ZWt0b3LDs3cgc8WCw7N3LCBva3JlxZtsYW55Y2ggcsOzd25pZcW8IG1pYW5lbSBvc2FkemXFhCBzxYLDs3cgKGFuZy4gd29yZCBlbWJlZGRpbmdzKS4gV2VrdG9yeSB1enlza2FuZSBwcnp5IHXFvHljaXUgdGVjaG5pa2kga29kb3dhbmlhIHogZ29yxIVjxIUgamVkeW5rxIUgc8SFIGJpbmFybmUsIHJ6YWRraWUgKHphd2llcmFqxIUgZ8WCw7N3bmllIHplcmEpIGkgd3lzb2tvd3ltaWFyb3dlIChsaWN6YmEgaWNoIHd5bWlhcsOzdyBqZXN0IHLDs3duYSBsaWN6YmllIHPFgsOzdyB3Y2hvZHrEhWN5Y2ggdyBza8WCYWQgc8WCb3duaWthKS4gT3NhZHplbmlhIHPFgsOzdyBjaGFyYWt0ZXJ5enVqxIUgc2nEmSBuaXNrxIUgbGljemLEhSB3eW1pYXLDs3cuIFPEhSB0byBnxJlzdGUgd2VrdG9yeSB6bWllbm5vcHJ6ZWNpbmtvd2UgKHBhdHJ6IHJ5c3VuZWsgNi4yKS4gT3NhZHplbmlhIHPFgsOzdywgdyBwcnplY2l3aWXFhHN0d2llIGRvIHdla3RvcsOzdyB1enlza2FueWNoIHphIHBvbW9jxIUgdGVjaG5pa2kga29kb3dhbmlhIHogZ29yxIVjxIUgamVkeW5rxIUsIHPEhSB1Y3pvbmUgeiBkYW55Y2guIE9zYWR6ZW5pYSBzxYLDs3cgbWFqxIUgend5a2xlIDI1NiwgNTEyIHd5bWlhcsOzdywgYSB3IHByenlwYWRrdSBwcmFjeSB6IGJhcmR6byBkdcW8eW1pIHPFgm93bmlrYW1pIG5hd2V0IDEwMjQgd3ltaWFyeS4gUHJhY2EgeiB3ZWt0b3JhbWkgc8WCw7N3IHpha29kb3dhbnltaSBtZXRvZMSFIGdvcsSFY2VqIGplZHlua2kgend5a2xlIHByb3dhZHppIGRvIHd5Z2VuZXJvd2FuaWEgd2VrdG9yw7N3IG1hasSFY3ljaCAyMCAwMDAgbHViIHdpxJljZWogd3ltaWFyw7N3ICh3ZWt0b3IgbWFqxIVjeSAyMCAwMDAgd3ltaWFyw7N3IG1vxbxlIGJ5xIcgdcW8eXR5IGRvIHpha29kb3dhbmlhIDIwIDAwMCB0b2tlbsOzdykuIEphayB3aWRhxIcsIG9zYWR6ZW5pYSBzxYLDs3cgcG96d2FsYWrEhSB6YWtvZG93YcSHIHdpxJljZWogaW5mb3JtYWNqaSB3IG1uaWVqc3plaiBsaWN6YmllIHd5bWlhcsOzdy4NCg0KIVt3b3JkIGVtYmVkZGluZ3MgdnMuIG9uZSBob3QgZW5jb2RpbmddKGltZ1w2XzFfMi5wbmcpDQoNCk9zYWR6ZW5pYSBzxYLDs3cgbW/FvG5hIHR3b3J6ecSHIG5hIGR3YSBzcG9zb2J5Og0KDQoqCU1vZ8SFIG9uZSBiecSHIHVjem9uZSB3cmF6IHogZ8WCw7N3bnltIHphZGFuaWVtIChucC4ga2xhc3lmaWthY2rEhSBkb2t1bWVudHUgbHViIHByemV3aWR5d2FuaWVtIHNlbnR5bWVudHUpLiBXIHRha2ltIHByenlwYWRrdSBwcmFjxJkgemFjenluYSBzacSZIG9kIGxvc293eWNoIHdla3RvcsOzdyBzxYLDs3csIGEgbmFzdMSZcG5pZSB6YXdhcnRvxZvEhyB0eWNoIHdla3RvcsOzdyBqZXN0IHVjem9uYSB3IHRha2kgc2FtIHNwb3PDs2IgamFrIHdhZ2kgc2llY2kgbmV1cm9ub3dlai4NCioJRG8gbW9kZWx1IG1vxbxuYSB6YcWCYWRvd2HEhyBvc2FkemVuaWEgc8WCw7N3IHV0d29yem9uZSBwb2RjemFzIHByYWN5IG5hZCBpbm55bSBwcm9ibGVtZW0gdWN6ZW5pYSBtYXN6eW5vd2Vnby4gVGFraWUgb3NhZHplbmlhIG9rcmXFm2xhbXkgbWlhbmVtIHVwcnplZG5pbyB3eXRyZW5vd2FueWNoIG9zYWR6ZcWEIHPFgsOzdy4NCg0KUHJ6eWpyenlqbXkgc2nEmSBvYnUgcm96d2nEhXphbmlvbS4NCg0KDQojIyBVY3plbmllIG9zYWR6ZcWEIHPFgsOzdyBwcnp5IHXFvHljaXUgd2Fyc3R3eSBvc2FkemFqxIVjZWoNCg0KDQpOYWpwcm9zdHN6eW0gc3Bvc29iZW0gcG93acSFemFuaWEgZ8SZc3RlZ28gd2VrdG9yYSB6ZSBzxYJvd2VtIGplc3Qgd3licmFuaWUgbG9zb3dlZ28gd2VrdG9yYS4gUHJvYmxlbWVtIHogdGFraW0gcm96d2nEhXphbmllIGplc3QgdG8sIMW8ZSB3eW5pa293YSBwcnplc3RyemXFhCBvc2FkemXFhCBuaWUgbWEgc3RydWt0dXJ5IOKAlCBucC4gc8WCb3dhIHBpxJlrbnkgaSDFm2xpY3pueSBtb2fEhSBza2/FhGN6ecSHIHcgenVwZcWCbmllIHLDs8W8bnljaCBvc2FkemVuaWFjaCBwb21pbW8gdGVnbywgxbxlIHcgd2nEmWtzem/Fm2NpIHpkYcWEIHPEhSB0byBzeW5vbmlteS4gU2llxIcgbmV1cm9ub3dhIG1vxbxlIG1pZcSHIHByb2JsZW0geiB1cG9yesSFZGtvd2FuaWVtIHRhayB6YXN6dW1pYW5laiBpIHBvemJhd2lvbmVqIHN0cnVrdHVyeSBwcnplc3RyemVuaSBvc2FkemXFhC4NCg0KUHJ6ZWpkxbpteSBuYSBwb3ppb20gcGV3bmVqIGFic3RyYWtjamkuIEdlb21ldHJ5Y3puYSB6YWxlxbxub8WbxIcgbWnEmWR6eSB3ZWt0b3JhbWkgc8WCw7N3IHBvd2lubmEgb2R6d2llcmNpZWRsYcSHIHNlbWFudHljem7EhSB6YWxlxbxub8WbxIcgbWnEmWR6eSB0eW1pIHPFgm93YW1pLiBPc2FkemVuaWEgc8WCw7N3IG1hasSFIG9kendpZXJjaWVkbGHEhyBqxJl6eWsgdyBwcnplc3RyemVuaSBnZW9tZXRyeWN6bmVqLiBXIHBvcHJhd25laiBwcnplc3RyemVuaSBvc2FkemXFhCBzeW5vbmlteSBwb3dpbm55IGJ5xIcgb3NhZHpvbmUgdyBwb2RvYm55Y2ggd2VrdG9yYWNoIHPFgsOzdy4gT2fDs2xuaWUgcnplY3ogYmlvcsSFYywgb2RsZWfFgm/Fm8SHIGdlb21ldHJ5Y3puYSAobnAuIG9kbGVnxYJvxZvEhyBMMikgbWnEmWR6eSBkb3dvbG55bWkgZHdvbWEgc8WCb3dhbWkgcG93aW5uYSBvZHp3aWVyY2llZGxhxIcgcsOzxbxuaWPEmSBzZW1hbnR5Y3puxIUgbWnEmWR6eSBuaW1pLiBXIHByemVzdHJ6ZW5pIG9zYWR6ZcWEIG5pZSB0eWxrbyBvZGxlZ8WCb8WbxIcgbWEgem5hY3plbmllIOKAlCByw7N3bmllxbwga2llcnVua2kgcG93aW5ueSBtaWXEhyBva3JlxZtsb25lIHpuYWN6ZW5pZS4gV3lqYcWbbmlqbXkgdG8gbmEga29ua3JldG55bSBwcnp5a8WCYWR6aWUuDQoNClcgcnplY3p5d2lzdG/Fm2NpIHByemVzdHJ6ZW5pZSBvc2FkemFuaWEgcG96d2FsYWrEhSBuYSB3eWtvbmFuaWUgdHJhbnNmb3JtYWNqaSB6bWllbmlhasSFY3ljaCBwxYJlxIcgbHViIHV6eXNrYW5pZSBsaWN6YnkgbW5vZ2llai4gRG9kYWrEhWMgd2VrdG9yIOKAnnJvZHphanUgxbxlxYRza2llZ2/igJ0gZG8gd2VrdG9yYSDigJ5rcsOzbOKAnSwgdXp5c2thbXkgd2VrdG9yIOKAnmtyw7Nsb3dh4oCdLiBEb2RhasSFYyBkbyB3ZWt0b3JhIOKAnmtyw7Ns4oCdIHdla3RvciDigJ5saWN6YmEgbW5vZ2HigJ0sIHV6eXNrYW15IHdla3RvciDigJ5rcsOzbG93aWXigJ0uIFByemVzdHJ6ZW5pZSBvc2FkemXFhCBzxYLDs3cgend5a2xlIHphd2llcmFqxIUgdHlzacSFY2Ugd2VrdG9yw7N3LCBrdMOzcmUgbW/FvG5hIHppbnRlcnByZXRvd2HEhyBpIHoga3TDs3J5Y2ggbW/FvG5hIHBvdGVuY2phbG5pZSBza29yenlzdGHEhy4NCg0KQ3p5IGlzdG5pZWplIGRvc2tvbmHFgmEgcHJ6ZXN0cnplxYQgb3NhZHphbmlhIHPFgsOzdywga3TDs3JhIGlkZWFsbmllIG9kZGHFgmFieSB6YWxlxbxub8WbY2kgbWnEmWR6eSBzxYJvd2FtaSB1xbx5d2FueW1pIHByemV6IGx1ZHppPyBCecSHIG1vxbxlLCBhbGUgbmlrdCBqZWogamVzemN6ZSBuaWUgdXp5c2thxYIuIE5pZSBtYSBjemVnb8WbIHRha2llZ28gamFrIGrEmXp5ayBsdWR6a2kg4oCUIGx1ZHppZSBwb3PFgnVndWrEhSBzacSZIHdpZWxvbWEgcsOzxbxueW1pIGrEmXp5a2FtaSwga3TDs3JlIG5pZSBzxIUgaXpvbWV0cnljem5lIChqxJl6eWsgc3Rhbm93aSBvZHp3aWVyY2llZGxlbmllIGt1bHR1cnkgaSBva3JlxZtsb25lZ28ga29udGVrc3R1KS4gVyBwcmFrdHljZSB0bywgY3p5IGRhbmEgcHJ6ZXN0cnplxYQgb3NhZHplxYQgamVzdCBkb2JyYSwgemFsZcW8eSBnxYLDs3duaWUgb2QgcHJvYmxlbXUsIGt0w7NyeSBjaGNlbXkgcm96d2nEhXphxIcuIElkZWFsbmEgcHJ6ZXN0cnplxYQgb3NhZHplxYQgc8WCw7N3IGrEmXp5a2EgYW5naWVsc2tpZWdvIG1vZGVsdSBhbmFsaXp5IHNlbnR5bWVudHUgcmVjZW56amkgZmlsbcOzdyB3eWdsxIVkYSBpbmFjemVqIG9kIGlkZWFsbmVqIHByemVzdHJ6ZW5pIG9zYWR6ZcWEIHPFgsOzdyBqxJl6eWthIGFuZ2llbHNraWVnbyBtb2RlbHUga2xhc3lmaWt1asSFY2VnbyBkb2t1bWVudHkgcHJhd25lLiBXeW5pa2EgdG8geiB0ZWdvLCDFvGUgdyBrYcW8ZHltIHogdHljaCBwcm9ibGVtw7N3IHdhxbxuZSBzxIUgaW5uZSB6YWxlxbxub8WbY2kgc2VtYW50eWN6bmUuDQoNCg0KVyB6d2nEhXprdSB6IHR5bSB3YXJ0byB0cmVub3dhxIcgbm93xIUgcHJ6ZXN0cnplxYQgb3NhZHplxYQgcHJ6eSBva2F6amkgcm96d2nEhXp5d2FuaWEgbm93ZWdvIHByb2JsZW11LiBOYSBzemN6xJnFm2NpZSBwcm9jZXMgdGVuIHXFgmF0d2lhIGFsZ29yeXRtIHByb3BhZ2Fjamkgd3N0ZWN6bmVqIG9yYXogcGFraWV0IEtlcmFzLiBXc3p5c3RrbyBzcHJvd2FkemEgc2nEmSBkbyB3eXVjemVuaWEgd2FydG/Fm2NpIHdhZyB3YXJzdHd5IG9zYWR6YW5pYSBsYXllcl9lbWJlZGRpbmcuDQoNCmBgYHtyfQ0KbGlicmFyeShrZXJhcykNCg0KIyBXYXJzdHdhIGVtYmVkZGluZyBwcnp5am11amUgcHJ6eW5ham1uaWVqIGR3YSBhcmd1bWVudHk6DQojIGxpY3pixJkgdG9rZW7Ds3cgKHR1dGFqIDEwMDA6IDEgKyBtYWtzeW1hbG55IGluZGVrcyBzxYJvd2EpIGkgbGljemLEmSB3eW1pYXLDs3cgb3NhZHplxYQgKHR1dGFqIDY0KS4NCmVtYmVkZGluZ19sYXllciA8LSBsYXllcl9lbWJlZGRpbmcoaW5wdXRfZGltID0gMTAwMCwgb3V0cHV0X2RpbSA9IDY0KSANCmBgYA0KDQpXYXJzdHfEmSBlbWJlZGRpbmcgbmFqbGVwaWVqIGplc3Qgcm96dW1pZcSHIGpha28gc8WCb3duaWsgbWFwdWrEhWN5IGNhxYJrb3dpdG9saWN6Ym93ZSBpbmRla3N5IG96bmFjemFqxIVjZSBva3JlxZtsb25lIHPFgm93YSBuYSBnxJlzdGUgd2VrdG9yeS4gUHJ6eWptdWplIG9uYSBuYSB3ZWrFm2NpdSB3YXJ0b8WbY2kgY2HFgmtvd2l0b2xpY3pib3dlIGkgd3lzenVrdWplIGplIHcgd2V3bsSZdHJ6bnltIHPFgm93bmlrdSwgendyYWNhasSFYyB6d2nEhXphbmUgeiBuaW1pIHdla3RvcnkuIE5hIHJ5c3Vua3UgNi40IHByemVkc3Rhd2lvbm8gc2NoZW1hdCBvcGVyYWNqaSB3eXN6dWtpd2FuaWEgdyBzxYJvd25pa3UuDQoNCldhcnN0d2EgZW1iZWRkaW5nIHByenlqbXVqZSBuYSB3ZWrFm2NpdSBkd3V3eW1pYXJvd3kgdGVuc29yIG8ga3N6dGHFgmNpZSAocHLDs2JraSwgZMWCdWdvxZvEh19zZWt3ZW5jamkpLiBLYcW8ZHkgZWxlbWVudCB0ZWdvIHRlbnNvcmEgamVzdCBzZWt3ZW5jasSFIGxpY3piIGNhxYJrb3dpdHljaC4gV2Fyc3R3YSB0YSBtb8W8ZSBvc2FkemHEhyBzZWt3ZW5jamUgbyB6bWllbm5laiBkxYJ1Z2/Fm2NpOiBkbyB6YXByZXplbnRvd2FuZWogdyBwb3ByemVkbmltIHByenlrxYJhZHppZSB3YXJzdHd5IEVtYmVkZGluZyBtb8W8bGl3ZSBqZXN0IGtpZXJvd2FuaWUgd3NhZMOzdyBvIGtzenRhxYJjaWUgKDMyLCAxMCkgKDMyIHNla3dlbmNqZSBvIGTFgnVnb8WbY2kgcsOzd25laiAxMCkgbHViIHdzYWTDs3cgbyBrc3p0YcWCY2llICg2NCwgMTUpICh3c2FkIDY0IHNla3dlbmNqaSBvIGTFgnVnb8WbY2kgMTUpLiBXc3p5c3RraWUgc2Vrd2VuY2plIHdjaG9kesSFY2UgdyBza8WCYWQgd3NhZHUgbXVzesSFIG1pZcSHIHTEmSBzYW3EhSBkxYJ1Z2/Fm8SHICh3eW5pa2EgdG8geiBmYWt0dSB1bWllc3pjemFuaWEgaWNoIHcgdHltIHNhbXltIHRlbnNvcnplKSwgYSB3acSZYyBzZWt3ZW5jamUsIGt0w7NyZSBzxIUga3LDs3RzemUgb2QgaW5ueWNoLCBuYWxlxbx5IGRvcGXFgm5pxIcgemVyYW1pLCBhIHNla3dlbmNqZSwga3TDs3JlIHPEhSBkxYJ1xbxzemUsIG5hbGXFvHkgdWNpxIXEhy4NCg0KV2Fyc3R3YSB0YSB6d3JhY2EgdHLDs2p3eW1pYXJvd3kgdGVuc29yIHptaWVubm9wcnplY2lua293eSBvIGtzenRhxYJjaWUgKHByw7Nia2ksIGTFgnVnb8WbxIdfc2Vrd2VuY2ppLCBsaWN6YmFfd3ltaWFyw7N3X29zYWR6ZW5pYSkuIFRha2kgdHLDs2p3eW1pYXJvd3kgdGVuc29yIG1vxbxlIHpvc3RhxIcgcHJ6ZXR3b3J6b255IHByemV6IHdhcnN0d8SZIFJOTiBsdWIgamVkbm93eW1pYXJvd8SFIHdhcnN0d8SZIGtvbndvbHVjeWpuxIUgKG9iYSByb3p3acSFemFuaWEgb3Bpc3rEmSB3IGtvbGVqbnljaCBzZWtjamFjaCkNCi4NClBvZGN6YXMgdHdvcnplbmlhIGluc3RhbmNqaSB3YXJzdHd5IGVtYmVkZGluZyBqZWogd2FnaSAod2V3bsSZdHJ6bnkgc8WCb3duaWsgd2VrdG9yw7N3IHRva2Vuw7N3KSBzxIUgaW5pY2pvd2FuZSBsb3Nvd28gKHRhayBqYWsgdyBwcnp5cGFka3Uga2HFvGRlaiBpbm5laiB3YXJzdHd5KS4gUG9kY3phcyB0cmVub3dhbmlhIHdla3Rvcnkgc8WCw7N3IHPEhSBzdG9wbmlvd28gZG9zdHJhamFuZSBwcnp5IHXFvHljaXUgYWxnb3J5dG11IHByb3BhZ2Fjamkgd3N0ZWN6bmVqLCBjbyBwcm93YWR6aSBkbyB1enlza2FuaWEgcHJ6ZXN0cnplbmksIHoga3TDs3JlaiBtb8W8ZSBrb3J6eXN0YcSHIG1vZGVsLiBQbyBwZcWCbnltIHd5dHJlbm93YW5pdSBwcnplc3RyemXFhCBvc2FkemXFhCBixJlkemllIG1pYcWCYSBjaGFyYWt0ZXIgc3RydWt0dXJ5IHd5c3BlY2phbGl6b3dhbmVqIHBvZCBrxIV0ZW0gcm96d2nEhXp5d2FuaWEgcHJvYmxlbXUsIGRvIGt0w7NyZWdvIHRyZW5vd2FueSBqZXN0IG1vZGVsLg0KDQpaYXN0b3N1am15IHRvIHJvendpxIV6YW5pZSB3IG1vZGVsdSBwcnpld2lkdWrEhWN5bSBzZW50eW1lbnQgcmVjZW56amkgZmlsbXUgd2Nob2R6xIVjZWogdyBza8WCYWQgemJpb3J1IElNREIgKHogemFkYW5pZW0gdHltIHByw7Nib3dhbGnFm215IHNpxJkgem1pZXJ6ecSHIGp1xbwgd2N6ZcWbbmllaikuIFphY3puaWpteSBvZCBzenlia2llZ28gcHJ6eWdvdG93YW5pYSBkYW55Y2guIE9ncmFuaWN6eW15IHphd2FydG/Fm8SHIHJlY2VuemppIGRvIDEwIDAwMCBuYWpjesSZxZtjaWVqIHBvamF3aWFqxIVjeWNoIHNpxJkgdyBuaWNoIHPFgsOzdyAoemFiaWVnIHRha2kgc3Rvc293YWxpxZtteSByw7N3bmllxbwgcG9kY3phcyBwaWVyd3N6ZWdvIHBvZGVqxZtjaWEgZG8gdGVnbyBwcm9ibGVtdSksIGEgbmFzdMSZcG5pZSB1dG5pZW15IHJlY2VuemplIHBvIHphbGVkd2llIDIwIHPFgm93YWNoLiBTaWXEhyBixJlkemllIHVjennEhyBzacSZIG/Fm21pb3d5bWlhcm93eWNoIG9zYWR6ZcWEIGthxbxkZWdvIHogMTAgMDAwIHPFgsOzdywgemFtaWVuaSB3ZWrFm2Npb3fEhSBzZWt3ZW5jasSZIHdhcnRvxZtjaSBjYcWCa293aXRvbGljemJvd3ljaCAoZHd1d3ltaWFyb3d5IHRlbnNvciBjYcWCa293aXRvbGljemJvd3kpIG5hIHNla3dlbmNqZSBvc2Fkem9uZSAodHLDs2p3eW1pYXJvd3kgdGVuc29yIHptaWVubm9wcnplY2lua293eSksIHNwxYJhc3pjenkgdGVuIHRlbnNvciBkbyBkd8OzY2ggd3ltaWFyw7N3IGkgd3l0cmVudWplIHBvamVkeW5jesSFIHdhcnN0d8SZIERlbnNlIHpuYWpkdWrEhWPEhSBzacSZIG5hIGtvxYRjdSBrbGFzeWZpa2F0b3JhLg0KDQoNCmBgYHtyfQ0KIyBMaWN6YmEgc8WCw7N3IGFuYWxpem93YW55Y2ggdyBjaGFyYWt0ZXJ6ZSB3YWcuDQptYXhfZmVhdHVyZXMgPC0gMTAwMDANCiMgVWNpbmEgcmVjZW56amUsIHcga3TDs3J5Y2ggd3lzdMSZcHVqZSB0YSBsaWN6YmEgc8WCw7N3IA0KIyAoc8WCw7N3IHphbGljemFueWNoIGRvIHpiaW9ydSBtYXhfZmVhdHVyZXMgbmFqY3rEmcWbY2llaiB3eXN0xJlwdWrEhWN5Y2ggc8WCw7N3KS4NCm1heGxlbiA8LSAyMA0KDQojIMWBYWR1amUgZGFuZSB3IGZvcm1pZSBsaXN0IHdhcnRvxZtjaSBjYcWCa293aXRvbGljemJvd3ljaC4NCmltZGIgPC0gZGF0YXNldF9pbWRiKG51bV93b3JkcyA9IG1heF9mZWF0dXJlcykNCmMoYyh4X3RyYWluLCB5X3RyYWluKSwgYyh4X3Rlc3QsIHlfdGVzdCkpICU8LSUgaW1kYg0KDQojIFphbWllbmlhbXkgbGlzdHkgbGljemIgY2HFgmtvd2l0eWNoIG5hIGR3dXd5bWlhcm93eSB0ZW5zb3Igd2FydG/Fm2NpIGNhxYJrb3dpdG9saWN6Ym93eWNoIG8ga3N6dGHFgmNpZSAocHLDs2JraSwgbWF4bGVuKS4NCnhfdHJhaW4gPC0gcGFkX3NlcXVlbmNlcyh4X3RyYWluLCBtYXhsZW4gPSBtYXhsZW4pDQp4X3Rlc3QgPC0gcGFkX3NlcXVlbmNlcyh4X3Rlc3QsIG1heGxlbiA9IG1heGxlbikNCmBgYA0KDQpgYGB7ciwgZWNobz1UUlVFLCByZXN1bHRzPSdoaWRlJ30NCm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgDQogICMgT2tyZcWbbGFteSBtYWtzeW1hbG7EhSBkxYJ1Z2/Fm8SHIGRhbnljaCB3ZWrFm2Npb3d5Y2ggd2Fyc3R3eSBlbWJlZGRpbmcsDQogICMgY28gdW1vxbxsaXdpIHDDs8W6bmllanN6ZSBzcMWCYXN6Y3plbmllIG9zYWR6b255Y2ggZGFueWNoIHdlasWbY2lvd3ljaC4NCiAgbGF5ZXJfZW1iZWRkaW5nKGlucHV0X2RpbSA9IDEwMDAwLCBvdXRwdXRfZGltID0gOCwgDQogICAgICAgICAgICAgICAgICBpbnB1dF9sZW5ndGggPSBtYXhsZW4pICU+JSANCiAgIyBTcMWCYXN6Y3phbmllIHRyw7Nqd3ltaWFyb3dlZ28gdGVuc29yYSBvc2FkemXFhCANCiAgIyB3IGNlbHUgdXp5c2thbmlhIGR3dXd5bWlhcm93ZWdvIHRlbnNvcmEgbyBrc3p0YcWCY2llIChwcsOzYmtpLCBtYXhsZW4gKiA4KS4NCiAgbGF5ZXJfZmxhdHRlbigpICU+JSANCiAgIyBEb2Rhd2FuaWUgb3N0YXRuaWVqIHdhcnN0d3kga2xhc3lmaWthdG9yYS4NCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiKSANCg0KbW9kZWwgJT4lIGNvbXBpbGUoDQogIG9wdGltaXplciA9ICJybXNwcm9wIiwNCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwNCiAgbWV0cmljcyA9IGMoImFjYyIpDQopDQoNCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgNCiAgeF90cmFpbiwgeV90cmFpbiwNCiAgZXBvY2hzID0gMTAsDQogIGJhdGNoX3NpemUgPSAzMiwNCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMg0KKQ0KYGBgDQoNClBvZGN6YXMgd2FsaWRhY2ppIHV6eXNrdWplbXkgZG9rxYJhZG5vxZvEhyBuYSBwb3ppb21pZSB+NzUlLiBKZXN0IHRvIHd5bmlrIGRvxZvEhyBkb2JyeSwgYmlvcsSFYyBwb2QgdXdhZ8SZIHRvLCDFvGUgYW5hbGl6dWplbXkgdHlsa28gMjAgcGllcndzenljaCBzxYLDs3cga2HFvGRlaiByZWNlbnpqaS4gWndyw7PEhyB1d2FnxJkgbmEgdG8sIMW8ZSBzcMWCYXN6Y3plbmllIG9zYWR6b255Y2ggc2Vrd2VuY2ppIGkgdHJlbm93YW5pZSBqZWRuZWogZ8Ozcm5laiB3YXJzdHd5IGRlbnNlIHByb3dhZHppIGRvIHV6eXNrYW5pYSBtb2RlbHUgdHJha3R1asSFY2VnbyBuaWV6YWxlxbxuaWUga2HFvGRlIHPFgm93byBzZWt3ZW5jamkgd2VqxZtjaW93ZWouIE1vZGVsIG5pZSBhbmFsaXp1amUgemFsZcW8bm/Fm2NpIG1pxJlkenkgc8WCb3dhbWkgaSBzdHJ1a3R1cnkgemRhbmlhIChwb3RyYWt0dWplIG5wLiB6ZGFuaWEgdGhpcyBtb3ZpZSBpcyBhIGJvbWIgaSB0aGlzIG1vdmllIGlzIHRoZSBib21iIGpha28gbmFsZcW8xIVjZSBkbyByZWNlbnpqaSBuZWdhdHl3bnljaCkuIE8gd2llbGUgbGVwc3p5bSByb3p3acSFemFuaWVtIGplc3QgZG9kYW5pZSBkbyBvc2Fkem9uZWogc2Vrd2VuY2ppIHJla3VyZW5jeWpueWNoIHdhcnN0dyBsdWIgamVkbm93eW1pYXJvd3ljaCB3YXJzdHcga29ud29sdWN5am55Y2gg4oCUIHBvendvbGkgdG8gbmEgdWN6ZW5pZSBzacSZIGNlY2ggYmlvcsSFY3ljaCBwb2QgdXdhZ8SZIGNhxYJvxZvEhyBzZWt3ZW5jamkuIFJvendpxIV6YW5pYSB0ZSBvcGlzesSZIHcga29sZWpueWNoIHNla2NqYWNoLg0KDQojIyBVxbx5d2FuaWUgdHJlbm93YW55Y2ggd2N6ZcWbbmllaiBvc2FkemXFhCBzxYLDs3cNCg0KDQpDemFzYW1pIGR5c3BvbnVqZSBzacSZIHRhayBtYcWCxIUgaWxvxZtjacSFIGRvc3TEmXBueWNoIGRhbnljaCwgxbxlIG5pZW1vxbxsaXdlIGplc3Qga29yenlzdGFuaWUgeiBzYW15Y2ggdHljaCBkYW55Y2ggdyBjZWx1IHd5dHJlbm93YW5pYSBvc2FkemVuaWEgc8WCw7N3IHfFgmHFm2Npd2VnbyBkbGEgcHJvYmxlbXUuIENvIG1vxbxuYSB6cm9iacSHIHcgdGFraWVqIHN5dHVhY2ppPw0KDQpaYW1pYXN0IHRyZW5vd2HEhyBvc2FkemVuaWEgc8WCw7N3IMWCxIVjem5pZSB6IHJvendpxIV6eXdhbnltIHByb2JsZW1lbSwgbW/FvG5hIHcgdGFraW0gcHJ6eXBhZGt1IHphxYJhZG93YcSHIG9zYWR6YWrEhWNlIHdla3RvcnkgeiB1dHdvcnpvbmVqIHdjemXFm25pZWogcHJ6ZXN0cnplbmkgb3NhZHphbmlhLCBvIGt0w7NyZWogd2lhZG9tbywgxbxlIG1hIG9kcG93aWVkbmnEhSBzdHJ1a3R1csSZIGkgd8WCYcWbY2l3b8WbY2kg4oCUIHVqbXVqZSBvZ8OzbG5lIGFzcGVrdHkgc3RydWt0dXJ5IGrEmXp5a2EuIFNlbnMgdcW8eXdhbmlhIHd5dHJlbm93YW55Y2ggd2N6ZcWbbmllaiBvc2FkemXFhCBzxYLDs3cgcG9kY3phcyBwcnpldHdhcnphbmlhIGrEmXp5a2EgbmF0dXJhbG5lZ28gamVzdCB0YWtpIHNhbSBqYWsgc2VucyB1xbx5d2FuaWEgd3l0cmVub3dhbnljaCB3Y3plxZtuaWVqIGtvbndvbHVjeWpueWNoIHNpZWNpIG5ldXJvbm93eWNoIHBvZGN6YXMga2xhc3lmaWthY2ppIG9icmF6w7N3IOKAlCBqZcW8ZWxpIG5pZSBkeXNwb251amVteSB3eXN0YXJjemFqxIVjxIUgaWxvxZtjacSFIGRhbnljaCwgYWJ5IG1vZGVsIG3Ds2fFgiB3eXRyZW5vd2HEhyBzYW1vZHppZWxuaWUgcHJha3R5Y3puZSBjZWNoeSwgYSBjZWNoeSwga3TDs3J5Y2ggd3l0cmVub3dhbmlhIG9jemVrdWplbXksIG1hasSFIGNoYXJha3RlciBvZ8OzbG55LCB0byB3IHByenlwYWRrdSBjZWNoIHdpenVhbG55Y2ggaSBzZW1hbnR5Y3pueWNoIG1vxbxlbXkgdcW8eXdhxIcgY2VjaCB3eXVjem9ueWNoIHByemV6IG1vZGVsZSBwcmFjdWrEhWNlIG5hZCBpbm55bWkgcHJvYmxlbWFtaS4NCg0KVGVnbyB0eXB1IG9zYWR6ZW5pYSBzxYLDs3cgdHdvcnp5IHNpxJkgend5a2xlLCBrb3J6eXN0YWrEhWMgemUgc3RhdHlzdHlrIHd5c3TEmXBvd2FuaWEgc8WCw7N3IChvYnNlcndhY2ppIHRlZ28sIGpha2llIHPFgm93YSB3eXN0xJlwdWrEhSBvYm9rIHNpZWJpZSB3IHpkYW5pYWNoIGx1YiBkb2t1bWVudGFjaCkuIFJvYmkgc2nEmSB0byB6YSBwb21vY8SFIHdpZWx1IHRlY2huaWsuIE5pZWt0w7NyZSB6IG5pY2ggc8SFIG9wYXJ0ZSBuYSBzaWVjaWFjaCBuZXVyb25vd3ljaC4gUG9teXPFgiB0d29yemVuaWEgZ8SZc3RlaiBuaXNrb3d5bWlhcm93ZWogb3NhZHphasSFY2VqIHByemVzdHJ6ZW5pIHPFgsOzdyB0d29yem9uZWogdyBzcG9zw7NiIG5pZW5hZHpvcm93YW55IGJ5xYIgcG9jesSFdGtvd28gYW5hbGl6b3dhbnkgcHJ6ZXogQmVuZ2lvIGkgaW5ueWNoIG5hIHBvY3rEhXRrdSBsYXQgZHd1dHlzacSZY3pueWNoICwgYWxlIHp5c2thxYIgcG9wdWxhcm5vxZvEhyB3IHphc3Rvc293YW5pYWNoIHByYWt0eWN6bnljaCBkb3BpZXJvIHBvIG9wdWJsaWtvd2FuaXUgbmFqc8WCeW5uaWVqc3plZ28gYWxnb3J5dG11IG9zYWR6YW5pYSBzxYLDs3cg4oCUIGFsZ29yeXRtdSBXb3JkMnZlYy4gQWxnb3J5dG0gdGVuIHpvc3RhxYIgb3ByYWNvd2FueSB3IDIwMTMgci4gcHJ6ZXogcHJhY293bmlrYSBmaXJteSBHb29nbGUgVG9tYXNhIE1pa29sb3ZhLiBXeW1pYXJ5IGFsZ29yeXRtdSBXb3JkMnZlYyBvZHp3aWVyY2llZGxhasSFIG9rcmXFm2xvbmUgd8WCYcWbY2l3b8WbY2kgc2VtYW50eWN6bmUsIHRha2llIGphayByb2R6YWogKHDFgmXEhykuIA0KDQpJc3RuaWVqZSB3aWVsZSBnb3Rvd3ljaCBiYXogb3NhZHplxYQgc8WCw7N3LCBrdMOzcmUgbW/FvG5hIHBvYnJhxIcgaSB6YWltcG9ydG93YcSHIGRvIHdhcnN0d3kgRW1iZWRkaW5nIHBha2lldHUgS2VyYXMuIEplZG55bSB6IHRha2ljaCByb3p3acSFemHFhCBqZXN0IHfFgmHFm25pZSBXb3JkMnZlYy4gSW5ueW0gcG9wdWxhcm55bSBwcm9qZWt0ZW0gamVzdCBHbG9iYWwgVmVjdG9ycyBmb3IgV29yZCBSZXByZXNlbnRhdGlvbiAoR2xvVmUpLiBQcm9qZWt0IHRlbiB6b3N0YcWCIG9wcmFjb3dhbnkgdyAyMDE0IHIuIHByemV6IGJhZGFjenkgeiBVbml3ZXJzeXRldHUgU3RhbmZvcmRhLiBUYSB0ZWNobmlrYSBvc2FkemFuaWEga29yenlzdGEgeiBmYWt0b3J5emFjamkgbWFjaWVyenkgd2FydG/Fm2NpIHN0YXR5c3R5Y3pueWNoIG9rcmXFm2xhasSFY3ljaCB3c3DDs2xuZSB3eXN0xJlwb3dhbmllIHPFgsOzdy4gSmVqIGF1dG9yenkgdWRvc3TEmXBuaWxpIGdvdG93ZSBvc2FkemVuaWEgbWlsaW9uw7N3IGFuZ2llbHNraWNoIHPFgsOzdyB1dHdvcnpvbmUgbmEgcG9kc3Rhd2llIGRhbnljaCB6IHNlcndpc8OzdyBXaWtpcGVkaWEgaSBDb21tb24gQ3Jhd2wuDQoNClByenlqcnp5am15IHNpxJkgc3Rvc293YW5pdSBvc2FkemXFhCBHbG9WZSB3IG1vZGVsYWNoIEtlcmFzLiBXIHRha2ltIHNhbSBzcG9zw7NiIG1vxbxuYSBrb3J6eXN0YcSHIHogb3NhZHplxYQgV29yZDJ2ZWMgaSBpbm55Y2ggYmF6IG9zYWR6ZcWEIHPFgsOzdy4gUHJhY3VqxIVjIG5hZCB0eW0gcHJ6eWvFgmFkZW0sIHByenlwb21uaW15IHLDs3duaWXFvCBzb2JpZSBvcGlzYW5lIHdjemXFm25pZWogdGVjaG5pa2kgdG9rZW5pemFjamkgdGVrc3R1IOKAlCBwcmFjxJkgemFjem5pZW15IG9kIHN1cm93ZWdvIHRla3N0dS4NCg0KIyMgxYHEhWN6ZW5pZSB3c3p5c3RraWNoIHRlY2huaWs6IG9kIHN1cm93ZWdvIHRla3N0dSBkbyBvc2FkemVuaWEgc8WCw7N3DQoNCg0KQsSZZHppZW15IGtvcnp5c3RhxIcgeiBtb2RlbHUgcG9kb2JuZWdvIGRvIHRlZ28sIGt0w7NyeSB6b3N0YcWCIHByemVkIGNod2lsxIUgb3Bpc2FueSDigJQgemFtaWVuaW15IHpkYW5pYSBuYSBzZWt3ZW5jamUgd2VrdG9yw7N3LCBzcMWCYXN6Y3p5bXkgamUgaSBixJlkemllbXkgdHJlbm93YcSHIGfDs3JuxIUgd2Fyc3R3xJkgZGVuc2UgbmFzemVnbyBtb2RlbHUuIFR5bSByYXplbSBixJlkemllbXkgamVkbmFrIGtvcnp5c3RhxIcgeiBnb3Rvd3ljaCBvc2FkemXFhCBzxYLDs3cuIFphbWlhc3Qgd3lrb3J6eXN0eXdhxIcgcG9kZGFuZSB0b2tlbml6YWNqaSBkYW5lIHpiaW9ydSBJTURCIGRvxYLEhWN6b25lZ28gZG8gcGFraWV0dSBLZXJhcywgemFjem5pZW15IG9kIHBvZHN0YXcg4oCUIHBvYmllcnplbXkgZGFuZSB3IHBvc3RhY2kgc3Vyb3dlZ28gdGVrc3R1Lg0KDQojIyMgUG9iaWVyYW5pZSBkYW55Y2ggemJpb3J1IElNREIgdyBwb3N0YWNpIHN1cm93ZWdvIHRla3N0dQ0KDQoNCk5hIHBvY3rEhXRrdSBtdXNpbXkgd2VqxZvEhyBuYSBzdHJvbsSZIGh0dHA6Ly9haS5zdGFuZm9yZC5lZHUvfmFtYWFzL2RhdGEvc2VudGltZW50IHBvYnJhxIcgYXJjaGl3dW0geiBzdXJvd3ltIHpiaW9yZW0gZGFueWNoIElNREIgaSBqZSByb3pwYWtvd2HEhy4NCg0KTmFzdMSZcG5pZSBuYWxlxbx5IHplYnJhxIcgcG9zemN6ZWfDs2xuZSByZWNlbnpqZSB0d29yesSFY2UgdHJlbmluZ293eSB6YmnDs3IgZGFueWNoIGkgcHJ6ZWRzdGF3acSHIGplIHcgZm9ybWllIGxpc3R5IMWCYcWEY3VjaMOzdyDigJQga2HFvGRhIHJlY2VuemphIHBvd2lubmEgdHdvcnp5xIcgb2RkemllbG55IMWCYcWEY3VjaC4gRXR5a2lldHkgcmVjZW56amkgKG9rcmXFm2xlbmlhIGljaCB0b251KSB1bWllxZtjaW15IG5hIGxpxZtjaWUgbGFiZWxzLg0KDQpgYGB7cn0NCmltZGJfZGlyIDwtICJ+L0Rvd25sb2Fkcy9hY2xJbWRiIg0KdHJhaW5fZGlyIDwtIGZpbGUucGF0aChpbWRiX2RpciwgInRyYWluIikNCg0KbGFiZWxzIDwtIGMoKQ0KdGV4dHMgPC0gYygpDQoNCmZvciAobGFiZWxfdHlwZSBpbiBjKCJuZWciLCAicG9zIikpIHsNCiAgbGFiZWwgPC0gc3dpdGNoKGxhYmVsX3R5cGUsIG5lZyA9IDAsIHBvcyA9IDEpDQogIGRpcl9uYW1lIDwtIGZpbGUucGF0aCh0cmFpbl9kaXIsIGxhYmVsX3R5cGUpDQogIGZvciAoZm5hbWUgaW4gbGlzdC5maWxlcyhkaXJfbmFtZSwgcGF0dGVybiA9IGdsb2IycngoIioudHh0IiksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZnVsbC5uYW1lcyA9IFRSVUUpKSB7DQogICAgdGV4dHMgPC0gYyh0ZXh0cywgcmVhZENoYXIoZm5hbWUsIGZpbGUuaW5mbyhmbmFtZSkkc2l6ZSkpDQogICAgbGFiZWxzIDwtIGMobGFiZWxzLCBsYWJlbCkNCiAgfQ0KfQ0KYGBgDQoNCiMjIyBUb2tlbml6YWNqYSBkYW55Y2gNCg0KDQpEb2tvbmFqbXkga29ud2Vyc2ppIGRhbnljaCB0ZWtzdG93eWNoIG5hIHdla3RvcnkgaSBwcnp5Z290dWpteSB6YmnDs3IgdHJlbmluZ293eSBpIHpiacOzciB3YWxpZGFjeWpueSwga29yenlzdGFqxIVjIHogcHJ6ZWRzdGF3aW9ueWNoIHdjemXFm25pZWoga29uY2VwY2ppLiBUcmVub3dhbmUgd2N6ZcWbbmllaiBvc2FkemVuaWEgc8WCw7N3IHPEhSBzemN6ZWfDs2xuaWUgcHJ6eWRhdG5lIHBvZGN6YXMgcHJhY3ksIGdkeSBtYSBzacSZIGRvc3TEmXAgZG8gbWHFgmVqIGlsb8WbY2kgZGFueWNoIHRyZW5pbmdvd3ljaCAodyBwcnplY2l3bnltIHJhemllIG9zYWR6ZW5pYSB6b3B0eW1hbGl6b3dhbmUgcG9kIGvEhXRlbSBkYW5lZ28gcHJvYmxlbXUgc3ByYXdkesSFIHNpxJkgbyB3aWVsZSBsZXBpZWopLCBhIHdpxJljIG9ncmFuaWN6bXkgdHJlbmluZ293eSB6YmnDs3IgZGFueWNoIGRvIDIwMCBwaWVyd3N6eWNoIHByw7NiZWsuIELEmWR6aWVteSB0cmVub3dhxIcga2xhc3lmaWthdG9yIHJlY2VuemppIGZpbG3Ds3cgbmEgemFsZWR3aWUgMjAwIHByenlrxYJhZGFjaC4NCg0KYGBge3J9DQpsaWJyYXJ5KGtlcmFzKQ0KDQptYXhsZW4gPC0gMTAwICAgICAgICAgICAgICAgICAjIFNrcmFjYSByZWNlbnpqxJkgZG8gMTAwIHPFgsOzdy4NCnRyYWluaW5nX3NhbXBsZXMgPC0gMjAwICAgICAgICMgVHJlbm93YW5pZSBuYSAyMDAgcHLDs2JrYWNoLg0KdmFsaWRhdGlvbl9zYW1wbGVzIDwtIDEwMDAwICAgIyBXYWxpZGFjamEgbmEgMTAwMDAgcHLDs2Jlay4NCm1heF93b3JkcyA8LSAxMDAwMCAgICAgICAgICAgICMgQmllcnplbXkgcG9kIHV3YWfEmSB0eWxrbyAxMCAwMDAgc8WCw7N3IG5hamN6xJnFm2NpZWogd3lzdMSZcHVqxIVjeWNoIHcgemJpb3J6ZS4NCg0KdG9rZW5pemVyIDwtIHRleHRfdG9rZW5pemVyKG51bV93b3JkcyA9IG1heF93b3JkcykgJT4lIA0KICBmaXRfdGV4dF90b2tlbml6ZXIodGV4dHMpDQoNCnNlcXVlbmNlcyA8LSB0ZXh0c190b19zZXF1ZW5jZXModG9rZW5pemVyLCB0ZXh0cykNCg0Kd29yZF9pbmRleCA9IHRva2VuaXplciR3b3JkX2luZGV4DQpjYXQoIlpuYWxlemlvbm8iLCBsZW5ndGgod29yZF9pbmRleCksICJ1bmlrYXRvd3ljaCB0b2tlbsOzdy5cbiIpDQoNCmRhdGEgPC0gcGFkX3NlcXVlbmNlcyhzZXF1ZW5jZXMsIG1heGxlbiA9IG1heGxlbikNCg0KbGFiZWxzIDwtIGFzLmFycmF5KGxhYmVscykNCmNhdCgiS3N6dGHFgnQgdGVuc29yYSBkYW55Y2g6IiwgZGltKGRhdGEpLCAiXG4iKQ0KY2F0KCdLc3p0YcWCdCB0ZW5zb3JhIGV0eWtpZXQ6JywgZGltKGxhYmVscyksICJcbiIpDQoNCiMgRHppZWxpIGRhbmUgbmEgemJpw7NyIHRyZW5pbmdvd3kgaSB6YmnDs3Igd2FsaWRhY3lqbnksIA0KIyBhbGUgbmFqcGllcncgZGFuZSBzxIUgdXN0YXdpYW5lIHcgbG9zb3dlaiBrb2xlam5vxZtjaSANCiMgKG9iZWNuaWUgcHLDs2JraSBzxIUgdXN0YXdpb25lIHcga29sZWpub8WbY2kgb2QgcmVjZW56amkgbmVnYXR5d255Y2ggZG8gcmVjZW56amkgcG96eXR5d255Y2gpLg0KaW5kaWNlcyA8LSBzYW1wbGUoMTpucm93KGRhdGEpKQ0KdHJhaW5pbmdfaW5kaWNlcyA8LSBpbmRpY2VzWzE6dHJhaW5pbmdfc2FtcGxlc10NCnZhbGlkYXRpb25faW5kaWNlcyA8LSBpbmRpY2VzWyh0cmFpbmluZ19zYW1wbGVzICsgMSk6IA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKHRyYWluaW5nX3NhbXBsZXMgKyB2YWxpZGF0aW9uX3NhbXBsZXMpXQ0KDQp4X3RyYWluIDwtIGRhdGFbdHJhaW5pbmdfaW5kaWNlcyxdDQp5X3RyYWluIDwtIGxhYmVsc1t0cmFpbmluZ19pbmRpY2VzXQ0KDQp4X3ZhbCA8LSBkYXRhW3ZhbGlkYXRpb25faW5kaWNlcyxdDQp5X3ZhbCA8LSBsYWJlbHNbdmFsaWRhdGlvbl9pbmRpY2VzXQ0KYGBgDQoNCiMjIyBQb2JpZXJhbmllIG9zYWR6ZcWEIHPFgsOzdyBHbG9WZQ0KDQoNClplIHN0cm9ueSAgaHR0cHM6Ly9ubHAuc3RhbmZvcmQuZWR1L3Byb2plY3RzL2dsb3ZlLyBtb8W8ZW15IHBvYnJhxIcgZ290b3dlIG9zYWR6ZW5pYSBzxYLDs3cgd3lnZW5lcm93YW5lIHcgMjAxNCByLiBuYSBwb2RzdGF3aWUgYXJ0eWt1xYLDs3cgdyBzZXJ3aXNpZSBXaWtpcGVkaWEuIEplc3QgdG8gYXJjaGl3dW0gWklQIHpham11asSFY2UgODIyIE1CIChwbGlrIGdsb3ZlLjZCLnppcCkuIFphd2llcmEgb25vIHN0dXd5bWlhcm93ZSB3ZWt0b3J5IG9zYWR6ZcWEIDQwMCAwMDAgc8WCw7N3IChtb8W8bmEgamUganXFvCBva3JlxZtsYcSHIG1pYW5lbSB0b2tlbsOzdykuIFJvenBha3VqbXkgdG8gYXJjaGl3dW0uDQoNCiMjIyBXc3TEmXBuZSBwcnpldHdhcnphbmllIG9zYWR6ZcWEDQoNCg0KUHJ6ZXByb3dhZMW6bXkgb3BlcmFjasSZIHBhcnNvd2FuaWEgcm96cGFrb3dhbmVnbyBwbGlrdSB0ZWtzdG93ZWdvIHcgY2VsdSB6YnVkb3dhbmlhIGluZGVrc3UgcHJ6eXBpc3VqxIVjZWdvIHPFgm93YSAodyBmb3JtaWUgxYJhxYRjdWNow7N3KSBkbyByZXByZXplbnRhY2ppIHdla3Rvcm93eWNoICh3ZWt0b3LDs3cgbGljemIpLg0KDQpgYGB7cn0NCmdsb3ZlX2RpciA9ICd+L0Rvd25sb2Fkcy9nbG92ZS42QicNCmxpbmVzIDwtIHJlYWRMaW5lcyhmaWxlLnBhdGgoZ2xvdmVfZGlyLCAiZ2xvdmUuNkIuMTAwZC50eHQiKSkNCg0KZW1iZWRkaW5nc19pbmRleCA8LSBuZXcuZW52KGhhc2ggPSBUUlVFLCBwYXJlbnQgPSBlbXB0eWVudigpKQ0KZm9yIChpIGluIDE6bGVuZ3RoKGxpbmVzKSkgew0KICBsaW5lIDwtIGxpbmVzW1tpXV0NCiAgdmFsdWVzIDwtIHN0cnNwbGl0KGxpbmUsICIgIilbWzFdXQ0KICB3b3JkIDwtIHZhbHVlc1tbMV1dDQogIGVtYmVkZGluZ3NfaW5kZXhbW3dvcmRdXSA8LSBhcy5kb3VibGUodmFsdWVzWy0xXSkNCn0NCg0KY2F0KCJabmFsZXppb25vIiwgbGVuZ3RoKGVtYmVkZGluZ3NfaW5kZXgpLCAid2VrdG9yw7N3IHPFgsOzdy5cbiIpDQpgYGANCg0KTmFzdMSZcG5pZSBixJlkemllbXkgYnVkb3dhxIcgbWFjaWVyeiBvc2FkemXFhCwga3TDs3LEhSBtb8W8bmEgemHFgmFkb3dhxIcgZG8gd2Fyc3R3eSBlbWJlZGRpbmcuIE1hY2llcnogdGEgbXVzaSBtaWXEhyBrc3p0YcWCdCAobWF4X3dvcmRzLCBlbWJlZGRpbmdfZGltKSwgYSBlbGVtZW50IGkgbWEgemF3aWVyYcSHIHdla3RvciBlbWJlZGRpbmdfZGltLXd5bWlhcm93eSBkbGEgc8WCb3dhIG8gaW5kZWtzaWUgaSB6YnVkb3dhbnltIHBvZGN6YXMgdG9rZW5pemFjamkuIFp3csOzY2lteSB1d2FnxJkgbmEgdG8sIMW8ZSBpbmRla3MgMCBuaWUgbWEgb2Rwb3dpYWRhxIcgxbxhZG5lbXUgc8WCb3d1IGFuaSB0b2tlbm93aSDigJQgamVzdCB0byBwbyBwcm9zdHUgd3lwZcWCbmlhY3ogbWllanNjYS4NCg0KYGBge3J9DQplbWJlZGRpbmdfZGltIDwtIDEwMA0KDQplbWJlZGRpbmdfbWF0cml4IDwtIGFycmF5KDAsIGMobWF4X3dvcmRzLCBlbWJlZGRpbmdfZGltKSkNCg0KZm9yICh3b3JkIGluIG5hbWVzKHdvcmRfaW5kZXgpKSB7DQogIGluZGV4IDwtIHdvcmRfaW5kZXhbW3dvcmRdXQ0KICBpZiAoaW5kZXggPCBtYXhfd29yZHMpIHsNCiAgICBlbWJlZGRpbmdfdmVjdG9yIDwtIGVtYmVkZGluZ3NfaW5kZXhbW3dvcmRdXQ0KICAgIGlmICghaXMubnVsbChlbWJlZGRpbmdfdmVjdG9yKSkNCiAgICAgICMgU8WCb3dhIG5pZXpuYWxlemlvbmUgdyBvc2FkemFueW0gaW5kZWtzaWUgem9zdGFuxIUgemFzdMSFcGlvbmUgemVyYW1pDQogICAgICBlbWJlZGRpbmdfbWF0cml4W2luZGV4KzEsXSA8LSBlbWJlZGRpbmdfdmVjdG9yDQogIH0NCn0NCmBgYA0KDQojIyMgRGVmaW5pb3dhbmllIG1vZGVsdQ0KDQpCxJlkemllbXkga29yenlzdGHEhyB6IHRlaiBzYW1laiBhcmNoaXRla3R1cnkgY28gd2Ugd2N6ZcWbbmllanN6eW0gbW9kZWx1Og0KDQpgYGB7cn0NCm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgDQogIGxheWVyX2VtYmVkZGluZyhpbnB1dF9kaW0gPSBtYXhfd29yZHMsIG91dHB1dF9kaW0gPSBlbWJlZGRpbmdfZGltLCANCiAgICAgICAgICAgICAgICAgIGlucHV0X2xlbmd0aCA9IG1heGxlbikgJT4lIA0KICBsYXllcl9mbGF0dGVuKCkgJT4lIA0KICBsYXllcl9kZW5zZSh1bml0cyA9IDMyLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUgDQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIikNCg0Kc3VtbWFyeShtb2RlbCkNCmBgYA0KDQojIyMgxYFhZG93YW5pZSBvc2FkemXFhCBHbG9WZSBkbyBtb2RlbHUNCg0KDQpXYXJzdHdhIGVtYmVkZGluZyBtYSBwb2plZHluY3rEhSBtYWNpZXJ6IHdhZzogZHd1d3ltaWFyb3fEhSBtYWNpZXJ6IHdhcnRvxZtjaSB6bWllbm5vcHJ6ZWNpbmtvd3ljaCwgdyBrdMOzcmVqIGthxbxkeSBlbGVtZW50IGkgamVzdCB3ZWt0b3JlbSBzxYJvd2Egc2tvamFyem9ueW0geiBpbmRla3NlbSBpLiBNdXNpbXkgcG8gcHJvc3R1IHphxYJhZG93YcSHIHByenlnb3Rvd2FuxIUgbWFjaWVyeiBHbG9WZSBkbyB3YXJzdHd5IGVtYmVkZGluZywgYsSZZMSFY2VqIHBpZXJ3c3rEhSB3YXJzdHfEhSBtb2RlbHUuDQoNCmBgYHtyfQ0KZ2V0X2xheWVyKG1vZGVsLCBpbmRleCA9IDEpICU+JSANCiAgc2V0X3dlaWdodHMobGlzdChlbWJlZGRpbmdfbWF0cml4KSkgJT4lIA0KICBmcmVlemVfd2VpZ2h0cygpDQpgYGANCg0KRG9kYXRrb3dvIHphbXJvemlteSB3YXJzdHfEmSBlbWJlZGRpbmcuIFNlbnNvd25vxZvEhyB0ZWdvIHJvendpxIV6YW5pYSB3eWphxZtuaWHFgmVtIGp1xbwgcHJ6eSBva2F6amkga29yenlzdGFuaWEgeiB3eXRyZW5vd2FueWNoIHdjemXFm25pZWogY2VjaCBrb253b2x1Y3lqbnljaCBzaWVjaSBuZXVyb25vd3ljaCDigJQgZ2R5IG5pZWt0w7NyZSBlbGVtZW50eSBtb2RlbHUgKHRha2llIGphayB3YXJzdHdhIGVtYmVkZGluZykgem9zdGHFgnkgd3l0cmVub3dhbmUgd2N6ZcWbbmllaiwgYSBpbm5lIGVsZW1lbnR5ICh0YWtpZSBqYWsga2xhc3lmaWthdG9yKSBzxIUgaW5pY2pvd2FuZSBsaWN6YmFtaSBsb3Nvd3ltaSwgbmllIHBvd2lubm8gZG9jaG9kemnEhyBkbyBtb2R5Zmlrb3dhbmlhIHd5dHJlbm93YW55Y2ggd2N6ZcWbbmllaiBlbGVtZW50w7N3IG1vZGVsdSwgcG9uaWV3YcW8IGRvcHJvd2FkemkgdG8gZG8gem5pa25pxJljaWEgemFwaXNhbmVqIHcgbmljaCB3aWVkenkuIER1xbxlIHdhcnRvxZtjaSBha3R1YWxpemFjamkgZ3JhZGllbnR1IHd5d2/FgmFuZSBsb3Nvd3ltIGNoYXJha3RlcmVtIG5pZWt0w7NyeWNoIGVsZW1lbnTDs3cgbW9kZWx1IHNwb3dvZHVqxIUgd3Byb3dhZHplbmllIGR1xbx5Y2ggem1pYW4gdyB3eXRyZW5vd2FueWNoIGNlY2hhY2guDQoNCiMjIyBUcmVub3dhbmllIGkgZXdhbHVhY2phIG1vZGVsdQ0KDQpTa29tcGlsdWpteSBtb2RlbCBpIGdvIHd5dHJlbnVqbXkuDQoNCmBgYHtyLCBlY2hvPVRSVUUsIHJlc3VsdHM9J2hpZGUnfQ0KbW9kZWwgJT4lIGNvbXBpbGUoDQogIG9wdGltaXplciA9ICJybXNwcm9wIiwNCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwNCiAgbWV0cmljcyA9IGMoImFjYyIpDQopDQoNCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgNCiAgeF90cmFpbiwgeV90cmFpbiwNCiAgZXBvY2hzID0gMjAsDQogIGJhdGNoX3NpemUgPSAzMiwNCiAgdmFsaWRhdGlvbl9kYXRhID0gbGlzdCh4X3ZhbCwgeV92YWwpDQopDQoNCnNhdmVfbW9kZWxfd2VpZ2h0c19oZGY1KG1vZGVsLCAicHJlX3RyYWluZWRfZ2xvdmVfbW9kZWwuaDUiKQ0KYGBgDQoNClRlcmF6IG1vxbxlbXkgd3lnZW5lcm93YcSHIHd5a3Jlc3kgaWx1c3RydWrEhWNlIHptaWFueSB3eWRham5vxZtjaSBtb2RlbHUgbmEgcHJ6ZXN0cnplbmkgY3phc3UgOg0KDQpgYGB7cn0NCnBsb3QoaGlzdG9yeSkNCmBgYA0KDQpNb2RlbCB6YWN6eW5hIHN6eWJrbyB1bGVnYcSHIG5hZG1pZXJuZW11IGRvcGFzb3dhbml1LCBjbyBuaWUgamVzdCBuaWN6eW0gemFza2FrdWrEhWN5bSBwcnp5IHRhayBtYcWCZWogbGljemJpZSBwcsOzYmVrIHdjaG9kesSFY3ljaCB3IHNrxYJhZCB0cmVuaW5nb3dlZ28gemJpb3J1IGRhbnljaC4gWiB0ZWdvIHNhbWVnbyBwb3dvZHUgZG9rxYJhZG5vxZvEhyB3YWxpZGFjamkgY2hhcmFrdGVyeXp1amUgc2nEmSBkdcW8xIUgem1pZW5ub8WbY2nEhSwgYWxlIHd5ZGFqZSBzacSZIHV6eXNraXdhxIcgc3pjenl0b3fEhSB3YXJ0b8WbxIcgbmEgcG96aW9taWUgcHJ6ZWtyYWN6YWrEhWN5bSA1MCUuDQoNClByenkgdGFrIG1hxYJlaiBsaWN6YmllIHByw7NiZWsgd2Nob2R6xIVjeWNoIHcgc2vFgmFkIHRyZW5pbmdvd2VnbyB6YmlvcnUgZGFueWNoIGNoYXJha3RlcnlzdHlrYSBtb2RlbHUgdyBkdcW8ZWogbWllcnplIHphbGXFvHkgb2QgdGVnbywga3TDs3JlIDIwMCBwcsOzYmVrIHpvc3RhbmllIHd5YnJhbnljaCBkbyB0cmVuaW5nb3dlZ28gemJpb3J1IGRhbnljaCAocHLDs2JraSB0ZSBzxIUgd3liaWVyYW5lIHcgc3Bvc8OzYiBsb3Nvd3kpLiBKZcW8ZWxpIFR3w7NqIG1vZGVsIHV6eXNrdWplIHd5cmHFum5pZSBnb3JzemUgcGFyYW1ldHJ5LCB0byBzcHLDs2J1aiB3eWxvc293YcSHIGlubnkgemVzdGF3IDIwMCBwcsOzYmVrICh0byB0eWxrbyDEh3dpY3plbmllLCBwb2RjemFzIHByYWN5IG5hZCBwcmF3ZHppd3ltIHByb2JsZW1lbSBuaWUgZHlzcG9udWplIHNpxJkgbW/FvGxpd2/Fm2NpxIUgd3licmFuaWEgZGFueWNoIHRyZW5pbmdvd3ljaCkuDQoNCk1vxbxlbXkgcsOzd25pZcW8IHd5dHJlbm93YcSHIHRlbiBzYW0gbW9kZWwgYmV6IMWCYWRvd2FuaWEgd3l0cmVub3dhbnljaCBvc2FkemXFhCBzxYLDs3cgaSBiZXogemFtcmHFvGFuaWEgd2Fyc3R3eSBvc2FkemXFhC4gVyB0YWtpbSBwcnp5cGFka3UgbW9kZWwgYsSZZHppZSB0cmVub3dhxIcgb3NhZHplbmlhIHRva2Vuw7N3IHdlasWbY2lvd3ljaCB3xYJhxZtjaXdlIGRsYSBuYXN6ZWdvIHByb2JsZW11LiBSb3p3acSFemFuaWUgdG8sIG9nw7NsbmllIHJ6ZWN6IGJpb3LEhWMsIHNwcmF3ZHphIHNpxJkgbyB3aWVsZSBsZXBpZWogb2Qgc3Rvc293YW5pYSBnb3Rvd3ljaCBvc2FkemXFhCBzxYLDs3cgdyBwcnp5cGFka3UgcHJvYmxlbcOzdywgdyBrdMOzcnljaCBkeXNwb251amUgc2nEmSBvYnN6ZXJueW0gemJpb3JlbSBkYW55Y2guIER5c3BvbnVqZW15IHpiaW9yZW0gdHlsa28gMjAwIHByw7NiZWsgdHJlbmluZ293eWNoLCBhbGUgcG9taW1vIHRvIHd5cHLDs2J1am15IHRvIHJvendpxIV6YW5pZSAocGF0cnogcnlzdW5layA2LjYpLg0KDQpgYGB7ciwgZWNobz1UUlVFLCByZXN1bHRzPSdoaWRlJ30NCm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgDQogIGxheWVyX2VtYmVkZGluZyhpbnB1dF9kaW0gPSBtYXhfd29yZHMsIG91dHB1dF9kaW0gPSBlbWJlZGRpbmdfZGltLCANCiAgICAgICAgICAgICAgICAgIGlucHV0X2xlbmd0aCA9IG1heGxlbikgJT4lIA0KICBsYXllcl9mbGF0dGVuKCkgJT4lIA0KICBsYXllcl9kZW5zZSh1bml0cyA9IDMyLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUgDQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIikNCg0KbW9kZWwgJT4lIGNvbXBpbGUoDQogIG9wdGltaXplciA9ICJybXNwcm9wIiwNCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwNCiAgbWV0cmljcyA9IGMoImFjYyIpDQopDQoNCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgNCiAgeF90cmFpbiwgeV90cmFpbiwNCiAgZXBvY2hzID0gMjAsDQogIGJhdGNoX3NpemUgPSAzMiwNCiAgdmFsaWRhdGlvbl9kYXRhID0gbGlzdCh4X3ZhbCwgeV92YWwpDQopDQpgYGANCg0KYGBge3J9DQpwbG90KGhpc3RvcnkpDQpgYGANCg0KRG9rxYJhZG5vxZvEhyB3YWxpZGFjamkgdXRyenltdWplIHNpxJkgbmEgcG96aW9taWUgemJsacW8b255bSBkbyA1MCUuIFcgdHltIHByenlwYWRrdSBrb3J6eXN0YW5pZSB6IGdvdG93eWNoIG9zYWR6ZcWEIHPFgsOzdyBwb3p3b2xpxYJvIG5hIHV6eXNrYW5pZSBsZXBzenljaCB3eW5pa8Ozdy4gSmXFvGVsaSB6d2nEmWtzenlteSBsaWN6YsSZIHByw7NiZWsgd2Nob2R6xIVjeWNoIHcgc2vFgmFkIHRyZW5pbmdvd2VnbyB6YmlvcnUgZGFueWNoIGRvxZvEhyBzenlia28gdXp5c2thbXkgbGVwc3rEhSB3eWRham5vxZvEhyBvZCB0ZWosIGt0w7NyxIUgdXp5c2thbGnFm215LCBrb3J6eXN0YWrEhWMgeiBnb3Rvd3ljaCBvc2FkemXFhCBzxYLDs3cuIFNwcsOzYnVqIHpyb2JpxIcgdG8gc2Ftb2R6aWVsbmllIHcgcmFtYWNoIMSHd2ljemXFhC4NCg0KTmEga29uaWVjIG1vxbxlbXkgc3ByYXdkemnEhyB3eWRham5vxZvEhyBtb2RlbHUgcG9kY3phcyBwcnpldHdhcnphbmlhIGRhbnljaCB0ZXN0b3d5Y2guIFcgdHltIGNlbHUgbXVzaW15IHphbWllbmnEhyBkYW5lIHRlc3Rvd2UgbmEgdG9rZW55Og0KDQoNCmBgYHtyfQ0KdGVzdF9kaXIgPC0gZmlsZS5wYXRoKGltZGJfZGlyLCAidGVzdCIpDQoNCmxhYmVscyA8LSBjKCkNCnRleHRzIDwtIGMoKQ0KDQpmb3IgKGxhYmVsX3R5cGUgaW4gYygibmVnIiwgInBvcyIpKSB7DQogIGxhYmVsIDwtIHN3aXRjaChsYWJlbF90eXBlLCBuZWcgPSAwLCBwb3MgPSAxKQ0KICBkaXJfbmFtZSA8LSBmaWxlLnBhdGgodGVzdF9kaXIsIGxhYmVsX3R5cGUpDQogIGZvciAoZm5hbWUgaW4gbGlzdC5maWxlcyhkaXJfbmFtZSwgcGF0dGVybiA9IGdsb2IycngoIioudHh0IiksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZnVsbC5uYW1lcyA9IFRSVUUpKSB7DQogICAgdGV4dHMgPC0gYyh0ZXh0cywgcmVhZENoYXIoZm5hbWUsIGZpbGUuaW5mbyhmbmFtZSkkc2l6ZSkpDQogICAgbGFiZWxzIDwtIGMobGFiZWxzLCBsYWJlbCkNCiAgfQ0KfQ0KDQpzZXF1ZW5jZXMgPC0gdGV4dHNfdG9fc2VxdWVuY2VzKHRva2VuaXplciwgdGV4dHMpDQp4X3Rlc3QgPC0gcGFkX3NlcXVlbmNlcyhzZXF1ZW5jZXMsIG1heGxlbiA9IG1heGxlbikNCnlfdGVzdCA8LSBhcy5hcnJheShsYWJlbHMpDQpgYGANCg0KVGVyYXogbW/FvGVteSB6YcWCYWRvd2HEhyBwaWVyd3N6eSBtb2RlbCBpIG9jZW5pxIcgZWZla3R5IGplZ28gcHJhY3k6DQoNCmBgYHtyfQ0KbW9kZWwgJT4lIA0KICBsb2FkX21vZGVsX3dlaWdodHNfaGRmNSgicHJlX3RyYWluZWRfZ2xvdmVfbW9kZWwuaDUiKSAlPiUgDQogIGV2YWx1YXRlKHhfdGVzdCwgeV90ZXN0LCB2ZXJib3NlID0gMCkNCmBgYA0KDQpVenlza3VqZW15IGRva8WCYWRub8WbxIcgdGVzdG93xIUgbmEgcG96aW9taWUgNTYlLiBQcmFjYSB6IG1hxYLEhSBsaWN6YsSFIHByw7NiZWsgdHJlbmluZ293eWNoIGplc3QgbmFwcmF3ZMSZIHRydWRuYSENCg==