### 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

  1. Review NLP workflows and data structures in R
  2. Explore different type of DTM matrix type vector representations of text.
  3. Add different types of dimensionality reduction techniques to the repertoir.
  4. HAve a peak into word-embeddings
  5. 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.

example

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")
  • some preprocessing
# 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') 
  • We can also ad bigrams
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) 
  • Sidenote

  • Here, we can further preprocess to do whatever we would like, such as obtaining a dtm

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()
  • Some plotting
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.

alt text

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)
  • Plot it
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

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