Summary

This week’s project involves training a predictive model to distinguish between spam and ham emails. The general steps can be broken down as follows:

  • Read emails into R
  • Label emails as spam or ham
  • Cleanse emails to 1) work well with the functions and 2) work well with the training and prediction processes
  • Split the emails into training and test sets
  • Train various models on the training set
  • Test models on the test set
library(tm)
library(stringr)
library(XML)
library(RTextTools)
library(ROCR)
library(dplyr)
library(ggplot2)
setwd("~/Google Drive/CUNY/git/DATA607/Week10")
#setwd("C:/Users/bhao/Google Drive/CUNY/git/DATA607/Week10")
set.seed(123)

Custom data cleansing functions

I wrote two functions to separate the email cleansing and corpus cleansing processes.

# create function to cleanse emails by removing non ASCII characters
cleanse_emails = function(dat) {
  dat = str_c(dat, collapse = " ")
  dat2 <- unlist(strsplit(dat, split=" "))
  dat3 <- grep("dat2", iconv(dat2, "latin1", "ASCII", sub="dat2"))
  if (length(dat3) == 0) {
    dat4 = dat2
  } else dat4 = dat2[-dat3]
  dat5 <- paste(dat4, collapse = " ")
}
cleanse_corpus = function(x) {
  x = tm_map(x, PlainTextDocument)  # to make tm package play nice
  x = tm_map(x, removeNumbers)
  x = tm_map(x, removePunctuation)  # leaving punctuation in case relevant to spam ham filter
  x = tm_map(x, stripWhitespace)
  x = tm_map(x, stemDocument)
  x = tm_map(x, content_transformer(tolower))  # content_transformer required because tolower not tm function
  x = tm_map(x, removeWords, stopwords('en'))
}

Importing emails into document corpus

Using the tm library, I created an email corpus of documents, each of which contained the cleansed email content and its spam/ham label as metadata.

spam_path = '20021010_spam/'
# add spam to email corpus
# create initial corpus
email = readLines(str_c(spam_path, list.files(spam_path)[1]))
email = cleanse_emails(email)
email_corpus = Corpus(VectorSource(email))
email_corpus = cleanse_corpus(email_corpus)
meta(email_corpus[[1]], 'spam') = 1
for (i in 2:length(list.files(spam_path))) {
  email = readLines(str_c(spam_path, list.files(spam_path)[i]))
  email = cleanse_emails(email)
  # add meta data to house spam classification
  if (length(email) != 0) {
    tmp_corpus = Corpus(VectorSource(email))
    tmp_corpus = cleanse_corpus(tmp_corpus)
    meta(tmp_corpus[[1]], 'spam') = 1
    email_corpus = c(email_corpus, tmp_corpus)
  }
}
easy_ham_path = '20021010_easy_ham/'
# add ham to email corpus
for (i in 1:length(list.files(easy_ham_path))) {
  email = readLines(str_c(easy_ham_path, list.files(easy_ham_path)[i]))
  email = cleanse_emails(email)
  # add meta data to house spam classification
  if (length(email) != 0) {
    tmp_corpus = Corpus(VectorSource(email))
    tmp_corpus = cleanse_corpus(tmp_corpus)
    meta(tmp_corpus[[1]], 'spam') = 0
    email_corpus = c(email_corpus, tmp_corpus)
  }
}
hard_ham_path = '20021010_hard_ham/'
# add ham to email corpus
for (i in 1:length(list.files(hard_ham_path))) {
  email = readLines(str_c(hard_ham_path, list.files(hard_ham_path)[i]))
  email = cleanse_emails(email)
  # add meta data to house spam classification
  if (length(email) != 0) {
    tmp_corpus = Corpus(VectorSource(email))
    tmp_corpus = cleanse_corpus(tmp_corpus)
    meta(tmp_corpus[[1]], 'spam') = 0
    email_corpus = c(email_corpus, tmp_corpus)
  }
}
incomplete final line found on '20021010_hard_ham/0231.7c6cc716ce3f3bfad7130dd3c8d7b072'incomplete final line found on '20021010_hard_ham/0250.7c6cc716ce3f3bfad7130dd3c8d7b072'

Randomizing email corpus

Because 1) the emails were read in order of spam, easy ham and hard ham and 2) the RTextTools container object requires the training data to represent the 1:X documents and the test data to represent the X:N documents, I had to randomize the emails within the corpus before creating the container object.

# randomize corpus order
N = length(email_corpus)
rand_index = sample(1:N)
email_corpus = email_corpus[rand_index]
tm_filter(email_corpus[1:100], FUN = function(x) meta(x)[['spam']] == 1)  # check that spam and ham have been randomized
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 17
# create document term matrix 
dtm = DocumentTermMatrix(email_corpus)
dtm = removeSparseTerms(dtm, 1-(10/length(email_corpus)))
#inspect(dtm[100:120, 100:110])
spam_labels = unlist(meta(email_corpus, 'spam'))

Modeling with the RTextTools package

With the data randomized, I created an RTextTools container, which in turn would be used to train the various models and analyze their respective results. I trained three algorithms: 1) support vector machine (SVM), 2) classification tree (TREE) and 3) maximum entropy (MAXENT). As illustrated in the confusion matrices and summary analytics below, the accuracy of all three models is superb. Out of 660 emails in the test set, the models only misclassified 7-8 emails in total.

# create container
container = create_container(dtm, labels = spam_labels, 
                             trainSize = 1:round(N*0.8,0), testSize = (round(N*0.8,0)+1):N,  # use 80% of data for train
                             virgin = FALSE)
# estimation procedure
svm_model = train_model(container, 'SVM')
#cross_validate(container, nfold = 5, algorithm = 'SVM')
tree_model = train_model(container, 'TREE')
#cross_validate(container, nfold = 5, algorithm = 'TREE')
maxent_model = train_model(container, 'MAXENT')
#cross_validate(container, nfold = 5, algorithm = 'MAXENT')
svm_out = classify_model(container, svm_model)
tree_out = classify_model(container, tree_model)
maxent_out = classify_model(container, maxent_model)
# evaluation
labels_out = data.frame(
  correct_label = spam_labels[(round(N*0.8,0)+1):N], 
  svm = as.character(svm_out[,1]),
  tree = as.character(tree_out[,1]),
  maxent = as.character(maxent_out[,1]),
  stringsAsFactors = FALSE
)
# svm confusion table
table(actual = labels_out[,1], predicted = labels_out[,2])
      predicted
actual   0   1
     0 553   3
     1   5  99
# tree confusion table
table(actual = labels_out[,1], predicted = labels_out[,3])
      predicted
actual   0   1
     0 549   7
     1   0 104
# maxent confusion table
table(actual = labels_out[,1], predicted = labels_out[,4])
      predicted
actual   0   1
     0 554   2
     1   6  98
# analytics
svm_analytics = create_analytics(container, svm_out)
summary(svm_analytics)
ENSEMBLE SUMMARY

       n-ENSEMBLE COVERAGE n-ENSEMBLE RECALL
n >= 1                   1              0.99


ALGORITHM PERFORMANCE

SVM_PRECISION    SVM_RECALL    SVM_FSCORE 
        0.980         0.970         0.975 
tree_analytics = create_analytics(container, tree_out)
summary(tree_analytics)
ENSEMBLE SUMMARY

       n-ENSEMBLE COVERAGE n-ENSEMBLE RECALL
n >= 1                   1              0.99


ALGORITHM PERFORMANCE

TREE_PRECISION    TREE_RECALL    TREE_FSCORE 
         0.970          0.995          0.980 
maxent_analytics = create_analytics(container, maxent_out)
summary(maxent_analytics)
ENSEMBLE SUMMARY

       n-ENSEMBLE COVERAGE n-ENSEMBLE RECALL
n >= 1                   1              0.99


ALGORITHM PERFORMANCE

MAXENTROPY_PRECISION    MAXENTROPY_RECALL    MAXENTROPY_FSCORE 
               0.985                0.970                0.975 

Limitations of RTextTools

The RTextTools models worked wonderfully - both extremely fast and accurate; however, the package does not allow for much customization and/or fine tuning. Initially, I did not realize that the probabilities in the classify_model objects were label-specific until Judd Andermann pointed that out to me. With his pointers, I was finally able to produce ROC curves for the RTextTools models, an example of which I’ve produced below.

That said, I had already moved on to using the caret models (as outlined below), which still offered more flexibility in terms of model parameter tuning.

svm_out2 = svm_out %>% mutate(SVM_PROB2 = ifelse(SVM_LABEL == 0, 1 - SVM_PROB, SVM_PROB)) 
tree_out2 = tree_out %>% mutate(TREE_PROB2 = ifelse(TREE_LABEL == 0, 1 - TREE_PROB, TREE_PROB)) 
maxent_out2 = maxent_out %>% mutate(MAXENTROPY_PROB2 = ifelse(MAXENTROPY_LABEL == 0, 1 - MAXENTROPY_PROB,
                                                          MAXENTROPY_PROB)) 
# create ROCR prediction object
svm_pred1 = prediction(svm_out2$SVM_PROB2, labels_out$correct_label)
tree_pred1 = prediction(tree_out2$TREE_PROB2, labels_out$correct_label)
maxent_pred1 = prediction(maxent_out2$MAXENTROPY_PROB2, labels_out$correct_label)
# create ROCR performance object
svm_perf1 = performance(svm_pred1, measure="tpr", x.measure="fpr")
tree_perf1 = performance(tree_pred1, measure="tpr", x.measure="fpr")
maxent_perf1 = performance(maxent_pred1, measure="tpr", x.measure="fpr")
plot(svm_perf1)
lines(tree_perf1@x.values[[1]], tree_perf1@y.values[[1]], col = 2)
lines(maxent_perf1@x.values[[1]], maxent_perf1@y.values[[1]], col = 3)
legend('bottomright', legend = c('svm', 'tree', 'maxent'), 
   lty=1, col=c('black', 'red', 'green'), bty='n', cex=.75)

Modeling with the caret package

Whereas the RTextTools package is specific to text mining, the caret package is a much more general purpose predictive modeling package. In this particular case, training models using the caret package was considerably slower and/or much more prone to error. For example, training the random forest models took more than 30 minutes; I stopped the training at that point and am unsure if it would have actually completed correctly. Training the Naive Bayes and neural net models resulted in errors for which I was unable to find solutions.

That said, I was able to train linear/logistic regression and support vector machine models. The beauty of the caret package is that it allows for user-friendly, automatic and user-defined model tuning during the training process:

  • The glmnet model has two tuning parameters: 1) alpha (0 to 1 or 100% lasso to 100% ridge) and 2) lambda (0 to Inf or the size of the penalty), and the svmRadial model has two tuning parameters: 1) sigma (0 to Inf or the complexity of the model or the bias-variance trade-off) and 2) C or the cost regularization parameter . By default, the caret train function will select the tuning parameters that produce the best out-of-sample results based on a user-specified metric, e.g. accuracy, ROC, etc. and bootstrapped resampling
  • The caret trainControl object then allows the user to define the resampling method, summary function for two class classification and more
  • The caret tuneLength and tuneGrid arguments allow the user to further control the set of parameter values over which to tune the models
# attempt modeling using caret package
library(caret)
Loading required package: lattice
#library(parallel)  
#library(doParallel)  # for OSX use library(doMC)  
library(caTools)
# convert dtm to matrix
full_mat = as.matrix(dtm)
train_mat = full_mat[1:round(N*0.8,0),]
test_mat = full_mat[(round(N*0.8,0)+1):N,]
# caret twoClassSummary requires non-numeric classes
spam_labels = factor(spam_labels, levels = c(0, 1), labels = c('ham', 'spam'))  
train_y = spam_labels[1:round(N*0.8,0)]
test_y = spam_labels[(round(N*0.8,0)+1):N]
#use_cores = detectCores()-1
#cl = makeCluster(use_cores)
#registerDoParallel(cl)  # for OSX use registerDoMC(cl)
# summaryFunction = twoClassSummary allows train() function to use AUC metric to rank models; 
# classProbs = TRUE allows summaryFunction to work properly
myControl = trainControl(method = 'cv', number = 5, summaryFunction = twoClassSummary, classProbs = T, verboseIter = T)
# glmnet = a general linear model with combination of lasso regression (penalizes number of non-zero coefficients) and
# ridge regression (penalizes absolute magnitude of coefficients); 
# glmnet has two tuning parameters: 1) alpha (0 to 1 or 100% lasso to 100% ridge) and 2) lambda (0 to Inf or the size
# of the penalty)
glm = train(x = train_mat, y = train_y, method = 'glmnet', metric = 'ROC', family = 'binomial', trControl = myControl)
Loading required package: glmnet
Loading required package: Matrix
Loading required package: foreach
foreach: simple, scalable parallel programming from Revolution Analytics
Use Revolution R for scalability, fault tolerance and more.
http://www.revolutionanalytics.com
Loaded glmnet 2.0-5
+ Fold1: alpha=0.10, lambda=0.1517 
- Fold1: alpha=0.10, lambda=0.1517 
+ Fold1: alpha=0.55, lambda=0.1517 
- Fold1: alpha=0.55, lambda=0.1517 
+ Fold1: alpha=1.00, lambda=0.1517 
- Fold1: alpha=1.00, lambda=0.1517 
+ Fold2: alpha=0.10, lambda=0.1517 
- Fold2: alpha=0.10, lambda=0.1517 
+ Fold2: alpha=0.55, lambda=0.1517 
- Fold2: alpha=0.55, lambda=0.1517 
+ Fold2: alpha=1.00, lambda=0.1517 
- Fold2: alpha=1.00, lambda=0.1517 
+ Fold3: alpha=0.10, lambda=0.1517 
- Fold3: alpha=0.10, lambda=0.1517 
+ Fold3: alpha=0.55, lambda=0.1517 
- Fold3: alpha=0.55, lambda=0.1517 
+ Fold3: alpha=1.00, lambda=0.1517 
- Fold3: alpha=1.00, lambda=0.1517 
+ Fold4: alpha=0.10, lambda=0.1517 
- Fold4: alpha=0.10, lambda=0.1517 
+ Fold4: alpha=0.55, lambda=0.1517 
- Fold4: alpha=0.55, lambda=0.1517 
+ Fold4: alpha=1.00, lambda=0.1517 
- Fold4: alpha=1.00, lambda=0.1517 
+ Fold5: alpha=0.10, lambda=0.1517 
- Fold5: alpha=0.10, lambda=0.1517 
+ Fold5: alpha=0.55, lambda=0.1517 
- Fold5: alpha=0.55, lambda=0.1517 
+ Fold5: alpha=1.00, lambda=0.1517 
- Fold5: alpha=1.00, lambda=0.1517 
Aggregating results
Selecting tuning parameters
Fitting alpha = 0.1, lambda = 0.048 on full training set
#            tuneGrid = expand.grid(alpha = seq(0, 1, 0.1), lambda = seq(0, 0.1, 0.01)))
glm
glmnet 

2642 samples
5718 predictors
   2 classes: 'ham', 'spam' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 2114, 2114, 2113, 2113, 2114 
Resampling results across tuning parameters:

  alpha  lambda      ROC        Sens       Spec     
  0.10   0.01517099  0.9996734  0.9986637  0.9874051
  0.10   0.04797487  0.9997074  0.9986637  0.9798418
  0.10   0.15170987  0.9991682  0.9991091  0.9547152
  0.55   0.01517099  0.9986537  0.9986637  0.9748101
  0.55   0.04797487  0.9969496  0.9995546  0.9546835
  0.55   0.15170987  0.9868267  1.0000000  0.9244620
  1.00   0.01517099  0.9985528  0.9991091  0.9723101
  1.00   0.04797487  0.9977380  1.0000000  0.9320253
  1.00   0.15170987  0.9575661  1.0000000  0.0025000

ROC was used to select the optimal model using  the largest value.
The final values used for the model were alpha = 0.1 and lambda = 0.04797487. 
plot(glm)

glm_pred = predict(glm, newdata = test_mat, type = 'raw')
colAUC(as.numeric(glm_pred), test_y, plotROC = T)
                  [,1]
ham vs. spam 0.9807692

confusionMatrix(glm_pred, test_y)
Confusion Matrix and Statistics

          Reference
Prediction ham spam
      ham  556    4
      spam   0  100
                                          
               Accuracy : 0.9939          
                 95% CI : (0.9846, 0.9983)
    No Information Rate : 0.8424          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.9768          
 Mcnemar's Test P-Value : 0.1336          
                                          
            Sensitivity : 1.0000          
            Specificity : 0.9615          
         Pos Pred Value : 0.9929          
         Neg Pred Value : 1.0000          
             Prevalence : 0.8424          
         Detection Rate : 0.8424          
   Detection Prevalence : 0.8485          
      Balanced Accuracy : 0.9808          
                                          
       'Positive' Class : ham             
                                          
# support vector machine
svm = train(x = train_mat, y = train_y, method = 'svmRadial', metric = 'ROC', family = 'binomial', trControl = myControl)
Loading required package: kernlab

Attaching package: ‘kernlab’

The following object is masked from ‘package:ggplot2’:

    alpha
+ Fold1: sigma=0.0002201, C=0.25 
- Fold1: sigma=0.0002201, C=0.25 
+ Fold1: sigma=0.0002201, C=0.50 
- Fold1: sigma=0.0002201, C=0.50 
+ Fold1: sigma=0.0002201, C=1.00 
- Fold1: sigma=0.0002201, C=1.00 
+ Fold2: sigma=0.0002201, C=0.25 
- Fold2: sigma=0.0002201, C=0.25 
+ Fold2: sigma=0.0002201, C=0.50 
- Fold2: sigma=0.0002201, C=0.50 
+ Fold2: sigma=0.0002201, C=1.00 
- Fold2: sigma=0.0002201, C=1.00 
+ Fold3: sigma=0.0002201, C=0.25 
- Fold3: sigma=0.0002201, C=0.25 
+ Fold3: sigma=0.0002201, C=0.50 
- Fold3: sigma=0.0002201, C=0.50 
+ Fold3: sigma=0.0002201, C=1.00 
- Fold3: sigma=0.0002201, C=1.00 
+ Fold4: sigma=0.0002201, C=0.25 
- Fold4: sigma=0.0002201, C=0.25 
+ Fold4: sigma=0.0002201, C=0.50 
- Fold4: sigma=0.0002201, C=0.50 
+ Fold4: sigma=0.0002201, C=1.00 
- Fold4: sigma=0.0002201, C=1.00 
+ Fold5: sigma=0.0002201, C=0.25 
- Fold5: sigma=0.0002201, C=0.25 
+ Fold5: sigma=0.0002201, C=0.50 
- Fold5: sigma=0.0002201, C=0.50 
+ Fold5: sigma=0.0002201, C=1.00 
- Fold5: sigma=0.0002201, C=1.00 
Aggregating results
Selecting tuning parameters
Fitting sigma = 0.00022, C = 1 on full training set
svm
Support Vector Machines with Radial Basis Function Kernel 

2642 samples
5718 predictors
   2 classes: 'ham', 'spam' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 2113, 2114, 2114, 2113, 2114 
Resampling results across tuning parameters:

  C     ROC        Sens       Spec     
  0.25  0.9943622  0.9861915  0.8967089
  0.50  0.9959278  0.9946548  0.8816139
  1.00  0.9977465  0.9968820  0.8892089

Tuning parameter 'sigma' was held constant at a value of 0.0002200635
ROC was used to select the optimal model using  the largest value.
The final values used for the model were sigma = 0.0002200635 and C = 1. 
plot(svm)

svm_pred = predict(svm, newdata = test_mat, type = 'raw')
colAUC(as.numeric(svm_pred), test_y, plotROC = T)
                  [,1]
ham vs. spam 0.9348022

confusionMatrix(svm_pred, test_y)
Confusion Matrix and Statistics

          Reference
Prediction ham spam
      ham  553   13
      spam   3   91
                                          
               Accuracy : 0.9758          
                 95% CI : (0.9609, 0.9861)
    No Information Rate : 0.8424          
    P-Value [Acc > NIR] : < 2e-16         
                                          
                  Kappa : 0.905           
 Mcnemar's Test P-Value : 0.02445         
                                          
            Sensitivity : 0.9946          
            Specificity : 0.8750          
         Pos Pred Value : 0.9770          
         Neg Pred Value : 0.9681          
             Prevalence : 0.8424          
         Detection Rate : 0.8379          
   Detection Prevalence : 0.8576          
      Balanced Accuracy : 0.9348          
                                          
       'Positive' Class : ham             
                                          
# naive bayes -- FAILED
# nb = train(x = train_mat, y = train_y, method = 'nb', metric = 'ROC', family = 'binomial', trControl = myControl)
# plot(nb)
# nb_pred = predict(nb, newdata = test_mat, type = 'raw')
# colAUC(as.numeric(nb_pred), test_y, plotROC = T)
# random forest -- TOO SLOW
#rf = train(x = train_mat, y = train_y, method = 'rf', metric = 'ROC', family = 'binomial', trControl = myControl)
# ranger = random forest model which has one tuning parameter mtry (2 to 100) or the number of variables to consider
# at each split; this can be controlled by adjusting the tuneLength parameter or by defining a custom tuneGrid
# -- FAILED
#rf = train(x = train_mat, y = make.names(train_y), method = 'ranger', metric = 'ROC', trControl = myControl)
# neural network -- FAILED
#nnet = train(x = train_mat, y = train_y, method = 'nnet', metric = 'ROC', family = 'binomial', trControl = myControl)
#stopCluster(cl)
#registerDoSEQ()

A note on ROC curves for spam detection

Using the glmnet ROC curve as an example, the model achieves an extremely high true positive rate (TPR > 95%) without incurring any false positives (FPR = 0%). Practically speaking, this means that one could select a probability threshold that would not incorrectly classify any ham as spam while letting through a small amount of spam. This might be an appropriate setting for a business, where a misclassification of ham as spam could result in real business damage. Conversely, for personal email purposes, one may find it acceptable to miss a real email or two in exchange for few spam emails in one’s inbox. Either way, the ROC curve can help the modeler decide on the probability threshold appropriate for the given situation.

Comparing caret trained models

The caretEnsemble package then allows us compare model results. The dotplot function plots the ROC and 95% confidence intervals for each model. As illustrated below, the glmnet model outperforms the svmRadial model quite significantly.

library(caretEnsemble)

Attaching package: ‘caretEnsemble’

The following object is masked from ‘package:ggplot2’:

    autoplot
# compare models using resample
model_list = list(glm = glm, svm = svm)
resamps = resamples(model_list)
summary(resamps)

Call:
summary.resamples(object = resamps)

Models: glm, svm 
Number of resamples: 5 

ROC 
      Min. 1st Qu. Median   Mean 3rd Qu.   Max. NA's
glm 0.9994  0.9995 0.9998 0.9997  0.9999 1.0000    0
svm 0.9958  0.9978 0.9978 0.9977  0.9982 0.9992    0

Sens 
      Min. 1st Qu. Median   Mean 3rd Qu. Max. NA's
glm 0.9955  0.9978 1.0000 0.9987       1    1    0
svm 0.9933  0.9933 0.9978 0.9969       1    1    0

Spec 
      Min. 1st Qu. Median   Mean 3rd Qu.   Max. NA's
glm 0.9620   0.975 0.9873 0.9798  0.9873 0.9875    0
svm 0.8354   0.875 0.8875 0.8892  0.9241 0.9241    0
dotplot(resamps, metric = 'ROC')

Conclusion

The glmnet model outperformed all other models trained using both the RTextTools and caret packages as it only produced 4 errors and had the higher AUC value. It also has several other factors in its favor:

  • It tends to be relatively fast to train
  • Because of the lasso regression component, which seeks to minimize the number of non-zero coefficients, it can be used for feature selection
  • It is interpretable as the coefficients provide information about direction and magnitude
  • Lastly, the availability of the ROC curve allows the modeler to decide which classification probability threshold is most appropriate for the situation at hand
LS0tCnRpdGxlOiAiSGFvLVdlZWsxMCIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgdGhlbWU6IHlldGkKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKLS0tCgojU3VtbWFyeSMgIApUaGlzIHdlZWsncyBwcm9qZWN0IGludm9sdmVzIHRyYWluaW5nIGEgcHJlZGljdGl2ZSBtb2RlbCB0byBkaXN0aW5ndWlzaCBiZXR3ZWVuIHNwYW0gYW5kIGhhbSBlbWFpbHMuIFRoZSBnZW5lcmFsIHN0ZXBzIGNhbiBiZSBicm9rZW4gZG93biBhcyBmb2xsb3dzOiAgCgoqIFJlYWQgZW1haWxzIGludG8gUiAgCiogTGFiZWwgZW1haWxzIGFzIHNwYW0gb3IgaGFtICAKKiBDbGVhbnNlIGVtYWlscyB0byAxKSB3b3JrIHdlbGwgd2l0aCB0aGUgZnVuY3Rpb25zIGFuZCAyKSB3b3JrIHdlbGwgd2l0aCB0aGUgdHJhaW5pbmcgYW5kIHByZWRpY3Rpb24gcHJvY2Vzc2VzICAKKiBTcGxpdCB0aGUgZW1haWxzIGludG8gdHJhaW5pbmcgYW5kIHRlc3Qgc2V0cyAgCiogVHJhaW4gdmFyaW91cyBtb2RlbHMgb24gdGhlIHRyYWluaW5nIHNldCAgCiogVGVzdCBtb2RlbHMgb24gdGhlIHRlc3Qgc2V0ICAKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkodG0pCmxpYnJhcnkoc3RyaW5ncikKbGlicmFyeShYTUwpCmxpYnJhcnkoUlRleHRUb29scykKbGlicmFyeShST0NSKQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGdncGxvdDIpCgpzZXR3ZCgifi9Hb29nbGUgRHJpdmUvQ1VOWS9naXQvREFUQTYwNy9XZWVrMTAiKQojc2V0d2QoIkM6L1VzZXJzL2JoYW8vR29vZ2xlIERyaXZlL0NVTlkvZ2l0L0RBVEE2MDcvV2VlazEwIikKc2V0LnNlZWQoMTIzKQpgYGAKCiNDdXN0b20gZGF0YSBjbGVhbnNpbmcgZnVuY3Rpb25zIyAgCkkgd3JvdGUgdHdvIGZ1bmN0aW9ucyB0byBzZXBhcmF0ZSB0aGUgZW1haWwgY2xlYW5zaW5nIGFuZCBjb3JwdXMgY2xlYW5zaW5nIHByb2Nlc3Nlcy4gIAoKYGBge3J9CiMgY3JlYXRlIGZ1bmN0aW9uIHRvIGNsZWFuc2UgZW1haWxzIGJ5IHJlbW92aW5nIG5vbiBBU0NJSSBjaGFyYWN0ZXJzCmNsZWFuc2VfZW1haWxzID0gZnVuY3Rpb24oZGF0KSB7CiAgZGF0ID0gc3RyX2MoZGF0LCBjb2xsYXBzZSA9ICIgIikKICBkYXQyIDwtIHVubGlzdChzdHJzcGxpdChkYXQsIHNwbGl0PSIgIikpCiAgZGF0MyA8LSBncmVwKCJkYXQyIiwgaWNvbnYoZGF0MiwgImxhdGluMSIsICJBU0NJSSIsIHN1Yj0iZGF0MiIpKQogIGlmIChsZW5ndGgoZGF0MykgPT0gMCkgewogICAgZGF0NCA9IGRhdDIKICB9IGVsc2UgZGF0NCA9IGRhdDJbLWRhdDNdCiAgZGF0NSA8LSBwYXN0ZShkYXQ0LCBjb2xsYXBzZSA9ICIgIikKfQoKY2xlYW5zZV9jb3JwdXMgPSBmdW5jdGlvbih4KSB7CiAgeCA9IHRtX21hcCh4LCBQbGFpblRleHREb2N1bWVudCkgICMgdG8gbWFrZSB0bSBwYWNrYWdlIHBsYXkgbmljZQogIHggPSB0bV9tYXAoeCwgcmVtb3ZlTnVtYmVycykKICB4ID0gdG1fbWFwKHgsIHJlbW92ZVB1bmN0dWF0aW9uKSAgIyBsZWF2aW5nIHB1bmN0dWF0aW9uIGluIGNhc2UgcmVsZXZhbnQgdG8gc3BhbSBoYW0gZmlsdGVyCiAgeCA9IHRtX21hcCh4LCBzdHJpcFdoaXRlc3BhY2UpCiAgeCA9IHRtX21hcCh4LCBzdGVtRG9jdW1lbnQpCiAgeCA9IHRtX21hcCh4LCBjb250ZW50X3RyYW5zZm9ybWVyKHRvbG93ZXIpKSAgIyBjb250ZW50X3RyYW5zZm9ybWVyIHJlcXVpcmVkIGJlY2F1c2UgdG9sb3dlciBub3QgdG0gZnVuY3Rpb24KICB4ID0gdG1fbWFwKHgsIHJlbW92ZVdvcmRzLCBzdG9wd29yZHMoJ2VuJykpCn0KYGBgCgojSW1wb3J0aW5nIGVtYWlscyBpbnRvIGRvY3VtZW50IGNvcnB1cyMgIApVc2luZyB0aGUgYHRtYCBsaWJyYXJ5LCBJIGNyZWF0ZWQgYW4gZW1haWwgY29ycHVzIG9mIGRvY3VtZW50cywgZWFjaCBvZiB3aGljaCBjb250YWluZWQgdGhlIGNsZWFuc2VkIGVtYWlsIGNvbnRlbnQgYW5kIGl0cyBzcGFtL2hhbSBsYWJlbCBhcyBtZXRhZGF0YS4gIAoKYGBge3J9CnNwYW1fcGF0aCA9ICcyMDAyMTAxMF9zcGFtLycKIyBhZGQgc3BhbSB0byBlbWFpbCBjb3JwdXMKIyBjcmVhdGUgaW5pdGlhbCBjb3JwdXMKZW1haWwgPSByZWFkTGluZXMoc3RyX2Moc3BhbV9wYXRoLCBsaXN0LmZpbGVzKHNwYW1fcGF0aClbMV0pKQplbWFpbCA9IGNsZWFuc2VfZW1haWxzKGVtYWlsKQplbWFpbF9jb3JwdXMgPSBDb3JwdXMoVmVjdG9yU291cmNlKGVtYWlsKSkKZW1haWxfY29ycHVzID0gY2xlYW5zZV9jb3JwdXMoZW1haWxfY29ycHVzKQptZXRhKGVtYWlsX2NvcnB1c1tbMV1dLCAnc3BhbScpID0gMQoKZm9yIChpIGluIDI6bGVuZ3RoKGxpc3QuZmlsZXMoc3BhbV9wYXRoKSkpIHsKICBlbWFpbCA9IHJlYWRMaW5lcyhzdHJfYyhzcGFtX3BhdGgsIGxpc3QuZmlsZXMoc3BhbV9wYXRoKVtpXSkpCiAgZW1haWwgPSBjbGVhbnNlX2VtYWlscyhlbWFpbCkKCiAgIyBhZGQgbWV0YSBkYXRhIHRvIGhvdXNlIHNwYW0gY2xhc3NpZmljYXRpb24KICBpZiAobGVuZ3RoKGVtYWlsKSAhPSAwKSB7CiAgICB0bXBfY29ycHVzID0gQ29ycHVzKFZlY3RvclNvdXJjZShlbWFpbCkpCiAgICB0bXBfY29ycHVzID0gY2xlYW5zZV9jb3JwdXModG1wX2NvcnB1cykKICAgIG1ldGEodG1wX2NvcnB1c1tbMV1dLCAnc3BhbScpID0gMQogICAgZW1haWxfY29ycHVzID0gYyhlbWFpbF9jb3JwdXMsIHRtcF9jb3JwdXMpCiAgfQp9CgplYXN5X2hhbV9wYXRoID0gJzIwMDIxMDEwX2Vhc3lfaGFtLycKIyBhZGQgaGFtIHRvIGVtYWlsIGNvcnB1cwpmb3IgKGkgaW4gMTpsZW5ndGgobGlzdC5maWxlcyhlYXN5X2hhbV9wYXRoKSkpIHsKICBlbWFpbCA9IHJlYWRMaW5lcyhzdHJfYyhlYXN5X2hhbV9wYXRoLCBsaXN0LmZpbGVzKGVhc3lfaGFtX3BhdGgpW2ldKSkKICBlbWFpbCA9IGNsZWFuc2VfZW1haWxzKGVtYWlsKQoKICAjIGFkZCBtZXRhIGRhdGEgdG8gaG91c2Ugc3BhbSBjbGFzc2lmaWNhdGlvbgogIGlmIChsZW5ndGgoZW1haWwpICE9IDApIHsKICAgIHRtcF9jb3JwdXMgPSBDb3JwdXMoVmVjdG9yU291cmNlKGVtYWlsKSkKICAgIHRtcF9jb3JwdXMgPSBjbGVhbnNlX2NvcnB1cyh0bXBfY29ycHVzKQogICAgbWV0YSh0bXBfY29ycHVzW1sxXV0sICdzcGFtJykgPSAwCiAgICBlbWFpbF9jb3JwdXMgPSBjKGVtYWlsX2NvcnB1cywgdG1wX2NvcnB1cykKICB9Cn0KCmhhcmRfaGFtX3BhdGggPSAnMjAwMjEwMTBfaGFyZF9oYW0vJwojIGFkZCBoYW0gdG8gZW1haWwgY29ycHVzCmZvciAoaSBpbiAxOmxlbmd0aChsaXN0LmZpbGVzKGhhcmRfaGFtX3BhdGgpKSkgewogIGVtYWlsID0gcmVhZExpbmVzKHN0cl9jKGhhcmRfaGFtX3BhdGgsIGxpc3QuZmlsZXMoaGFyZF9oYW1fcGF0aClbaV0pKQogIGVtYWlsID0gY2xlYW5zZV9lbWFpbHMoZW1haWwpCgogICMgYWRkIG1ldGEgZGF0YSB0byBob3VzZSBzcGFtIGNsYXNzaWZpY2F0aW9uCiAgaWYgKGxlbmd0aChlbWFpbCkgIT0gMCkgewogICAgdG1wX2NvcnB1cyA9IENvcnB1cyhWZWN0b3JTb3VyY2UoZW1haWwpKQogICAgdG1wX2NvcnB1cyA9IGNsZWFuc2VfY29ycHVzKHRtcF9jb3JwdXMpCiAgICBtZXRhKHRtcF9jb3JwdXNbWzFdXSwgJ3NwYW0nKSA9IDAKICAgIGVtYWlsX2NvcnB1cyA9IGMoZW1haWxfY29ycHVzLCB0bXBfY29ycHVzKQogIH0KfQpgYGAKCiNSYW5kb21pemluZyBlbWFpbCBjb3JwdXMjICAKQmVjYXVzZSAxKSB0aGUgZW1haWxzIHdlcmUgcmVhZCBpbiBvcmRlciBvZiBzcGFtLCBlYXN5IGhhbSBhbmQgaGFyZCBoYW0gYW5kIDIpIHRoZSBgUlRleHRUb29sc2AgY29udGFpbmVyIG9iamVjdCByZXF1aXJlcyB0aGUgdHJhaW5pbmcgZGF0YSB0byByZXByZXNlbnQgdGhlIDE6WCBkb2N1bWVudHMgYW5kIHRoZSB0ZXN0IGRhdGEgdG8gcmVwcmVzZW50IHRoZSBYOk4gZG9jdW1lbnRzLCBJIGhhZCB0byByYW5kb21pemUgdGhlIGVtYWlscyB3aXRoaW4gdGhlIGNvcnB1cyBiZWZvcmUgY3JlYXRpbmcgdGhlIGNvbnRhaW5lciBvYmplY3QuICAKCmBgYHtyfQojIHJhbmRvbWl6ZSBjb3JwdXMgb3JkZXIKTiA9IGxlbmd0aChlbWFpbF9jb3JwdXMpCnJhbmRfaW5kZXggPSBzYW1wbGUoMTpOKQplbWFpbF9jb3JwdXMgPSBlbWFpbF9jb3JwdXNbcmFuZF9pbmRleF0KdG1fZmlsdGVyKGVtYWlsX2NvcnB1c1sxOjEwMF0sIEZVTiA9IGZ1bmN0aW9uKHgpIG1ldGEoeClbWydzcGFtJ11dID09IDEpICAjIGNoZWNrIHRoYXQgc3BhbSBhbmQgaGFtIGhhdmUgYmVlbiByYW5kb21pemVkCgojIGNyZWF0ZSBkb2N1bWVudCB0ZXJtIG1hdHJpeCAKZHRtID0gRG9jdW1lbnRUZXJtTWF0cml4KGVtYWlsX2NvcnB1cykKZHRtID0gcmVtb3ZlU3BhcnNlVGVybXMoZHRtLCAxLSgxMC9sZW5ndGgoZW1haWxfY29ycHVzKSkpCiNpbnNwZWN0KGR0bVsxMDA6MTIwLCAxMDA6MTEwXSkKc3BhbV9sYWJlbHMgPSB1bmxpc3QobWV0YShlbWFpbF9jb3JwdXMsICdzcGFtJykpCmBgYAoKI01vZGVsaW5nIHdpdGggdGhlIFJUZXh0VG9vbHMgcGFja2FnZSMgIApXaXRoIHRoZSBkYXRhIHJhbmRvbWl6ZWQsIEkgY3JlYXRlZCBhbiBgUlRleHRUb29sc2AgY29udGFpbmVyLCB3aGljaCBpbiB0dXJuIHdvdWxkIGJlIHVzZWQgdG8gdHJhaW4gdGhlIHZhcmlvdXMgbW9kZWxzIGFuZCBhbmFseXplIHRoZWlyIHJlc3BlY3RpdmUgcmVzdWx0cy4gSSB0cmFpbmVkIHRocmVlIGFsZ29yaXRobXM6IDEpIHN1cHBvcnQgdmVjdG9yIG1hY2hpbmUgKFNWTSksIDIpIGNsYXNzaWZpY2F0aW9uIHRyZWUgKFRSRUUpIGFuZCAzKSBtYXhpbXVtIGVudHJvcHkgKE1BWEVOVCkuIEFzIGlsbHVzdHJhdGVkIGluIHRoZSBjb25mdXNpb24gbWF0cmljZXMgYW5kIHN1bW1hcnkgYW5hbHl0aWNzIGJlbG93LCB0aGUgYWNjdXJhY3kgb2YgYWxsIHRocmVlIG1vZGVscyBpcyBzdXBlcmIuIE91dCBvZiA2NjAgZW1haWxzIGluIHRoZSB0ZXN0IHNldCwgdGhlIG1vZGVscyBvbmx5IG1pc2NsYXNzaWZpZWQgNy04IGVtYWlscyBpbiB0b3RhbC4gICAKCmBgYHtyfQojIGNyZWF0ZSBjb250YWluZXIKY29udGFpbmVyID0gY3JlYXRlX2NvbnRhaW5lcihkdG0sIGxhYmVscyA9IHNwYW1fbGFiZWxzLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpblNpemUgPSAxOnJvdW5kKE4qMC44LDApLCB0ZXN0U2l6ZSA9IChyb3VuZChOKjAuOCwwKSsxKTpOLCAgIyB1c2UgODAlIG9mIGRhdGEgZm9yIHRyYWluCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmlyZ2luID0gRkFMU0UpCgojIGVzdGltYXRpb24gcHJvY2VkdXJlCnN2bV9tb2RlbCA9IHRyYWluX21vZGVsKGNvbnRhaW5lciwgJ1NWTScpCiNjcm9zc192YWxpZGF0ZShjb250YWluZXIsIG5mb2xkID0gNSwgYWxnb3JpdGhtID0gJ1NWTScpCnRyZWVfbW9kZWwgPSB0cmFpbl9tb2RlbChjb250YWluZXIsICdUUkVFJykKI2Nyb3NzX3ZhbGlkYXRlKGNvbnRhaW5lciwgbmZvbGQgPSA1LCBhbGdvcml0aG0gPSAnVFJFRScpCm1heGVudF9tb2RlbCA9IHRyYWluX21vZGVsKGNvbnRhaW5lciwgJ01BWEVOVCcpCiNjcm9zc192YWxpZGF0ZShjb250YWluZXIsIG5mb2xkID0gNSwgYWxnb3JpdGhtID0gJ01BWEVOVCcpCgpzdm1fb3V0ID0gY2xhc3NpZnlfbW9kZWwoY29udGFpbmVyLCBzdm1fbW9kZWwpCnRyZWVfb3V0ID0gY2xhc3NpZnlfbW9kZWwoY29udGFpbmVyLCB0cmVlX21vZGVsKQptYXhlbnRfb3V0ID0gY2xhc3NpZnlfbW9kZWwoY29udGFpbmVyLCBtYXhlbnRfbW9kZWwpCgojIGV2YWx1YXRpb24KbGFiZWxzX291dCA9IGRhdGEuZnJhbWUoCiAgY29ycmVjdF9sYWJlbCA9IHNwYW1fbGFiZWxzWyhyb3VuZChOKjAuOCwwKSsxKTpOXSwgCiAgc3ZtID0gYXMuY2hhcmFjdGVyKHN2bV9vdXRbLDFdKSwKICB0cmVlID0gYXMuY2hhcmFjdGVyKHRyZWVfb3V0WywxXSksCiAgbWF4ZW50ID0gYXMuY2hhcmFjdGVyKG1heGVudF9vdXRbLDFdKSwKICBzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UKKQoKIyBzdm0gY29uZnVzaW9uIHRhYmxlCnRhYmxlKGFjdHVhbCA9IGxhYmVsc19vdXRbLDFdLCBwcmVkaWN0ZWQgPSBsYWJlbHNfb3V0WywyXSkKIyB0cmVlIGNvbmZ1c2lvbiB0YWJsZQp0YWJsZShhY3R1YWwgPSBsYWJlbHNfb3V0WywxXSwgcHJlZGljdGVkID0gbGFiZWxzX291dFssM10pCiMgbWF4ZW50IGNvbmZ1c2lvbiB0YWJsZQp0YWJsZShhY3R1YWwgPSBsYWJlbHNfb3V0WywxXSwgcHJlZGljdGVkID0gbGFiZWxzX291dFssNF0pCgojIGFuYWx5dGljcwpzdm1fYW5hbHl0aWNzID0gY3JlYXRlX2FuYWx5dGljcyhjb250YWluZXIsIHN2bV9vdXQpCnN1bW1hcnkoc3ZtX2FuYWx5dGljcykKdHJlZV9hbmFseXRpY3MgPSBjcmVhdGVfYW5hbHl0aWNzKGNvbnRhaW5lciwgdHJlZV9vdXQpCnN1bW1hcnkodHJlZV9hbmFseXRpY3MpCm1heGVudF9hbmFseXRpY3MgPSBjcmVhdGVfYW5hbHl0aWNzKGNvbnRhaW5lciwgbWF4ZW50X291dCkKc3VtbWFyeShtYXhlbnRfYW5hbHl0aWNzKQpgYGAKCiNMaW1pdGF0aW9ucyBvZiBSVGV4dFRvb2xzIyAgClRoZSBgUlRleHRUb29sc2AgbW9kZWxzIHdvcmtlZCB3b25kZXJmdWxseSAtIGJvdGggZXh0cmVtZWx5IGZhc3QgYW5kIGFjY3VyYXRlOyBob3dldmVyLCB0aGUgcGFja2FnZSBkb2VzIG5vdCBhbGxvdyBmb3IgbXVjaCBjdXN0b21pemF0aW9uIGFuZC9vciBmaW5lIHR1bmluZy4gSW5pdGlhbGx5LCBJIGRpZCBub3QgcmVhbGl6ZSB0aGF0IHRoZSBwcm9iYWJpbGl0aWVzIGluIHRoZSBjbGFzc2lmeV9tb2RlbCBvYmplY3RzIHdlcmUgbGFiZWwtc3BlY2lmaWMgdW50aWwgSnVkZCBBbmRlcm1hbm4gcG9pbnRlZCB0aGF0IG91dCB0byBtZS4gV2l0aCBoaXMgcG9pbnRlcnMsIEkgd2FzIGZpbmFsbHkgYWJsZSB0byBwcm9kdWNlIFJPQyBjdXJ2ZXMgZm9yIHRoZSBgUlRleHRUb29sc2AgbW9kZWxzLCBhbiBleGFtcGxlIG9mIHdoaWNoIEkndmUgcHJvZHVjZWQgYmVsb3cuICAKClRoYXQgc2FpZCwgSSBoYWQgYWxyZWFkeSBtb3ZlZCBvbiB0byB1c2luZyB0aGUgYGNhcmV0YCBtb2RlbHMgKGFzIG91dGxpbmVkIGJlbG93KSwgd2hpY2ggc3RpbGwgb2ZmZXJlZCBtb3JlIGZsZXhpYmlsaXR5IGluIHRlcm1zIG9mIG1vZGVsIHBhcmFtZXRlciB0dW5pbmcuICAKCmBgYHtyfQpzdm1fb3V0MiA9IHN2bV9vdXQgJT4lIG11dGF0ZShTVk1fUFJPQjIgPSBpZmVsc2UoU1ZNX0xBQkVMID09IDAsIDEgLSBTVk1fUFJPQiwgU1ZNX1BST0IpKSAKdHJlZV9vdXQyID0gdHJlZV9vdXQgJT4lIG11dGF0ZShUUkVFX1BST0IyID0gaWZlbHNlKFRSRUVfTEFCRUwgPT0gMCwgMSAtIFRSRUVfUFJPQiwgVFJFRV9QUk9CKSkgCm1heGVudF9vdXQyID0gbWF4ZW50X291dCAlPiUgbXV0YXRlKE1BWEVOVFJPUFlfUFJPQjIgPSBpZmVsc2UoTUFYRU5UUk9QWV9MQUJFTCA9PSAwLCAxIC0gTUFYRU5UUk9QWV9QUk9CLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgTUFYRU5UUk9QWV9QUk9CKSkgCgojIGNyZWF0ZSBST0NSIHByZWRpY3Rpb24gb2JqZWN0CnN2bV9wcmVkMSA9IHByZWRpY3Rpb24oc3ZtX291dDIkU1ZNX1BST0IyLCBsYWJlbHNfb3V0JGNvcnJlY3RfbGFiZWwpCnRyZWVfcHJlZDEgPSBwcmVkaWN0aW9uKHRyZWVfb3V0MiRUUkVFX1BST0IyLCBsYWJlbHNfb3V0JGNvcnJlY3RfbGFiZWwpCm1heGVudF9wcmVkMSA9IHByZWRpY3Rpb24obWF4ZW50X291dDIkTUFYRU5UUk9QWV9QUk9CMiwgbGFiZWxzX291dCRjb3JyZWN0X2xhYmVsKQojIGNyZWF0ZSBST0NSIHBlcmZvcm1hbmNlIG9iamVjdApzdm1fcGVyZjEgPSBwZXJmb3JtYW5jZShzdm1fcHJlZDEsIG1lYXN1cmU9InRwciIsIHgubWVhc3VyZT0iZnByIikKdHJlZV9wZXJmMSA9IHBlcmZvcm1hbmNlKHRyZWVfcHJlZDEsIG1lYXN1cmU9InRwciIsIHgubWVhc3VyZT0iZnByIikKbWF4ZW50X3BlcmYxID0gcGVyZm9ybWFuY2UobWF4ZW50X3ByZWQxLCBtZWFzdXJlPSJ0cHIiLCB4Lm1lYXN1cmU9ImZwciIpCgpwbG90KHN2bV9wZXJmMSkKbGluZXModHJlZV9wZXJmMUB4LnZhbHVlc1tbMV1dLCB0cmVlX3BlcmYxQHkudmFsdWVzW1sxXV0sIGNvbCA9IDIpCmxpbmVzKG1heGVudF9wZXJmMUB4LnZhbHVlc1tbMV1dLCBtYXhlbnRfcGVyZjFAeS52YWx1ZXNbWzFdXSwgY29sID0gMykKbGVnZW5kKCdib3R0b21yaWdodCcsIGxlZ2VuZCA9IGMoJ3N2bScsICd0cmVlJywgJ21heGVudCcpLCAKICAgbHR5PTEsIGNvbD1jKCdibGFjaycsICdyZWQnLCAnZ3JlZW4nKSwgYnR5PSduJywgY2V4PS43NSkKYGBgCgojTW9kZWxpbmcgd2l0aCB0aGUgY2FyZXQgcGFja2FnZSMgIApXaGVyZWFzIHRoZSBgUlRleHRUb29sc2AgcGFja2FnZSBpcyBzcGVjaWZpYyB0byB0ZXh0IG1pbmluZywgdGhlIGBjYXJldGAgcGFja2FnZSBpcyBhIG11Y2ggbW9yZSBnZW5lcmFsIHB1cnBvc2UgcHJlZGljdGl2ZSBtb2RlbGluZyBwYWNrYWdlLiBJbiB0aGlzIHBhcnRpY3VsYXIgY2FzZSwgdHJhaW5pbmcgbW9kZWxzIHVzaW5nIHRoZSBgY2FyZXRgIHBhY2thZ2Ugd2FzIGNvbnNpZGVyYWJseSBzbG93ZXIgYW5kL29yIG11Y2ggbW9yZSBwcm9uZSB0byBlcnJvci4gRm9yIGV4YW1wbGUsIHRyYWluaW5nIHRoZSByYW5kb20gZm9yZXN0IG1vZGVscyB0b29rIG1vcmUgdGhhbiAzMCBtaW51dGVzOyBJIHN0b3BwZWQgdGhlIHRyYWluaW5nIGF0IHRoYXQgcG9pbnQgYW5kIGFtIHVuc3VyZSBpZiBpdCB3b3VsZCBoYXZlIGFjdHVhbGx5IGNvbXBsZXRlZCBjb3JyZWN0bHkuIFRyYWluaW5nIHRoZSBOYWl2ZSBCYXllcyBhbmQgbmV1cmFsIG5ldCBtb2RlbHMgcmVzdWx0ZWQgaW4gZXJyb3JzIGZvciB3aGljaCBJIHdhcyB1bmFibGUgdG8gZmluZCBzb2x1dGlvbnMuICAKClRoYXQgc2FpZCwgSSB3YXMgYWJsZSB0byB0cmFpbiBsaW5lYXIvbG9naXN0aWMgcmVncmVzc2lvbiBhbmQgc3VwcG9ydCB2ZWN0b3IgbWFjaGluZSBtb2RlbHMuIFRoZSBiZWF1dHkgb2YgdGhlIGBjYXJldGAgcGFja2FnZSBpcyB0aGF0IGl0IGFsbG93cyBmb3IgdXNlci1mcmllbmRseSwgYXV0b21hdGljIGFuZCB1c2VyLWRlZmluZWQgbW9kZWwgdHVuaW5nIGR1cmluZyB0aGUgdHJhaW5pbmcgcHJvY2VzczogIAoKKiBUaGUgYGdsbW5ldGAgbW9kZWwgaGFzIHR3byB0dW5pbmcgcGFyYW1ldGVyczogMSkgYWxwaGEgKDAgdG8gMSBvciAxMDAlIGxhc3NvIHRvIDEwMCUgcmlkZ2UpIGFuZCAyKSBsYW1iZGEgKDAgdG8gSW5mIG9yIHRoZSBzaXplIG9mIHRoZSBwZW5hbHR5KSwgYW5kIHRoZSBgc3ZtUmFkaWFsYCBtb2RlbCBoYXMgdHdvIHR1bmluZyBwYXJhbWV0ZXJzOiAxKSBzaWdtYSAoMCB0byBJbmYgb3IgdGhlIGNvbXBsZXhpdHkgb2YgdGhlIG1vZGVsIG9yIHRoZSBiaWFzLXZhcmlhbmNlIHRyYWRlLW9mZikgYW5kIDIpIEMgb3IgdGhlIGNvc3QgcmVndWxhcml6YXRpb24gcGFyYW1ldGVyIC4gQnkgZGVmYXVsdCwgdGhlIGBjYXJldGAgYHRyYWluYCBmdW5jdGlvbiB3aWxsIHNlbGVjdCB0aGUgdHVuaW5nIHBhcmFtZXRlcnMgdGhhdCBwcm9kdWNlIHRoZSBiZXN0IG91dC1vZi1zYW1wbGUgcmVzdWx0cyBiYXNlZCBvbiBhIHVzZXItc3BlY2lmaWVkIG1ldHJpYywgZS5nLiBhY2N1cmFjeSwgUk9DLCBldGMuIGFuZCBib290c3RyYXBwZWQgcmVzYW1wbGluZyAgICAKKiBUaGUgYGNhcmV0YCBgdHJhaW5Db250cm9sYCBvYmplY3QgdGhlbiBhbGxvd3MgdGhlIHVzZXIgdG8gZGVmaW5lIHRoZSByZXNhbXBsaW5nIG1ldGhvZCwgc3VtbWFyeSBmdW5jdGlvbiBmb3IgdHdvIGNsYXNzIGNsYXNzaWZpY2F0aW9uIGFuZCBtb3JlICAKKiBUaGUgYGNhcmV0YCBgdHVuZUxlbmd0aGAgYW5kIGB0dW5lR3JpZGAgYXJndW1lbnRzIGFsbG93IHRoZSB1c2VyIHRvIGZ1cnRoZXIgY29udHJvbCB0aGUgc2V0IG9mIHBhcmFtZXRlciB2YWx1ZXMgb3ZlciB3aGljaCB0byB0dW5lIHRoZSBtb2RlbHMgIAoKYGBge3J9CiMgYXR0ZW1wdCBtb2RlbGluZyB1c2luZyBjYXJldCBwYWNrYWdlCmxpYnJhcnkoY2FyZXQpCiNsaWJyYXJ5KHBhcmFsbGVsKSAgCiNsaWJyYXJ5KGRvUGFyYWxsZWwpICAjIGZvciBPU1ggdXNlIGxpYnJhcnkoZG9NQykgIApsaWJyYXJ5KGNhVG9vbHMpCgojIGNvbnZlcnQgZHRtIHRvIG1hdHJpeApmdWxsX21hdCA9IGFzLm1hdHJpeChkdG0pCnRyYWluX21hdCA9IGZ1bGxfbWF0WzE6cm91bmQoTiowLjgsMCksXQp0ZXN0X21hdCA9IGZ1bGxfbWF0Wyhyb3VuZChOKjAuOCwwKSsxKTpOLF0KIyBjYXJldCB0d29DbGFzc1N1bW1hcnkgcmVxdWlyZXMgbm9uLW51bWVyaWMgY2xhc3NlcwpzcGFtX2xhYmVscyA9IGZhY3RvcihzcGFtX2xhYmVscywgbGV2ZWxzID0gYygwLCAxKSwgbGFiZWxzID0gYygnaGFtJywgJ3NwYW0nKSkgIAp0cmFpbl95ID0gc3BhbV9sYWJlbHNbMTpyb3VuZChOKjAuOCwwKV0KdGVzdF95ID0gc3BhbV9sYWJlbHNbKHJvdW5kKE4qMC44LDApKzEpOk5dCgojdXNlX2NvcmVzID0gZGV0ZWN0Q29yZXMoKS0xCiNjbCA9IG1ha2VDbHVzdGVyKHVzZV9jb3JlcykKI3JlZ2lzdGVyRG9QYXJhbGxlbChjbCkgICMgZm9yIE9TWCB1c2UgcmVnaXN0ZXJEb01DKGNsKQoKIyBzdW1tYXJ5RnVuY3Rpb24gPSB0d29DbGFzc1N1bW1hcnkgYWxsb3dzIHRyYWluKCkgZnVuY3Rpb24gdG8gdXNlIEFVQyBtZXRyaWMgdG8gcmFuayBtb2RlbHM7IAojIGNsYXNzUHJvYnMgPSBUUlVFIGFsbG93cyBzdW1tYXJ5RnVuY3Rpb24gdG8gd29yayBwcm9wZXJseQpteUNvbnRyb2wgPSB0cmFpbkNvbnRyb2wobWV0aG9kID0gJ2N2JywgbnVtYmVyID0gNSwgc3VtbWFyeUZ1bmN0aW9uID0gdHdvQ2xhc3NTdW1tYXJ5LCBjbGFzc1Byb2JzID0gVCwgdmVyYm9zZUl0ZXIgPSBUKQoKIyBnbG1uZXQgPSBhIGdlbmVyYWwgbGluZWFyIG1vZGVsIHdpdGggY29tYmluYXRpb24gb2YgbGFzc28gcmVncmVzc2lvbiAocGVuYWxpemVzIG51bWJlciBvZiBub24temVybyBjb2VmZmljaWVudHMpIGFuZAojIHJpZGdlIHJlZ3Jlc3Npb24gKHBlbmFsaXplcyBhYnNvbHV0ZSBtYWduaXR1ZGUgb2YgY29lZmZpY2llbnRzKTsgCiMgZ2xtbmV0IGhhcyB0d28gdHVuaW5nIHBhcmFtZXRlcnM6IDEpIGFscGhhICgwIHRvIDEgb3IgMTAwJSBsYXNzbyB0byAxMDAlIHJpZGdlKSBhbmQgMikgbGFtYmRhICgwIHRvIEluZiBvciB0aGUgc2l6ZQojIG9mIHRoZSBwZW5hbHR5KQpnbG0gPSB0cmFpbih4ID0gdHJhaW5fbWF0LCB5ID0gdHJhaW5feSwgbWV0aG9kID0gJ2dsbW5ldCcsIG1ldHJpYyA9ICdST0MnLCBmYW1pbHkgPSAnYmlub21pYWwnLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wpCiMgICAgICAgICAgICB0dW5lR3JpZCA9IGV4cGFuZC5ncmlkKGFscGhhID0gc2VxKDAsIDEsIDAuMSksIGxhbWJkYSA9IHNlcSgwLCAwLjEsIDAuMDEpKSkKZ2xtCnBsb3QoZ2xtKQpnbG1fcHJlZCA9IHByZWRpY3QoZ2xtLCBuZXdkYXRhID0gdGVzdF9tYXQsIHR5cGUgPSAncmF3JykKY29sQVVDKGFzLm51bWVyaWMoZ2xtX3ByZWQpLCB0ZXN0X3ksIHBsb3RST0MgPSBUKQpjb25mdXNpb25NYXRyaXgoZ2xtX3ByZWQsIHRlc3RfeSkKCiMgc3VwcG9ydCB2ZWN0b3IgbWFjaGluZQpzdm0gPSB0cmFpbih4ID0gdHJhaW5fbWF0LCB5ID0gdHJhaW5feSwgbWV0aG9kID0gJ3N2bVJhZGlhbCcsIG1ldHJpYyA9ICdST0MnLCBmYW1pbHkgPSAnYmlub21pYWwnLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wpCnN2bQpwbG90KHN2bSkKc3ZtX3ByZWQgPSBwcmVkaWN0KHN2bSwgbmV3ZGF0YSA9IHRlc3RfbWF0LCB0eXBlID0gJ3JhdycpCmNvbEFVQyhhcy5udW1lcmljKHN2bV9wcmVkKSwgdGVzdF95LCBwbG90Uk9DID0gVCkKY29uZnVzaW9uTWF0cml4KHN2bV9wcmVkLCB0ZXN0X3kpCgojIG5haXZlIGJheWVzIC0tIEZBSUxFRAojIG5iID0gdHJhaW4oeCA9IHRyYWluX21hdCwgeSA9IHRyYWluX3ksIG1ldGhvZCA9ICduYicsIG1ldHJpYyA9ICdST0MnLCBmYW1pbHkgPSAnYmlub21pYWwnLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wpCiMgcGxvdChuYikKIyBuYl9wcmVkID0gcHJlZGljdChuYiwgbmV3ZGF0YSA9IHRlc3RfbWF0LCB0eXBlID0gJ3JhdycpCiMgY29sQVVDKGFzLm51bWVyaWMobmJfcHJlZCksIHRlc3RfeSwgcGxvdFJPQyA9IFQpCgojIHJhbmRvbSBmb3Jlc3QgLS0gVE9PIFNMT1cKI3JmID0gdHJhaW4oeCA9IHRyYWluX21hdCwgeSA9IHRyYWluX3ksIG1ldGhvZCA9ICdyZicsIG1ldHJpYyA9ICdST0MnLCBmYW1pbHkgPSAnYmlub21pYWwnLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wpCgojIHJhbmdlciA9IHJhbmRvbSBmb3Jlc3QgbW9kZWwgd2hpY2ggaGFzIG9uZSB0dW5pbmcgcGFyYW1ldGVyIG10cnkgKDIgdG8gMTAwKSBvciB0aGUgbnVtYmVyIG9mIHZhcmlhYmxlcyB0byBjb25zaWRlcgojIGF0IGVhY2ggc3BsaXQ7IHRoaXMgY2FuIGJlIGNvbnRyb2xsZWQgYnkgYWRqdXN0aW5nIHRoZSB0dW5lTGVuZ3RoIHBhcmFtZXRlciBvciBieSBkZWZpbmluZyBhIGN1c3RvbSB0dW5lR3JpZAojIC0tIEZBSUxFRAojcmYgPSB0cmFpbih4ID0gdHJhaW5fbWF0LCB5ID0gbWFrZS5uYW1lcyh0cmFpbl95KSwgbWV0aG9kID0gJ3JhbmdlcicsIG1ldHJpYyA9ICdST0MnLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wpCgojIG5ldXJhbCBuZXR3b3JrIC0tIEZBSUxFRAojbm5ldCA9IHRyYWluKHggPSB0cmFpbl9tYXQsIHkgPSB0cmFpbl95LCBtZXRob2QgPSAnbm5ldCcsIG1ldHJpYyA9ICdST0MnLCBmYW1pbHkgPSAnYmlub21pYWwnLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wpCiNzdG9wQ2x1c3RlcihjbCkKI3JlZ2lzdGVyRG9TRVEoKQpgYGAKCiNBIG5vdGUgb24gUk9DIGN1cnZlcyBmb3Igc3BhbSBkZXRlY3Rpb24jICAKVXNpbmcgdGhlIGBnbG1uZXRgIFJPQyBjdXJ2ZSBhcyBhbiBleGFtcGxlLCB0aGUgbW9kZWwgYWNoaWV2ZXMgYW4gZXh0cmVtZWx5IGhpZ2ggdHJ1ZSBwb3NpdGl2ZSByYXRlIChUUFIgPiA5NSUpIHdpdGhvdXQgaW5jdXJyaW5nIGFueSBmYWxzZSBwb3NpdGl2ZXMgKEZQUiA9IDAlKS4gUHJhY3RpY2FsbHkgc3BlYWtpbmcsIHRoaXMgbWVhbnMgdGhhdCBvbmUgY291bGQgc2VsZWN0IGEgcHJvYmFiaWxpdHkgdGhyZXNob2xkIHRoYXQgd291bGQgbm90IGluY29ycmVjdGx5IGNsYXNzaWZ5IGFueSBoYW0gYXMgc3BhbSB3aGlsZSBsZXR0aW5nIHRocm91Z2ggYSBzbWFsbCBhbW91bnQgb2Ygc3BhbS4gVGhpcyBtaWdodCBiZSBhbiBhcHByb3ByaWF0ZSBzZXR0aW5nIGZvciBhIGJ1c2luZXNzLCB3aGVyZSBhIG1pc2NsYXNzaWZpY2F0aW9uIG9mIGhhbSBhcyBzcGFtIGNvdWxkIHJlc3VsdCBpbiByZWFsIGJ1c2luZXNzIGRhbWFnZS4gQ29udmVyc2VseSwgZm9yIHBlcnNvbmFsIGVtYWlsIHB1cnBvc2VzLCBvbmUgbWF5IGZpbmQgaXQgYWNjZXB0YWJsZSB0byBtaXNzIGEgcmVhbCBlbWFpbCBvciB0d28gaW4gZXhjaGFuZ2UgZm9yIGZldyBzcGFtIGVtYWlscyBpbiBvbmUncyBpbmJveC4gRWl0aGVyIHdheSwgdGhlIFJPQyBjdXJ2ZSBjYW4gaGVscCB0aGUgbW9kZWxlciBkZWNpZGUgb24gdGhlIHByb2JhYmlsaXR5IHRocmVzaG9sZCBhcHByb3ByaWF0ZSBmb3IgdGhlIGdpdmVuIHNpdHVhdGlvbi4gIAoKI0NvbXBhcmluZyBjYXJldCB0cmFpbmVkIG1vZGVscyMgIApUaGUgYGNhcmV0RW5zZW1ibGVgIHBhY2thZ2UgdGhlbiBhbGxvd3MgdXMgY29tcGFyZSBtb2RlbCByZXN1bHRzLiBUaGUgYGRvdHBsb3RgIGZ1bmN0aW9uIHBsb3RzIHRoZSBST0MgYW5kIDk1JSBjb25maWRlbmNlIGludGVydmFscyBmb3IgZWFjaCBtb2RlbC4gQXMgaWxsdXN0cmF0ZWQgYmVsb3csIHRoZSBgZ2xtbmV0YCBtb2RlbCBvdXRwZXJmb3JtcyB0aGUgYHN2bVJhZGlhbGAgbW9kZWwgcXVpdGUgc2lnbmlmaWNhbnRseS4gIAoKYGBge3J9CmxpYnJhcnkoY2FyZXRFbnNlbWJsZSkKCiMgY29tcGFyZSBtb2RlbHMgdXNpbmcgcmVzYW1wbGUKbW9kZWxfbGlzdCA9IGxpc3QoZ2xtID0gZ2xtLCBzdm0gPSBzdm0pCnJlc2FtcHMgPSByZXNhbXBsZXMobW9kZWxfbGlzdCkKc3VtbWFyeShyZXNhbXBzKQpkb3RwbG90KHJlc2FtcHMsIG1ldHJpYyA9ICdST0MnKQpgYGAKCiNDb25jbHVzaW9uIyAgClRoZSBgZ2xtbmV0YCBtb2RlbCBvdXRwZXJmb3JtZWQgYWxsIG90aGVyIG1vZGVscyB0cmFpbmVkIHVzaW5nIGJvdGggdGhlIGBSVGV4dFRvb2xzYCBhbmQgYGNhcmV0YCBwYWNrYWdlcyBhcyBpdCBvbmx5IHByb2R1Y2VkIDQgZXJyb3JzIGFuZCBoYWQgdGhlIGhpZ2hlciBBVUMgdmFsdWUuIEl0IGFsc28gaGFzIHNldmVyYWwgb3RoZXIgZmFjdG9ycyBpbiBpdHMgZmF2b3I6ICAKCiogSXQgdGVuZHMgdG8gYmUgcmVsYXRpdmVseSBmYXN0IHRvIHRyYWluICAKKiBCZWNhdXNlIG9mIHRoZSBsYXNzbyByZWdyZXNzaW9uIGNvbXBvbmVudCwgd2hpY2ggc2Vla3MgdG8gbWluaW1pemUgdGhlIG51bWJlciBvZiBub24temVybyBjb2VmZmljaWVudHMsIGl0IGNhbiBiZSB1c2VkIGZvciBmZWF0dXJlIHNlbGVjdGlvbiAgCiogSXQgaXMgaW50ZXJwcmV0YWJsZSBhcyB0aGUgY29lZmZpY2llbnRzIHByb3ZpZGUgaW5mb3JtYXRpb24gYWJvdXQgZGlyZWN0aW9uIGFuZCBtYWduaXR1ZGUgIAoqIExhc3RseSwgdGhlIGF2YWlsYWJpbGl0eSBvZiB0aGUgUk9DIGN1cnZlIGFsbG93cyB0aGUgbW9kZWxlciB0byBkZWNpZGUgd2hpY2ggY2xhc3NpZmljYXRpb24gcHJvYmFiaWxpdHkgdGhyZXNob2xkIGlzIG1vc3QgYXBwcm9wcmlhdGUgZm9yIHRoZSBzaXR1YXRpb24gYXQgaGFuZCAgCg==