### Load standardpackages
library(tidyverse) # Collection of all the good stuff like dplyr, ggplot2 ect.
library(magrittr) # For extra-piping operators (eg. %<>%)
library(tidytext)
This session
This session, we will
- Review NLP workflows and data structures in R
- Explore different type of DTM matrix type vector representations of text.
- Add different types of dimensionality reduction techniques to the repertoir.
- HAve a peak into word-embeddings
- Add some goddies on top
Refresher:
Bag of words model
- In order for a computer to understand text we need to somehow find a useful representation.
- If you need to compare different texts e.g. articles, you will probably go for keywords. These keywords may come from a keyword-list with for example 200 different keywords
- In that case you could represent each document with a (sparse) vector with 1 for “keyword present” and 0 for “keyword absent”
- We can also get a bit more sophoistocated and count the number of times a word from our dictionary occurs.
- For a corpus of documents that would give us a document-term matrix.
Let’s try creating a bag of words model from our initial example.
text <- tibble(id = c(1:6),
text = c('A text about cats.',
'A text about dogs.',
'And another text about a dog.',
'Why always writing about cats and dogs, always dogs?',
'There are too little text about cats but to many about dogs',
'Cats, cats, cats! I love cats soo much. Cats are way better than dogs'))
text_tidy <- text %>%
unnest_tokens(word, text, token = 'words') %>%
count(id, word)
The document-term matrix (DTM)
- The simplest form of vector representation of text is a ddocument-term matrix
- How to we get a document-term matrix now?
- We could do it by hand, with well-known
dplyr
syntax (Note: only works when you have one row per unique document-word pair)
text_tidy %>%
pivot_wider(names_from = word, values_from = n, values_fill = 0)
- We could also use
cast_dtm()
to create a DTM in the format of the tm
package.
text_dtm <- text_tidy %>%
cast_dtm(id, word, n)
text_dtm
<<DocumentTermMatrix (documents: 6, terms: 25)>>
Non-/sparse entries: 42/108
Sparsity : 72%
Maximal term length: 7
Weighting : term frequency (tf)
- We can simply convert ig to a tibble. Since there exists no direct transfer function, we have to first transform it to a matrix.
- Notice how we recover the rownames
text_dtm %>% as.matrix() %>% as_tibble(rownames = 'id')
- Sidenote: We can also tidy the DTM again to a tidy token-dataframe.
text_dtm %>% tidy()
- We also can directly use a similar function to cast a sparse matrix (which we for sure then also could transform to a tibble again)
text_tidy %>% cast_sparse(row = id, column = word, value = n)
6 x 25 sparse Matrix of class "dgCMatrix"
1 1 1 1 1 . . . . . . . . . . . . . . . . . . . . .
2 1 1 . 1 1 . . . . . . . . . . . . . . . . . . . .
3 1 1 . 1 . 1 1 1 . . . . . . . . . . . . . . . . .
4 . 1 1 . 2 1 . . 2 1 1 . . . . . . . . . . . . . .
5 . 2 1 1 1 . . . . . . 1 1 1 1 1 1 1 . . . . . . .
6 . . 5 . 1 . . . . . . 1 . . . . . . 1 1 1 1 1 1 1
- Finally, we could just apply a text recipe here
library(recipes)
library(textrecipes)
text %>%
recipe(~.) %>%
step_tokenize(text, token = 'words') %>% # tokenize
step_tf(text) %>% # TFIDF weighting
prep() %>% juice()
TF-IDF - Term Frequency - Inverse Document Frequency
- A token is important for a document if appears very often
- A token becomes less important for comparison across a corpus if it appears all over the place in the corpus
- Cat in a corpus of websites talking about cats is not that important
\[w_{i,j} = tf_{i,j}*log(\frac{N}{df_i})\]
- \(w_{i,j}\) = the TF-IDF score for a term i in a document j
- \(tf_{i,j}\) = number of occurence of term i in document j
- \(N\) = number of documents in the corpus
- \(df_i\) = number of documents with term i
# TFIDF weights
text_tidy %<>%
bind_tf_idf(term = word,
document = id,
n = n)
- We obviously could also cast a tf_idf weighted dtm…
text_tidy %>%
select(id, word, tf_idf) %>%
pivot_wider(names_from = word, values_from = tf_idf, values_fill = 0)
- btw: this is equivalent to just running a textrecipe like that:
text %>%
recipe(~.) %>%
step_tokenize(text, token = 'words') %>% # tokenize
step_tfidf(text) %>% # TFIDF weighting
prep() %>% juice()
- Sidenote, when we use a POS engine such as
spacyr
for tokenization, we can also add recipes for lematization, filter for POS etc.
text %>%
recipe(~.) %>%
step_tokenize(text, engine = "spacyr") %>%
step_pos_filter(text, keep_tags = "NOUN") %>%
step_lemma(text) %>%
step_tf(text) %>%
prep() %>%
juice()
- A last reminder on the powerful
pairwise_xx()
functions from the widyr
package
- For instance, pairwise similarities/distances
library(widyr)
text_tidy %>% pairwise_dist(id, word, tf_idf, method = "manhattan") %>%
mutate(similarity = 1 - (distance / max(distance)) ) %>%
select(-distance) %>%
arrange(desc(similarity))
Dimensionality reduction techniques
rm(list=ls())
- Ok, lets get first some more interesting data. We will work with the CORDIS project descriptions of EU Horizon 2020 projects again.
text <- read_csv('https://github.com/SDS-AAU/SDS-master/raw/master/M2/data/cordis-h2020reports.gz')
colnames(text) <- colnames(text) %>% str_to_lower()
text %<>%
select(-x1) %>%
rename(id = projectid) %>%
relocate(id) %>%
filter(language == 'en') %>%
drop_na(id)
- Lets create a tidy tokenlist
text_tidy <- text %>%
rename(text = summary) %>%
select(id, text) %>%
unnest_tokens(word, text, token = "words")
# preprocessing
text_tidy %<>%
filter(str_length(word) > 2 ) %>% # Remove words with less than 3 characters
filter(!(word %in% c('project', 'research'))) %>%
anti_join(stop_words, by = 'word')
text_tidy %<>%
unnest_tokens(word, word, token = 'ngrams', n = 2, n_min = 1) %>%
group_by(word) %>% filter(n() > 25) %>% ungroup()
text_tidy %>%
count(word, sort = TRUE)
- Lets finish this up and also add TF-IDF weights
text_tidy %<>%
count(id, word) %>%
bind_tf_idf(term = word,
document = id,
n = n) %>%
select(-tf, -idf)
- Is there a big difference?
text_tidy %>%
count(word, wt = tf_idf, sort = TRUE)
- And finally, lets get a DTM dataframe
text_dtm <- text_tidy %>%
select(id, word, n) %>%
pivot_wider(names_from = word, values_from = n, values_fill = 0)
- And, just in case, a TFIDF weighted version
- We could also prepare a recipe which doe pretty much the same…
recipe_base <- text %>%
rename(text = summary) %>%
select(id, text) %>%
# BAse recipe starts
recipe(~.) %>%
update_role(id, new_role = "id") %>% # Update role of ID
step_tokenize(text, token = 'words') %>% # tokenize
step_stopwords(text, keep = FALSE) %>% # remove stopwords
step_untokenize(text) %>% # Here we now have to first untokenize
step_tokenize(text, token = "ngrams", options = list(n = 1, n_min = 1)) %>% # and tokenize again
step_tokenfilter(text, min_times = 25)
recipe_base %>%
step_tf(text) %>%
prep() %>%
juice() %>%
head(100)
text_pca <- text_dtm %>%
column_to_rownames('id') %>%
prcomp(center = TRUE, scale. = TRUE, rank. = 10)
text_pca %>% glimpse()
List of 5
$ sdev : num [1:499] 3.54 3.05 2.86 2.8 2.66 ...
$ rotation: num [1:599, 1:10] 0.01561 0.00169 0.07306 -0.03103 0.01753 ...
..- attr(*, "dimnames")=List of 2
.. ..$ : chr [1:599] "aim" "allowing" "based" "blood" ...
.. ..$ : chr [1:10] "PC1" "PC2" "PC3" "PC4" ...
$ center : Named num [1:599] 0.2265 0.0541 0.6733 0.0701 0.1543 ...
..- attr(*, "names")= chr [1:599] "aim" "allowing" "based" "blood" ...
$ scale : Named num [1:599] 0.537 0.235 1.049 0.445 0.856 ...
..- attr(*, "names")= chr [1:599] "aim" "allowing" "based" "blood" ...
$ x : num [1:499, 1:10] -3.24 -0.94 -1.62 -1.25 -1.4 ...
..- attr(*, "dimnames")=List of 2
.. ..$ : chr [1:499] "115844" "633197" "633249" "633261" ...
.. ..$ : chr [1:10] "PC1" "PC2" "PC3" "PC4" ...
- attr(*, "class")= chr "prcomp"
text_pca[['x']] %>%
head()
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10
115844 -3.240084 -1.224083 1.0929219 -0.2966866 0.7300978 -0.1272715 0.1145893 -2.3920794 -0.01109556 0.1170174
633197 -0.939826 4.805016 1.9730777 2.0355810 -0.9698387 0.5595339 1.9143958 -2.0685189 -1.93477227 -1.4869024
633249 -1.620804 4.488997 0.3095817 2.5046982 -1.6895767 1.9497913 -0.5535784 -0.6742253 0.01409018 -1.1999591
633261 -1.249307 4.793337 1.3972610 3.9865897 -0.6584607 0.7980526 1.1628729 -2.7636819 -3.20517847 -0.6888338
633382 -1.399995 5.265843 0.1248408 4.1248219 -0.9627588 1.3542756 1.4368296 -0.6137203 -1.93784759 -0.8948685
633571 1.445999 2.273599 -0.7621658 1.2438623 -2.1143075 0.4455806 0.8693148 0.8680666 2.56183743 2.5522762
text_pca %>% tidy()
- Again, alternatively with a recipe…
recipe_pca <- recipe_base %>% # tokenize
step_tfidf(text, prefix = '') %>% # TFIDF weighting
step_pca(all_predictors(), num_comp = 10) %>% # PCA
prep()
recipe_pca %>% juice()
recipe_pca %>% juice() %>%
ggplot(aes(x = PC01, y = PC02)) +
geom_point()
- we can also use the tidy results of the recipe to do some more analytics
recipe_pca %>%
tidy(7) %>%
filter(component %in% paste0("PC", 1:4)) %>%
group_by(component) %>%
arrange(desc(value)) %>%
slice(c(1:2, (n()-2):n())) %>%
ungroup() %>%
mutate(component = fct_inorder(component)) %>%
ggplot(aes(value, terms, fill = terms)) +
geom_col(show.legend = FALSE) +
facet_wrap(~component, nrow = 1) +
labs(y = NULL)
- Note: Also check further for further dimensionlity reduction steps:
- tep_kpca():
- step_ica()
- step_isomap()
- step_nnmf()
Topic Models: Latent-Dirichlet-Allocation (LDA)
- While we already did it somewhat ‘on-the-fly’, here a more formal introduction to LDA
- In contrast to dimnesionality reduction techiques mostly aiming at preprocessing data or easing visualization, LDA more aims at EDA and interpretation
- It is a generative approach to identify topics (clusters) within the word-usage in documents.
- Topics are represented as a probability distribution over the words in the vocabulary. Hhigh probability words can be used to charactrize the topic.
- Documents are represented as a mixture of topics.
library(topicmodels)
text_dtm <- text_tidy %>%
cast_dtm(document = id, term = word, value = n)
text_lda <- text_dtm %>%
LDA(k = 6, method = "Gibbs",
control = list(seed = 1337))
- \(\beta\) is an output of the LDA model, indicating the propability that a word occurs in a certain topic.
- Therefore, loking at the top probability words of a topic often gives us a good intuition regarding its properties.
# LDA output is defined for tidy(), so we can easily extract it
lda_beta <- text_lda %>%
tidy(matrix = "beta")
lda_beta %>%
# slice
group_by(topic) %>%
arrange(topic, desc(beta)) %>%
slice(1:10) %>%
ungroup() %>%
# visualize
mutate(term = reorder_within(term, beta, topic)) %>%
group_by(topic, term) %>%
arrange(desc(beta)) %>%
ungroup() %>%
ggplot(aes(term, beta, fill = as.factor(topic))) +
geom_col(show.legend = FALSE) +
coord_flip() +
scale_x_reordered() +
labs(title = "Top 10 terms in each LDA topic",
x = NULL, y = expression(beta)) +
facet_wrap(~ topic, ncol = 3, scales = "free")
- Documents are represented as a mix of topics. This association of a document to a topic is captured by \(\gamma\)
lda_gamma <- text_lda %>%
tidy(matrix = "gamma")
lda_gamma %>%
group_by(topic) %>%
arrange(desc(gamma)) %>%
slice(1:10) %>%
ungroup() %>%
left_join(text %>% select(id, projectacronym) %>% mutate(id = id %>% as.character()), by = c('document' = 'id'))
- Note that an LDA can also be performed via a recipe:
recipe_lda <- recipe_base %>% # tokenize
step_lda(text, num_topics = 6) %>% # LDA
prep()
recipe_lda %>% juice() %>%
head(100)
- As a bonus, a great way to interactively visualize LDA’s.
- It’s a bit cumbersome in R, though…
library(LDAvis)
# A bit of a lenghty function....
topicmodels_json_ldavis <- function(fitted, doc_dtm, method = "PCA"){
require(topicmodels); require(dplyr); require(LDAvis)
# Find required quantities
phi <- posterior(text_lda)$terms %>% as.matrix() # Topic-term distribution
theta <- posterior(fitted)$topics %>% as.matrix() # Document-topic matrix
text_tidy <- doc_dtm %>% tidy()
vocab <- colnames(phi)
doc_length <- tibble(document = rownames(theta)) %>% left_join(text_tidy %>% count(document, wt = count), by = 'document')
tf <- tibble(term = vocab) %>% left_join(text_tidy %>% count(term, wt = count), by = "term")
if(method == "PCA"){mds <- jsPCA}
if(method == "TSNE"){library(tsne); mds <- function(x){tsne(svd(x)$u)} }
# Convert to json
json_lda <- LDAvis::createJSON(phi = phi, theta = theta, vocab = vocab, doc.length = doc_length %>% pull(n), term.frequency = tf %>% pull(n),
reorder.topics = FALSE, mds.method = mds,plot.opts = list(xlab = "Dim.1", ylab = "Dim.2"))
return(json_lda)
}
library(LDAvis)
json_lda <- topicmodels_json_ldavis(fitted = text_lda,
doc_dtm = text_dtm,
method = "TSNE")
json_lda %>% serVis() # For direct output
# json_lda %>% serVis(out.dir = 'LDAviz') # For saving the html
Didnt really figure out how to embedd the resulting plot, but the outcome can be seen here
Embeddings (Bonus)
One last thing we did not venture in yet, are embeddings
I will not go into details here, just see it as a peak of what’s to come in further sessions.
The idee of word embedding is (in a nutshell) that
There are packages on how to train own embeddings such as text2vec
, but we will for now not bother with that.
The only thing we will do for now is to load pretrained embeddings (GloVe, cf. Pennington et al, 2014)
library(textdata)
glove6b <- embedding_glove6b(dimensions = 100)
glove6b %>% head(1000)
- La voila, a large pretrained embedding model for around 400k of the most common words.
- We for now loaded the smallest of these embedding models, there exist way bigger ones.
- Lets join it with our tidy tokenlist
word_embeddings <- text_tidy %>%
inner_join(glove6b, by = c('word' = 'token'))
word_embeddings %>% head()
- We could now create average document embeddings by taking the mean over all dimensions
- We could also (even better) weight that by then word’s tfidf score.
doc_embeddings <- word_embeddings %>%
group_by(id) %>%
summarise(across(starts_with("d"), ~mean(.x / tf_idf, na.rm = TRUE)))
- These embddings could now be used for instance for some clustering or SML exercise
- I guess you can already see how to use these embeddings in an SML model.
library(uwot) # for UMAP
embeddings_umap <- doc_embeddings %>%
column_to_rownames("id") %>%
umap(n_neighbors = 15,
metric = "cosine",
min_dist = 0.01,
scale = TRUE,
verbose = TRUE,
n_threads = 8)
0% 10 20 30 40 50 60 70 80 90 100%
[----|----|----|----|----|----|----|----|----|----|
**************************************************|
embeddings_umap %<>% as.data.frame()
embeddings_umap %>%
ggplot(aes(x = V1, y = V2)) +
geom_point(shape = 21, alpha = 0.5)
- Ok, we see a rather clear seperation of documents.
- Just for fun, lets add a density based clustering (very good for spatial clustering) on top (even though we already see the results)
library(dbscan)
- Do the hirarchical density based clustering
embeddings_hdbscan <- embeddings_umap %>% as.matrix() %>% hdbscan(minPts = 15)
embeddings_umap %>%
bind_cols(cluster = embeddings_hdbscan$cluster %>% as.factor(),
prob = embeddings_hdbscan$membership_prob) %>%
ggplot(aes(x = V1, y = V2, col = cluster)) +
geom_point(aes(alpha = prob), shape = 21)
- Note: We can also assigne the embeddings via a recipe
- Unfortunately, we can not do a TFIDF weighting here ‘out-of-the-box’, but have to work with average embeddings instead.
recipe_embedding <- recipe_base %>% # tokenize
step_word_embeddings(text, embeddings = glove6b, aggregation = 'mean')
recipe_embedding %>% prep() %>% juice() %>%
head(100)
library(embed)
recipe_umap <- recipe_embedding %>%
step_umap(starts_with('w_embed'), n_neighbors = 15)
recipe_umap %>% prep() %>% juice() %>%
head(100)
—>
- So, that’s all I have for now
Summary
- There are many ways to convert text data into a vector representation.
- These range from simple and weighted bags-of-words, to topic models, over different types of dimensionality reduction to finally word and document embeddings.
- All of them are useful, depending on the purpose.
Endnotes
Packages & Ecosystem
textrecipes
: Text preprocessing recipes
embed
: Extra embedding recipes
topicmodels
: LDA topicmodelling in R
LDAvis
: A bit clunky but awesome interactive LDA visualizations
text2vec
: Package vor vector space modelling (aka embeddings & other vectorizations) of textdata
textdata
: Useful datasets for text, such as GloVe embeddings, sentiment lexica etc.
uwot
: UMAP for R
References
CHapters:
- Julia Silge and David Robinson (2020). Text Mining with R: A Tidy Approach, O’Reilly. Online available here
- Emil Hvidfeldt and Julia Silge (2020). Supervised Machine Learning for Text Analysis in R, online available here
Articles: * Blei, David M., Andrew Y. Ng, and Michael I. Jordan. “Latent dirichlet allocation.” Journal of machine Learning research 3, no. Jan (2003): 993-1022. * Jeffrey Pennington, Richard Socher, and Christopher D Manning. Glove: Global vectors for word representation. In Conference on Empirical Methods on Natural Language Processing (EMNLP), pages 1532–1543, 2014
Further sources
- Julia Silge’s Blog: Full of great examples of predictive modeling, NLP, and the combination fo both, using tidy ecosystems
- Emil Hvitfeldt’s Blog: Likewise, full of great examples of applied tidy ML & NLP in
Session Info
sessionInfo()
LS0tCnRpdGxlOiAnKFNvbWV3aGF0KSBhZHZhbmNlZCBOTFA6IHRleHQgdmVjdG9yaXphdGlvbicKYXV0aG9yOiAiRGFuaWVsIFMuIEhhaW4gKGRzaEBidXNpbmVzcy5hYXUuZGspIgpkYXRlOiAiVXBkYXRlZCBgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVCICVkLCAlWScpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKICAgIGRmX3ByaW50OiBwYWdlZAogICAgdG9jOiB5ZXMKICAgIHRvY19kZXB0aDogMgogICAgdG9jX2Zsb2F0OgogICAgICBjb2xsYXBzZWQ6IG5vCiAgICB0aGVtZTogZmxhdGx5CiAgaHRtbF9kb2N1bWVudDoKICAgIHRvYzogeWVzCiAgICB0b2NfZGVwdGg6ICcyJwogICAgZGZfcHJpbnQ6IHBhZ2VkCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CiMjIyBHZW5lcmljIHByZWFtYmxlCnJtKGxpc3Q9bHMoKSkKU3lzLnNldGVudihMQU5HID0gImVuIikgIyBGb3IgZW5nbGlzaCBsYW5ndWFnZQpvcHRpb25zKHNjaXBlbiA9IDUpICMgVG8gZGVhY3RpdmF0ZSBhbm5veWluZyBzY2llbnRpZmljIG51bWJlciBub3RhdGlvbgoKIyMjIEtuaXRyIG9wdGlvbnMKbGlicmFyeShrbml0cikgIyBGb3IgZGlzcGxheSBvZiB0aGUgbWFya2Rvd24Ka25pdHI6Om9wdHNfY2h1bmskc2V0KHdhcm5pbmc9RkFMU0UsCiAgICAgICAgICAgICAgICAgICAgIG1lc3NhZ2U9RkFMU0UsCiAgICAgICAgICAgICAgICAgICAgIGNvbW1lbnQ9RkFMU0UsIAogICAgICAgICAgICAgICAgICAgICBmaWcuYWxpZ249ImNlbnRlciIKICAgICAgICAgICAgICAgICAgICAgKQpgYGAKCmBgYHtyfQojIyMgTG9hZCBzdGFuZGFyZHBhY2thZ2VzCmxpYnJhcnkodGlkeXZlcnNlKSAjIENvbGxlY3Rpb24gb2YgYWxsIHRoZSBnb29kIHN0dWZmIGxpa2UgZHBseXIsIGdncGxvdDIgZWN0LgpsaWJyYXJ5KG1hZ3JpdHRyKSAjIEZvciBleHRyYS1waXBpbmcgb3BlcmF0b3JzIChlZy4gJTw+JSkKYGBgCgpgYGB7cn0KbGlicmFyeSh0aWR5dGV4dCkKYGBgCgojIFRoaXMgc2Vzc2lvbgoKVGhpcyBzZXNzaW9uLCB3ZSB3aWxsCgoxLiBSZXZpZXcgTkxQIHdvcmtmbG93cyBhbmQgZGF0YSBzdHJ1Y3R1cmVzIGluIFIKMi4gRXhwbG9yZSBkaWZmZXJlbnQgdHlwZSBvZiBEVE0gbWF0cml4IHR5cGUgdmVjdG9yIHJlcHJlc2VudGF0aW9ucyBvZiB0ZXh0LgozLiBBZGQgZGlmZmVyZW50IHR5cGVzIG9mIGRpbWVuc2lvbmFsaXR5IHJlZHVjdGlvbiB0ZWNobmlxdWVzIHRvIHRoZSByZXBlcnRvaXIuCjYuIEhBdmUgYSBwZWFrIGludG8gd29yZC1lbWJlZGRpbmdzCjUuIEFkZCBzb21lIGdvZGRpZXMgb24gdG9wCgojIFJlZnJlc2hlcjoKCiFbXShodHRwczovL3Nkcy1hYXUuZ2l0aHViLmlvL1NEUy1tYXN0ZXIvMDBfbWVkaWEvbmxwX3RpZHl3b3JrZmxvdy5wbmcpCgoKIyBCYWcgb2Ygd29yZHMgbW9kZWwKCiogSW4gb3JkZXIgZm9yIGEgY29tcHV0ZXIgdG8gdW5kZXJzdGFuZCB0ZXh0IHdlIG5lZWQgdG8gc29tZWhvdyBmaW5kIGEgdXNlZnVsIHJlcHJlc2VudGF0aW9uLgoqIElmIHlvdSBuZWVkIHRvIGNvbXBhcmUgZGlmZmVyZW50IHRleHRzIGUuZy4gYXJ0aWNsZXMsIHlvdSB3aWxsIHByb2JhYmx5IGdvIGZvciBrZXl3b3Jkcy4gVGhlc2Uga2V5d29yZHMgbWF5IGNvbWUgZnJvbSBhIGtleXdvcmQtbGlzdCB3aXRoIGZvciBleGFtcGxlIDIwMCBkaWZmZXJlbnQga2V5d29yZHMKKiBJbiB0aGF0IGNhc2UgeW91IGNvdWxkIHJlcHJlc2VudCBlYWNoIGRvY3VtZW50IHdpdGggYSAoc3BhcnNlKSB2ZWN0b3Igd2l0aCAxIGZvciAia2V5d29yZCBwcmVzZW50IiBhbmQgMCBmb3IgImtleXdvcmQgYWJzZW50IgoqIFdlIGNhbiBhbHNvIGdldCBhIGJpdCBtb3JlIHNvcGhvaXN0b2NhdGVkIGFuZCBjb3VudCB0aGUgbnVtYmVyIG9mIHRpbWVzIGEgd29yZCBmcm9tIG91ciBkaWN0aW9uYXJ5IG9jY3Vycy4KKiBGb3IgYSBjb3JwdXMgb2YgZG9jdW1lbnRzIHRoYXQgd291bGQgZ2l2ZSB1cyBhIGRvY3VtZW50LXRlcm0gbWF0cml4LgoKIVtleGFtcGxlXShodHRwczovL2kuc3RhY2suaW1ndXIuY29tL0MxVU1zLnBuZykKCkxldCdzIHRyeSBjcmVhdGluZyBhIGJhZyBvZiB3b3JkcyBtb2RlbCBmcm9tIG91ciBpbml0aWFsIGV4YW1wbGUuCgpgYGB7cn0KdGV4dCA8LSB0aWJibGUoaWQgPSBjKDE6NiksCiAgICAgICAgICAgICAgIHRleHQgPSBjKCdBIHRleHQgYWJvdXQgY2F0cy4nLAogICAgICAgICAgICAgICAgICAgICAgICAnQSB0ZXh0IGFib3V0IGRvZ3MuJywKICAgICAgICAgICAgICAgICAgICAgICAgJ0FuZCBhbm90aGVyIHRleHQgYWJvdXQgYSBkb2cuJywKICAgICAgICAgICAgICAgICAgICAgICAgJ1doeSBhbHdheXMgd3JpdGluZyBhYm91dCBjYXRzIGFuZCBkb2dzLCBhbHdheXMgZG9ncz8nLAogICAgICAgICAgICAgICAgICAgICAgICAnVGhlcmUgYXJlIHRvbyBsaXR0bGUgdGV4dCBhYm91dCBjYXRzIGJ1dCB0byBtYW55IGFib3V0IGRvZ3MnLAogICAgICAgICAgICAgICAgICAgICAgICAnQ2F0cywgY2F0cywgY2F0cyEgSSBsb3ZlIGNhdHMgc29vIG11Y2guIENhdHMgYXJlIHdheSBiZXR0ZXIgdGhhbiBkb2dzJykpCmBgYAoKYGBge3J9CnRleHRfdGlkeSA8LSB0ZXh0ICU+JSAKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lIAogIGNvdW50KGlkLCB3b3JkKQpgYGAKCgojIyBUaGUgZG9jdW1lbnQtdGVybSBtYXRyaXggKERUTSkKCiogVGhlIHNpbXBsZXN0IGZvcm0gb2YgdmVjdG9yIHJlcHJlc2VudGF0aW9uIG9mIHRleHQgaXMgYSBkZG9jdW1lbnQtdGVybSBtYXRyaXgKKiBIb3cgdG8gd2UgZ2V0IGEgZG9jdW1lbnQtdGVybSBtYXRyaXggbm93PwoqIFdlIGNvdWxkIGRvIGl0IGJ5IGhhbmQsIHdpdGggd2VsbC1rbm93biBgZHBseXJgIHN5bnRheCAoTm90ZTogb25seSB3b3JrcyB3aGVuIHlvdSBoYXZlIG9uZSByb3cgcGVyIHVuaXF1ZSBkb2N1bWVudC13b3JkIHBhaXIpCgpgYGB7cn0KdGV4dF90aWR5ICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSB3b3JkLCB2YWx1ZXNfZnJvbSA9IG4sIHZhbHVlc19maWxsID0gMCkKYGBgCgoqIFdlIGNvdWxkIGFsc28gdXNlIGBjYXN0X2R0bSgpYCB0byBjcmVhdGUgYSBEVE0gaW4gdGhlIGZvcm1hdCBvZiB0aGUgYHRtYCBwYWNrYWdlLgoKYGBge3J9CnRleHRfZHRtIDwtIHRleHRfdGlkeSAlPiUKICBjYXN0X2R0bShpZCwgd29yZCwgbikKYGBgCgpgYGB7cn0KdGV4dF9kdG0gCmBgYAoKKiBXZSBjYW4gc2ltcGx5IGNvbnZlcnQgaWcgdG8gYSB0aWJibGUuIFNpbmNlIHRoZXJlIGV4aXN0cyBubyBkaXJlY3QgdHJhbnNmZXIgZnVuY3Rpb24sIHdlIGhhdmUgdG8gZmlyc3QgdHJhbnNmb3JtIGl0IHRvIGEgbWF0cml4LgoqIE5vdGljZSBob3cgd2UgcmVjb3ZlciB0aGUgcm93bmFtZXMKCmBgYHtyfQp0ZXh0X2R0bSAlPiUgYXMubWF0cml4KCkgJT4lIGFzX3RpYmJsZShyb3duYW1lcyA9ICdpZCcpIApgYGAKCiogU2lkZW5vdGU6IFdlIGNhbiBhbHNvIHRpZHkgdGhlIERUTSBhZ2FpbiB0byBhIHRpZHkgdG9rZW4tZGF0YWZyYW1lLgoKYGBge3J9CnRleHRfZHRtICU+JSB0aWR5KCkKYGBgCiogV2UgYWxzbyBjYW4gZGlyZWN0bHkgdXNlIGEgc2ltaWxhciBmdW5jdGlvbiB0byBjYXN0IGEgc3BhcnNlIG1hdHJpeCAod2hpY2ggd2UgZm9yIHN1cmUgdGhlbiBhbHNvIGNvdWxkIHRyYW5zZm9ybSB0byBhIHRpYmJsZSBhZ2FpbikKCmBgYHtyfQp0ZXh0X3RpZHkgJT4lIGNhc3Rfc3BhcnNlKHJvdyA9IGlkLCBjb2x1bW4gPSB3b3JkLCB2YWx1ZSA9IG4pCmBgYAoKKiBGaW5hbGx5LCB3ZSBjb3VsZCBqdXN0IGFwcGx5IGEgdGV4dCByZWNpcGUgaGVyZQoKYGBge3J9CmxpYnJhcnkocmVjaXBlcykKbGlicmFyeSh0ZXh0cmVjaXBlcykKYGBgCgpgYGB7cn0KdGV4dCAlPiUKICByZWNpcGUofi4pICU+JSAKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lICMgdG9rZW5pemUKICBzdGVwX3RmKHRleHQpICU+JSAjIFRGSURGIHdlaWdodGluZwogIHByZXAoKSAlPiUganVpY2UoKQpgYGAKCgojIyBURi1JREYgLSBUZXJtIEZyZXF1ZW5jeSAtIEludmVyc2UgRG9jdW1lbnQgRnJlcXVlbmN5CgoqIEEgdG9rZW4gaXMgaW1wb3J0YW50IGZvciBhIGRvY3VtZW50IGlmIGFwcGVhcnMgdmVyeSBvZnRlbgoqIEEgdG9rZW4gYmVjb21lcyBsZXNzIGltcG9ydGFudCBmb3IgY29tcGFyaXNvbiBhY3Jvc3MgYSBjb3JwdXMgaWYgaXQgYXBwZWFycyBhbGwgb3ZlciB0aGUgcGxhY2UgaW4gdGhlIGNvcnB1cwoqICpDYXQqIGluIGEgY29ycHVzIG9mIHdlYnNpdGVzIHRhbGtpbmcgYWJvdXQgY2F0cyBpcyBub3QgdGhhdCBpbXBvcnRhbnQKCiQkd197aSxqfSA9IHRmX3tpLGp9KmxvZyhcZnJhY3tOfXtkZl9pfSkkJAoKLSAkd197aSxqfSQgPSB0aGUgVEYtSURGIHNjb3JlIGZvciBhIHRlcm0gaSBpbiBhIGRvY3VtZW50IGoKLSAkdGZfe2ksan0kID0gbnVtYmVyIG9mIG9jY3VyZW5jZSBvZiB0ZXJtIGkgaW4gZG9jdW1lbnQgagotICROJCA9IG51bWJlciBvZiBkb2N1bWVudHMgaW4gdGhlIGNvcnB1cwotICRkZl9pJCA9IG51bWJlciBvZiBkb2N1bWVudHMgd2l0aCB0ZXJtIGkKCmBgYHtyfQojIFRGSURGIHdlaWdodHMKdGV4dF90aWR5ICU8PiUKICBiaW5kX3RmX2lkZih0ZXJtID0gd29yZCwKICAgICAgICAgICAgICBkb2N1bWVudCA9IGlkLAogICAgICAgICAgICAgIG4gPSBuKQpgYGAKCiogV2Ugb2J2aW91c2x5IGNvdWxkIGFsc28gY2FzdCBhIHRmX2lkZiB3ZWlnaHRlZCBkdG0uLi4KCmBgYHtyfQp0ZXh0X3RpZHkgJT4lCiAgc2VsZWN0KGlkLCB3b3JkLCB0Zl9pZGYpICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSB3b3JkLCB2YWx1ZXNfZnJvbSA9IHRmX2lkZiwgdmFsdWVzX2ZpbGwgPSAwKQpgYGAKCiogYnR3OiB0aGlzIGlzIGVxdWl2YWxlbnQgdG8ganVzdCBydW5uaW5nIGEgdGV4dHJlY2lwZSBsaWtlIHRoYXQ6CgpgYGB7cn0KdGV4dCAlPiUKICByZWNpcGUofi4pICU+JSAKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lICMgdG9rZW5pemUKICBzdGVwX3RmaWRmKHRleHQpICU+JSAjIFRGSURGIHdlaWdodGluZwogIHByZXAoKSAlPiUganVpY2UoKQpgYGAKCiogU2lkZW5vdGUsIHdoZW4gd2UgdXNlIGEgUE9TIGVuZ2luZSBzdWNoIGFzIGBzcGFjeXJgIGZvciB0b2tlbml6YXRpb24sIHdlIGNhbiBhbHNvIGFkZCByZWNpcGVzIGZvciBsZW1hdGl6YXRpb24sIGZpbHRlciBmb3IgUE9TIGV0Yy4KCmBgYHtyfQp0ZXh0ICU+JQogIHJlY2lwZSh+LikgJT4lIAogIHN0ZXBfdG9rZW5pemUodGV4dCwgZW5naW5lID0gInNwYWN5ciIpICU+JQogIHN0ZXBfcG9zX2ZpbHRlcih0ZXh0LCBrZWVwX3RhZ3MgPSAiTk9VTiIpICU+JQogIHN0ZXBfbGVtbWEodGV4dCkgJT4lCiAgc3RlcF90Zih0ZXh0KSAlPiUKICBwcmVwKCkgJT4lCiAganVpY2UoKQpgYGAKCiogQSBsYXN0IHJlbWluZGVyIG9uIHRoZSBwb3dlcmZ1bCBgcGFpcndpc2VfeHgoKWAgZnVuY3Rpb25zIGZyb20gdGhlIGB3aWR5cmAgcGFja2FnZQoqIEZvciBpbnN0YW5jZSwgcGFpcndpc2Ugc2ltaWxhcml0aWVzL2Rpc3RhbmNlcwoKYGBge3J9CmxpYnJhcnkod2lkeXIpCmBgYAoKYGBge3J9CnRleHRfdGlkeSAlPiUgcGFpcndpc2VfZGlzdChpZCwgd29yZCwgdGZfaWRmLCBtZXRob2QgPSAibWFuaGF0dGFuIikgJT4lCiAgbXV0YXRlKHNpbWlsYXJpdHkgPSAxIC0gKGRpc3RhbmNlIC8gbWF4KGRpc3RhbmNlKSkgKSAlPiUKICBzZWxlY3QoLWRpc3RhbmNlKSAlPiUKICBhcnJhbmdlKGRlc2Moc2ltaWxhcml0eSkpCmBgYAoKCiMgRGltZW5zaW9uYWxpdHkgcmVkdWN0aW9uIHRlY2huaXF1ZXMKCmBgYHtyfQpybShsaXN0PWxzKCkpCmBgYAoKKiBPaywgbGV0cyBnZXQgZmlyc3Qgc29tZSBtb3JlIGludGVyZXN0aW5nIGRhdGEuIFdlIHdpbGwgd29yayB3aXRoIHRoZSBDT1JESVMgcHJvamVjdCBkZXNjcmlwdGlvbnMgb2YgRVUgSG9yaXpvbiAyMDIwIHByb2plY3RzIGFnYWluLgoKYGBge3J9CnRleHQgPC0gcmVhZF9jc3YoJ2h0dHBzOi8vZ2l0aHViLmNvbS9TRFMtQUFVL1NEUy1tYXN0ZXIvcmF3L21hc3Rlci9NMi9kYXRhL2NvcmRpcy1oMjAyMHJlcG9ydHMuZ3onKQpgYGAKCmBgYHtyfQpjb2xuYW1lcyh0ZXh0KSA8LSBjb2xuYW1lcyh0ZXh0KSAlPiUgc3RyX3RvX2xvd2VyKCkKdGV4dCAlPD4lCiAgc2VsZWN0KC14MSkgJT4lCiAgcmVuYW1lKGlkID0gcHJvamVjdGlkKSAlPiUKICByZWxvY2F0ZShpZCkgJT4lCiAgZmlsdGVyKGxhbmd1YWdlID09ICdlbicpICU+JQogIGRyb3BfbmEoaWQpCmBgYAoKKiBMZXRzIGNyZWF0ZSBhIHRpZHkgdG9rZW5saXN0CgpgYGB7cn0KdGV4dF90aWR5IDwtIHRleHQgJT4lCiAgcmVuYW1lKHRleHQgPSBzdW1tYXJ5KSAlPiUKICBzZWxlY3QoaWQsIHRleHQpICU+JQogIHVubmVzdF90b2tlbnMod29yZCwgdGV4dCwgdG9rZW4gPSAid29yZHMiKQpgYGAKCiogc29tZSBwcmVwcm9jZXNzaW5nCgpgYGB7cn0KIyBwcmVwcm9jZXNzaW5nCnRleHRfdGlkeSAlPD4lCiAgZmlsdGVyKHN0cl9sZW5ndGgod29yZCkgPiAyICkgJT4lICMgUmVtb3ZlIHdvcmRzIHdpdGggbGVzcyB0aGFuICAzIGNoYXJhY3RlcnMKICBmaWx0ZXIoISh3b3JkICVpbiUgYygncHJvamVjdCcsICdyZXNlYXJjaCcpKSkgJT4lCiAgYW50aV9qb2luKHN0b3Bfd29yZHMsIGJ5ID0gJ3dvcmQnKSAKYGBgCgoqIFdlIGNhbiBhbHNvIGFkIGJpZ3JhbXMKCmBgYHtyfQp0ZXh0X3RpZHkgJTw+JQogIHVubmVzdF90b2tlbnMod29yZCwgd29yZCwgdG9rZW4gPSAnbmdyYW1zJywgbiA9IDIsIG5fbWluID0gMSkgJT4lCiAgZ3JvdXBfYnkod29yZCkgJT4lIGZpbHRlcihuKCkgPiAyNSkgJT4lIHVuZ3JvdXAoKSAKYGBgCgpgYGB7cn0KdGV4dF90aWR5ICU+JQogIGNvdW50KHdvcmQsIHNvcnQgPSBUUlVFKQpgYGAKCiogTGV0cyBmaW5pc2ggdGhpcyB1cCBhbmQgYWxzbyBhZGQgVEYtSURGIHdlaWdodHMKCmBgYHtyfQp0ZXh0X3RpZHkgJTw+JQogIGNvdW50KGlkLCB3b3JkKSAlPiUKICBiaW5kX3RmX2lkZih0ZXJtID0gd29yZCwKICAgICAgICAgICAgICBkb2N1bWVudCA9IGlkLAogICAgICAgICAgICAgIG4gPSBuKSAlPiUKICBzZWxlY3QoLXRmLCAtaWRmKQpgYGAKCiogSXMgdGhlcmUgYSBiaWcgZGlmZmVyZW5jZT8KCmBgYHtyfQp0ZXh0X3RpZHkgJT4lCiAgY291bnQod29yZCwgd3QgPSB0Zl9pZGYsIHNvcnQgPSBUUlVFKQpgYGAKCiogQW5kIGZpbmFsbHksIGxldHMgZ2V0IGEgRFRNIGRhdGFmcmFtZSAKCmBgYHtyfQp0ZXh0X2R0bSA8LSB0ZXh0X3RpZHkgJT4lCiAgc2VsZWN0KGlkLCB3b3JkLCBuKSAlPiUKICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gd29yZCwgdmFsdWVzX2Zyb20gPSBuLCB2YWx1ZXNfZmlsbCA9IDApCmBgYAoKKiBBbmQsIGp1c3QgaW4gY2FzZSwgYSBURklERiB3ZWlnaHRlZCB2ZXJzaW9uCgpgYGB7ciwgaW5jbHVkZT1GQUxTRX0KdGV4dF9kdG1fdGZfaWRmIDwtIHRleHRfdGlkeSAlPiUKICBzZWxlY3QoaWQsIHdvcmQsIHRmX2lkZikgJT4lCiAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHdvcmQsIHZhbHVlc19mcm9tID0gdGZfaWRmLCB2YWx1ZXNfZmlsbCA9IDApCmBgYAoKKiBXZSBjb3VsZCBhbHNvIHByZXBhcmUgYSByZWNpcGUgd2hpY2ggZG9lIHByZXR0eSBtdWNoIHRoZSBzYW1lLi4uCgpgYGB7cn0KcmVjaXBlX2Jhc2UgPC0gdGV4dCAlPiUKICByZW5hbWUodGV4dCA9IHN1bW1hcnkpICU+JQogIHNlbGVjdChpZCwgdGV4dCkgJT4lCiAgIyBCQXNlIHJlY2lwZSBzdGFydHMKICByZWNpcGUofi4pICU+JSAKICB1cGRhdGVfcm9sZShpZCwgbmV3X3JvbGUgPSAiaWQiKSAlPiUgIyBVcGRhdGUgcm9sZSBvZiBJRAogIHN0ZXBfdG9rZW5pemUodGV4dCwgdG9rZW4gPSAnd29yZHMnKSAlPiUgIyB0b2tlbml6ZQogIHN0ZXBfc3RvcHdvcmRzKHRleHQsIGtlZXAgPSBGQUxTRSkgJT4lICMgcmVtb3ZlIHN0b3B3b3JkcwogIHN0ZXBfdW50b2tlbml6ZSh0ZXh0KSAlPiUgIyBIZXJlIHdlIG5vdyBoYXZlIHRvIGZpcnN0IHVudG9rZW5pemUKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gIm5ncmFtcyIsIG9wdGlvbnMgPSBsaXN0KG4gPSAxLCBuX21pbiA9IDEpKSAlPiUgIyBhbmQgdG9rZW5pemUgYWdhaW4KICBzdGVwX3Rva2VuZmlsdGVyKHRleHQsIG1pbl90aW1lcyA9IDI1KSAKYGBgCgoqIFNpZGVub3RlCgoqIEhlcmUsIHdlIGNhbiBmdXJ0aGVyIHByZXByb2Nlc3MgdG8gZG8gd2hhdGV2ZXIgd2Ugd291bGQgbGlrZSwgc3VjaCBhcyBvYnRhaW5pbmcgYSBkdG0KCmBgYHtyfQpyZWNpcGVfYmFzZSAlPiUgCiAgc3RlcF90Zih0ZXh0KSAlPiUgCiAgcHJlcCgpICU+JSAKICBqdWljZSgpICU+JSAKICBoZWFkKDEwMCkKYGBgCgpgYGB7cn0KdGV4dF9wY2EgPC0gdGV4dF9kdG0gJT4lIAogIGNvbHVtbl90b19yb3duYW1lcygnaWQnKSAlPiUgCiAgcHJjb21wKGNlbnRlciA9IFRSVUUsIHNjYWxlLiA9IFRSVUUsIHJhbmsuID0gMTApCmBgYAoKYGBge3J9CnRleHRfcGNhICU+JSBnbGltcHNlKCkKYGBgCgpgYGB7cn0KdGV4dF9wY2FbWyd4J11dICU+JQogIGhlYWQoKQpgYGAKCgpgYGB7cn0KdGV4dF9wY2EgJT4lIHRpZHkoKQpgYGAKCgoqIEFnYWluLCBhbHRlcm5hdGl2ZWx5IHdpdGggYSByZWNpcGUuLi4KCmBgYHtyfQpyZWNpcGVfcGNhIDwtIHJlY2lwZV9iYXNlICU+JSAjIHRva2VuaXplCiAgc3RlcF90ZmlkZih0ZXh0LCBwcmVmaXggPSAnJykgJT4lICMgVEZJREYgd2VpZ2h0aW5nCiAgc3RlcF9wY2EoYWxsX3ByZWRpY3RvcnMoKSwgbnVtX2NvbXAgPSAxMCkgJT4lICMgUENBCiAgcHJlcCgpIApgYGAKCmBgYHtyfQpyZWNpcGVfcGNhICU+JSBqdWljZSgpCmBgYAoqIFNvbWUgcGxvdHRpbmcKCmBgYHtyfQpyZWNpcGVfcGNhICU+JSBqdWljZSgpICU+JQogIGdncGxvdChhZXMoeCA9IFBDMDEsIHkgPSBQQzAyKSkgKwogIGdlb21fcG9pbnQoKSAKYGBgCiogd2UgY2FuIGFsc28gdXNlIHRoZSB0aWR5IHJlc3VsdHMgb2YgdGhlIHJlY2lwZSB0byBkbyBzb21lIG1vcmUgYW5hbHl0aWNzCgpgYGB7cn0KcmVjaXBlX3BjYSAlPiUKICB0aWR5KDcpICU+JQogIGZpbHRlcihjb21wb25lbnQgJWluJSBwYXN0ZTAoIlBDIiwgMTo0KSkgJT4lCiAgZ3JvdXBfYnkoY29tcG9uZW50KSAlPiUKICAgIGFycmFuZ2UoZGVzYyh2YWx1ZSkpICU+JQogICAgc2xpY2UoYygxOjIsIChuKCktMik6bigpKSkgJT4lCiAgdW5ncm91cCgpICU+JQogIG11dGF0ZShjb21wb25lbnQgPSBmY3RfaW5vcmRlcihjb21wb25lbnQpKSAlPiUKICBnZ3Bsb3QoYWVzKHZhbHVlLCB0ZXJtcywgZmlsbCA9IHRlcm1zKSkgKwogIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsKICBmYWNldF93cmFwKH5jb21wb25lbnQsIG5yb3cgPSAxKSArCiAgbGFicyh5ID0gTlVMTCkKYGBgCgoqICoqTm90ZSoqOiBBbHNvIGNoZWNrIGZ1cnRoZXIgZm9yIGZ1cnRoZXIgZGltZW5zaW9ubGl0eSByZWR1Y3Rpb24gc3RlcHM6CiAgICogdGVwX2twY2EoKToKICAgKiBzdGVwX2ljYSgpCiAgICogc3RlcF9pc29tYXAoKQogICAqIHN0ZXBfbm5tZigpCiAgIAoKIyBUb3BpYyBNb2RlbHM6IExhdGVudC1EaXJpY2hsZXQtQWxsb2NhdGlvbiAoTERBKQoKKiBXaGlsZSB3ZSBhbHJlYWR5IGRpZCBpdCBzb21ld2hhdCAnb24tdGhlLWZseScsIGhlcmUgYSBtb3JlIGZvcm1hbCBpbnRyb2R1Y3Rpb24gdG8gTERBCiogSW4gY29udHJhc3QgdG8gZGltbmVzaW9uYWxpdHkgcmVkdWN0aW9uIHRlY2hpcXVlcyBtb3N0bHkgYWltaW5nIGF0IHByZXByb2Nlc3NpbmcgZGF0YSBvciBlYXNpbmcgdmlzdWFsaXphdGlvbiwgTERBIG1vcmUgYWltcyBhdCBFREEgYW5kIGludGVycHJldGF0aW9uCiogSXQgaXMgYSBnZW5lcmF0aXZlIGFwcHJvYWNoIHRvIGlkZW50aWZ5IHRvcGljcyAoY2x1c3RlcnMpIHdpdGhpbiB0aGUgd29yZC11c2FnZSBpbiBkb2N1bWVudHMuCiAgICogVG9waWNzIGFyZSByZXByZXNlbnRlZCBhcyBhIHByb2JhYmlsaXR5IGRpc3RyaWJ1dGlvbiBvdmVyIHRoZSB3b3JkcyBpbiB0aGUgdm9jYWJ1bGFyeS4gSGhpZ2ggcHJvYmFiaWxpdHkgd29yZHMgY2FuIGJlIHVzZWQgdG8gY2hhcmFjdHJpemUgdGhlIHRvcGljLgogICAqIERvY3VtZW50cyBhcmUgcmVwcmVzZW50ZWQgYXMgYSBtaXh0dXJlIG9mIHRvcGljcy4KCiFbYWx0IHRleHRdKGh0dHBzOi8vbWlyby5tZWRpdW0uY29tL21heC8xNjAwLzEqcFpvX0ljeFcxR1Z1SDJ2UUtkb0lNUS5qcGVnKQoKYGBge3J9CmxpYnJhcnkodG9waWNtb2RlbHMpCmBgYAoKCmBgYHtyfQp0ZXh0X2R0bSA8LSB0ZXh0X3RpZHkgJT4lCiAgY2FzdF9kdG0oZG9jdW1lbnQgPSBpZCwgdGVybSA9IHdvcmQsIHZhbHVlID0gbikKYGBgCgpgYGB7cn0KdGV4dF9sZGEgPC0gdGV4dF9kdG0gJT4lIAogIExEQShrID0gNiwgbWV0aG9kID0gIkdpYmJzIiwKICAgICAgY29udHJvbCA9IGxpc3Qoc2VlZCA9IDEzMzcpKQpgYGAKCgoqICRcYmV0YSQgaXMgYW4gb3V0cHV0IG9mIHRoZSBMREEgbW9kZWwsIGluZGljYXRpbmcgdGhlIHByb3BhYmlsaXR5IHRoYXQgYSB3b3JkIG9jY3VycyBpbiBhIGNlcnRhaW4gdG9waWMuCiogVGhlcmVmb3JlLCBsb2tpbmcgYXQgdGhlIHRvcCBwcm9iYWJpbGl0eSB3b3JkcyBvZiBhIHRvcGljIG9mdGVuIGdpdmVzIHVzIGEgZ29vZCBpbnR1aXRpb24gcmVnYXJkaW5nIGl0cyBwcm9wZXJ0aWVzLgoKYGBge3J9CiMgTERBIG91dHB1dCBpcyBkZWZpbmVkIGZvciB0aWR5KCksIHNvIHdlIGNhbiBlYXNpbHkgZXh0cmFjdCBpdApsZGFfYmV0YSA8LSB0ZXh0X2xkYSAlPiUgCiAgdGlkeShtYXRyaXggPSAiYmV0YSIpIApgYGAKCmBgYHtyfQpsZGFfYmV0YSAlPiUKICAjIHNsaWNlCiAgZ3JvdXBfYnkodG9waWMpICU+JQogIGFycmFuZ2UodG9waWMsIGRlc2MoYmV0YSkpICU+JQogIHNsaWNlKDE6MTApICU+JQogIHVuZ3JvdXAoKSAlPiUKICAjIHZpc3VhbGl6ZQogIG11dGF0ZSh0ZXJtID0gcmVvcmRlcl93aXRoaW4odGVybSwgYmV0YSwgdG9waWMpKSAlPiUKICBncm91cF9ieSh0b3BpYywgdGVybSkgJT4lICAgIAogIGFycmFuZ2UoZGVzYyhiZXRhKSkgJT4lICAKICB1bmdyb3VwKCkgJT4lCiAgZ2dwbG90KGFlcyh0ZXJtLCBiZXRhLCBmaWxsID0gYXMuZmFjdG9yKHRvcGljKSkpICsKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArCiAgY29vcmRfZmxpcCgpICsKICBzY2FsZV94X3Jlb3JkZXJlZCgpICsKICBsYWJzKHRpdGxlID0gIlRvcCAxMCB0ZXJtcyBpbiBlYWNoIExEQSB0b3BpYyIsCiAgICAgICB4ID0gTlVMTCwgeSA9IGV4cHJlc3Npb24oYmV0YSkpICsKICBmYWNldF93cmFwKH4gdG9waWMsIG5jb2wgPSAzLCBzY2FsZXMgPSAiZnJlZSIpCmBgYAoKKiBEb2N1bWVudHMgYXJlIHJlcHJlc2VudGVkIGFzIGEgbWl4IG9mIHRvcGljcy4gVGhpcyBhc3NvY2lhdGlvbiBvZiBhIGRvY3VtZW50IHRvIGEgdG9waWMgaXMgY2FwdHVyZWQgYnkgJFxnYW1tYSQKCmBgYHtyfQpsZGFfZ2FtbWEgPC0gdGV4dF9sZGEgJT4lIAogIHRpZHkobWF0cml4ID0gImdhbW1hIikKYGBgCgoKYGBge3J9CmxkYV9nYW1tYSAlPiUKICBncm91cF9ieSh0b3BpYykgJT4lCiAgICBhcnJhbmdlKGRlc2MoZ2FtbWEpKSAlPiUgCiAgICBzbGljZSgxOjEwKSAlPiUKICB1bmdyb3VwKCkgJT4lCiAgbGVmdF9qb2luKHRleHQgJT4lIHNlbGVjdChpZCwgcHJvamVjdGFjcm9ueW0pICU+JSBtdXRhdGUoaWQgPSBpZCAlPiUgYXMuY2hhcmFjdGVyKCkpLCBieSA9IGMoJ2RvY3VtZW50JyA9ICdpZCcpKQpgYGAKCiogTm90ZSB0aGF0IGFuIExEQSBjYW4gYWxzbyBiZSBwZXJmb3JtZWQgdmlhIGEgcmVjaXBlOgoKYGBge3J9CnJlY2lwZV9sZGEgPC0gcmVjaXBlX2Jhc2UgJT4lICMgdG9rZW5pemUKICBzdGVwX2xkYSh0ZXh0LCBudW1fdG9waWNzID0gNikgJT4lICMgTERBCiAgcHJlcCgpIApgYGAKCmBgYHtyfQpyZWNpcGVfbGRhICU+JSBqdWljZSgpICU+JSAKICBoZWFkKDEwMCkKYGBgCgoqIEFzIGEgYm9udXMsIGEgZ3JlYXQgd2F5IHRvIGludGVyYWN0aXZlbHkgdmlzdWFsaXplIExEQSdzLgoqIEl0J3MgYSBiaXQgY3VtYmVyc29tZSBpbiBSLCB0aG91Z2guLi4KCmBgYHtyfQpsaWJyYXJ5KExEQXZpcykKYGBgCgoKYGBge3J9CiMgQSBiaXQgb2YgYSBsZW5naHR5IGZ1bmN0aW9uLi4uLgp0b3BpY21vZGVsc19qc29uX2xkYXZpcyA8LSBmdW5jdGlvbihmaXR0ZWQsIGRvY19kdG0sIG1ldGhvZCA9ICJQQ0EiKXsKICByZXF1aXJlKHRvcGljbW9kZWxzKTsgcmVxdWlyZShkcGx5cik7IHJlcXVpcmUoTERBdmlzKQogIAogICMgRmluZCByZXF1aXJlZCBxdWFudGl0aWVzCiAgcGhpIDwtIHBvc3Rlcmlvcih0ZXh0X2xkYSkkdGVybXMgJT4lIGFzLm1hdHJpeCgpICMgVG9waWMtdGVybSBkaXN0cmlidXRpb24KICB0aGV0YSA8LSBwb3N0ZXJpb3IoZml0dGVkKSR0b3BpY3MgJT4lIGFzLm1hdHJpeCgpICMgRG9jdW1lbnQtdG9waWMgbWF0cml4CiAgCiAgdGV4dF90aWR5IDwtIGRvY19kdG0gJT4lIHRpZHkoKQogIHZvY2FiIDwtIGNvbG5hbWVzKHBoaSkKICBkb2NfbGVuZ3RoIDwtIHRpYmJsZShkb2N1bWVudCA9IHJvd25hbWVzKHRoZXRhKSkgJT4lIGxlZnRfam9pbih0ZXh0X3RpZHkgJT4lIGNvdW50KGRvY3VtZW50LCB3dCA9IGNvdW50KSwgYnkgPSAnZG9jdW1lbnQnKQogIHRmIDwtIHRpYmJsZSh0ZXJtID0gdm9jYWIpICU+JSBsZWZ0X2pvaW4odGV4dF90aWR5ICU+JSBjb3VudCh0ZXJtLCB3dCA9IGNvdW50KSwgYnkgPSAidGVybSIpIAogIAogIGlmKG1ldGhvZCA9PSAiUENBIil7bWRzIDwtIGpzUENBfQogIGlmKG1ldGhvZCA9PSAiVFNORSIpe2xpYnJhcnkodHNuZSk7IG1kcyA8LSBmdW5jdGlvbih4KXt0c25lKHN2ZCh4KSR1KX0gfQogIAogICMgQ29udmVydCB0byBqc29uCiAganNvbl9sZGEgPC0gTERBdmlzOjpjcmVhdGVKU09OKHBoaSA9IHBoaSwgdGhldGEgPSB0aGV0YSwgdm9jYWIgPSB2b2NhYiwgZG9jLmxlbmd0aCA9IGRvY19sZW5ndGggJT4lIHB1bGwobiksIHRlcm0uZnJlcXVlbmN5ID0gdGYgJT4lIHB1bGwobiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlb3JkZXIudG9waWNzID0gRkFMU0UsIG1kcy5tZXRob2QgPSBtZHMscGxvdC5vcHRzID0gbGlzdCh4bGFiID0gIkRpbS4xIiwgeWxhYiA9ICJEaW0uMiIpKSAKICByZXR1cm4oanNvbl9sZGEpCn0KYGBgCgoKYGBge3J9CmxpYnJhcnkoTERBdmlzKQpqc29uX2xkYSA8LSB0b3BpY21vZGVsc19qc29uX2xkYXZpcyhmaXR0ZWQgPSB0ZXh0X2xkYSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRvY19kdG0gPSB0ZXh0X2R0bSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJUU05FIikKCmpzb25fbGRhICU+JSBzZXJWaXMoKSAjIEZvciBkaXJlY3Qgb3V0cHV0CiMganNvbl9sZGEgJT4lIHNlclZpcyhvdXQuZGlyID0gJ0xEQXZpeicpICMgRm9yIHNhdmluZyB0aGUgaHRtbApgYGAKCgo8aWZyYW1lIHdpZHRoPSIxMDAwIiBoZWlnaHQ9IjEwMDAiIHNyYz0iaHR0cHM6Ly9kYW5pZWwtaGFpbi5naXRodWIuaW8vTUxfY291cnNlX21hYXN0cmljaHQvbm90ZWJvb2tzL0xEQXZpei9pbmRleC5odG1sI3RvcGljPTAmbGFtYmRhPTEmdGVybT0iPiAKPHA+WW91ciBicm93c2VyIGRvZXMgbm90IHN1cHBvcnQgaWZyYW1lczwvcD4KPC9pZnJhbWU+IAoKRGlkbnQgcmVhbGx5IGZpZ3VyZSBvdXQgaG93IHRvIGVtYmVkZCB0aGUgcmVzdWx0aW5nIHBsb3QsIGJ1dCB0aGUgb3V0Y29tZSBjYW4gYmUgc2VlbiBbaGVyZV0oaHR0cHM6Ly9kYW5pZWwtaGFpbi5naXRodWIuaW8vTUxfY291cnNlX21hYXN0cmljaHQvbm90ZWJvb2tzL0xEQXZpei9pbmRleC5odG1sKQoKCiMgRW1iZWRkaW5ncyAoQm9udXMpCgoqIE9uZSBsYXN0IHRoaW5nIHdlIGRpZCBub3QgdmVudHVyZSBpbiB5ZXQsIGFyZSBlbWJlZGRpbmdzCiogSSB3aWxsIG5vdCBnbyBpbnRvIGRldGFpbHMgaGVyZSwganVzdCBzZWUgaXQgYXMgYSBwZWFrIG9mIHdoYXQncyB0byBjb21lIGluIGZ1cnRoZXIgc2Vzc2lvbnMuCiogVGhlIGlkZWUgb2Ygd29yZCBlbWJlZGRpbmcgaXMgKGluIGEgbnV0c2hlbGwpIHRoYXQKCgoqIFRoZXJlIGFyZSBwYWNrYWdlcyBvbiBob3cgdG8gdHJhaW4gb3duIGVtYmVkZGluZ3Mgc3VjaCBhcyBbYHRleHQydmVjYF0oaHR0cDovL3RleHQydmVjLm9yZy8pLCBidXQgd2Ugd2lsbCBmb3Igbm93IG5vdCBib3RoZXIgd2l0aCB0aGF0LgoqIFRoZSBvbmx5IHRoaW5nIHdlIHdpbGwgZG8gZm9yIG5vdyBpcyB0byBsb2FkIHByZXRyYWluZWQgZW1iZWRkaW5ncyAoR2xvVmUsIGNmLiBQZW5uaW5ndG9uIGV0IGFsLCAyMDE0KQoKCmBgYHtyfQpsaWJyYXJ5KHRleHRkYXRhKQoKZ2xvdmU2YiA8LSBlbWJlZGRpbmdfZ2xvdmU2YihkaW1lbnNpb25zID0gMTAwKQpnbG92ZTZiICU+JSBoZWFkKDEwMDApCmBgYAoKCiogTGEgdm9pbGEsIGEgbGFyZ2UgcHJldHJhaW5lZCBlbWJlZGRpbmcgbW9kZWwgZm9yIGFyb3VuZCA0MDBrIG9mIHRoZSBtb3N0IGNvbW1vbiB3b3Jkcy4gCiogV2UgZm9yIG5vdyBsb2FkZWQgdGhlIHNtYWxsZXN0IG9mIHRoZXNlIGVtYmVkZGluZyBtb2RlbHMsIHRoZXJlIGV4aXN0IHdheSBiaWdnZXIgb25lcy4KKiBMZXRzIGpvaW4gaXQgd2l0aCBvdXIgdGlkeSB0b2tlbmxpc3QKCmBgYHtyfQp3b3JkX2VtYmVkZGluZ3MgPC0gdGV4dF90aWR5ICU+JQogIGlubmVyX2pvaW4oZ2xvdmU2YiwgYnkgPSBjKCd3b3JkJyA9ICd0b2tlbicpKQpgYGAKCmBgYHtyfQp3b3JkX2VtYmVkZGluZ3MgJT4lIGhlYWQoKQpgYGAKCiogV2UgY291bGQgbm93IGNyZWF0ZSBhdmVyYWdlIGRvY3VtZW50IGVtYmVkZGluZ3MgYnkgdGFraW5nIHRoZSBtZWFuIG92ZXIgYWxsIGRpbWVuc2lvbnMKKiBXZSBjb3VsZCBhbHNvIChldmVuIGJldHRlcikgd2VpZ2h0IHRoYXQgYnkgdGhlbiB3b3JkJ3MgdGZpZGYgc2NvcmUuCgpgYGB7cn0KZG9jX2VtYmVkZGluZ3MgPC0gd29yZF9lbWJlZGRpbmdzICU+JQogIGdyb3VwX2J5KGlkKSAlPiUKICBzdW1tYXJpc2UoYWNyb3NzKHN0YXJ0c193aXRoKCJkIiksIH5tZWFuKC54IC8gdGZfaWRmLCBuYS5ybSA9IFRSVUUpKSkKYGBgCgoqIFRoZXNlIGVtYmRkaW5ncyBjb3VsZCBub3cgYmUgdXNlZCBmb3IgaW5zdGFuY2UgZm9yIHNvbWUgY2x1c3RlcmluZyBvciBTTUwgZXhlcmNpc2UKKiBJIGd1ZXNzIHlvdSBjYW4gYWxyZWFkeSBzZWUgaG93IHRvIHVzZSB0aGVzZSBlbWJlZGRpbmdzIGluIGFuIFNNTCBtb2RlbC4KCmBgYHtyfQpsaWJyYXJ5KHV3b3QpICMgZm9yIFVNQVAKYGBgCgoKYGBge3J9CmVtYmVkZGluZ3NfdW1hcCA8LSBkb2NfZW1iZWRkaW5ncyAgJT4lIAogIGNvbHVtbl90b19yb3duYW1lcygiaWQiKSAlPiUKICB1bWFwKG5fbmVpZ2hib3JzID0gMTUsIAogICAgICAgbWV0cmljID0gImNvc2luZSIsIAogICAgICAgbWluX2Rpc3QgPSAwLjAxLCAKICAgICAgIHNjYWxlID0gVFJVRSwKICAgICAgIHZlcmJvc2UgPSBUUlVFLCAKICAgICAgIG5fdGhyZWFkcyA9IDgpIApgYGAKCmBgYHtyfQplbWJlZGRpbmdzX3VtYXAgJTw+JSBhcy5kYXRhLmZyYW1lKCkKYGBgCgoKYGBge3J9CmVtYmVkZGluZ3NfdW1hcCAgJT4lIAogIGdncGxvdChhZXMoeCA9IFYxLCB5ID0gVjIpKSArIAogIGdlb21fcG9pbnQoc2hhcGUgPSAyMSwgYWxwaGEgPSAwLjUpIApgYGAKCiogT2ssIHdlIHNlZSBhIHJhdGhlciBjbGVhciBzZXBlcmF0aW9uIG9mIGRvY3VtZW50cy4KKiBKdXN0IGZvciBmdW4sIGxldHMgYWRkIGEgZGVuc2l0eSBiYXNlZCBjbHVzdGVyaW5nICh2ZXJ5IGdvb2QgZm9yIHNwYXRpYWwgY2x1c3RlcmluZykgb24gdG9wIChldmVuIHRob3VnaCB3ZSBhbHJlYWR5IHNlZSB0aGUgcmVzdWx0cykKCmBgYHtyfQpsaWJyYXJ5KGRic2NhbikKYGBgCgoqIERvIHRoZSBoaXJhcmNoaWNhbCBkZW5zaXR5IGJhc2VkIGNsdXN0ZXJpbmcKICAgICAgIApgYGB7cn0KZW1iZWRkaW5nc19oZGJzY2FuIDwtIGVtYmVkZGluZ3NfdW1hcCAlPiUgYXMubWF0cml4KCkgJT4lIGhkYnNjYW4obWluUHRzID0gMTUpCmBgYAoKKiBQbG90IGl0CgpgYGB7cn0KZW1iZWRkaW5nc191bWFwICU+JSAKICBiaW5kX2NvbHMoY2x1c3RlciA9IGVtYmVkZGluZ3NfaGRic2NhbiRjbHVzdGVyICU+JSBhcy5mYWN0b3IoKSwgCiAgICAgICAgICAgIHByb2IgPSBlbWJlZGRpbmdzX2hkYnNjYW4kbWVtYmVyc2hpcF9wcm9iKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBWMSwgeSA9IFYyLCBjb2wgPSBjbHVzdGVyKSkgKyAKICBnZW9tX3BvaW50KGFlcyhhbHBoYSA9IHByb2IpLCBzaGFwZSA9IDIxKSAKYGBgCgoqIE5vdGU6IFdlIGNhbiBhbHNvIGFzc2lnbmUgdGhlIGVtYmVkZGluZ3MgdmlhIGEgcmVjaXBlCiogVW5mb3J0dW5hdGVseSwgd2UgY2FuIG5vdCBkbyBhIFRGSURGIHdlaWdodGluZyBoZXJlICdvdXQtb2YtdGhlLWJveCcsIGJ1dCBoYXZlIHRvIHdvcmsgd2l0aCBhdmVyYWdlIGVtYmVkZGluZ3MgaW5zdGVhZC4KCgpgYGB7cn0KcmVjaXBlX2VtYmVkZGluZyA8LSByZWNpcGVfYmFzZSAlPiUgIyB0b2tlbml6ZQogIHN0ZXBfd29yZF9lbWJlZGRpbmdzKHRleHQsIGVtYmVkZGluZ3MgPSBnbG92ZTZiLCBhZ2dyZWdhdGlvbiA9ICdtZWFuJykKYGBgCgpgYGB7cn0KcmVjaXBlX2VtYmVkZGluZyAlPiUgcHJlcCgpICU+JSBqdWljZSgpICU+JSAKICBoZWFkKDEwMCkKYGBgCgoKPCEtLS0KKiBTYW1lIGdvZXMgZm9yIFVNQVAsIHdoaWNoIGNhbiBiZSBhY2Nlc3NkIGluIHJlY2lwZXMgdmlhIHRoZSB0aGUgcGFja2FnZSBgZW1iZWRgIHBja2FnZS4KKiBIb3dldmVyLGBlbWJlZGAgaXMgYSBiaXQgaGVhdnkgaW4gdGVybXMgb2YgZGVwZW5kZW5jaWVzLCBzaW5jZSBpdCB1c2VzIGBrZXJhc2AgYW5kIGB0ZW5zb3JmbG93YCwgYSBkZWVwIGxlYXJuaW5nIGZyYW1ld29rLCBpbiB0aGUgYmFja2dyb3VibmQsIGFuZCBpcyBpbiBuZWVkIHRvIGluc3RhbGwgYW5vdGhlciBtaW5pLWNvbmRhIGVudmlyb21lbnQuIAoqIElmIHlvdSBoYXZlIG5vIGV4cGVyaWVuY2Ugd2l0aCBga2VyYXNgIGFuZCBgdGVuc29yZmxvd2Agc28gZmFyLCBJIHN1Z2dlc3QgeW91IHdhaXQgd2l0aCB0aGlzIG9uZSB1bnRpbCBsYXRlciBzZXNzaW9ucyB3aGVuIHdlIHByb3Blcmx5IGludHJvZHVjZSBpdC4KCmBgYHtyfQpsaWJyYXJ5KGVtYmVkKQpgYGAKCmBgYHtyfQpyZWNpcGVfdW1hcCA8LSByZWNpcGVfZW1iZWRkaW5nICU+JQogIHN0ZXBfdW1hcChzdGFydHNfd2l0aCgnd19lbWJlZCcpLCBuX25laWdoYm9ycyA9IDE1KSAKYGBgCgpgYGB7cn0KcmVjaXBlX3VtYXAgJT4lIHByZXAoKSAlPiUganVpY2UoKSAlPiUgCiAgaGVhZCgxMDApCmBgYAoKLS0tPgoKKiBTbywgdGhhdCdzIGFsbCBJIGhhdmUgZm9yIG5vdwoKIyBTdW1tYXJ5CgoqIFRoZXJlIGFyZSBtYW55IHdheXMgdG8gY29udmVydCB0ZXh0IGRhdGEgaW50byBhIHZlY3RvciByZXByZXNlbnRhdGlvbi4KKiBUaGVzZSByYW5nZSBmcm9tIHNpbXBsZSBhbmQgd2VpZ2h0ZWQgYmFncy1vZi13b3JkcywgdG8gdG9waWMgbW9kZWxzLCBvdmVyIGRpZmZlcmVudCB0eXBlcyBvZiBkaW1lbnNpb25hbGl0eSByZWR1Y3Rpb24gdG8gZmluYWxseSB3b3JkIGFuZCBkb2N1bWVudCBlbWJlZGRpbmdzLgoqIEFsbCBvZiB0aGVtIGFyZSB1c2VmdWwsIGRlcGVuZGluZyBvbiB0aGUgcHVycG9zZS4KCiMgRW5kbm90ZXMKCiMjIyBQYWNrYWdlcyAmIEVjb3N5c3RlbQoKKiBbYHRleHRyZWNpcGVzYF0oaHR0cHM6Ly90ZXh0cmVjaXBlcy50aWR5bW9kZWxzLm9yZy8pOiBUZXh0IHByZXByb2Nlc3NpbmcgcmVjaXBlcwoqIFtgZW1iZWRgXShodHRwczovL2VtYmVkLnRpZHltb2RlbHMub3JnLyk6IEV4dHJhIGVtYmVkZGluZyByZWNpcGVzCiogW2B0b3BpY21vZGVsc2BdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy90b3BpY21vZGVscy92aWduZXR0ZXMvdG9waWNtb2RlbHMucGRmKTogTERBIHRvcGljbW9kZWxsaW5nIGluIFIKKiBbYExEQXZpc2BdKGh0dHBzOi8vZ2l0aHViLmNvbS9jcHNpZXZlcnQvTERBdml6KTogQSBiaXQgY2x1bmt5IGJ1dCBhd2Vzb21lIGludGVyYWN0aXZlIExEQSB2aXN1YWxpemF0aW9ucwoqIFtgdGV4dDJ2ZWNgXShodHRwOi8vdGV4dDJ2ZWMub3JnLyk6IFBhY2thZ2Ugdm9yIHZlY3RvciBzcGFjZSBtb2RlbGxpbmcgKGFrYSBlbWJlZGRpbmdzICYgb3RoZXIgdmVjdG9yaXphdGlvbnMpIG9mIHRleHRkYXRhCiogW2B0ZXh0ZGF0YWBdKGh0dHBzOi8vZ2l0aHViLmNvbS9FbWlsSHZpdGZlbGR0L3RleHRkYXRhKTogVXNlZnVsIGRhdGFzZXRzIGZvciB0ZXh0LCBzdWNoIGFzIEdsb1ZlIGVtYmVkZGluZ3MsIHNlbnRpbWVudCBsZXhpY2EgZXRjLgoqIFtgdXdvdGBdKGh0dHBzOi8vZ2l0aHViLmNvbS9qbG1lbHZpbGxlL3V3b3QpOiBVTUFQIGZvciBSCgojIyMgUmVmZXJlbmNlcyAKCkNIYXB0ZXJzOgoKKiBKdWxpYSBTaWxnZSBhbmQgRGF2aWQgUm9iaW5zb24gKDIwMjApLiBUZXh0IE1pbmluZyB3aXRoIFI6IEEgVGlkeSBBcHByb2FjaCwgT+KAmVJlaWxseS4gT25saW5lIGF2YWlsYWJsZSBbaGVyZV0oaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tLykKICAgKiBbQ2hhcHRlciA2XShodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vdG9waWNtb2RlbGluZy5odG1sKTogeHh4CiogRW1pbCBIdmlkZmVsZHQgYW5kIEp1bGlhIFNpbGdlICgyMDIwKS4gU3VwZXJ2aXNlZCBNYWNoaW5lIExlYXJuaW5nIGZvciBUZXh0IEFuYWx5c2lzIGluIFIsIG9ubGluZSBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vc21sdGFyLmNvbS8pCiAgICogW0NoYXB0ZXIgNV0oaHR0cHM6Ly9zbWx0YXIuY29tL2VtYmVkZGluZ3MuaHRtbCk6IFdvcmQgRW1iZWRkaW5ncwoKCkFydGljbGVzOgoqIEJsZWksIERhdmlkIE0uLCBBbmRyZXcgWS4gTmcsIGFuZCBNaWNoYWVsIEkuIEpvcmRhbi4gIkxhdGVudCBkaXJpY2hsZXQgYWxsb2NhdGlvbi4iIEpvdXJuYWwgb2YgbWFjaGluZSBMZWFybmluZyByZXNlYXJjaCAzLCBuby4gSmFuICgyMDAzKTogOTkzLTEwMjIuCiogSmVmZnJleSBQZW5uaW5ndG9uLCBSaWNoYXJkIFNvY2hlciwgYW5kIENocmlzdG9waGVyIEQgTWFubmluZy4gR2xvdmU6IEdsb2JhbCB2ZWN0b3JzIGZvciB3b3JkIHJlcHJlc2VudGF0aW9uLiBJbiBDb25mZXJlbmNlIG9uIEVtcGlyaWNhbCBNZXRob2RzIG9uIE5hdHVyYWwgTGFuZ3VhZ2UgUHJvY2Vzc2luZyAoRU1OTFApLCBwYWdlcyAxNTMy4oCTMTU0MywgMjAxNAoKIyMjIEZ1cnRoZXIgc291cmNlcwoKKiBbSnVsaWEgU2lsZ2UncyBCbG9nXShodHRwczovL2p1bGlhc2lsZ2UuY29tLyk6IEZ1bGwgb2YgZ3JlYXQgZXhhbXBsZXMgb2YgcHJlZGljdGl2ZSBtb2RlbGluZywgTkxQLCBhbmQgdGhlIGNvbWJpbmF0aW9uIGZvIGJvdGgsIHVzaW5nIHRpZHkgZWNvc3lzdGVtcwoqIFtFbWlsIEh2aXRmZWxkdCdzIEJsb2ddKGh0dHBzOi8vd3d3Lmh2aXRmZWxkdC5tZS8pOiBMaWtld2lzZSwgZnVsbCBvZiBncmVhdCBleGFtcGxlcyBvZiBhcHBsaWVkIHRpZHkgTUwgJiBOTFAgaW4gCgojIyMgU2Vzc2lvbiBJbmZvCgpgYGB7cn0Kc2Vzc2lvbkluZm8oKQpgYGAKCgoKCgoK