This is a project to build a spam filter for SMS messages using the multinomial Naive Bayes algorithm.

library (readr)
library(stringr)
library(ggplot2)
library(dplyr)
library(purrr)
library(tidyverse)

Import the data as a data frame and rename the columns.

setwd("C:/Users/Ana/Desktop/Data Analytics/CSV Files")
data <- read_tsv("SMSSpamCollection.tsv", col_names = FALSE)
Parsed with column specification:
cols(
  X1 = col_character(),
  X2 = col_character()
)
39 parsing failures.
row col           expected actual                    file
283  X2 delimiter or quote        'SMSSpamCollection.tsv'
283  X2 delimiter or quote      H 'SMSSpamCollection.tsv'
454  X2 delimiter or quote        'SMSSpamCollection.tsv'
454  X2 delimiter or quote      Y 'SMSSpamCollection.tsv'
454  X2 delimiter or quote        'SMSSpamCollection.tsv'
... ... .................. ...... .......................
See problems(...) for more details.
colnames(data) <- c("label", "sms")
head(data, 100)

Explore dataset

nrow(data)
[1] 4837
ncol(data)
[1] 2
data %>% group_by(label) %>% summarise(Freq = n(), percent = n()/nrow(data)*100)

The dataset contains 4837 messages. 87% of all the messages are ham. 13% of all the messages are spam.

Need to create: -a training data set which contains 80% of all the messages (at random) -a cross-validation set which contains 10% of all the messages (at random) -a test set which contains 10% of all the messages (at random)

0.8*nrow(data)
[1] 3869.6
0.1*nrow(data)
[1] 483.7
3870+483+484
[1] 4837

The training set will contain 3870 messages The cross-validation set will contain 483 messages The test set will contain 484 messages

set.seed(2)
random_set_of_numbers <- sample(1:4837, 4837)
training_index <- random_set_of_numbers[1:3870]
x_valid_index <- random_set_of_numbers[3871:4353]
test_index <- random_set_of_numbers[4354:4837]

training_data <- data[training_index,]
x_valid_data <- data[x_valid_index,]
test_data <- data[test_index,]

training_data %>% group_by(label) %>% summarise(Freq = n(), percent = n()/nrow(training_data)*100)
x_valid_data %>% group_by(label) %>% summarise(Freq = n(), percent = n()/nrow(x_valid_data)*100)
test_data %>% group_by(label) %>% summarise(Freq = n(), percent = n()/nrow(test_data)*100)

Clean the sms column by removing all punctuation and converting all letters to lowercase

head(training_data, 20)

training_data_2 <- training_data %>%
  mutate(sms = str_replace_all(sms, "[:punct:]", "")) %>%
  mutate(sms = str_replace_all(sms, "[:digit:]", "")) %>%
  mutate(sms = tolower(sms))

head(training_data_2, 20)

Create a vocabulary of all the unique words in the training set


all_vocab <- c()

for(i in 1:nrow(training_data_2)){
  indv_words <- unlist(str_split(training_data_2[i,2], "\\s+"))
  all_vocab <- c(all_vocab, indv_words)
}

vocab <- unique(all_vocab)
head(vocab, 100)
  [1] "sunshine"  "quiz"      "win"       "a"         "super"     "sony"      "dvd"       "recorder" 
  [9] "if"        "you"       "canname"   "the"       "capital"   "of"        "australia" "text"     
 [17] "mquiz"     "to"        "b"         "asked"     "mobile"    "chatlines" "inclu"     "in"       
 [25] "free"      "mins"      "india"     "cust"      "servs"     "sed"       "yes"       "ler"      
 [33] "got"       "mega"      "bill"      "dont"      "giv"       "shit"      "bailiff"   "due"      
 [41] "days"      "i"         "o"         "£"         "want"      "meeting"   "da"        "will"     
 [49] "call"      "havent"    "eaten"     "all"       "day"       "im"        "sitting"   "here"     
 [57] "staring"   "at"        "this"      "juicy"     "pizza"     "and"       "cant"      "eat"      
 [65] "it"        "these"     "meds"      "are"       "ruining"   "my"        "life"      ""         
 [73] "nightswe"  "nt"        "staying"   "port"      "step"      "liaotoo"   "ex"        "night"    
 [81] "has"       "ended"     "for"       "another"   "morning"   "come"      "special"   "way"      
 [89] "may"       "smile"     "like"      "sunny"     "rays"      "leaves"    "your"      "worries"  
 [97] "blue"      "bay"       "thank"     "princess" 

Now, calculate other parameters which are constant for all calculations: P(Spam) P(Ham) N_spam (number of words in all the spam messages, not the number of spam messages, nor the number of unique words) N_ham (number of words in all the ham messages, not the number of ham messages, nor the number of unique words) N_vocabulary (number of unique words in the training data set)

p_spam <- 3364/(3364+506)
p_ham <- 1-p_spam
n_vocab <- length(vocab)

p_spam
[1] 0.8692506
p_ham
[1] 0.1307494
n_vocab
[1] 7660

Next, need to calculate n_spam and n_ham. To do this, create a vector containing all the words in all the spam messages. n_spam is the length of this vector. Then create a vector containing all the words in all the ham messages. n_ham is the length of this vector. First, split the data to contain two separate dataframes, one for spam messages and one for ham messages.

spam_training <- training_data_2 %>% 
  filter(label == "spam") 

ham_training <- training_data_2 %>%
  filter(label == "ham")

alpha <- 1

#sapply(strsplit(test, " "), length)

Then create the vectors containing all the words in the spam messages. And then do the same for ham messages.

all_spam_words <- c()

for(i in 1:nrow(spam_training)) {
split <- unlist(strsplit(spam_training$sms[i], "\\s+"))
all_spam_words <- c(all_spam_words, split)
}

head(all_spam_words, 100)
  [1] "sunshine"   "quiz"       "win"        "a"          "super"      "sony"       "dvd"        "recorder"  
  [9] "if"         "you"        "canname"    "the"        "capital"    "of"         "australia"  "text"      
 [17] "mquiz"      "to"         "b"          "asked"      "mobile"     "if"         "chatlines"  "inclu"     
 [25] "in"         "free"       "mins"       "india"      "cust"       "servs"      "sed"        "yes"       
 [33] "ler"        "got"        "mega"       "bill"       "dont"       "giv"        "a"          "shit"      
 [41] "bailiff"    "due"        "in"         "days"       "i"          "o"          "£"          "want"      
 [49] "£"          "urgent"     "your"       "mobile"     "was"        "awarded"    "a"          "£"         
 [57] "bonus"      "caller"     "prize"      "on"         "our"        "final"      "attempt"    "contact"   
 [65] "u"          "call"       "you"        "are"        "guaranteed" "the"        "latest"     "nokia"     
 [73] "phone"      "a"          "gb"         "ipod"       "mp"         "player"     "or"         "a"         
 [81] "£"          "prize"      "txt"        "word"       "collect"    "to"         "no"         "ibhltd"    
 [89] "ldnwh"      "pmtmsgrcvd" "enjoy"      "the"        "jamster"    "videosound" "gold"       "club"      
 [97] "with"       "your"       "credits"    "for"       
n_spam <- length(all_spam_words)
n_spam
[1] 11045
all_ham_words <- c()

for(i in 1:nrow(ham_training)) {
split <- unlist(strsplit(ham_training$sms[i], "\\s+"))
all_ham_words <- c(all_ham_words, split)
}

head(all_ham_words, 100)
  [1] "in"        "meeting"   "da"        "i"         "will"      "call"      "you"       "havent"   
  [9] "eaten"     "all"       "day"       "im"        "sitting"   "here"      "staring"   "at"       
 [17] "this"      "juicy"     "pizza"     "and"       "i"         "cant"      "eat"       "it"       
 [25] "these"     "meds"      "are"       "ruining"   "my"        "life"      ""          "nightswe" 
 [33] "nt"        "staying"   "at"        "port"      "step"      "liaotoo"   "ex"        "night"    
 [41] "has"       "ended"     "for"       "another"   "day"       "morning"   "has"       "come"     
 [49] "in"        "a"         "special"   "way"       "may"       "you"       "smile"     "like"     
 [57] "the"       "sunny"     "rays"      "and"       "leaves"    "your"      "worries"   "at"       
 [65] "the"       "blue"      "blue"      "bay"       "thank"     "you"       "princess"  "you"      
 [73] "are"       "so"        "sexy"      "mm"        "you"       "ask"       "him"       "to"       
 [81] "come"      "its"       "enough"    "oh"        "mr"        "sheffield" "you"       "wanna"    
 [89] "play"      "that"      "game"      "okay"      "youre"     "the"       "boss"      "and"      
 [97] "im"        "the"       "nanny"     "you"      
n_ham <- length(all_ham_words)
n_ham
[1] 55740

Next, create a function which takes the vector vocab (the vector that contains all unique vocabulary) and outputs a list of each word with its corresponding probability of occuring within the spam messages. The function first calculates how many time each word appears in the spam messages n_occurances_in_spam. Then it uses this, and the other parameters (calculated above), to calculate pword|spam. i.e. the probability of a particular word occuring, given a message is spam.

find_word_in_spam <- function(word, alpha){
  logic_v <- all_spam_words == word
  n_occurances_in_spam <- sum(logic_v)
  p_word_gvn_spam <- (n_occurances_in_spam+alpha)/(n_spam + alpha*n_vocab)
  return(p_word_gvn_spam)
}

p_list_spam <- map2(vocab, 1, find_word_in_spam)
names(p_list_spam) <- vocab

head(p_list_spam, 10)
$sunshine
[1] 0.0004811548

$quiz
[1] 0.0006415397

$win
[1] 0.002566159

$a
[1] 0.01475541

$super
[1] 0.0001603849

$sony
[1] 0.0003742315

$dvd
[1] 0.0003207698

$recorder
[1] 0.0001069233

$`if`
[1] 0.001283079

$you
[1] 0.01079925

Do the same as above, but for ham messages. i.e. Create a function which takes the vector vocab and outputs a list with each word with its corresponding probability of occuring within the ham messages. The function first calculates how many time each word appears in the ham messages n_occurances_in_ham. Then it uses this, and the other parameters (calculated above), to calculate pword|ham. i.e. the probability of a particular word occuring, given a message is ham.

find_word_in_ham <- function(word, alpha){
  logic_v <- all_ham_words == word
  n_occurances_in_ham <- sum(logic_v)
  p_word_gvn_ham <- (n_occurances_in_ham+alpha)/(n_ham + alpha*n_vocab)
  return(p_word_gvn_ham)
}

p_list_ham <- map2(vocab, 1, find_word_in_ham)
names(p_list_ham) <- vocab

head(p_list_ham, 10)
$sunshine
[1] 1.577287e-05

$quiz
[1] 1.577287e-05

$win
[1] 0.0002208202

$a
[1] 0.01364353

$super
[1] 6.309148e-05

$sony
[1] 3.154574e-05

$dvd
[1] 3.154574e-05

$recorder
[1] 1.577287e-05

$`if`
[1] 0.00455836

$you
[1] 0.02334385

Next, I just do some testing here to see if I think its working…

test_message <- "hello im running a test sentance to check that this is working"
test_message
[1] "hello im running a test sentance to check that this is working"
vec_test_words <- unlist(strsplit(test_message, "\\s+"))
vec_test_words
 [1] "hello"    "im"       "running"  "a"        "test"     "sentance" "to"       "check"    "that"    
[10] "this"     "is"       "working" 
a <- unlist(p_list_ham[c(vec_test_words)])
a
       hello           im      running            a         test           to        check         that 
0.0006388702 0.0067975082 0.0000902559 0.0152921814 0.0003203200 0.0232559374 0.0004265034 0.0069921778 
        this           is      working 
0.0035589141 0.0101422858 0.0003911089 
p_message_ham <- p_ham*prod(a)
p_message_ham
[1] 2.457824e-31
b <- unlist(p_list_spam[c(vec_test_words)])
b
       hello           im      running            a         test           to        check         that 
2.624672e-04 6.858014e-04 8.466684e-06 2.329185e-02 8.466684e-06 4.047921e-02 9.313352e-05 1.701803e-03 
        this           is      working 
5.427144e-03 9.237152e-03 8.466684e-06 
p_message_spam <- p_spam*prod(b)
p_message_spam
[1] 7.114088e-37
if_else(p_message_ham>=p_message_spam, "ham", "spam")
[1] "ham"

That looks reasonable.

NExt, write a function which takes in messages and returns whether or not it deems it to be spam or ham

classification_function <- function(message) {
  cleaned_1 <- str_replace_all(message, "[:punct:]", "") 
  cleaned_2 <- str_replace_all(cleaned_1, "[:digit:]", "")
  cleaned_3 <- tolower(cleaned_2)
  split_message <- unlist(strsplit(cleaned_3, "\\s+"))
  #print(split_message)
  a <- unlist(p_list_ham[c(split_message)])
  p_message_ham <- p_ham*prod(a)
  b <- unlist(p_list_spam[c(split_message)])
  p_message_spam <- p_spam*prod(b)
  if_else(p_message_ham>=p_message_spam, "ham", "spam")
}

classification_function(test_message)
[1] "ham"
filter_output <- unlist(map(training_data$sms, classification_function))
comparison <- cbind(training_data, filter_output)

head(comparison)
NA
NA
sum(comparison$label == comparison$filter_output)/nrow(comparison)
[1] 0.9416021

The filter was 94% accurate!

Now assess the alpha value. Previously, alpha has been set to 1. However, see how the accuracy varies depending on the value of alpha.

alpha_range <- seq(0.1, 1, by = 0.1)

for(alpha in alpha_range) {

p_list_spam <- map2(vocab, alpha, find_word_in_spam)
names(p_list_spam) <- vocab

p_list_ham <- map2(vocab, alpha, find_word_in_ham)
names(p_list_ham) <- vocab

filter_output <- unlist(map(x_valid_data$sms, classification_function))
comparison <- cbind(x_valid_data$label, filter_output)

accuracy <- sum(comparison[,1] == comparison[,2])/nrow(comparison)
nrow(comparison)
#print(accuracy)
cat("The accuracy of the spam filter with alpha =", alpha, " is", (accuracy*100), "%", "\n")
}
The accuracy of the spam filter with alpha = 0.1  is 95.85921 % 
The accuracy of the spam filter with alpha = 0.2  is 95.03106 % 
The accuracy of the spam filter with alpha = 0.3  is 95.2381 % 
The accuracy of the spam filter with alpha = 0.4  is 95.03106 % 
The accuracy of the spam filter with alpha = 0.5  is 95.03106 % 
The accuracy of the spam filter with alpha = 0.6  is 94.82402 % 
The accuracy of the spam filter with alpha = 0.7  is 94.61698 % 
The accuracy of the spam filter with alpha = 0.8  is 94.61698 % 
The accuracy of the spam filter with alpha = 0.9  is 94.61698 % 
The accuracy of the spam filter with alpha = 1  is 94.40994 % 

From this, it seems that the lower alpha numbers produce a more accurate comparison. Therefore, alpha = 0.1 will be used for the test set.


alpha <- 0.1

p_list_spam <- map2(vocab, alpha, find_word_in_spam)
names(p_list_spam) <- vocab

p_list_ham <- map2(vocab, alpha, find_word_in_ham)
names(p_list_ham) <- vocab

filter_output <- unlist(map(test_data$sms, classification_function))
comparison <- cbind(test_data, filter_output)

accuracy <- sum(comparison$label == comparison$filter_output)/nrow(comparison)

nrow(comparison)
[1] 484
print(accuracy)
[1] 0.9504132
cat("With alpha =", alpha, "the spam filter correctly predicted", (accuracy*100), "%", "of all messages as being spam or ham", "\n")
With alpha = 0.1 the spam filter correctly predicted 95.04132 % of all messages as being spam or ham 
head(comparison)
NA

The algorithm has correctly predicted 95% of messages from the test data set as being either spam or ham.

LS0tDQp0aXRsZTogIlNwYW0gRmlsdGVyIC0gTmFpdmUgQmF5ZXMiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KVGhpcyBpcyBhIHByb2plY3QgdG8gYnVpbGQgYSBzcGFtIGZpbHRlciBmb3IgU01TIG1lc3NhZ2VzIHVzaW5nIHRoZSBtdWx0aW5vbWlhbCBOYWl2ZSBCYXllcyBhbGdvcml0aG0uDQpgYGB7cn0NCmxpYnJhcnkgKHJlYWRyKQ0KbGlicmFyeShzdHJpbmdyKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkocHVycnIpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmBgYA0KSW1wb3J0IHRoZSBkYXRhIGFzIGEgZGF0YSBmcmFtZSBhbmQgcmVuYW1lIHRoZSBjb2x1bW5zLg0KYGBge3J9DQpzZXR3ZCgiQzovVXNlcnMvQW5hL0Rlc2t0b3AvRGF0YSBBbmFseXRpY3MvQ1NWIEZpbGVzIikNCmRhdGEgPC0gcmVhZF90c3YoIlNNU1NwYW1Db2xsZWN0aW9uLnRzdiIsIGNvbF9uYW1lcyA9IEZBTFNFKQ0KY29sbmFtZXMoZGF0YSkgPC0gYygibGFiZWwiLCAic21zIikNCmhlYWQoZGF0YSwgMTAwKQ0KYGBgDQpFeHBsb3JlIGRhdGFzZXQNCmBgYHtyfQ0KbnJvdyhkYXRhKQ0KbmNvbChkYXRhKQ0KZGF0YSAlPiUgZ3JvdXBfYnkobGFiZWwpICU+JSBzdW1tYXJpc2UoRnJlcSA9IG4oKSwgcGVyY2VudCA9IG4oKS9ucm93KGRhdGEpKjEwMCkNCmBgYA0KVGhlIGRhdGFzZXQgY29udGFpbnMgNDgzNyBtZXNzYWdlcy4NCjg3JSBvZiBhbGwgdGhlIG1lc3NhZ2VzIGFyZSBoYW0uDQoxMyUgb2YgYWxsIHRoZSBtZXNzYWdlcyBhcmUgc3BhbS4NCg0KTmVlZCB0byBjcmVhdGU6DQotYSB0cmFpbmluZyBkYXRhIHNldCB3aGljaCBjb250YWlucyA4MCUgb2YgYWxsIHRoZSBtZXNzYWdlcyAoYXQgcmFuZG9tKQ0KLWEgY3Jvc3MtdmFsaWRhdGlvbiBzZXQgd2hpY2ggY29udGFpbnMgMTAlIG9mIGFsbCB0aGUgbWVzc2FnZXMgKGF0IHJhbmRvbSkNCi1hIHRlc3Qgc2V0IHdoaWNoIGNvbnRhaW5zIDEwJSBvZiBhbGwgdGhlIG1lc3NhZ2VzIChhdCByYW5kb20pDQpgYGB7cn0NCjAuOCpucm93KGRhdGEpDQowLjEqbnJvdyhkYXRhKQ0KDQozODcwKzQ4Mys0ODQNCmBgYA0KVGhlIHRyYWluaW5nIHNldCB3aWxsIGNvbnRhaW4gMzg3MCBtZXNzYWdlcw0KVGhlIGNyb3NzLXZhbGlkYXRpb24gc2V0IHdpbGwgY29udGFpbiA0ODMgbWVzc2FnZXMNClRoZSB0ZXN0IHNldCB3aWxsIGNvbnRhaW4gNDg0IG1lc3NhZ2VzDQoNCmBgYHtyfQ0KI2dlbmVyYXRlIGEgdmVjdG9yIG9mIHJhbmRvbSBudW1iZXJzIGZyb20gMSB0byA0ODM3IGFuZCB1c2UgdGhpcyB0byBnZXQgdGhlIHNhbXBsZXMgcmVxdWlyZWQuDQoNCnNldC5zZWVkKDIpDQpyYW5kb21fc2V0X29mX251bWJlcnMgPC0gc2FtcGxlKDE6NDgzNywgNDgzNykNCnRyYWluaW5nX2luZGV4IDwtIHJhbmRvbV9zZXRfb2ZfbnVtYmVyc1sxOjM4NzBdDQp4X3ZhbGlkX2luZGV4IDwtIHJhbmRvbV9zZXRfb2ZfbnVtYmVyc1szODcxOjQzNTNdDQp0ZXN0X2luZGV4IDwtIHJhbmRvbV9zZXRfb2ZfbnVtYmVyc1s0MzU0OjQ4MzddDQoNCnRyYWluaW5nX2RhdGEgPC0gZGF0YVt0cmFpbmluZ19pbmRleCxdDQp4X3ZhbGlkX2RhdGEgPC0gZGF0YVt4X3ZhbGlkX2luZGV4LF0NCnRlc3RfZGF0YSA8LSBkYXRhW3Rlc3RfaW5kZXgsXQ0KDQojc3VtbWFyaXNlIHRoZSBzZXBhcmF0ZSBkYXRhIHNldHMgdG8gZW5zdXJlIHRoZXkgYXJlIHJlcHJlc2VudGF0aXZlIG9mIHRoZSB3aG9sZSBkYXRhIHNldC4NCg0KdHJhaW5pbmdfZGF0YSAlPiUgZ3JvdXBfYnkobGFiZWwpICU+JSBzdW1tYXJpc2UoRnJlcSA9IG4oKSwgcGVyY2VudCA9IG4oKS9ucm93KHRyYWluaW5nX2RhdGEpKjEwMCkNCnhfdmFsaWRfZGF0YSAlPiUgZ3JvdXBfYnkobGFiZWwpICU+JSBzdW1tYXJpc2UoRnJlcSA9IG4oKSwgcGVyY2VudCA9IG4oKS9ucm93KHhfdmFsaWRfZGF0YSkqMTAwKQ0KdGVzdF9kYXRhICU+JSBncm91cF9ieShsYWJlbCkgJT4lIHN1bW1hcmlzZShGcmVxID0gbigpLCBwZXJjZW50ID0gbigpL25yb3codGVzdF9kYXRhKSoxMDApDQpgYGANCkNsZWFuIHRoZSBzbXMgY29sdW1uIGJ5IHJlbW92aW5nIGFsbCBwdW5jdHVhdGlvbiBhbmQgY29udmVydGluZyBhbGwgbGV0dGVycyB0byBsb3dlcmNhc2UNCmBgYHtyfQ0KI3JlbW92ZSBhbGwgcHVuY3R1YXRpb24sIG51bWJlcnMsIGFuZCBjb252ZXJ0IGFsbCB0byBsb3dlcmNhc2UuIA0KDQpoZWFkKHRyYWluaW5nX2RhdGEsIDIwKQ0KDQp0cmFpbmluZ19kYXRhXzIgPC0gdHJhaW5pbmdfZGF0YSAlPiUNCiAgbXV0YXRlKHNtcyA9IHN0cl9yZXBsYWNlX2FsbChzbXMsICJbOnB1bmN0Ol0iLCAiIikpICU+JQ0KICBtdXRhdGUoc21zID0gc3RyX3JlcGxhY2VfYWxsKHNtcywgIls6ZGlnaXQ6XSIsICIiKSkgJT4lDQogIG11dGF0ZShzbXMgPSB0b2xvd2VyKHNtcykpDQoNCmhlYWQodHJhaW5pbmdfZGF0YV8yLCAyMCkNCmBgYA0KQ3JlYXRlIGEgdm9jYWJ1bGFyeSBvZiBhbGwgdGhlIHVuaXF1ZSB3b3JkcyBpbiB0aGUgdHJhaW5pbmcgc2V0DQoNCmBgYHtyfQ0KDQojaXRlcmF0ZSB0aHJvdWdoIGFsbCB0aGUgcm93cyBvZiB0aGUgdHJhaW5pbmcgZGF0YSBzZXQsIHNwbGl0IGVhY2ggc3RyaW5nIGludG8gaXRzIGNvbXBvbmVudCB3b3JkcyBhbmQgdGhlbiBhZGQgdGhlbSB0byBhIHZlY3Rvci4gVGhlbiB1c2UgdGhlIHVuaXF1ZSgpIGZ1bmN0aW9uIHRvIG9ubHkga2VlcCB1bmlxdWUgd29yZHMgaW4gdGhlIHZlY3Rvci4gVGhpcyBiZWNvbWVzIHRoZSB2b2NhYnVsYXJ5LiANCg0KYWxsX3ZvY2FiIDwtIGMoKQ0KDQpmb3IoaSBpbiAxOm5yb3codHJhaW5pbmdfZGF0YV8yKSl7DQogIGluZHZfd29yZHMgPC0gdW5saXN0KHN0cl9zcGxpdCh0cmFpbmluZ19kYXRhXzJbaSwyXSwgIlxccysiKSkNCiAgYWxsX3ZvY2FiIDwtIGMoYWxsX3ZvY2FiLCBpbmR2X3dvcmRzKQ0KfQ0KDQp2b2NhYiA8LSB1bmlxdWUoYWxsX3ZvY2FiKQ0KaGVhZCh2b2NhYiwgMTAwKQ0KYGBgDQpOb3csIGNhbGN1bGF0ZSBvdGhlciBwYXJhbWV0ZXJzIHdoaWNoIGFyZSBjb25zdGFudCBmb3IgYWxsIGNhbGN1bGF0aW9uczoNClAoU3BhbSkNClAoSGFtKQ0KTl9zcGFtIChudW1iZXIgb2Ygd29yZHMgaW4gYWxsIHRoZSBzcGFtIG1lc3NhZ2VzLCBub3QgdGhlIG51bWJlciBvZiBzcGFtIG1lc3NhZ2VzLCBub3IgdGhlIG51bWJlciBvZiB1bmlxdWUgd29yZHMpDQpOX2hhbSAobnVtYmVyIG9mIHdvcmRzIGluIGFsbCB0aGUgaGFtIG1lc3NhZ2VzLCBub3QgdGhlIG51bWJlciBvZiBoYW0gbWVzc2FnZXMsIG5vciB0aGUgbnVtYmVyIG9mIHVuaXF1ZSB3b3JkcykNCk5fdm9jYWJ1bGFyeSAobnVtYmVyIG9mIHVuaXF1ZSB3b3JkcyBpbiB0aGUgdHJhaW5pbmcgZGF0YSBzZXQpDQoNCmBgYHtyfQ0KI3Bfc3BhbSBpcyB0aGUgbnVtYmVyIG9mIHNwYW0gbWVzc2FnZXMvdG90YWwgbnVtYmVyIG9mIG1lc3NhZ2VzIGluIHRoZSB0cmFpbmluZyBkYXRhIHNldC4gcF9oYW0gPSAxIC0gcF9zcGFtLiANCg0KcF9zcGFtIDwtIDMzNjQvKDMzNjQrNTA2KQ0KcF9oYW0gPC0gMS1wX3NwYW0NCm5fdm9jYWIgPC0gbGVuZ3RoKHZvY2FiKQ0KYWxwaGEgPC0gMQ0KDQpwX3NwYW0NCnBfaGFtDQpuX3ZvY2FiDQphbHBoYQ0KDQpgYGANCk5leHQsIG5lZWQgdG8gY2FsY3VsYXRlIG5fc3BhbSBhbmQgbl9oYW0uIFRvIGRvIHRoaXMsIGNyZWF0ZSBhIHZlY3RvciBjb250YWluaW5nIGFsbCB0aGUgd29yZHMgaW4gYWxsIHRoZSBzcGFtIG1lc3NhZ2VzLiBuX3NwYW0gaXMgdGhlIGxlbmd0aCBvZiB0aGlzIHZlY3Rvci4gVGhlbiBjcmVhdGUgYSB2ZWN0b3IgY29udGFpbmluZyBhbGwgdGhlIHdvcmRzIGluIGFsbCB0aGUgaGFtIG1lc3NhZ2VzLiBuX2hhbSBpcyB0aGUgbGVuZ3RoIG9mIHRoaXMgdmVjdG9yLg0KRmlyc3QsIHNwbGl0IHRoZSBkYXRhIHRvIGNvbnRhaW4gdHdvIHNlcGFyYXRlIGRhdGFmcmFtZXMsIG9uZSBmb3Igc3BhbSBtZXNzYWdlcyBhbmQgb25lIGZvciBoYW0gbWVzc2FnZXMuIA0KYGBge3J9DQojY3JlYXRlIGRhdGFmcmFtZSBvZiBvbmx5IHNwYW0gbWVzc2FnZXMgZnJvbSB0aGUgdHJhaW5pbmcgZGF0YSBzZXQuDQoNCnNwYW1fdHJhaW5pbmcgPC0gdHJhaW5pbmdfZGF0YV8yICU+JSANCiAgZmlsdGVyKGxhYmVsID09ICJzcGFtIikgDQoNCiNjcmVhdGUgZGF0YWZyYW1lIG9mIG9ubHkgaGFtIG1lc3NhZ2VzIGZyb20gdGhlIHRyYWluaW5nIGRhdGEgc2V0Lg0KDQpoYW1fdHJhaW5pbmcgPC0gdHJhaW5pbmdfZGF0YV8yICU+JQ0KICBmaWx0ZXIobGFiZWwgPT0gImhhbSIpDQoNCmBgYA0KVGhlbiBjcmVhdGUgdGhlIHZlY3RvcnMgY29udGFpbmluZyBhbGwgdGhlIHdvcmRzIGluIHRoZSBzcGFtIG1lc3NhZ2VzLiBBbmQgdGhlbiBkbyB0aGUgc2FtZSBmb3IgaGFtIG1lc3NhZ2VzLiANCmBgYHtyfQ0KI2l0ZXJhdGUgdGhyb3VnaCBhbGwgdGhlIHJvd3MgaW4gdGhlIHNwYW0gdHJhaW5pbmcgZGF0YSBzZXQsIHNwbGl0IHRoZSBzdHJpbmcgYW5kIGFkZCB0byB2ZWN0b3IuIG5fc3BhbSA9IGxlbmd0aCBvZiB0aGlzIHZlY3Rvci4gDQoNCmFsbF9zcGFtX3dvcmRzIDwtIGMoKQ0KDQpmb3IoaSBpbiAxOm5yb3coc3BhbV90cmFpbmluZykpIHsNCnNwbGl0IDwtIHVubGlzdChzdHJzcGxpdChzcGFtX3RyYWluaW5nJHNtc1tpXSwgIlxccysiKSkNCmFsbF9zcGFtX3dvcmRzIDwtIGMoYWxsX3NwYW1fd29yZHMsIHNwbGl0KQ0KfQ0KDQpoZWFkKGFsbF9zcGFtX3dvcmRzLCAxMDApDQpuX3NwYW0gPC0gbGVuZ3RoKGFsbF9zcGFtX3dvcmRzKQ0Kbl9zcGFtDQoNCiNpdGVyYXRlIHRocm91Z2ggYWxsIHRoZSByb3dzIGluIHRoZSBoYW0gdHJhaW5pbmcgZGF0YSBzZXQsIHNwbGl0IHRoZSBzdHJpbmcgYW5kIGFkZCB0byB2ZWN0b3IuIG5faGFtID0gbGVuZ3RoIG9mIHRoaXMgdmVjdG9yLiANCg0KYWxsX2hhbV93b3JkcyA8LSBjKCkNCg0KZm9yKGkgaW4gMTpucm93KGhhbV90cmFpbmluZykpIHsNCnNwbGl0IDwtIHVubGlzdChzdHJzcGxpdChoYW1fdHJhaW5pbmckc21zW2ldLCAiXFxzKyIpKQ0KYWxsX2hhbV93b3JkcyA8LSBjKGFsbF9oYW1fd29yZHMsIHNwbGl0KQ0KfQ0KDQpoZWFkKGFsbF9oYW1fd29yZHMsIDEwMCkNCm5faGFtIDwtIGxlbmd0aChhbGxfaGFtX3dvcmRzKQ0Kbl9oYW0NCmBgYA0KTmV4dCwgY3JlYXRlIGEgZnVuY3Rpb24gd2hpY2ggdGFrZXMgdGhlIHZlY3RvciBgdm9jYWJgICh0aGUgdmVjdG9yIHRoYXQgY29udGFpbnMgYWxsIHVuaXF1ZSB2b2NhYnVsYXJ5KSBhbmQgb3V0cHV0cyBhIGxpc3Qgb2YgZWFjaCB3b3JkIHdpdGggaXRzIGNvcnJlc3BvbmRpbmcgcHJvYmFiaWxpdHkgb2Ygb2NjdXJpbmcgd2l0aGluIHRoZSBzcGFtIG1lc3NhZ2VzLiANClRoZSBmdW5jdGlvbiBmaXJzdCBjYWxjdWxhdGVzIGhvdyBtYW55IHRpbWUgZWFjaCB3b3JkIGFwcGVhcnMgaW4gdGhlIHNwYW0gbWVzc2FnZXMgYG5fb2NjdXJhbmNlc19pbl9zcGFtYC4gVGhlbiBpdCB1c2VzIHRoaXMsIGFuZCB0aGUgb3RoZXIgcGFyYW1ldGVycyAoY2FsY3VsYXRlZCBhYm92ZSksIHRvIGNhbGN1bGF0ZSBwd29yZHxzcGFtLiBpLmUuIHRoZSBwcm9iYWJpbGl0eSBvZiBhIHBhcnRpY3VsYXIgd29yZCBvY2N1cmluZywgZ2l2ZW4gYSBtZXNzYWdlIGlzIHNwYW0uDQpgYGB7cn0NCmZpbmRfd29yZF9pbl9zcGFtIDwtIGZ1bmN0aW9uKHdvcmQsIGFscGhhKXsNCiAgbG9naWNfdiA8LSBhbGxfc3BhbV93b3JkcyA9PSB3b3JkDQogIG5fb2NjdXJhbmNlc19pbl9zcGFtIDwtIHN1bShsb2dpY192KQ0KICBwX3dvcmRfZ3ZuX3NwYW0gPC0gKG5fb2NjdXJhbmNlc19pbl9zcGFtK2FscGhhKS8obl9zcGFtICsgYWxwaGEqbl92b2NhYikNCiAgcmV0dXJuKHBfd29yZF9ndm5fc3BhbSkNCn0NCg0KcF9saXN0X3NwYW0gPC0gbWFwMih2b2NhYiwgMSwgZmluZF93b3JkX2luX3NwYW0pDQpuYW1lcyhwX2xpc3Rfc3BhbSkgPC0gdm9jYWINCg0KaGVhZChwX2xpc3Rfc3BhbSwgMTApDQoNCmBgYA0KRG8gdGhlIHNhbWUgYXMgYWJvdmUsIGJ1dCBmb3IgaGFtIG1lc3NhZ2VzLiANCmkuZS4gQ3JlYXRlIGEgZnVuY3Rpb24gd2hpY2ggdGFrZXMgdGhlIHZlY3RvciBgdm9jYWJgIGFuZCBvdXRwdXRzIGEgbGlzdCB3aXRoIGVhY2ggd29yZCB3aXRoIGl0cyBjb3JyZXNwb25kaW5nIHByb2JhYmlsaXR5IG9mIG9jY3VyaW5nIHdpdGhpbiB0aGUgaGFtIG1lc3NhZ2VzLiANClRoZSBmdW5jdGlvbiBmaXJzdCBjYWxjdWxhdGVzIGhvdyBtYW55IHRpbWUgZWFjaCB3b3JkIGFwcGVhcnMgaW4gdGhlIGhhbSBtZXNzYWdlcyBgbl9vY2N1cmFuY2VzX2luX2hhbWAuIFRoZW4gaXQgdXNlcyB0aGlzLCBhbmQgdGhlIG90aGVyIHBhcmFtZXRlcnMgKGNhbGN1bGF0ZWQgYWJvdmUpLCB0byBjYWxjdWxhdGUgcHdvcmR8aGFtLiBpLmUuIHRoZSBwcm9iYWJpbGl0eSBvZiBhIHBhcnRpY3VsYXIgd29yZCBvY2N1cmluZywgZ2l2ZW4gYSBtZXNzYWdlIGlzIGhhbS4NCg0KYGBge3J9DQpmaW5kX3dvcmRfaW5faGFtIDwtIGZ1bmN0aW9uKHdvcmQsIGFscGhhKXsNCiAgbG9naWNfdiA8LSBhbGxfaGFtX3dvcmRzID09IHdvcmQNCiAgbl9vY2N1cmFuY2VzX2luX2hhbSA8LSBzdW0obG9naWNfdikNCiAgcF93b3JkX2d2bl9oYW0gPC0gKG5fb2NjdXJhbmNlc19pbl9oYW0rYWxwaGEpLyhuX2hhbSArIGFscGhhKm5fdm9jYWIpDQogIHJldHVybihwX3dvcmRfZ3ZuX2hhbSkNCn0NCg0KcF9saXN0X2hhbSA8LSBtYXAyKHZvY2FiLCAxLCBmaW5kX3dvcmRfaW5faGFtKQ0KbmFtZXMocF9saXN0X2hhbSkgPC0gdm9jYWINCg0KaGVhZChwX2xpc3RfaGFtLCAxMCkNCmBgYA0KTmV4dCwgSSBqdXN0IGRvIHNvbWUgdGVzdGluZyBoZXJlIHRvIHNlZSBpZiBJIHRoaW5rIGl0cyB3b3JraW5nLi4uDQpgYGB7cn0NCnRlc3RfbWVzc2FnZSA8LSAiaGVsbG8gaW0gcnVubmluZyBhIHRlc3Qgc2VudGFuY2UgdG8gY2hlY2sgdGhhdCB0aGlzIGlzIHdvcmtpbmciDQp0ZXN0X21lc3NhZ2UNCg0KdmVjX3Rlc3Rfd29yZHMgPC0gdW5saXN0KHN0cnNwbGl0KHRlc3RfbWVzc2FnZSwgIlxccysiKSkNCnZlY190ZXN0X3dvcmRzDQoNCg0KYSA8LSB1bmxpc3QocF9saXN0X2hhbVtjKHZlY190ZXN0X3dvcmRzKV0pDQphDQpwX21lc3NhZ2VfaGFtIDwtIHBfaGFtKnByb2QoYSkNCnBfbWVzc2FnZV9oYW0NCg0KYiA8LSB1bmxpc3QocF9saXN0X3NwYW1bYyh2ZWNfdGVzdF93b3JkcyldKQ0KYg0KcF9tZXNzYWdlX3NwYW0gPC0gcF9zcGFtKnByb2QoYikNCnBfbWVzc2FnZV9zcGFtDQoNCmlmX2Vsc2UocF9tZXNzYWdlX2hhbT49cF9tZXNzYWdlX3NwYW0sICJoYW0iLCAic3BhbSIpDQoNCmBgYA0KVGhhdCBsb29rcyByZWFzb25hYmxlLg0KDQpORXh0LCB3cml0ZSBhIGZ1bmN0aW9uIHdoaWNoIHRha2VzIGluIG1lc3NhZ2VzIGFuZCByZXR1cm5zIHdoZXRoZXIgb3Igbm90IGl0IGRlZW1zIGl0IHRvIGJlIHNwYW0gb3IgaGFtDQoNCmBgYHtyfQ0KI3RoZSBjbGFzc2lmaWNhdGlvbiBmdW5jdGlvbiBmaXJzdCBuZWVkcyB0byB0YWtlIGFuIHNtcyBpbnB1dCBhbmQgY2xlYW4gdGhlIGRhdGEuIFRoZW4gaXQgbmVlZHMgdG8gc3BsaXQgdXAgZWFjaCBzbXMgaW50byBhIHZlY3RvciBjb250YWluaW5nIGVhY2ggd29yZC4gSXQgdGhlbiB1c2VzIHRoaXMgdmVjdG9yIG9mIHdvcmRzIHRvIGluZGV4IHRoZSBsaXN0IG9mIHByb2JhYmlsaXRpZXMgYW5kIHJldHVybnMgdGhlIHByb2JhYmlsaXRpZXMgb2YgdGhvc2Ugd29yZHMuIEl0IGFzc2lnbnMgdGhvc2UgcHJvYmFiaWxpdGllcyB0byBhIG5ldyB2ZWN0b3IsIGEuIFRvIGNhbGN1bGF0ZSB0aGUgcHJvYmFiaWxpdHkgb2YgdGhlIHBhcnRpY3VsYXIgbWVzc2FnZSBiZWluZyBoYW0sIGl0IHRoZW4gbXVsdGlwbGllcyBwX2hhbSpwcm9kKGEpIChwcm9kKGEpIG11bHRpcGxpZXMgYWxsIHRoZSB2YWx1ZXMgaW4gdmVjdG9yIGEgdG9nZXRoZXIpLiBUaGUgc2FtZSBpcyBkb25lIGZvciBzcGFtLiANCg0KY2xhc3NpZmljYXRpb25fZnVuY3Rpb24gPC0gZnVuY3Rpb24obWVzc2FnZSkgew0KICBjbGVhbmVkXzEgPC0gc3RyX3JlcGxhY2VfYWxsKG1lc3NhZ2UsICJbOnB1bmN0Ol0iLCAiIikgDQogIGNsZWFuZWRfMiA8LSBzdHJfcmVwbGFjZV9hbGwoY2xlYW5lZF8xLCAiWzpkaWdpdDpdIiwgIiIpDQogIGNsZWFuZWRfMyA8LSB0b2xvd2VyKGNsZWFuZWRfMikNCiAgc3BsaXRfbWVzc2FnZSA8LSB1bmxpc3Qoc3Ryc3BsaXQoY2xlYW5lZF8zLCAiXFxzKyIpKQ0KICBhIDwtIHVubGlzdChwX2xpc3RfaGFtW2Moc3BsaXRfbWVzc2FnZSldKQ0KICBwX21lc3NhZ2VfaGFtIDwtIHBfaGFtKnByb2QoYSkNCiAgYiA8LSB1bmxpc3QocF9saXN0X3NwYW1bYyhzcGxpdF9tZXNzYWdlKV0pDQogIHBfbWVzc2FnZV9zcGFtIDwtIHBfc3BhbSpwcm9kKGIpDQogIGlmX2Vsc2UocF9tZXNzYWdlX2hhbT49cF9tZXNzYWdlX3NwYW0sICJoYW0iLCAic3BhbSIpDQp9DQoNCmNsYXNzaWZpY2F0aW9uX2Z1bmN0aW9uKHRlc3RfbWVzc2FnZSkNCg0KYGBgDQoNCmBgYHtyfQ0KDQojbWFwIHRoaXMgZnVuY3Rpb24gdGhyb3VnaCBhbGwgdGhlIHRyYWluaW5nX2RhdGEgbWVzc2FnZXMgYW5kIGNvbXBhcmUgdGhlIG91dHB1dCB3aXRoIHRoZSBvcmlnaW5pYWwgKGRlZW1lZCB0byBiZSB0cnVlKSBjbGFzc2lmaWNhdGlvbi4gDQoNCmZpbHRlcl9vdXRwdXQgPC0gdW5saXN0KG1hcCh0cmFpbmluZ19kYXRhJHNtcywgY2xhc3NpZmljYXRpb25fZnVuY3Rpb24pKQ0KY29tcGFyaXNvbiA8LSBjYmluZCh0cmFpbmluZ19kYXRhLCBmaWx0ZXJfb3V0cHV0KQ0KDQpoZWFkKGNvbXBhcmlzb24pDQoNCg0KYGBgDQpgYGB7cn0NCiNjb21wYXJlIHRoZSB0cnVlIGNsYXNzaWZpY2F0aW9uIHdpdGggdGhlIG9uZSBjYWxjdWxhdGVkIGJ5IHRoZSBhbGdvcml0aG0uDQoNCnN1bShjb21wYXJpc29uJGxhYmVsID09IGNvbXBhcmlzb24kZmlsdGVyX291dHB1dCkvbnJvdyhjb21wYXJpc29uKQ0KYGBgDQpUaGUgZmlsdGVyIHdhcyA5NCUgYWNjdXJhdGUhIA0KDQpOb3cgYXNzZXNzIHRoZSBhbHBoYSB2YWx1ZS4gUHJldmlvdXNseSwgYWxwaGEgaGFzIGJlZW4gc2V0IHRvIDEuIEhvd2V2ZXIsIHNlZSBob3cgdGhlIGFjY3VyYWN5IHZhcmllcyBkZXBlbmRpbmcgb24gdGhlIHZhbHVlIG9mIGFscGhhLg0KYGBge3J9DQojY3JlYXRlIHZlY3RvciBvZiBkaWZmZXJlbnQgdmFsdWVzIG9mIGFscGhhIHRvIHRyeQ0KDQphbHBoYV9yYW5nZSA8LSBzZXEoMC4xLCAxLCBieSA9IDAuMSkNCg0KI2l0ZXJhdGUgdGhyb3VnaCB0aGUgdmFsdWVzIG9mIGFscGhhIHRvIHJlY2FsY3VsYXRlIHRoZSBhY2N1cmFjeSwgdGhpcyB0aW1lIGJhc2VkIG9uIHRoZSBjcm9zcy12YWxpZGF0aW9uIGRhdGFzZXQuIA0KDQpmb3IoYWxwaGEgaW4gYWxwaGFfcmFuZ2UpIHsNCg0KcF9saXN0X3NwYW0gPC0gbWFwMih2b2NhYiwgYWxwaGEsIGZpbmRfd29yZF9pbl9zcGFtKQ0KbmFtZXMocF9saXN0X3NwYW0pIDwtIHZvY2FiDQoNCnBfbGlzdF9oYW0gPC0gbWFwMih2b2NhYiwgYWxwaGEsIGZpbmRfd29yZF9pbl9oYW0pDQpuYW1lcyhwX2xpc3RfaGFtKSA8LSB2b2NhYg0KDQpmaWx0ZXJfb3V0cHV0IDwtIHVubGlzdChtYXAoeF92YWxpZF9kYXRhJHNtcywgY2xhc3NpZmljYXRpb25fZnVuY3Rpb24pKQ0KY29tcGFyaXNvbiA8LSBjYmluZCh4X3ZhbGlkX2RhdGEkbGFiZWwsIGZpbHRlcl9vdXRwdXQpDQoNCmFjY3VyYWN5IDwtIHN1bShjb21wYXJpc29uWywxXSA9PSBjb21wYXJpc29uWywyXSkvbnJvdyhjb21wYXJpc29uKQ0KbnJvdyhjb21wYXJpc29uKQ0KDQpjYXQoIlRoZSBhY2N1cmFjeSBvZiB0aGUgc3BhbSBmaWx0ZXIgd2l0aCBhbHBoYSA9IiwgYWxwaGEsICIgaXMiLCAoYWNjdXJhY3kqMTAwKSwgIiUiLCAiXG4iKQ0KfQ0KDQpgYGANCkZyb20gdGhpcywgaXQgc2VlbXMgdGhhdCB0aGUgbG93ZXIgYWxwaGEgbnVtYmVycyBwcm9kdWNlIGEgbW9yZSBhY2N1cmF0ZSBjb21wYXJpc29uLiANClRoZXJlZm9yZSwgYWxwaGEgPSAwLjEgd2lsbCBiZSB1c2VkIGZvciB0aGUgdGVzdCBzZXQuIA0KYGBge3J9DQojc2V0IGFscGhhID0gMC4xIGFzIHRoaXMgaGFzIHNob3duIHRvIHByb2R1Y2UgdGhlIGhpZ2hlc3QgYWNjdXJhY3kNCg0KYWxwaGEgPC0gMC4xDQoNCiNyZWNyZWF0ZSB0aGUgcHJvYmFiaWxpdHkgbGlzdHMgd2l0aCBhbHBoYSA9IDAuMQ0KDQpwX2xpc3Rfc3BhbSA8LSBtYXAyKHZvY2FiLCBhbHBoYSwgZmluZF93b3JkX2luX3NwYW0pDQpuYW1lcyhwX2xpc3Rfc3BhbSkgPC0gdm9jYWINCg0KcF9saXN0X2hhbSA8LSBtYXAyKHZvY2FiLCBhbHBoYSwgZmluZF93b3JkX2luX2hhbSkNCm5hbWVzKHBfbGlzdF9oYW0pIDwtIHZvY2FiDQoNCiNtYXAgdGhlIHRlc3QgZGF0YSBzZXQgdGhyb3VnaCB0aGUgY2xhc3NpZmljYXRpb24gZnVuY3Rpb24gYmFzZWQgb24gdGhlIHByb2JhYmlsaXR5IGxpc3RzIGNhbGN1bGF0ZWQganVzdCBhYm92ZSANCg0KZmlsdGVyX291dHB1dCA8LSB1bmxpc3QobWFwKHRlc3RfZGF0YSRzbXMsIGNsYXNzaWZpY2F0aW9uX2Z1bmN0aW9uKSkNCmNvbXBhcmlzb24gPC0gY2JpbmQodGVzdF9kYXRhLCBmaWx0ZXJfb3V0cHV0KQ0KDQojZGV0ZXJtaW5lIGhvdyBhY2N1cmF0ZSB0aGUgZmlsdGVyIGlzDQoNCmFjY3VyYWN5IDwtIHN1bShjb21wYXJpc29uJGxhYmVsID09IGNvbXBhcmlzb24kZmlsdGVyX291dHB1dCkvbnJvdyhjb21wYXJpc29uKQ0KDQpjYXQoIldpdGggYWxwaGEgPSIsIGFscGhhLCAidGhlIHNwYW0gZmlsdGVyIGNvcnJlY3RseSBwcmVkaWN0ZWQiLCAoYWNjdXJhY3kqMTAwKSwgIiUiLCAib2YgYWxsIG1lc3NhZ2VzIGFzIGJlaW5nIHNwYW0gb3IgaGFtIiwgIlxuIikNCg0KaGVhZChjb21wYXJpc29uKQ0KDQpgYGANClRoZSBhbGdvcml0aG0gaGFzIGNvcnJlY3RseSBwcmVkaWN0ZWQgOTUlIG9mIG1lc3NhZ2VzIGZyb20gdGhlIHRlc3QgZGF0YSBzZXQgYXMgYmVpbmcgZWl0aGVyIHNwYW0gb3IgaGFtLg0KDQoNCg0K