By: Lawrence Lo

Introduction

Web scraping is the extraction of data from websites that can be used for further analytic procedures. This presentation is a demonstration of web scraping in R using the rvest package. In this demonstration, we will be web scraping thousands of data points across multiple web pages from metacritic, one of the largest online aggregation sites for media product reviews.

Although some website data can appear conveniently in a table-like format, we will be scraping data scattered into unconventional data formats. For this demonstration we will be extracting data on the top user rated music albums from metacritic. The layout of the website appears below:

Metacritic top 100 user rated albums

Metacritic top 100 user rated albums

From this site, we can see the following information:

We will extract all of this information and conform it to a data frame that can be analyzed and further manipulated.

This presetation will cover the following topics:

  1. Using CSS selectors.
  2. Manual extraction of data into dataframes from single web pages.
  3. Automated extraction over multiple web pages simultaneously.
  4. Simple descriptive and inferential statistics on extracted data.

1. CSS selectors

Cascading Style Sheets (CSS) selectors are tools used to target specific HTML elements on a web page. These are helpful tools outside of R that are used to gather specific HTML information neccesary for using the rvest functions. A popular CSS selector that is demonstrated here is selectorgadget. Selectorgadget can be installed as a google chrome extension and uses a simple point and click interface.

For example, using the selectorgadget on the album names will produce the following screen:

Selector gadget highlighting album names

Selector gadget highlighting album names

In the small selectorgadget user interface overlayed on the website, we can see the “.product_title a” CSS text appear. This text will need to be input into R in order to extract the album names from the web page.

Here is another example where the user scores have been selected:

Selector gadget highlighting user scores names

Selector gadget highlighting user scores names

Now, the CSS text is “.positive”; this will be used to extract the user ratings.

Now that we can use the selectorgadget to extract the CSS information we need, let’s use this within R.

2. Extracting individual fields

Before we begin, let’s take a look at the R packages we will be using:

First, we’ll load these packages into R.

rr library(rvest) library(dplyr) library(stringr) library(ggplot2)

The read_html function takes the url of the web page we will be scraping. Let’s assign this to the object, ‘wp’.

rr wp <- read_html(://www.metacritic.com/browse/albums/release-date/available/userscore?view=detailed&page=0)

In the first example of using the selectorgadget, we found the CSS field related to album names was“.product_title a”. We can input this into the html_nodes function as shown below to extract the album names. Here we assign the album names to the object, ‘album_name’, and take a look at the first few entries.

rr album_name <- wp %>% html_nodes(.product_title a) %>% html_text() head(album_name)

[1] \Scarlet's Walk\         \Good Kid

Using the selectorgadget to highlight the artist names produces the CSS field text, “.product_artist”. Let’s assign this data to the object, ‘artist_name’.

rr artist_name <- wp %>% html_nodes(.product_artist) %>% html_text() head(artist_name)

[1] \ - Tori Amos\      \ - Kendrick Lamar\ \ - Kendrick Lamar\
[4] \ - Kenny Chesney\  \ - Led Zeppelin\   \ - Blur\          

Notice that each artist has a " - " prefix. Web scraping can often provide little blemishes in our data such as this. We can eliminate this with the following modification to the code:

rr artist_name <- wp %>% html_nodes(.product_artist) %>% html_text() %>% str_replace( - ,\) head(artist_name)

[1] \Tori Amos\      \Kendrick Lamar\ \Kendrick Lamar\ \Kenny Chesney\ 
[5] \Led Zeppelin\   \Blur\          

Let’s extract the user scores into the object, ‘user_score’.

rr user_score <- wp %>% html_nodes(.positive) %>% html_text() head(user_score)

[1] \9.1\ \9.1\ \9.1\ \9.0\ \9.0\ \9.0\

From glancing at the first few entries, we see that these have been extracted as character strings. Let’s modify our code to convert this into numeric entries.

rr user_score <- wp %>% html_nodes(.positive) %>% html_text() %>% as.numeric() head(user_score)

[1] 9.1 9.1 9.1 9.0 9.0 9.0

The metacritic scores can be handled in a similar way.

rr meta_score <- wp %>% html_nodes(.textscore) %>% html_text() %>% as.numeric() head(meta_score)

[1] 76 91 96 67 97 88

The genre data will appear in a non-optimal format.

rr genre <- wp %>% html_nodes(.genre .data) %>% html_text() head(genre)

[1] \\n                                                                                    Rock

The following code will achieve 2 things. First, it will format the data to be more compatible as an entry within a dataframe cell. Second, we will extract only the primary genre for ease of further analyses.

rr primary_genre <- wp %>% html_nodes(.genre .data) %>% html_text() %>% str_squish() %>% str_replace(
```

[1] \Rock\    \Rap\     \Rap\     \Country\ \Rock\    \Rock\   

When using the selectorgadget, it will indicate a count of how many elements that have been selected. Upon selecting the genre data, it is quickly noticeable that 4 of the 100 displayed albums are without a genre listing. We can confirm this discrepancy in R.

rr data.frame(album_name_length = length(album_name), genre_length = length(primary_genre))

Unfortunately, the web extraction does not notice missing fields and therefore cannot correctly place missing data indicators where they need to be. This can be a common hurdle in web scraping. Overcoming these hurdles requires some flexibility in R.

We could visibly inspect the webpage for missing genre entries and place the missing values in our data manually, but an automated approach will allow us to handle other webpages without manual inspection. After using the selector gadget to highlight simultaneous fields, we can extract a larger subset including the user score, genre, and date.

rr test1 <- wp %>% html_nodes(.data) %>% html_text() %>% str_squish() head(test1)

[1] \Oct 29

Converting this into a numeric vector, we should notice a pattern of (NA,NA,numeric).

rr test2 <- as.numeric(test1)

NAs introduced by coercion

rr head(test2)

[1] NA NA 76 NA NA 91

Using this pattern, we can use the following for-loop routine to impute the missing data as ‘NA’ into the proper location.

rr #Identify count of missings NAcount <- 100-length(primary_genre) #Store NA indicators NAwhich <- rep(0, NAcount) #Loop for finding (NA,NA,numeric) patterns for(i in 1:NAcount){ for(j in 0:100){ #cat(i,j,!is.na(test2[3*j+2]),\n) if(!is.na(test2[3*j+2])){ test2 <- append(test2, NA, 3*j) NAwhich[i] <- j+1 break } } } #Impute NAs in correct order for(i in NAwhich){ primary_genre <- append(primary_genre, NA, i-1) }

The release data will be our last extracted variable.

rr release_date <- wp %>% html_nodes(.release_date .data) %>% html_text() head(release_date)

[1] \Oct 29

From this release data character string, let’s extract the year into a separate numeric variable for our later analysis.

rr release_year <- release_date %>% str_replace(.*


<!-- rnb-source-end -->

<!-- rnb-output-begin eyJkYXRhIjoiWzFdIDIwMDIgMjAxMiAyMDE1IDIwMDQgMjAwMyAyMDAwXG4ifQ== -->

[1] 2002 2012 2015 2004 2003 2000 ```

Finally, let’s enter all this scraped data into a data frame.

rr data_music <- data.frame(album_name, artist_name, user_score, meta_score, primary_genre, release_date, release_year) head(data_music)

Now we have a data frame with all of our web scraped data.

3. Automated scraping across multiple web pages

Now that we have demonstrated extracting data variables from a single web page, let’s generalize this method to extract similar data across multiple web pages.

First, we will define all the individual steps above into a function that will take a url as an input and will produce the desired data frame as an output.

rr meta_music_webscrape <- function(url){

wp <- read_html(url)

album_name <- wp %>% html_nodes(.product_title a) %>% html_text() album_name

artist_name <- wp %>% html_nodes(.product_artist) %>% html_text() %>% str_replace( - ,\) artist_name

user_score <- wp %>% html_nodes(.positive) %>% html_text() %>% as.numeric() user_score

meta_score <- wp %>% html_nodes(.textscore) %>% html_text() %>% as.numeric() meta_score

primary_genre <- wp %>% html_nodes(.genre .data) %>% html_text() %>% str_squish() %>% str_replace(
```

Let’s test this custom function on the same web page to confirm that we get the same data frame.

rr url1 <- ://www.metacritic.com/browse/albums/release-date/available/userscore?view=detailed&page=0
out1 <- meta_music_webscrape(url1)

NAs introduced by coercion

rr head(out1)

Now let’s test it on a second similar web page, giving the 101-200 listing of top user rated artists.

rr url2 <- ://www.metacritic.com/browse/albums/release-date/available/userscore?view=detailed&page=1
out2 <- meta_music_webscrape(url2)

NAs introduced by coercion

rr head(out2)

Now let’s automate this function over 10 web pages, collecting data on the top 1000 user rated artists. This can be done by applying the custom function within a for-loop while using the paste0 function to call sequential web pages. We’ll call this data frame, ‘top1000’.

rr #Create empty data frame top1000 <- data.frame(album_name = character(), artist_name = character(), user_score = numeric(), meta_score = numeric(), primary_genre = character(), release_date = character(), release_year = numeric()) for(i in 0:9){ top1000 <- rbind(top1000, meta_music_webscrape(paste0( ://www.metacritic.com/browse/albums/release-date/available/userscore?view=detailed&page=, i ))) }

NAs introduced by coercionNAs introduced by coercionNAs introduced by coercionNAs introduced by coercionNAs introduced by coercionNAs introduced by coercionNAs introduced by coercionNAs introduced by coercionNAs introduced by coercionNAs introduced by coercion

Let’s check the structure of this data frame.

rr str(top1000)

'data.frame':   1000 obs. of  7 variables:
 $ album_name   : Factor w/ 996 levels \()\,\10

Now that we have extracted all this data, let’s do some simple analysis.

4. Analysis of web scraped data

Let’s begin by looking at some summary statistcs of the ‘user_score’ variable.

rr summary(top1000$user_score)

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  8.700   8.800   8.800   8.828   8.900   9.800 

rr paste(deviation: , sd(top1000$user_score))

[1] \standard deviation:  0.0727492436555354\

Let’s plot the distribution of this variable.

rr ggplot(top1000, aes(x = user_score)) + geom_bar() + labs(title=of user scores)

Now the same summary for the metacritic scores.

rr summary(top1000$meta_score)

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  47.00   72.00   78.00   77.06   82.25  100.00 

rr paste(deviation: , sd(top1000$meta_score))

[1] \standard deviation:  8.33352288072719\

rr ggplot(top1000, aes(x = meta_score)) + geom_histogram() + labs(title=of metacritic scores)

What does the distribution of primary genres look like?

rr table(top1000$primary_genre)


               Country                  Dance             Electronic 
                    13                     17                     66 
                  Folk            Heavy Metal                  Indie 
                     8                      3                    269 
                  Jazz                    Pop               Pop/Rock 
                     3                     43                    150 
                   R&B                    Rap                   Rock 
                    24                     73                    270 
  [There is no word to      Adult Alternative             Album Rock 
                     1                      7                      1 
  Alternative Pop/Rock           Altternative             Doom Metal 
                     3                      1                      1 
            Indie Rock             Industrial             Soundtrack 
                     2                      1                      3 
                 World            Alternative       Alternative-Folk 
                     1                      4                      1 
Alternative/Indie Rock                Britpop              Classical 
                     2                      1                      2 
                   Emo      Singer/Songwriter                   Soul 
                     4                      2                      1 
    Adult Contemporary              Americana            Avant-Garde 
                     3                      1                      2 
                Gospel              Post-Rock           Southern Rap 
                     1                      1                      1 
              Trip-Hop              Bluegrass           Experimental 
                     1                      2                      1 

As we can see, there are several minor genres that have few entries. Let’s collapse this variable into an alternate form that only has the top 5 genres and an ‘other’ category capturing the remaining genres.

rr Other_genres <- setdiff(unique(top1000\(primary_genre), names(sort(table(top1000\)primary_genre), decreasing = T)[1:5]))

top1000\(primary_genre2 <- top1000\)primary_genre levels(top1000\(primary_genre2) <- c(levels(top1000\)primary_genre2), ) top1000\(primary_genre2[top1000\)primary_genre2 %in% Other_genres] <- as.factor() top1000\(primary_genre2 <- droplevels(top1000\)primary_genre2) table(top1000$primary_genre2)


Electronic      Indie   Pop/Rock        Rap       Rock      Other 
        66        269        150         73        270        172 

Now let’s visualize this table into a bar plot.

rr ggplot(top1000, aes(x = primary_genre2)) + geom_bar() + geom_text(aes(y = ..count.. -10, label = paste0(round(prop.table(..count..),4) * 100, ‘%’)), stat = ‘count’, position = position_dodge(.1), size = 3)+ labs(title=of primary genres)

It appears as if ‘rock’ and ‘indie’ are the most popular genres within the top 1000 user rated albums.

How about the distribution of album relase years?

rr table(top1000$release_year)


1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 
   6   50   73   72   87   88   65   49   47   72   82   41   65   65   51 
2014 2015 2016 2017 
  43   24   16    4 

rr ggplot(top1000, aes(x = release_year)) + geom_histogram(binwidth = 1) + labs(title=of release years)

It appears as if the most popular music was released in the years 2003, 2004, and 2009.

Now for some basic inferential statistics looking at some relationships between these variables.

What is the relationship between user score and the metacritic score?

rr paste(of user score and meta score: , cor(top1000\(user_score,top1000\)meta_score))

[1] \Correlation of user score and meta score:  0.172502580668426\

rr ggplot(top1000, aes(x= user_score, y = meta_score)) + geom_point() + geom_smooth(method=lm) + labs(title=of user and metacritic scores)

From the rather small correlation coefficient of .17 and the round dispersion of the scatterplot, it appears as if the relationship between user and metacritic score is moderate at best.

Let’s run a few regression models to look at some genre by rating relationships. Within these top 1000 albums, is there a difference in the average user rating by genre type?

rr fit1 <- lm(user_score ~ relevel(primary_genre2, ref = ), data = top1000) summary(fit1)


Call:
lm(formula = user_score ~ relevel(primary_genre2, ref = \Other\), 
    data = top1000)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.13836 -0.02907 -0.02825  0.07093  0.97067 

Coefficients:
                                                   Estimate Std. Error
(Intercept)                                       8.8290698  0.0055522
relevel(primary_genre2, ref = \Other\)Electronic -0.0108879  0.0105434
relevel(primary_genre2, ref = \Other\)Indie      -0.0008170  0.0071090
relevel(primary_genre2, ref = \Other\)Pop/Rock    0.0002636  0.0081348
relevel(primary_genre2, ref = \Other\)Rap         0.0092864  0.0101715
relevel(primary_genre2, ref = \Other\)Rock       -0.0038846  0.0071038
                                                  t value Pr(>|t|)    
(Intercept)                                      1590.197   <2e-16 ***
relevel(primary_genre2, ref = \Other\)Electronic   -1.033    0.302    
relevel(primary_genre2, ref = \Other\)Indie        -0.115    0.909    
relevel(primary_genre2, ref = \Other\)Pop/Rock      0.032    0.974    
relevel(primary_genre2, ref = \Other\)Rap           0.913    0.361    
relevel(primary_genre2, ref = \Other\)Rock         -0.547    0.585    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.07282 on 994 degrees of freedom
Multiple R-squared:  0.003172,  Adjusted R-squared:  -0.001842 
F-statistic: 0.6326 on 5 and 994 DF,  p-value: 0.6749

It appears as if there are no statistically significant differences in user ratings by genre type.

How about genre type differences in metacritic scores?

rr fit2 <- lm(meta_score ~ relevel(primary_genre2, ref = ), data = top1000) summary(fit2)


Call:
lm(formula = meta_score ~ relevel(primary_genre2, ref = \Other\), 
    data = top1000)

Residuals:
     Min       1Q   Median       3Q      Max 
-29.1233  -5.0523   0.3365   5.0669  25.9444 

Coefficients:
                                                 Estimate Std. Error
(Intercept)                                       77.0523     0.6174
relevel(primary_genre2, ref = \Other\)Electronic  -0.6584     1.1724
relevel(primary_genre2, ref = \Other\)Indie        1.8808     0.7905
relevel(primary_genre2, ref = \Other\)Pop/Rock     0.8810     0.9046
relevel(primary_genre2, ref = \Other\)Rap          3.0710     1.1311
relevel(primary_genre2, ref = \Other\)Rock        -2.9968     0.7899
                                                 t value Pr(>|t|)    
(Intercept)                                      124.803  < 2e-16 ***
relevel(primary_genre2, ref = \Other\)Electronic  -0.562 0.574537    
relevel(primary_genre2, ref = \Other\)Indie        2.379 0.017539 *  
relevel(primary_genre2, ref = \Other\)Pop/Rock     0.974 0.330319    
relevel(primary_genre2, ref = \Other\)Rap          2.715 0.006740 ** 
relevel(primary_genre2, ref = \Other\)Rock        -3.794 0.000157 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 8.097 on 994 degrees of freedom
Multiple R-squared:  0.06068,   Adjusted R-squared:  0.05595 
F-statistic: 12.84 on 5 and 994 DF,  p-value: 4.038e-12

It appears as if there are some statistically significant differences here; let’s visualize this.

rr ggplot(top1000, aes(x = primary_genre2, y = meta_score)) + geom_boxplot() + labs(title=of metacritic score by primary genre)

Within these top 1000 albums, the metacritic scores seemed to prefer rap and indie genres while disfavoring rock.

Conclusion

In this presentation we collected data on the top 1000 user rated albums from metacritic. We first introduced the selectorgadget tool and R packages necessary for web scraping. Second, we demonstrated basic use of these functions for extracting single layers from individual web pages. We proceeded to automate this process for collection of large quantities of data over multiple web pages. Finally, we carried out some basic descriptive and inferential statistics on this web scraped data.

Here are some things we found out about the top 1000 user rated albums:

LS0tDQp0aXRsZTogIldlYnNjcmFwaW5nIGluIFI6IE1ldGFjcml0aWMgbXVzaWMgYWxidW0gZGF0YSINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCkJ5OiBMYXdyZW5jZSBMbw0KDQoqIEVtYWlsOiBvbGxlY25lcndhbDE4MEBnbWFpbC5jb20NCg0KKiBXZWJzaXRlOiBodHRwczovL29sbGVjbmVyd2FsMTgwLndpeHNpdGUuY29tL3dlYnNpdGUNCg0KIyMjIEludHJvZHVjdGlvbg0KDQpXZWIgc2NyYXBpbmcgaXMgdGhlIGV4dHJhY3Rpb24gb2YgZGF0YSBmcm9tIHdlYnNpdGVzIHRoYXQgY2FuIGJlIHVzZWQgZm9yIGZ1cnRoZXIgYW5hbHl0aWMgcHJvY2VkdXJlcy4gIFRoaXMgcHJlc2VudGF0aW9uIGlzIGEgZGVtb25zdHJhdGlvbiBvZiB3ZWIgc2NyYXBpbmcgaW4gUiB1c2luZyB0aGUgW3J2ZXN0XShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvcnZlc3QvcnZlc3QucGRmKSBwYWNrYWdlLiAgSW4gdGhpcyBkZW1vbnN0cmF0aW9uLCB3ZSB3aWxsIGJlIHdlYiBzY3JhcGluZyB0aG91c2FuZHMgb2YgZGF0YSBwb2ludHMgYWNyb3NzIG11bHRpcGxlIHdlYiBwYWdlcyBmcm9tIFttZXRhY3JpdGljXShodHRwOi8vd3d3Lm1ldGFjcml0aWMuY29tLyksIG9uZSBvZiB0aGUgbGFyZ2VzdCBvbmxpbmUgYWdncmVnYXRpb24gc2l0ZXMgZm9yIG1lZGlhIHByb2R1Y3QgcmV2aWV3cy4NCg0KQWx0aG91Z2ggc29tZSB3ZWJzaXRlIGRhdGEgY2FuIGFwcGVhciBjb252ZW5pZW50bHkgaW4gYSB0YWJsZS1saWtlIGZvcm1hdCwgd2Ugd2lsbCBiZSBzY3JhcGluZyBkYXRhIHNjYXR0ZXJlZCBpbnRvIHVuY29udmVudGlvbmFsIGRhdGEgZm9ybWF0cy4gIEZvciB0aGlzIGRlbW9uc3RyYXRpb24gd2Ugd2lsbCBiZSBleHRyYWN0aW5nIGRhdGEgb24gdGhlIHRvcCB1c2VyIHJhdGVkIG11c2ljIGFsYnVtcyBmcm9tIG1ldGFjcml0aWMuICBUaGUgbGF5b3V0IG9mIHRoZSBbd2Vic2l0ZV0oaHR0cDovL3d3dy5tZXRhY3JpdGljLmNvbS9icm93c2UvYWxidW1zL3JlbGVhc2UtZGF0ZS9hdmFpbGFibGUvdXNlcnNjb3JlP3ZpZXc9ZGV0YWlsZWQmcGFnZT0wKSBhcHBlYXJzIGJlbG93Og0KDQohWypNZXRhY3JpdGljIHRvcCAxMDAgdXNlciByYXRlZCBhbGJ1bXMqXShtZXRhY3JpdGljMC5wbmcpDQoNCkZyb20gdGhpcyBzaXRlLCB3ZSBjYW4gc2VlIHRoZSBmb2xsb3dpbmcgaW5mb3JtYXRpb246DQoNCiogVGhlIGFsYnVtIG5hbWUNCiogVGhlIGFydGlzdCBuYW1lDQoqIFRoZSB1c2VyIHJhdGluZyBzY29yZQ0KKiBUaGUgbWV0YWNyaXRpYyBzY29yZQ0KKiBUaGUgcmVsZWFzZSBkYXRhDQoqIFRoZSBnZW5yZSBvZiBtdXNpYw0KDQpXZSB3aWxsIGV4dHJhY3QgYWxsIG9mIHRoaXMgaW5mb3JtYXRpb24gYW5kIGNvbmZvcm0gaXQgdG8gYSBkYXRhIGZyYW1lIHRoYXQgY2FuIGJlIGFuYWx5emVkIGFuZCBmdXJ0aGVyIG1hbmlwdWxhdGVkLiAgDQoNClRoaXMgcHJlc2V0YXRpb24gd2lsbCBjb3ZlciB0aGUgZm9sbG93aW5nIHRvcGljczoNCg0KMS4gVXNpbmcgQ1NTIHNlbGVjdG9ycy4NCjIuIE1hbnVhbCBleHRyYWN0aW9uIG9mIGRhdGEgaW50byBkYXRhZnJhbWVzIGZyb20gc2luZ2xlIHdlYiBwYWdlcy4NCjMuIEF1dG9tYXRlZCBleHRyYWN0aW9uIG92ZXIgbXVsdGlwbGUgd2ViIHBhZ2VzIHNpbXVsdGFuZW91c2x5Lg0KNC4gU2ltcGxlIGRlc2NyaXB0aXZlIGFuZCBpbmZlcmVudGlhbCBzdGF0aXN0aWNzIG9uIGV4dHJhY3RlZCBkYXRhLg0KDQoNCiMjIyAxLiBDU1Mgc2VsZWN0b3JzDQoNCkNhc2NhZGluZyBTdHlsZSBTaGVldHMgKENTUykgc2VsZWN0b3JzIGFyZSB0b29scyB1c2VkIHRvIHRhcmdldCBzcGVjaWZpYyBIVE1MIGVsZW1lbnRzIG9uIGEgd2ViIHBhZ2UuICBUaGVzZSBhcmUgaGVscGZ1bCB0b29scyBvdXRzaWRlIG9mIFIgdGhhdCBhcmUgdXNlZCB0byBnYXRoZXIgc3BlY2lmaWMgSFRNTCBpbmZvcm1hdGlvbiBuZWNjZXNhcnkgZm9yIHVzaW5nIHRoZSAqcnZlc3QqIGZ1bmN0aW9ucy4gQSBwb3B1bGFyIENTUyBzZWxlY3RvciB0aGF0IGlzIGRlbW9uc3RyYXRlZCBoZXJlIGlzIFtzZWxlY3RvcmdhZGdldC5dKGh0dHBzOi8vc2VsZWN0b3JnYWRnZXQuY29tLykgIFNlbGVjdG9yZ2FkZ2V0IGNhbiBiZSBpbnN0YWxsZWQgYXMgYSBnb29nbGUgY2hyb21lIGV4dGVuc2lvbiBhbmQgdXNlcyBhIHNpbXBsZSBwb2ludCBhbmQgY2xpY2sgaW50ZXJmYWNlLg0KDQpGb3IgZXhhbXBsZSwgdXNpbmcgdGhlIHNlbGVjdG9yZ2FkZ2V0IG9uIHRoZSBhbGJ1bSBuYW1lcyB3aWxsIHByb2R1Y2UgdGhlIGZvbGxvd2luZyBzY3JlZW46DQoNCiFbKlNlbGVjdG9yIGdhZGdldCBoaWdobGlnaHRpbmcgYWxidW0gbmFtZXMqXShtZXRhY3JpdGljMS5wbmcpDQoNCkluIHRoZSBzbWFsbCBzZWxlY3RvcmdhZGdldCB1c2VyIGludGVyZmFjZSBvdmVybGF5ZWQgb24gdGhlIHdlYnNpdGUsIHdlIGNhbiBzZWUgdGhlICIucHJvZHVjdF90aXRsZSBhIiBDU1MgdGV4dCBhcHBlYXIuICBUaGlzIHRleHQgd2lsbCBuZWVkIHRvIGJlIGlucHV0IGludG8gUiBpbiBvcmRlciB0byBleHRyYWN0IHRoZSBhbGJ1bSBuYW1lcyBmcm9tIHRoZSB3ZWIgcGFnZS4NCg0KSGVyZSBpcyBhbm90aGVyIGV4YW1wbGUgd2hlcmUgdGhlIHVzZXIgc2NvcmVzIGhhdmUgYmVlbiBzZWxlY3RlZDoNCg0KIVsqU2VsZWN0b3IgZ2FkZ2V0IGhpZ2hsaWdodGluZyB1c2VyIHNjb3JlcyBuYW1lcypdKG1ldGFjcml0aWMyLnBuZykNCg0KTm93LCB0aGUgQ1NTIHRleHQgaXMgIi5wb3NpdGl2ZSI7IHRoaXMgd2lsbCBiZSB1c2VkIHRvIGV4dHJhY3QgdGhlIHVzZXIgcmF0aW5ncy4NCg0KTm93IHRoYXQgd2UgY2FuIHVzZSB0aGUgc2VsZWN0b3JnYWRnZXQgdG8gZXh0cmFjdCB0aGUgQ1NTIGluZm9ybWF0aW9uIHdlIG5lZWQsIGxldCdzIHVzZSB0aGlzIHdpdGhpbiBSLg0KDQojIyMgMi4gRXh0cmFjdGluZyBpbmRpdmlkdWFsIGZpZWxkcw0KDQpCZWZvcmUgd2UgYmVnaW4sIGxldCdzIHRha2UgYSBsb29rIGF0IHRoZSBSIHBhY2thZ2VzIHdlIHdpbGwgYmUgdXNpbmc6DQoNCiogKnJ2ZXN0KjogYSBwb3B1bGFyIHdlYnNjcmFwaW5nIHBhY2thZ2UgZm9yIFINCiogKmRwbHlyKjogYSBnZW5lcmFsIFIgcGFja2FnZSBmb3IgZGF0YSBtYW5pcHVsYXRpb24NCiogKnN0cmluZ3IqOiBhIHVzZWZ1bCBwYWNrYWdlIGZvciBjaGFyYWN0ZXIgc3RyaW5nIG1hbmlwdWxhdGlvbg0KKiAqZ2dwbG90Mio6IGEgcG9wdWxhciBkYXRhIHZpc3VhbGl6YXRpb24gcGFja2FnZQ0KDQpGaXJzdCwgd2UnbGwgbG9hZCB0aGVzZSBwYWNrYWdlcyBpbnRvIFIuDQoNCmBgYHtyfQ0KbGlicmFyeShydmVzdCkNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KHN0cmluZ3IpDQpsaWJyYXJ5KGdncGxvdDIpDQpgYGANCg0KVGhlICpyZWFkX2h0bWwqIGZ1bmN0aW9uIHRha2VzIHRoZSB1cmwgb2YgdGhlIHdlYiBwYWdlIHdlIHdpbGwgYmUgc2NyYXBpbmcuICBMZXQncyBhc3NpZ24gdGhpcyB0byB0aGUgb2JqZWN0LCAnd3AnLg0KDQpgYGB7cn0NCndwIDwtIHJlYWRfaHRtbCgiaHR0cDovL3d3dy5tZXRhY3JpdGljLmNvbS9icm93c2UvYWxidW1zL3JlbGVhc2UtZGF0ZS9hdmFpbGFibGUvdXNlcnNjb3JlP3ZpZXc9ZGV0YWlsZWQmcGFnZT0wIikNCg0KYGBgDQoNCkluIHRoZSBmaXJzdCBleGFtcGxlIG9mIHVzaW5nIHRoZSBzZWxlY3RvcmdhZGdldCwgd2UgZm91bmQgdGhlIENTUyBmaWVsZCByZWxhdGVkIHRvIGFsYnVtIG5hbWVzIHdhcyIucHJvZHVjdF90aXRsZSBhIi4gIFdlIGNhbiBpbnB1dCB0aGlzIGludG8gdGhlICpodG1sX25vZGVzKiBmdW5jdGlvbiBhcyBzaG93biBiZWxvdyB0byBleHRyYWN0IHRoZSBhbGJ1bSBuYW1lcy4gIEhlcmUgd2UgYXNzaWduIHRoZSBhbGJ1bSBuYW1lcyB0byB0aGUgb2JqZWN0LCAnYWxidW1fbmFtZScsIGFuZCB0YWtlIGEgbG9vayBhdCB0aGUgZmlyc3QgZmV3IGVudHJpZXMuDQoNCmBgYHtyfQ0KYWxidW1fbmFtZSA8LSB3cCAlPiUNCiAgaHRtbF9ub2RlcygiLnByb2R1Y3RfdGl0bGUgYSIpICU+JQ0KICBodG1sX3RleHQoKQ0KaGVhZChhbGJ1bV9uYW1lKQ0KYGBgDQoNClVzaW5nIHRoZSBzZWxlY3RvcmdhZGdldCB0byBoaWdobGlnaHQgdGhlIGFydGlzdCBuYW1lcyBwcm9kdWNlcyB0aGUgQ1NTIGZpZWxkIHRleHQsICIucHJvZHVjdF9hcnRpc3QiLiAgTGV0J3MgYXNzaWduIHRoaXMgZGF0YSB0byB0aGUgb2JqZWN0LCAnYXJ0aXN0X25hbWUnLg0KDQpgYGB7cn0NCmFydGlzdF9uYW1lIDwtIHdwICU+JQ0KICBodG1sX25vZGVzKCIucHJvZHVjdF9hcnRpc3QiKSAlPiUNCiAgaHRtbF90ZXh0KCkNCmhlYWQoYXJ0aXN0X25hbWUpDQpgYGANCg0KTm90aWNlIHRoYXQgZWFjaCBhcnRpc3QgaGFzIGEgIiAtICIgcHJlZml4LiAgV2ViIHNjcmFwaW5nIGNhbiBvZnRlbiBwcm92aWRlIGxpdHRsZSBibGVtaXNoZXMgaW4gb3VyIGRhdGEgc3VjaCBhcyB0aGlzLiAgV2UgY2FuIGVsaW1pbmF0ZSB0aGlzIHdpdGggdGhlIGZvbGxvd2luZyBtb2RpZmljYXRpb24gdG8gdGhlIGNvZGU6DQoNCmBgYHtyfQ0KYXJ0aXN0X25hbWUgPC0gd3AgJT4lDQogIGh0bWxfbm9kZXMoIi5wcm9kdWN0X2FydGlzdCIpICU+JQ0KICBodG1sX3RleHQoKSAlPiUgc3RyX3JlcGxhY2UoIiAtICIsIiIpDQpoZWFkKGFydGlzdF9uYW1lKQ0KYGBgDQoNCkxldCdzIGV4dHJhY3QgdGhlIHVzZXIgc2NvcmVzIGludG8gdGhlIG9iamVjdCwgJ3VzZXJfc2NvcmUnLg0KDQpgYGB7cn0NCnVzZXJfc2NvcmUgPC0gd3AgJT4lDQogIGh0bWxfbm9kZXMoIi5wb3NpdGl2ZSIpICU+JQ0KICBodG1sX3RleHQoKQ0KaGVhZCh1c2VyX3Njb3JlKQ0KYGBgDQoNCkZyb20gZ2xhbmNpbmcgYXQgdGhlIGZpcnN0IGZldyBlbnRyaWVzLCB3ZSBzZWUgdGhhdCB0aGVzZSBoYXZlIGJlZW4gZXh0cmFjdGVkIGFzIGNoYXJhY3RlciBzdHJpbmdzLiAgTGV0J3MgbW9kaWZ5IG91ciBjb2RlIHRvIGNvbnZlcnQgdGhpcyBpbnRvIG51bWVyaWMgZW50cmllcy4NCg0KYGBge3J9DQp1c2VyX3Njb3JlIDwtIHdwICU+JQ0KICBodG1sX25vZGVzKCIucG9zaXRpdmUiKSAlPiUNCiAgaHRtbF90ZXh0KCkgJT4lIGFzLm51bWVyaWMoKQ0KaGVhZCh1c2VyX3Njb3JlKQ0KYGBgDQoNClRoZSBtZXRhY3JpdGljIHNjb3JlcyBjYW4gYmUgaGFuZGxlZCBpbiBhIHNpbWlsYXIgd2F5Lg0KDQpgYGB7cn0NCm1ldGFfc2NvcmUgPC0gd3AgJT4lDQogIGh0bWxfbm9kZXMoIi50ZXh0c2NvcmUiKSAlPiUNCiAgaHRtbF90ZXh0KCkgJT4lIGFzLm51bWVyaWMoKQ0KaGVhZChtZXRhX3Njb3JlKQ0KYGBgDQoNClRoZSBnZW5yZSBkYXRhIHdpbGwgYXBwZWFyIGluIGEgbm9uLW9wdGltYWwgZm9ybWF0Lg0KDQpgYGB7cn0NCmdlbnJlIDwtIHdwICU+JQ0KICBodG1sX25vZGVzKCIuZ2VucmUgLmRhdGEiKSAlPiUNCiAgaHRtbF90ZXh0KCkNCmhlYWQoZ2VucmUpDQpgYGANCg0KVGhlIGZvbGxvd2luZyBjb2RlIHdpbGwgYWNoaWV2ZSAyIHRoaW5ncy4gIEZpcnN0LCBpdCB3aWxsIGZvcm1hdCB0aGUgZGF0YSB0byBiZSBtb3JlIGNvbXBhdGlibGUgYXMgYW4gZW50cnkgd2l0aGluIGEgZGF0YWZyYW1lIGNlbGwuICBTZWNvbmQsIHdlIHdpbGwgZXh0cmFjdCBvbmx5IHRoZSBwcmltYXJ5IGdlbnJlIGZvciBlYXNlIG9mIGZ1cnRoZXIgYW5hbHlzZXMuDQoNCmBgYHtyfQ0KcHJpbWFyeV9nZW5yZSA8LSB3cCAlPiUNCiAgaHRtbF9ub2RlcygiLmdlbnJlIC5kYXRhIikgJT4lDQogIGh0bWxfdGV4dCgpICU+JSBzdHJfc3F1aXNoKCkgJT4lIHN0cl9yZXBsYWNlKCIsLioiLCIiKQ0KaGVhZChwcmltYXJ5X2dlbnJlKQ0KYGBgDQoNCldoZW4gdXNpbmcgdGhlIHNlbGVjdG9yZ2FkZ2V0LCBpdCB3aWxsIGluZGljYXRlIGEgY291bnQgb2YgaG93IG1hbnkgZWxlbWVudHMgdGhhdCBoYXZlIGJlZW4gc2VsZWN0ZWQuICBVcG9uIHNlbGVjdGluZyB0aGUgZ2VucmUgZGF0YSwgaXQgaXMgcXVpY2tseSBub3RpY2VhYmxlIHRoYXQgNCBvZiB0aGUgMTAwIGRpc3BsYXllZCBhbGJ1bXMgYXJlIHdpdGhvdXQgYSBnZW5yZSBsaXN0aW5nLiAgV2UgY2FuIGNvbmZpcm0gdGhpcyBkaXNjcmVwYW5jeSBpbiBSLiAgICANCg0KYGBge3J9DQpkYXRhLmZyYW1lKGFsYnVtX25hbWVfbGVuZ3RoID0gbGVuZ3RoKGFsYnVtX25hbWUpLA0KICAgICAgICAgICBnZW5yZV9sZW5ndGggPSBsZW5ndGgocHJpbWFyeV9nZW5yZSkpDQpgYGANCg0KVW5mb3J0dW5hdGVseSwgdGhlIHdlYiBleHRyYWN0aW9uIGRvZXMgbm90IG5vdGljZSBtaXNzaW5nIGZpZWxkcyBhbmQgdGhlcmVmb3JlIGNhbm5vdCBjb3JyZWN0bHkgcGxhY2UgbWlzc2luZyBkYXRhIGluZGljYXRvcnMgd2hlcmUgdGhleSBuZWVkIHRvIGJlLiAgVGhpcyBjYW4gYmUgYSBjb21tb24gaHVyZGxlIGluIHdlYiBzY3JhcGluZy4gIE92ZXJjb21pbmcgdGhlc2UgaHVyZGxlcyByZXF1aXJlcyBzb21lIGZsZXhpYmlsaXR5IGluIFIuDQoNCldlIGNvdWxkIHZpc2libHkgaW5zcGVjdCB0aGUgd2VicGFnZSBmb3IgbWlzc2luZyBnZW5yZSBlbnRyaWVzIGFuZCBwbGFjZSB0aGUgbWlzc2luZyB2YWx1ZXMgaW4gb3VyIGRhdGEgbWFudWFsbHksIGJ1dCBhbiBhdXRvbWF0ZWQgYXBwcm9hY2ggd2lsbCBhbGxvdyB1cyB0byBoYW5kbGUgb3RoZXIgd2VicGFnZXMgd2l0aG91dCBtYW51YWwgaW5zcGVjdGlvbi4gIEFmdGVyIHVzaW5nIHRoZSBzZWxlY3RvciBnYWRnZXQgdG8gaGlnaGxpZ2h0IHNpbXVsdGFuZW91cyBmaWVsZHMsIHdlIGNhbiBleHRyYWN0IGEgbGFyZ2VyIHN1YnNldCBpbmNsdWRpbmcgdGhlIHVzZXIgc2NvcmUsIGdlbnJlLCBhbmQgZGF0ZS4NCg0KYGBge3J9DQp0ZXN0MSA8LSB3cCAlPiUNCiAgaHRtbF9ub2RlcygiLmRhdGEiKSAlPiUNCiAgaHRtbF90ZXh0KCkgJT4lIHN0cl9zcXVpc2goKQ0KaGVhZCh0ZXN0MSkNCmBgYA0KDQpDb252ZXJ0aW5nIHRoaXMgaW50byBhIG51bWVyaWMgdmVjdG9yLCB3ZSBzaG91bGQgbm90aWNlIGEgcGF0dGVybiBvZiAoTkEsTkEsbnVtZXJpYykuDQoNCmBgYHtyfQ0KdGVzdDIgPC0gYXMubnVtZXJpYyh0ZXN0MSkNCmhlYWQodGVzdDIpDQpgYGANCg0KVXNpbmcgdGhpcyBwYXR0ZXJuLCB3ZSBjYW4gdXNlIHRoZSBmb2xsb3dpbmcgZm9yLWxvb3Agcm91dGluZSB0byBpbXB1dGUgdGhlIG1pc3NpbmcgZGF0YSBhcyAnTkEnIGludG8gdGhlIHByb3BlciBsb2NhdGlvbi4NCg0KYGBge3J9DQojSWRlbnRpZnkgY291bnQgb2YgbWlzc2luZ3MNCk5BY291bnQgPC0gMTAwLWxlbmd0aChwcmltYXJ5X2dlbnJlKQ0KDQojU3RvcmUgTkEgaW5kaWNhdG9ycw0KTkF3aGljaCA8LSByZXAoMCwgTkFjb3VudCkNCg0KI0xvb3AgZm9yIGZpbmRpbmcgKE5BLE5BLG51bWVyaWMpIHBhdHRlcm5zDQpmb3IoaSBpbiAxOk5BY291bnQpew0KICBmb3IoaiBpbiAwOjEwMCl7DQogICAgI2NhdChpLGosIWlzLm5hKHRlc3QyWzMqaisyXSksIlxuIikNCiAgICBpZighaXMubmEodGVzdDJbMypqKzJdKSl7DQogICAgICB0ZXN0MiA8LSBhcHBlbmQodGVzdDIsIE5BLCAzKmopDQogICAgICBOQXdoaWNoW2ldIDwtIGorMQ0KICAgICAgYnJlYWsNCiAgICB9DQogIH0NCn0NCg0KI0ltcHV0ZSBOQXMgaW4gY29ycmVjdCBvcmRlcg0KZm9yKGkgaW4gTkF3aGljaCl7DQogIHByaW1hcnlfZ2VucmUgPC0gYXBwZW5kKHByaW1hcnlfZ2VucmUsIE5BLCBpLTEpIA0KfQ0KYGBgDQoNClRoZSByZWxlYXNlIGRhdGEgd2lsbCBiZSBvdXIgbGFzdCBleHRyYWN0ZWQgdmFyaWFibGUuDQoNCmBgYHtyfQ0KcmVsZWFzZV9kYXRlIDwtIHdwICU+JQ0KICBodG1sX25vZGVzKCIucmVsZWFzZV9kYXRlIC5kYXRhIikgJT4lDQogIGh0bWxfdGV4dCgpIA0KaGVhZChyZWxlYXNlX2RhdGUpDQpgYGANCg0KRnJvbSB0aGlzIHJlbGVhc2UgZGF0YSBjaGFyYWN0ZXIgc3RyaW5nLCBsZXQncyBleHRyYWN0IHRoZSB5ZWFyIGludG8gYSBzZXBhcmF0ZSBudW1lcmljIHZhcmlhYmxlIGZvciBvdXIgbGF0ZXIgYW5hbHlzaXMuDQoNCmBgYHtyfQ0KcmVsZWFzZV95ZWFyIDwtIHJlbGVhc2VfZGF0ZSAlPiUNCiAgc3RyX3JlcGxhY2UoIi4qLCIsIiIpICU+JSBzdHJfc3F1aXNoKCkgJT4lIGFzLm51bWVyaWMoKQ0KaGVhZChyZWxlYXNlX3llYXIpDQpgYGANCg0KRmluYWxseSwgbGV0J3MgZW50ZXIgYWxsIHRoaXMgc2NyYXBlZCBkYXRhIGludG8gYSBkYXRhIGZyYW1lLg0KDQpgYGB7cn0NCmRhdGFfbXVzaWMgPC0gZGF0YS5mcmFtZShhbGJ1bV9uYW1lLA0KICAgICAgICAgICAgICAgICAgICAgICAgIGFydGlzdF9uYW1lLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHVzZXJfc2NvcmUsDQogICAgICAgICAgICAgICAgICAgICAgICAgbWV0YV9zY29yZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBwcmltYXJ5X2dlbnJlLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHJlbGVhc2VfZGF0ZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICByZWxlYXNlX3llYXIpDQpoZWFkKGRhdGFfbXVzaWMpDQpgYGANCg0KTm93IHdlIGhhdmUgYSBkYXRhIGZyYW1lIHdpdGggYWxsIG9mIG91ciB3ZWIgc2NyYXBlZCBkYXRhLg0KDQojIyMgMy4gQXV0b21hdGVkIHNjcmFwaW5nIGFjcm9zcyBtdWx0aXBsZSB3ZWIgcGFnZXMNCg0KTm93IHRoYXQgd2UgaGF2ZSBkZW1vbnN0cmF0ZWQgZXh0cmFjdGluZyBkYXRhIHZhcmlhYmxlcyBmcm9tIGEgc2luZ2xlIHdlYiBwYWdlLCBsZXQncyBnZW5lcmFsaXplIHRoaXMgbWV0aG9kIHRvIGV4dHJhY3Qgc2ltaWxhciBkYXRhIGFjcm9zcyBtdWx0aXBsZSB3ZWIgcGFnZXMuDQoNCkZpcnN0LCB3ZSB3aWxsIGRlZmluZSBhbGwgdGhlIGluZGl2aWR1YWwgc3RlcHMgYWJvdmUgaW50byBhIGZ1bmN0aW9uIHRoYXQgd2lsbCB0YWtlIGEgdXJsIGFzIGFuIGlucHV0IGFuZCB3aWxsIHByb2R1Y2UgdGhlIGRlc2lyZWQgZGF0YSBmcmFtZSBhcyBhbiBvdXRwdXQuDQoNCmBgYHtyfQ0KbWV0YV9tdXNpY193ZWJzY3JhcGUgPC0gZnVuY3Rpb24odXJsKXsNCiAgDQogIHdwIDwtIHJlYWRfaHRtbCh1cmwpDQogIA0KICBhbGJ1bV9uYW1lIDwtIHdwICU+JQ0KICAgIGh0bWxfbm9kZXMoIi5wcm9kdWN0X3RpdGxlIGEiKSAlPiUNCiAgICBodG1sX3RleHQoKQ0KICBhbGJ1bV9uYW1lDQogIA0KICBhcnRpc3RfbmFtZSA8LSB3cCAlPiUNCiAgICBodG1sX25vZGVzKCIucHJvZHVjdF9hcnRpc3QiKSAlPiUNCiAgICBodG1sX3RleHQoKSAlPiUgc3RyX3JlcGxhY2UoIiAtICIsIiIpDQogIGFydGlzdF9uYW1lDQogIA0KICB1c2VyX3Njb3JlIDwtIHdwICU+JQ0KICAgIGh0bWxfbm9kZXMoIi5wb3NpdGl2ZSIpICU+JQ0KICAgIGh0bWxfdGV4dCgpICU+JSBhcy5udW1lcmljKCkNCiAgdXNlcl9zY29yZQ0KICANCiAgbWV0YV9zY29yZSA8LSB3cCAlPiUNCiAgICBodG1sX25vZGVzKCIudGV4dHNjb3JlIikgJT4lDQogICAgaHRtbF90ZXh0KCkgJT4lIGFzLm51bWVyaWMoKQ0KICBtZXRhX3Njb3JlDQogIA0KICBwcmltYXJ5X2dlbnJlIDwtIHdwICU+JQ0KICAgIGh0bWxfbm9kZXMoIi5nZW5yZSAuZGF0YSIpICU+JQ0KICAgIGh0bWxfdGV4dCgpICU+JSBzdHJfc3F1aXNoKCkgJT4lIHN0cl9yZXBsYWNlKCIsLioiLCIiKQ0KICBwcmltYXJ5X2dlbnJlDQogIA0KICByZWxlYXNlX2RhdGUgPC0gd3AgJT4lDQogICAgaHRtbF9ub2RlcygiLnJlbGVhc2VfZGF0ZSAuZGF0YSIpICU+JQ0KICAgIGh0bWxfdGV4dCgpIA0KICByZWxlYXNlX2RhdGUNCiAgDQogIHJlbGVhc2VfeWVhciA8LSByZWxlYXNlX2RhdGUgJT4lDQogICAgc3RyX3JlcGxhY2UoIi4qLCIsIiIpICU+JSBzdHJfc3F1aXNoKCkgJT4lIGFzLm51bWVyaWMoKQ0KICByZWxlYXNlX3llYXINCiAgDQogIE5BY291bnQgPC0gMTAwLWxlbmd0aChwcmltYXJ5X2dlbnJlKQ0KICBOQXdoaWNoIDwtIHJlcCgwLCBOQWNvdW50KQ0KICANCiAgdGVzdDEgPC0gd3AgJT4lDQogICAgaHRtbF9ub2RlcygiLmRhdGEiKSAlPiUNCiAgICBodG1sX3RleHQoKSAlPiUgc3RyX3NxdWlzaCgpDQogIHRlc3QxDQogIHRlc3QyIDwtIGFzLm51bWVyaWModGVzdDEpDQogIA0KICAjTG9vcCBmb3IgZmluZGluZyAoTkEsTkEsbnVtZXJpYykgcGF0dGVybnMNCiAgZm9yKGkgaW4gMTpOQWNvdW50KXsNCiAgICBmb3IoaiBpbiAwOjEwMCl7DQogICAgICAjY2F0KGksaiwhaXMubmEodGVzdDJbMypqKzJdKSwiXG4iKQ0KICAgICAgaWYoIWlzLm5hKHRlc3QyWzMqaisyXSkpew0KICAgICAgICB0ZXN0MiA8LSBhcHBlbmQodGVzdDIsIE5BLCAzKmopDQogICAgICAgIE5Bd2hpY2hbaV0gPC0gaisxDQogICAgICAgIGJyZWFrDQogICAgICB9DQogICAgfQ0KICB9DQogIA0KICBmb3IoaSBpbiBOQXdoaWNoKXsNCiAgICBwcmltYXJ5X2dlbnJlIDwtIGFwcGVuZChwcmltYXJ5X2dlbnJlLCBOQSwgaS0xKSANCiAgfQ0KICANCiAgZGF0YV9tdXNpYyA8LSBkYXRhLmZyYW1lKGFsYnVtX25hbWUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBhcnRpc3RfbmFtZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHVzZXJfc2NvcmUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRhX3Njb3JlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJpbWFyeV9nZW5yZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlbGVhc2VfZGF0ZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlbGVhc2VfeWVhcikNCiAgDQogIHJldHVybihkYXRhX211c2ljKQ0KfQ0KYGBgDQoNCkxldCdzIHRlc3QgdGhpcyBjdXN0b20gZnVuY3Rpb24gb24gdGhlIHNhbWUgd2ViIHBhZ2UgdG8gY29uZmlybSB0aGF0IHdlIGdldCB0aGUgc2FtZSBkYXRhIGZyYW1lLg0KDQpgYGB7cn0NCnVybDEgPC0gImh0dHA6Ly93d3cubWV0YWNyaXRpYy5jb20vYnJvd3NlL2FsYnVtcy9yZWxlYXNlLWRhdGUvYXZhaWxhYmxlL3VzZXJzY29yZT92aWV3PWRldGFpbGVkJnBhZ2U9MCINCm91dDEgPC0gbWV0YV9tdXNpY193ZWJzY3JhcGUodXJsMSkNCmhlYWQob3V0MSkNCmBgYA0KDQpOb3cgbGV0J3MgdGVzdCBpdCBvbiBhIHNlY29uZCBzaW1pbGFyIHdlYiBwYWdlLCBnaXZpbmcgdGhlIDEwMS0yMDAgbGlzdGluZyBvZiB0b3AgdXNlciByYXRlZCBhcnRpc3RzLg0KDQpgYGB7cn0NCnVybDIgPC0gImh0dHA6Ly93d3cubWV0YWNyaXRpYy5jb20vYnJvd3NlL2FsYnVtcy9yZWxlYXNlLWRhdGUvYXZhaWxhYmxlL3VzZXJzY29yZT92aWV3PWRldGFpbGVkJnBhZ2U9MSINCm91dDIgPC0gbWV0YV9tdXNpY193ZWJzY3JhcGUodXJsMikNCmhlYWQob3V0MikNCmBgYA0KDQpOb3cgbGV0J3MgYXV0b21hdGUgdGhpcyBmdW5jdGlvbiBvdmVyIDEwIHdlYiBwYWdlcywgY29sbGVjdGluZyBkYXRhIG9uIHRoZSB0b3AgMTAwMCB1c2VyIHJhdGVkIGFydGlzdHMuICBUaGlzIGNhbiBiZSBkb25lIGJ5IGFwcGx5aW5nIHRoZSBjdXN0b20gZnVuY3Rpb24gd2l0aGluIGEgZm9yLWxvb3Agd2hpbGUgdXNpbmcgdGhlICpwYXN0ZTAqIGZ1bmN0aW9uIHRvIGNhbGwgc2VxdWVudGlhbCB3ZWIgcGFnZXMuICBXZSdsbCBjYWxsIHRoaXMgZGF0YSBmcmFtZSwgJ3RvcDEwMDAnLg0KDQpgYGB7cn0NCiNDcmVhdGUgZW1wdHkgZGF0YSBmcmFtZQ0KdG9wMTAwMCA8LSBkYXRhLmZyYW1lKGFsYnVtX25hbWUgPSBjaGFyYWN0ZXIoKSwNCiAgICAgICAgICAgICAgICAgICAgICBhcnRpc3RfbmFtZSA9IGNoYXJhY3RlcigpLA0KICAgICAgICAgICAgICAgICAgICAgIHVzZXJfc2NvcmUgPSBudW1lcmljKCksDQogICAgICAgICAgICAgICAgICAgICAgbWV0YV9zY29yZSA9IG51bWVyaWMoKSwNCiAgICAgICAgICAgICAgICAgICAgICBwcmltYXJ5X2dlbnJlID0gY2hhcmFjdGVyKCksDQogICAgICAgICAgICAgICAgICAgICAgcmVsZWFzZV9kYXRlID0gY2hhcmFjdGVyKCksDQogICAgICAgICAgICAgICAgICAgICAgcmVsZWFzZV95ZWFyID0gbnVtZXJpYygpKQ0KDQpmb3IoaSBpbiAwOjkpew0KICB0b3AxMDAwIDwtIHJiaW5kKHRvcDEwMDAsDQogICAgICAgICAgICAgICAgICAgbWV0YV9tdXNpY193ZWJzY3JhcGUocGFzdGUwKA0KICAgICAgICAgICAgICAgICAgICAgImh0dHA6Ly93d3cubWV0YWNyaXRpYy5jb20vYnJvd3NlL2FsYnVtcy9yZWxlYXNlLWRhdGUvYXZhaWxhYmxlL3VzZXJzY29yZT92aWV3PWRldGFpbGVkJnBhZ2U9IiwNCiAgICAgICAgICAgICAgICAgICAgIGkNCiAgICAgICAgICAgICAgICAgICApKSkNCn0NCmBgYA0KDQpMZXQncyBjaGVjayB0aGUgc3RydWN0dXJlIG9mIHRoaXMgZGF0YSBmcmFtZS4NCg0KYGBge3J9DQpzdHIodG9wMTAwMCkNCmBgYA0KDQpOb3cgdGhhdCB3ZSBoYXZlIGV4dHJhY3RlZCBhbGwgdGhpcyBkYXRhLCBsZXQncyBkbyBzb21lIHNpbXBsZSBhbmFseXNpcy4NCg0KIyMjIDQuIEFuYWx5c2lzIG9mIHdlYiBzY3JhcGVkIGRhdGENCg0KTGV0J3MgYmVnaW4gYnkgbG9va2luZyBhdCBzb21lIHN1bW1hcnkgc3RhdGlzdGNzIG9mIHRoZSAndXNlcl9zY29yZScgdmFyaWFibGUuDQoNCmBgYHtyfQ0Kc3VtbWFyeSh0b3AxMDAwJHVzZXJfc2NvcmUpDQpwYXN0ZSgic3RhbmRhcmQgZGV2aWF0aW9uOiAiLCBzZCh0b3AxMDAwJHVzZXJfc2NvcmUpKQ0KYGBgDQoNCkxldCdzIHBsb3QgdGhlIGRpc3RyaWJ1dGlvbiBvZiB0aGlzIHZhcmlhYmxlLg0KDQpgYGB7cn0NCmdncGxvdCh0b3AxMDAwLCBhZXMoeCA9IHVzZXJfc2NvcmUpKSArDQogIGdlb21fYmFyKCkgKw0KICBsYWJzKHRpdGxlPSJEaXN0cmlidXRpb24gb2YgdXNlciBzY29yZXMiKQ0KYGBgDQoNCk5vdyB0aGUgc2FtZSBzdW1tYXJ5IGZvciB0aGUgbWV0YWNyaXRpYyBzY29yZXMuDQoNCmBgYHtyfQ0Kc3VtbWFyeSh0b3AxMDAwJG1ldGFfc2NvcmUpDQpwYXN0ZSgic3RhbmRhcmQgZGV2aWF0aW9uOiAiLCBzZCh0b3AxMDAwJG1ldGFfc2NvcmUpKQ0KYGBgDQoNCmBgYHtyfQ0KZ2dwbG90KHRvcDEwMDAsIGFlcyh4ID0gbWV0YV9zY29yZSkpICsNCiAgZ2VvbV9oaXN0b2dyYW0oKSArDQogIGxhYnModGl0bGU9IkRpc3RyaWJ1dGlvbiBvZiBtZXRhY3JpdGljIHNjb3JlcyIpDQpgYGANCg0KV2hhdCBkb2VzIHRoZSBkaXN0cmlidXRpb24gb2YgcHJpbWFyeSBnZW5yZXMgbG9vayBsaWtlPw0KDQpgYGB7cn0NCnRhYmxlKHRvcDEwMDAkcHJpbWFyeV9nZW5yZSkNCmBgYA0KDQpBcyB3ZSBjYW4gc2VlLCB0aGVyZSBhcmUgc2V2ZXJhbCBtaW5vciBnZW5yZXMgdGhhdCBoYXZlIGZldyBlbnRyaWVzLiAgTGV0J3MgY29sbGFwc2UgdGhpcyB2YXJpYWJsZSBpbnRvIGFuIGFsdGVybmF0ZSBmb3JtIHRoYXQgb25seSBoYXMgdGhlIHRvcCA1IGdlbnJlcyBhbmQgYW4gJ290aGVyJyBjYXRlZ29yeSBjYXB0dXJpbmcgdGhlIHJlbWFpbmluZyBnZW5yZXMuDQoNCmBgYHtyfQ0KT3RoZXJfZ2VucmVzIDwtIHNldGRpZmYodW5pcXVlKHRvcDEwMDAkcHJpbWFyeV9nZW5yZSksDQogICAgICAgICAgICAgICAgIG5hbWVzKHNvcnQodGFibGUodG9wMTAwMCRwcmltYXJ5X2dlbnJlKSwgZGVjcmVhc2luZyA9IFQpWzE6NV0pKQ0KICAgICAgICAgICAgICAgICANCnRvcDEwMDAkcHJpbWFyeV9nZW5yZTIgPC0gdG9wMTAwMCRwcmltYXJ5X2dlbnJlDQpsZXZlbHModG9wMTAwMCRwcmltYXJ5X2dlbnJlMikgPC0gYyhsZXZlbHModG9wMTAwMCRwcmltYXJ5X2dlbnJlMiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiT3RoZXIiKQ0KdG9wMTAwMCRwcmltYXJ5X2dlbnJlMlt0b3AxMDAwJHByaW1hcnlfZ2VucmUyICVpbiUgT3RoZXJfZ2VucmVzXSA8LSBhcy5mYWN0b3IoIk90aGVyIikgDQp0b3AxMDAwJHByaW1hcnlfZ2VucmUyIDwtIGRyb3BsZXZlbHModG9wMTAwMCRwcmltYXJ5X2dlbnJlMikNCg0KdGFibGUodG9wMTAwMCRwcmltYXJ5X2dlbnJlMikNCmBgYA0KDQpOb3cgbGV0J3MgdmlzdWFsaXplIHRoaXMgdGFibGUgaW50byBhIGJhciBwbG90Lg0KDQpgYGB7cn0NCmdncGxvdCh0b3AxMDAwLCBhZXMoeCA9IHByaW1hcnlfZ2VucmUyKSkgKw0KICBnZW9tX2JhcigpICsNCiAgZ2VvbV90ZXh0KGFlcyh5ID0gLi5jb3VudC4uIC0xMCwgDQogICAgICAgICAgICAgICAgbGFiZWwgPSBwYXN0ZTAocm91bmQocHJvcC50YWJsZSguLmNvdW50Li4pLDQpICogMTAwLCAnJScpKSwgDQogICAgICAgICAgICBzdGF0ID0gJ2NvdW50JywgDQogICAgICAgICAgICBwb3NpdGlvbiA9IHBvc2l0aW9uX2RvZGdlKC4xKSwgDQogICAgICAgICAgICBzaXplID0gMykrDQogIGxhYnModGl0bGU9IkRpc3RyaWJ1dGlvbiBvZiBwcmltYXJ5IGdlbnJlcyIpDQoNCmBgYA0KDQpJdCBhcHBlYXJzIGFzIGlmICdyb2NrJyBhbmQgJ2luZGllJyBhcmUgdGhlIG1vc3QgcG9wdWxhciBnZW5yZXMgd2l0aGluIHRoZSB0b3AgMTAwMCB1c2VyIHJhdGVkIGFsYnVtcy4NCg0KSG93IGFib3V0IHRoZSBkaXN0cmlidXRpb24gb2YgYWxidW0gcmVsYXNlIHllYXJzPw0KDQpgYGB7cn0NCnRhYmxlKHRvcDEwMDAkcmVsZWFzZV95ZWFyKQ0KDQpnZ3Bsb3QodG9wMTAwMCwgYWVzKHggPSByZWxlYXNlX3llYXIpKSArDQogIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMSkgKw0KICBsYWJzKHRpdGxlPSJEaXN0cmlidXRpb24gb2YgcmVsZWFzZSB5ZWFycyIpDQpgYGANCg0KSXQgYXBwZWFycyBhcyBpZiB0aGUgbW9zdCBwb3B1bGFyIG11c2ljIHdhcyByZWxlYXNlZCBpbiB0aGUgeWVhcnMgMjAwMywgMjAwNCwgYW5kIDIwMDkuDQoNCk5vdyBmb3Igc29tZSBiYXNpYyBpbmZlcmVudGlhbCBzdGF0aXN0aWNzIGxvb2tpbmcgYXQgc29tZSByZWxhdGlvbnNoaXBzIGJldHdlZW4gdGhlc2UgdmFyaWFibGVzLg0KDQpXaGF0IGlzIHRoZSByZWxhdGlvbnNoaXAgYmV0d2VlbiB1c2VyIHNjb3JlIGFuZCB0aGUgbWV0YWNyaXRpYyBzY29yZT8NCg0KYGBge3J9DQpwYXN0ZSgiQ29ycmVsYXRpb24gb2YgdXNlciBzY29yZSBhbmQgbWV0YSBzY29yZTogIiwNCiAgICAgIGNvcih0b3AxMDAwJHVzZXJfc2NvcmUsdG9wMTAwMCRtZXRhX3Njb3JlKSkNCg0KZ2dwbG90KHRvcDEwMDAsIGFlcyh4PSB1c2VyX3Njb3JlLCB5ID0gbWV0YV9zY29yZSkpICsNCiAgZ2VvbV9wb2ludCgpICsNCiAgZ2VvbV9zbW9vdGgobWV0aG9kPWxtKSArDQogIGxhYnModGl0bGU9IlNjYXR0ZXJwbG90IG9mIHVzZXIgYW5kIG1ldGFjcml0aWMgc2NvcmVzIikNCmBgYA0KDQpGcm9tIHRoZSByYXRoZXIgc21hbGwgY29ycmVsYXRpb24gY29lZmZpY2llbnQgb2YgLjE3IGFuZCB0aGUgcm91bmQgZGlzcGVyc2lvbiBvZiB0aGUgc2NhdHRlcnBsb3QsIGl0IGFwcGVhcnMgYXMgaWYgdGhlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIHVzZXIgYW5kIG1ldGFjcml0aWMgc2NvcmUgaXMgbW9kZXJhdGUgYXQgYmVzdC4NCg0KTGV0J3MgcnVuIGEgZmV3IHJlZ3Jlc3Npb24gbW9kZWxzIHRvIGxvb2sgYXQgc29tZSBnZW5yZSBieSByYXRpbmcgcmVsYXRpb25zaGlwcy4gIFdpdGhpbiB0aGVzZSB0b3AgMTAwMCBhbGJ1bXMsIGlzIHRoZXJlIGEgZGlmZmVyZW5jZSBpbiB0aGUgYXZlcmFnZSB1c2VyIHJhdGluZyBieSBnZW5yZSB0eXBlPw0KDQpgYGB7cn0NCmZpdDEgPC0gbG0odXNlcl9zY29yZSB+IHJlbGV2ZWwocHJpbWFyeV9nZW5yZTIsIHJlZiA9ICJPdGhlciIpLCBkYXRhID0gdG9wMTAwMCkNCnN1bW1hcnkoZml0MSkNCmBgYA0KDQpJdCBhcHBlYXJzIGFzIGlmIHRoZXJlIGFyZSBubyBzdGF0aXN0aWNhbGx5IHNpZ25pZmljYW50IGRpZmZlcmVuY2VzIGluIHVzZXIgcmF0aW5ncyBieSBnZW5yZSB0eXBlLg0KDQpIb3cgYWJvdXQgZ2VucmUgdHlwZSBkaWZmZXJlbmNlcyBpbiBtZXRhY3JpdGljIHNjb3Jlcz8NCg0KYGBge3J9DQpmaXQyIDwtIGxtKG1ldGFfc2NvcmUgfiByZWxldmVsKHByaW1hcnlfZ2VucmUyLCByZWYgPSAiT3RoZXIiKSwgZGF0YSA9IHRvcDEwMDApDQpzdW1tYXJ5KGZpdDIpDQpgYGANCg0KSXQgYXBwZWFycyBhcyBpZiB0aGVyZSBhcmUgc29tZSBzdGF0aXN0aWNhbGx5IHNpZ25pZmljYW50IGRpZmZlcmVuY2VzIGhlcmU7IGxldCdzIHZpc3VhbGl6ZSB0aGlzLg0KDQpgYGB7cn0NCmdncGxvdCh0b3AxMDAwLCBhZXMoeCA9IHByaW1hcnlfZ2VucmUyLCB5ID0gbWV0YV9zY29yZSkpICsNCiAgZ2VvbV9ib3hwbG90KCkgKw0KICBsYWJzKHRpdGxlPSJCb3hwbG90IG9mIG1ldGFjcml0aWMgc2NvcmUgYnkgcHJpbWFyeSBnZW5yZSIpDQoNCmBgYA0KDQpXaXRoaW4gdGhlc2UgdG9wIDEwMDAgYWxidW1zLCB0aGUgbWV0YWNyaXRpYyBzY29yZXMgc2VlbWVkIHRvIHByZWZlciByYXAgYW5kIGluZGllIGdlbnJlcyB3aGlsZSBkaXNmYXZvcmluZyByb2NrLg0KDQojIyMgQ29uY2x1c2lvbg0KDQpJbiB0aGlzIHByZXNlbnRhdGlvbiB3ZSBjb2xsZWN0ZWQgZGF0YSBvbiB0aGUgdG9wIDEwMDAgdXNlciByYXRlZCBhbGJ1bXMgZnJvbSBtZXRhY3JpdGljLiAgV2UgZmlyc3QgaW50cm9kdWNlZCB0aGUgc2VsZWN0b3JnYWRnZXQgdG9vbCBhbmQgUiBwYWNrYWdlcyBuZWNlc3NhcnkgZm9yIHdlYiBzY3JhcGluZy4gU2Vjb25kLCB3ZSBkZW1vbnN0cmF0ZWQgYmFzaWMgdXNlIG9mIHRoZXNlIGZ1bmN0aW9ucyBmb3IgZXh0cmFjdGluZyBzaW5nbGUgbGF5ZXJzIGZyb20gaW5kaXZpZHVhbCB3ZWIgcGFnZXMuICBXZSBwcm9jZWVkZWQgdG8gYXV0b21hdGUgdGhpcyBwcm9jZXNzIGZvciBjb2xsZWN0aW9uIG9mIGxhcmdlIHF1YW50aXRpZXMgb2YgZGF0YSBvdmVyIG11bHRpcGxlIHdlYiBwYWdlcy4gIEZpbmFsbHksIHdlIGNhcnJpZWQgb3V0IHNvbWUgYmFzaWMgZGVzY3JpcHRpdmUgYW5kIGluZmVyZW50aWFsIHN0YXRpc3RpY3Mgb24gdGhpcyB3ZWIgc2NyYXBlZCBkYXRhLg0KDQpIZXJlIGFyZSBzb21lIHRoaW5ncyB3ZSBmb3VuZCBvdXQgYWJvdXQgdGhlIHRvcCAxMDAwIHVzZXIgcmF0ZWQgYWxidW1zOg0KDQoqIFJvY2sgYW5kIEluZGllIGdlbnJlcyB3ZXJlIHRoZSBtb3N0IHBvcHVsYXIgcHJpbWFyeSBnZW5yZXMsIGNvbWJpbmluZyBmb3Igb3ZlciA1MCUgb2YgdGhlIGxpc3QuDQoqIFRoZSB5ZWFycyBvZiAyMDAzLCAyMDA0LCBhbmQgMjAwOSBwcm9kdWNlZCB0aGUgaGlnaGVzdCBudW1iZXIgb2YgYWxidW1zIG9uIHRoaXMgbGlzdC4NCiogVGhlIGNvcnJlbGF0aW9uIGJldHdlZW4gdXNlciBzY29yZSByYXRpbmcgYW5kIG1ldGFjcml0aWMgd2FzIC4xNywgaW5kaWNhdGluZyBhIHJhdGhlciBsb3cgcmVsYXRpb25zaGlwLg0KKiBNZXRhY3JpdGljIHNjb3JlcyB0ZW5kZWQgdG8gcHJlZmVyIFJhcCBhbmQgSW5kaWUgZ2VucmVzIG1vcmUgdGhhbiBSb2NrLg0KDQoNCg==