Creative Commons License

This is part of the online course Proteomics Data Analysis (PDA)

1 Background

This case-study is a subset of the data of the 6th study of the Clinical Proteomic Technology Assessment for Cancer (CPTAC). In this experiment, the authors spiked the Sigma Universal Protein Standard mixture 1 (UPS1) containing 48 different human proteins in a protein background of 60 ng/\(\mu\)L Saccharomyces cerevisiae strain BY4741. Two different spike-in concentrations were used: 6A (0.25 fmol UPS1 proteins/\(\mu\)L) and 6B (0.74 fmol UPS1 proteins/\(\mu\)L) [5]. We limited ourselves to the data of LTQ-Orbitrap W at site 56. The data were searched with MaxQuant version 1.5.2.8, and detailed search settings were described in Goeminne et al. (2016) [1]. Three replicates are available for each concentration.

2 Data

We first import the data from peptideRaws.txt file. This is the file containing your peptideRaw-level intensities. For a MaxQuant search [6], this peptideRaws.txt file can be found by default in the “path_to_raw_files/combined/txt/” folder from the MaxQuant output, with “path_to_raw_files” the folder where the raw files were saved. In this vignette, we use a MaxQuant peptideRaws file which is a subset of the cptac study. This data is available in the msdata package. To import the data we use the QFeatures package.

We generate the object peptideRawFile with the path to the peptideRaws.txt file. Using the grepEcols function, we find the columns that contain the expression data of the peptideRaws in the peptideRaws.txt file.

library(tidyverse)
library(limma)
library(QFeatures)
library(msqrob2)
library(plotly)

peptidesFile <- "https://raw.githubusercontent.com/statOmics/SGA2020/data/quantification/cptacAvsB_lab3/peptides.txt"

ecols <- grep(
  "Intensity\\.", 
  names(read.delim(peptidesFile))
  )

pe <- readQFeatures(
  table = peptidesFile,
  fnames = 1,
  ecol = ecols,
  name = "peptideRaw", sep="\t")

colnames(pe)
## CharacterList of length 1
## [["peptideRaw"]] Intensity.6A_7 Intensity.6A_8 ... Intensity.6B_9

In the following code chunk, we can extract the spikein condition from the raw file name.

cond <- which(
  strsplit(colnames(pe)[[1]][1], split = "")[[1]] == "A") # find where condition is stored

colData(pe)$condition <- substr(colnames(pe), cond, cond) %>%
  unlist %>%  
  as.factor

We calculate how many non zero intensities we have per peptide and this will be useful for filtering.

rowData(pe[["peptideRaw"]])$nNonZero <- rowSums(assay(pe[["peptideRaw"]]) > 0)

Peptides with zero intensities are missing peptides and should be represent with a NA value rather than 0.

pe <- zeroIsNA(pe, "peptideRaw") # convert 0 to NA

2.1 Data exploration

45% of all peptide intensities are missing and for some peptides we do not even measure a signal in any sample.

3 Preprocessing

This section preforms preprocessing for the peptide data. This include

  • log transformation,
  • filtering and
  • summarisation of the data.

3.1 Log transform the data

pe <- logTransform(pe, base = 2, i = "peptideRaw", name = "peptideLog")

3.2 Filtering

  1. Handling overlapping protein groups

In our approach a peptide can map to multiple proteins, as long as there is none of these proteins present in a smaller subgroup.

pe <- filterFeatures(pe, ~ Proteins %in% smallestUniqueGroups(rowData(pe[["peptideLog"]])$Proteins))
  1. Remove reverse sequences (decoys) and contaminants

We now remove the contaminants and peptides that map to decoy sequences.

pe <- filterFeatures(pe,~Reverse != "+")
pe <- filterFeatures(pe,~ Potential.contaminant != "+")
  1. Drop peptides that were only identified in one sample

We keep peptides that were observed at last twice.

pe <- filterFeatures(pe,~ nNonZero >=2)
nrow(pe[["peptideLog"]])
## [1] 7011

We keep 7011 peptides upon filtering.

3.3 Normalize the data using median centering

We normalize the data by substracting the sample median from every intensity for peptide \(p\) in a sample \(i\):

\[y_{ip}^\text{norm} = y_{ip} - \hat\mu_i\]

with \(\hat\mu_i\) the median intensity over all observed peptides in sample \(i\).

pe <- normalize(pe, 
                i = "peptideLog", 
                name = "peptideNorm", 
                method = "center.median")

3.4 Explore normalized data

Upon the normalisation the density curves are nicely registered

pe[["peptideNorm"]] %>% 
  assay %>%
  as.data.frame() %>%
  gather(sample, intensity) %>% 
  mutate(condition = colData(pe)[sample,"condition"]) %>%
  ggplot(aes(x = intensity,group = sample,color = condition)) + 
    geom_density()
## Warning: Removed 8167 rows containing non-finite values (stat_density).

We can visualize our data using a Multi Dimensional Scaling plot, eg. as provided by the limma package.

pe[["peptideNorm"]] %>% 
  assay %>%
  limma::plotMDS(col = as.numeric(colData(pe)$condition))

The first axis in the plot is showing the leading log fold changes (differences on the log scale) between the samples.

We notice that the leading differences (log FC) in the peptide data seems to be driven by technical variability. Indeed, the samples do not seem to be clearly separated according to the spike-in condition.

3.5 Summarization to protein level

  • By default robust summarization is used: fun = MsCoreUtils::robustSummary()
pe <- aggregateFeatures(pe,
  i = "peptideNorm",
  fcol = "Proteins",
  na.rm = TRUE,
  name = "protein")
## Your quantitative and row data contain missing values. Please read the
## relevant section(s) in the aggregateFeatures manual page regarding the
## effects of missing values on data aggregation.
plotMDS(assay(pe[["protein"]]), col = as.numeric(colData(pe)$condition))

Note that the samples upon robust summarisation show a clear separation according to the spike-in condition in the second dimension of the MDS plot.

4 Data Analysis

4.1 Estimation

We model the protein level expression values using msqrob. By default msqrob2 estimates the model parameters using robust regression.

We will model the data with a different group mean. The group is incoded in the variable condition of the colData. We can specify this model by using a formula with the factor condition as its predictor: formula = ~condition.

Note, that a formula always starts with a symbol ‘~’.

pe <- msqrob(object = pe, i = "protein", formula = ~condition)

4.2 Inference

First, we extract the parameter names of the model by looking at the first model. The models are stored in the row data of the assay under the default name msqrobModels.

getCoef(rowData(pe[["protein"]])$msqrobModels[[1]])
## (Intercept)  conditionB 
##   -2.672396    1.513682

We can also explore the design of the model that we specified using the the package ExploreModelMatrix

library(ExploreModelMatrix)
VisualizeDesign(colData(pe),~condition)$plotlist[[1]]

Spike-in condition A is the reference class. So the mean log2 expression for samples from condition A is ‘(Intercept). The mean log2 expression for samples from condition B is’(Intercept)+conditionB’. Hence, the average log2 fold change between condition b and condition a is modelled using the parameter ‘conditionB’. Thus, we assess the contrast ‘conditionB = 0’ with our statistical test.

L <- makeContrast("conditionB=0", parameterNames = c("conditionB"))
pe <- hypothesisTest(object = pe, i = "protein", contrast = L)

4.3 Plots

4.3.1 Volcano-plot

volcano <- ggplot(rowData(pe[["protein"]])$conditionB,
                  aes(x = logFC, y = -log10(pval), color = adjPval < 0.05)) +
  geom_point(cex = 2.5) +
  scale_color_manual(values = alpha(c("black", "red"), 0.5)) + theme_minimal()
volcano

Note, that 20 proteins are found to be differentially abundant.

4.3.2 Heatmap

We first select the names of the proteins that were declared signficant.

sigNames <- rowData(pe[["protein"]])$conditionB %>%
  rownames_to_column("protein") %>%
  filter(adjPval<0.05) %>%
  pull(protein)
heatmap(assay(pe[["protein"]])[sigNames, ])

The majority of the proteins are indeed UPS proteins. 1 yeast protein is returned. Note, that the yeast protein indeed shows evidence for differential abundance.

4.3.3 Boxplots

We make boxplot of the log2 FC and stratify according to the whether a protein is spiked or not.

rowData(pe[["protein"]])$conditionB %>%
  rownames_to_column(var = "protein") %>%
  ggplot(aes(x=grepl("UPS",protein),y=logFC)) +
  geom_boxplot() +
  xlab("UPS") +
  geom_segment(
    x = 1.5,
    xend = 2.5,
    y = log2(0.74/0.25),
    yend = log2(0.74/0.25),
    colour="red") +
  geom_segment(
    x = 0.5,
    xend = 1.5,
    y = 0,
    yend = 0,
    colour="red") +
  annotate(
    "text",
    x = c(1,2),
    y = c(0,log2(0.74/0.25))+.1,
    label = c(
      "log2 FC Ecoli = 0",
      paste0("log2 FC UPS = ",round(log2(0.74/0.25),2))
      ),
    colour = "red")
## Warning: Removed 167 rows containing non-finite values (stat_boxplot).

What do you observe?

4.3.4 Detail plots

We first extract the normalized peptideRaw expression values for a particular protein.

for (protName in sigNames)
{
pePlot <- pe[protName, , c("peptideNorm","protein")]
pePlotDf <- data.frame(longFormat(pePlot))
pePlotDf$assay <- factor(pePlotDf$assay,
                        levels = c("peptideNorm", "protein"))
pePlotDf$condition <- as.factor(colData(pePlot)[pePlotDf$colname, "condition"])

# plotting
p1 <- ggplot(data = pePlotDf,
       aes(x = colname, y = value, group = rowname)) +
    geom_line() + 
    geom_point() +  
    theme(axis.text.x = element_text(angle = 70, hjust = 1, vjust = 0.5)) +
    facet_grid(~assay) + 
    ggtitle(protName)
print(p1)

# plotting 2
p2 <- ggplot(pePlotDf, aes(x = colname, y = value, fill = condition)) +
  geom_boxplot(outlier.shape = NA) + 
  geom_point(
    position = position_jitter(width = .1),
    aes(shape = rowname)) +
  scale_shape_manual(values = 1:nrow(pePlotDf)) +
  labs(title = protName, x = "sample", y = "peptide intensity (log2)") + 
  theme(axis.text.x = element_text(angle = 70, hjust = 1, vjust = 0.5)) +
  facet_grid(~assay)
print(p2)
}

Note, that the yeast protein is only covered by 3 peptides. Only one peptide is picked up in condition A. This peptide is also only once observed in spike-in condition B. This puts a considerable burden upon the inference and could be avoided by more stringent filtering.

5 Session Info

With respect to reproducibility, it is highly recommended to include a session info in your script so that readers of your output can see your particular setup of R.

sessionInfo()
## R version 4.1.3 (2022-03-10)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 22.04.2 LTS
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.20.so
## 
## locale:
##  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
##  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
##  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
## [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
## 
## attached base packages:
## [1] parallel  stats4    stats     graphics  grDevices datasets  utils    
## [8] methods   base     
## 
## other attached packages:
##  [1] ExploreModelMatrix_1.4.0    plotly_4.9.4.1             
##  [3] msqrob2_1.0.0               QFeatures_1.2.0            
##  [5] MultiAssayExperiment_1.18.0 SummarizedExperiment_1.22.0
##  [7] Biobase_2.52.0              GenomicRanges_1.44.0       
##  [9] GenomeInfoDb_1.28.1         IRanges_2.26.0             
## [11] S4Vectors_0.30.0            BiocGenerics_0.38.0        
## [13] MatrixGenerics_1.4.3        matrixStats_0.61.0         
## [15] limma_3.48.1                forcats_0.5.1              
## [17] stringr_1.4.0               dplyr_1.0.7                
## [19] purrr_0.3.4                 readr_1.4.0                
## [21] tidyr_1.1.3                 tibble_3.1.6               
## [23] ggplot2_3.3.5               tidyverse_1.3.1            
## 
## loaded via a namespace (and not attached):
##  [1] minqa_1.2.4             colorspace_2.0-3        ellipsis_0.3.2         
##  [4] XVector_0.32.0          fs_1.5.2                clue_0.3-59            
##  [7] rstudioapi_0.13         farver_2.1.0            DT_0.18                
## [10] fansi_1.0.2             lubridate_1.7.10        xml2_1.3.2             
## [13] codetools_0.2-18        splines_4.1.3           knitr_1.37             
## [16] jsonlite_1.8.0          nloptr_1.2.2.2          broom_0.7.8            
## [19] cluster_2.1.2           dbplyr_2.1.1            shinydashboard_0.7.1   
## [22] shiny_1.6.0             BiocManager_1.30.16     compiler_4.1.3         
## [25] httr_1.4.2              backports_1.2.1         assertthat_0.2.1       
## [28] Matrix_1.4-0            fastmap_1.1.0           lazyeval_0.2.2         
## [31] cli_3.2.0               later_1.2.0             htmltools_0.5.2        
## [34] tools_4.1.3             gtable_0.3.0            glue_1.6.2             
## [37] GenomeInfoDbData_1.2.6  Rcpp_1.0.7              cellranger_1.1.0       
## [40] jquerylib_0.1.4         vctrs_0.3.8             nlme_3.1-155           
## [43] rintrojs_0.3.0          xfun_0.30               lme4_1.1-27.1          
## [46] rvest_1.0.0             mime_0.11               lifecycle_1.0.1        
## [49] renv_0.15.4             zlibbioc_1.38.0         MASS_7.3-55            
## [52] scales_1.1.1            promises_1.2.0.1        hms_1.1.0              
## [55] ProtGenerics_1.24.0     AnnotationFilter_1.16.0 yaml_2.3.5             
## [58] sass_0.4.0              stringi_1.7.6           highr_0.9              
## [61] boot_1.3-28             BiocParallel_1.26.1     rlang_1.0.2            
## [64] pkgconfig_2.0.3         bitops_1.0-7            evaluate_0.15          
## [67] lattice_0.20-45         htmlwidgets_1.5.3       labeling_0.4.2         
## [70] cowplot_1.1.1           tidyselect_1.1.1        magrittr_2.0.2         
## [73] R6_2.5.1                generics_0.1.0          DelayedArray_0.18.0    
## [76] DBI_1.1.1               pillar_1.7.0            haven_2.4.1            
## [79] withr_2.5.0             MsCoreUtils_1.4.0       RCurl_1.98-1.3         
## [82] modelr_0.1.8            crayon_1.5.0            utf8_1.2.2             
## [85] rmarkdown_2.13          grid_4.1.3              readxl_1.3.1           
## [88] data.table_1.14.0       reprex_2.0.0            digest_0.6.29          
## [91] xtable_1.8-4            httpuv_1.6.1            munsell_0.5.0          
## [94] viridisLite_0.4.0       bslib_0.3.1             shinyjs_2.0.0
LS0tCnRpdGxlOiAiSW50cm9kdWN0aW9uIHRvIHByb3Rlb21pY3MgZGF0YSBhbmFseXNpczogcm9idXN0IHN1bW1hcml6YXRpb24iCmF1dGhvcjogIkxpZXZlbiBDbGVtZW50IgpkYXRlOiAic3RhdE9taWNzLCBHaGVudCBVbml2ZXJzaXR5IChodHRwczovL3N0YXRvbWljcy5naXRodWIuaW8pIgpvdXRwdXQ6CiAgICBodG1sX2RvY3VtZW50OgogICAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCiAgICAgIHRoZW1lOiBmbGF0bHkKICAgICAgdG9jOiB0cnVlCiAgICAgIHRvY19mbG9hdDogdHJ1ZQogICAgICBoaWdobGlnaHQ6IHRhbmdvCiAgICAgIG51bWJlcl9zZWN0aW9uczogdHJ1ZQogICAgcGRmX2RvY3VtZW50OgogICAgICB0b2M6IHRydWUKICAgICAgbnVtYmVyX3NlY3Rpb25zOiB0cnVlCmxpbmtjb2xvcjogYmx1ZQp1cmxjb2xvcjogYmx1ZQpjaXRlY29sb3I6IGJsdWUKCmJpYmxpb2dyYXBoeTogbXNxcm9iMi5iaWIKCi0tLQoKPGEgcmVsPSJsaWNlbnNlIiBocmVmPSJodHRwczovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMtc2EvNC4wIj48aW1nIGFsdD0iQ3JlYXRpdmUgQ29tbW9ucyBMaWNlbnNlIiBzdHlsZT0iYm9yZGVyLXdpZHRoOjAiIHNyYz0iaHR0cHM6Ly9pLmNyZWF0aXZlY29tbW9ucy5vcmcvbC9ieS1uYy1zYS80LjAvODh4MzEucG5nIiAvPjwvYT4KClRoaXMgaXMgcGFydCBvZiB0aGUgb25saW5lIGNvdXJzZSBbUHJvdGVvbWljcyBEYXRhIEFuYWx5c2lzIChQREEpXShodHRwczovL3N0YXRvbWljcy5naXRodWIuaW8vUERBLykKCiMgQmFja2dyb3VuZApUaGlzIGNhc2Utc3R1ZHkgaXMgYSBzdWJzZXQgb2YgdGhlIGRhdGEgb2YgdGhlIDZ0aCBzdHVkeSBvZiB0aGUgQ2xpbmljYWwKUHJvdGVvbWljIFRlY2hub2xvZ3kgQXNzZXNzbWVudCBmb3IgQ2FuY2VyIChDUFRBQykuCkluIHRoaXMgZXhwZXJpbWVudCwgdGhlIGF1dGhvcnMgc3Bpa2VkIHRoZSBTaWdtYSBVbml2ZXJzYWwgUHJvdGVpbiBTdGFuZGFyZAptaXh0dXJlIDEgKFVQUzEpIGNvbnRhaW5pbmcgNDggZGlmZmVyZW50IGh1bWFuIHByb3RlaW5zIGluIGEgcHJvdGVpbiBiYWNrZ3JvdW5kCm9mIDYwIG5nLyRcbXUkTCBTYWNjaGFyb215Y2VzIGNlcmV2aXNpYWUgc3RyYWluIEJZNDc0MS4KVHdvIGRpZmZlcmVudCBzcGlrZS1pbiBjb25jZW50cmF0aW9ucyB3ZXJlIHVzZWQ6CjZBICgwLjI1IGZtb2wgVVBTMSBwcm90ZWlucy8kXG11JEwpIGFuZCA2QiAoMC43NCBmbW9sIFVQUzEgcHJvdGVpbnMvJFxtdSRMKSBbNV0uCldlIGxpbWl0ZWQgb3Vyc2VsdmVzIHRvIHRoZSBkYXRhIG9mIExUUS1PcmJpdHJhcCBXIGF0IHNpdGUgNTYuClRoZSBkYXRhIHdlcmUgc2VhcmNoZWQgd2l0aCBNYXhRdWFudCB2ZXJzaW9uIDEuNS4yLjgsIGFuZApkZXRhaWxlZCBzZWFyY2ggc2V0dGluZ3Mgd2VyZSBkZXNjcmliZWQgaW4gR29lbWlubmUgZXQgYWwuICgyMDE2KSBbMV0uClRocmVlIHJlcGxpY2F0ZXMgYXJlIGF2YWlsYWJsZSBmb3IgZWFjaCBjb25jZW50cmF0aW9uLgoKCiMgRGF0YQoKV2UgZmlyc3QgaW1wb3J0IHRoZSBkYXRhIGZyb20gcGVwdGlkZVJhd3MudHh0IGZpbGUuIFRoaXMgaXMgdGhlIGZpbGUgY29udGFpbmluZwp5b3VyIHBlcHRpZGVSYXctbGV2ZWwgaW50ZW5zaXRpZXMuIEZvciBhIE1heFF1YW50IHNlYXJjaCBbNl0sCnRoaXMgcGVwdGlkZVJhd3MudHh0IGZpbGUgY2FuIGJlIGZvdW5kIGJ5IGRlZmF1bHQgaW4gdGhlCiJwYXRoX3RvX3Jhd19maWxlcy9jb21iaW5lZC90eHQvIiBmb2xkZXIgZnJvbSB0aGUgTWF4UXVhbnQgb3V0cHV0LAp3aXRoICJwYXRoX3RvX3Jhd19maWxlcyIgdGhlIGZvbGRlciB3aGVyZSB0aGUgcmF3IGZpbGVzIHdlcmUgc2F2ZWQuCkluIHRoaXMgdmlnbmV0dGUsIHdlIHVzZSBhIE1heFF1YW50IHBlcHRpZGVSYXdzIGZpbGUgd2hpY2ggaXMgYSBzdWJzZXQKb2YgdGhlIGNwdGFjIHN0dWR5LiBUaGlzIGRhdGEgaXMgYXZhaWxhYmxlIGluIHRoZSBgbXNkYXRhYCBwYWNrYWdlLgpUbyBpbXBvcnQgdGhlIGRhdGEgd2UgdXNlIHRoZSBgUUZlYXR1cmVzYCBwYWNrYWdlLgoKV2UgZ2VuZXJhdGUgdGhlIG9iamVjdCBwZXB0aWRlUmF3RmlsZSB3aXRoIHRoZSBwYXRoIHRvIHRoZSBwZXB0aWRlUmF3cy50eHQgZmlsZS4KVXNpbmcgdGhlIGBncmVwRWNvbHNgIGZ1bmN0aW9uLCB3ZSBmaW5kIHRoZSBjb2x1bW5zIHRoYXQgY29udGFpbiB0aGUgZXhwcmVzc2lvbgpkYXRhIG9mIHRoZSBwZXB0aWRlUmF3cyBpbiB0aGUgcGVwdGlkZVJhd3MudHh0IGZpbGUuCgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KGxpbW1hKQpsaWJyYXJ5KFFGZWF0dXJlcykKbGlicmFyeShtc3Fyb2IyKQpsaWJyYXJ5KHBsb3RseSkKCnBlcHRpZGVzRmlsZSA8LSAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3N0YXRPbWljcy9TR0EyMDIwL2RhdGEvcXVhbnRpZmljYXRpb24vY3B0YWNBdnNCX2xhYjMvcGVwdGlkZXMudHh0IgoKZWNvbHMgPC0gZ3JlcCgKICAiSW50ZW5zaXR5XFwuIiwgCiAgbmFtZXMocmVhZC5kZWxpbShwZXB0aWRlc0ZpbGUpKQogICkKCnBlIDwtIHJlYWRRRmVhdHVyZXMoCiAgdGFibGUgPSBwZXB0aWRlc0ZpbGUsCiAgZm5hbWVzID0gMSwKICBlY29sID0gZWNvbHMsCiAgbmFtZSA9ICJwZXB0aWRlUmF3Iiwgc2VwPSJcdCIpCgpjb2xuYW1lcyhwZSkKYGBgCgpJbiB0aGUgZm9sbG93aW5nIGNvZGUgY2h1bmssIHdlIGNhbiBleHRyYWN0IHRoZSBzcGlrZWluIGNvbmRpdGlvbiBmcm9tIHRoZSByYXcgZmlsZSBuYW1lLgoKYGBge3J9CmNvbmQgPC0gd2hpY2goCiAgc3Ryc3BsaXQoY29sbmFtZXMocGUpW1sxXV1bMV0sIHNwbGl0ID0gIiIpW1sxXV0gPT0gIkEiKSAjIGZpbmQgd2hlcmUgY29uZGl0aW9uIGlzIHN0b3JlZAoKY29sRGF0YShwZSkkY29uZGl0aW9uIDwtIHN1YnN0cihjb2xuYW1lcyhwZSksIGNvbmQsIGNvbmQpICU+JQogIHVubGlzdCAlPiUgIAogIGFzLmZhY3RvcgpgYGAKCgpXZSBjYWxjdWxhdGUgaG93IG1hbnkgbm9uIHplcm8gaW50ZW5zaXRpZXMgd2UgaGF2ZSBwZXIgcGVwdGlkZSBhbmQgdGhpcwp3aWxsIGJlIHVzZWZ1bCBmb3IgZmlsdGVyaW5nLgoKYGBge3J9CnJvd0RhdGEocGVbWyJwZXB0aWRlUmF3Il1dKSRuTm9uWmVybyA8LSByb3dTdW1zKGFzc2F5KHBlW1sicGVwdGlkZVJhdyJdXSkgPiAwKQpgYGAKCgpQZXB0aWRlcyB3aXRoIHplcm8gaW50ZW5zaXRpZXMgYXJlIG1pc3NpbmcgcGVwdGlkZXMgYW5kIHNob3VsZCBiZSByZXByZXNlbnQKd2l0aCBhIGBOQWAgdmFsdWUgcmF0aGVyIHRoYW4gYDBgLgpgYGB7cn0KcGUgPC0gemVyb0lzTkEocGUsICJwZXB0aWRlUmF3IikgIyBjb252ZXJ0IDAgdG8gTkEKYGBgCgoKIyMgRGF0YSBleHBsb3JhdGlvbgoKYHIgZm9ybWF0KG1lYW4oaXMubmEoYXNzYXkocGVbWyJwZXB0aWRlUmF3Il1dKSkpKjEwMCxkaWdpdHM9MilgJSBvZiBhbGwgcGVwdGlkZQppbnRlbnNpdGllcyBhcmUgbWlzc2luZyBhbmQgZm9yIHNvbWUgcGVwdGlkZXMgd2UgZG8gbm90IGV2ZW4gbWVhc3VyZSBhIHNpZ25hbAppbiBhbnkgc2FtcGxlLgoKCiMgUHJlcHJvY2Vzc2luZwoKVGhpcyBzZWN0aW9uIHByZWZvcm1zIHByZXByb2Nlc3NpbmcgZm9yIHRoZSBwZXB0aWRlIGRhdGEuIApUaGlzIGluY2x1ZGUgCgotIGxvZyB0cmFuc2Zvcm1hdGlvbiwgCi0gZmlsdGVyaW5nIGFuZCAKLSBzdW1tYXJpc2F0aW9uIG9mIHRoZSBkYXRhLgoKIyMgTG9nIHRyYW5zZm9ybSB0aGUgZGF0YQoKYGBge3J9CnBlIDwtIGxvZ1RyYW5zZm9ybShwZSwgYmFzZSA9IDIsIGkgPSAicGVwdGlkZVJhdyIsIG5hbWUgPSAicGVwdGlkZUxvZyIpCmBgYAoKIyMgRmlsdGVyaW5nCgoxLiBIYW5kbGluZyBvdmVybGFwcGluZyBwcm90ZWluIGdyb3VwcwoKSW4gb3VyIGFwcHJvYWNoIGEgcGVwdGlkZSBjYW4gbWFwIHRvIG11bHRpcGxlIHByb3RlaW5zLCBhcyBsb25nIGFzIHRoZXJlIGlzCm5vbmUgb2YgdGhlc2UgcHJvdGVpbnMgcHJlc2VudCBpbiBhIHNtYWxsZXIgc3ViZ3JvdXAuCgpgYGB7cn0KcGUgPC0gZmlsdGVyRmVhdHVyZXMocGUsIH4gUHJvdGVpbnMgJWluJSBzbWFsbGVzdFVuaXF1ZUdyb3Vwcyhyb3dEYXRhKHBlW1sicGVwdGlkZUxvZyJdXSkkUHJvdGVpbnMpKQpgYGAKCjIuIFJlbW92ZSByZXZlcnNlIHNlcXVlbmNlcyAoZGVjb3lzKSBhbmQgY29udGFtaW5hbnRzCgpXZSBub3cgcmVtb3ZlIHRoZSBjb250YW1pbmFudHMgYW5kIHBlcHRpZGVzIHRoYXQgbWFwIHRvIGRlY295IHNlcXVlbmNlcy4KCmBgYHtyfQpwZSA8LSBmaWx0ZXJGZWF0dXJlcyhwZSx+UmV2ZXJzZSAhPSAiKyIpCnBlIDwtIGZpbHRlckZlYXR1cmVzKHBlLH4gUG90ZW50aWFsLmNvbnRhbWluYW50ICE9ICIrIikKYGBgCgozLiBEcm9wIHBlcHRpZGVzIHRoYXQgd2VyZSBvbmx5IGlkZW50aWZpZWQgaW4gb25lIHNhbXBsZQoKV2Uga2VlcCBwZXB0aWRlcyB0aGF0IHdlcmUgb2JzZXJ2ZWQgYXQgbGFzdCB0d2ljZS4KCmBgYHtyfQpwZSA8LSBmaWx0ZXJGZWF0dXJlcyhwZSx+IG5Ob25aZXJvID49MikKbnJvdyhwZVtbInBlcHRpZGVMb2ciXV0pCmBgYAoKV2Uga2VlcCBgciBucm93KHBlW1sicGVwdGlkZUxvZyJdXSlgIHBlcHRpZGVzIHVwb24gZmlsdGVyaW5nLgoKCiMjIE5vcm1hbGl6ZSB0aGUgZGF0YSB1c2luZyBtZWRpYW4gY2VudGVyaW5nIAoKV2Ugbm9ybWFsaXplIHRoZSBkYXRhIGJ5IHN1YnN0cmFjdGluZyB0aGUgc2FtcGxlIG1lZGlhbiBmcm9tIGV2ZXJ5IGludGVuc2l0eSBmb3IgcGVwdGlkZSAkcCQgIGluIGEgc2FtcGxlICRpJDogCgokJHlfe2lwfV5cdGV4dHtub3JtfSA9IHlfe2lwfSAtIFxoYXRcbXVfaSQkIAoKd2l0aCAkXGhhdFxtdV9pJCB0aGUgbWVkaWFuIGludGVuc2l0eSBvdmVyIGFsbCBvYnNlcnZlZCBwZXB0aWRlcyBpbiBzYW1wbGUgJGkkLgoKYGBge3J9CnBlIDwtIG5vcm1hbGl6ZShwZSwgCiAgICAgICAgICAgICAgICBpID0gInBlcHRpZGVMb2ciLCAKICAgICAgICAgICAgICAgIG5hbWUgPSAicGVwdGlkZU5vcm0iLCAKICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJjZW50ZXIubWVkaWFuIikKYGBgCgoKIyMgRXhwbG9yZSAgbm9ybWFsaXplZCBkYXRhCgpVcG9uIHRoZSBub3JtYWxpc2F0aW9uIHRoZSBkZW5zaXR5IGN1cnZlcyBhcmUgbmljZWx5IHJlZ2lzdGVyZWQKCmBgYHtyfQpwZVtbInBlcHRpZGVOb3JtIl1dICU+JSAKICBhc3NheSAlPiUKICBhcy5kYXRhLmZyYW1lKCkgJT4lCiAgZ2F0aGVyKHNhbXBsZSwgaW50ZW5zaXR5KSAlPiUgCiAgbXV0YXRlKGNvbmRpdGlvbiA9IGNvbERhdGEocGUpW3NhbXBsZSwiY29uZGl0aW9uIl0pICU+JQogIGdncGxvdChhZXMoeCA9IGludGVuc2l0eSxncm91cCA9IHNhbXBsZSxjb2xvciA9IGNvbmRpdGlvbikpICsgCiAgICBnZW9tX2RlbnNpdHkoKQpgYGAKCldlIGNhbiB2aXN1YWxpemUgb3VyIGRhdGEgdXNpbmcgYSBNdWx0aSBEaW1lbnNpb25hbCBTY2FsaW5nIHBsb3QsCmVnLiBhcyBwcm92aWRlZCBieSB0aGUgYGxpbW1hYCBwYWNrYWdlLgoKYGBge3J9CnBlW1sicGVwdGlkZU5vcm0iXV0gJT4lIAogIGFzc2F5ICU+JQogIGxpbW1hOjpwbG90TURTKGNvbCA9IGFzLm51bWVyaWMoY29sRGF0YShwZSkkY29uZGl0aW9uKSkKYGBgCgpUaGUgZmlyc3QgYXhpcyBpbiB0aGUgcGxvdCBpcyBzaG93aW5nIHRoZSBsZWFkaW5nIGxvZyBmb2xkIGNoYW5nZXMKKGRpZmZlcmVuY2VzIG9uIHRoZSBsb2cgc2NhbGUpIGJldHdlZW4gdGhlIHNhbXBsZXMuCgpXZSBub3RpY2UgdGhhdCB0aGUgbGVhZGluZyBkaWZmZXJlbmNlcyAobG9nIEZDKQppbiB0aGUgcGVwdGlkZSBkYXRhIHNlZW1zIHRvIGJlIGRyaXZlbiBieSB0ZWNobmljYWwgdmFyaWFiaWxpdHkuCkluZGVlZCwgdGhlIHNhbXBsZXMgZG8gbm90IHNlZW0gdG8gYmUgY2xlYXJseSBzZXBhcmF0ZWQgYWNjb3JkaW5nCnRvIHRoZSBzcGlrZS1pbiBjb25kaXRpb24uCgoKIyMgU3VtbWFyaXphdGlvbiB0byBwcm90ZWluIGxldmVsCgotIEJ5IGRlZmF1bHQgcm9idXN0IHN1bW1hcml6YXRpb24gaXMgdXNlZDogIGBmdW4gPSBNc0NvcmVVdGlsczo6cm9idXN0U3VtbWFyeSgpYAoKYGBge3Isd2FybmluZz1GQUxTRX0KcGUgPC0gYWdncmVnYXRlRmVhdHVyZXMocGUsCiAgaSA9ICJwZXB0aWRlTm9ybSIsCiAgZmNvbCA9ICJQcm90ZWlucyIsCiAgbmEucm0gPSBUUlVFLAogIG5hbWUgPSAicHJvdGVpbiIpCmBgYAoKCgpgYGB7cn0KcGxvdE1EUyhhc3NheShwZVtbInByb3RlaW4iXV0pLCBjb2wgPSBhcy5udW1lcmljKGNvbERhdGEocGUpJGNvbmRpdGlvbikpCmBgYAoKTm90ZSB0aGF0IHRoZSBzYW1wbGVzIHVwb24gcm9idXN0IHN1bW1hcmlzYXRpb24gc2hvdyBhIGNsZWFyIHNlcGFyYXRpb24gYWNjb3JkaW5nIHRvIHRoZSBzcGlrZS1pbiBjb25kaXRpb24gaW4gdGhlIHNlY29uZCBkaW1lbnNpb24gb2YgdGhlIE1EUyBwbG90LgoKIyBEYXRhIEFuYWx5c2lzCgojIyBFc3RpbWF0aW9uCgpXZSBtb2RlbCB0aGUgcHJvdGVpbiBsZXZlbCBleHByZXNzaW9uIHZhbHVlcyB1c2luZyBgbXNxcm9iYC4KQnkgZGVmYXVsdCBgbXNxcm9iMmAgZXN0aW1hdGVzIHRoZSBtb2RlbCBwYXJhbWV0ZXJzIHVzaW5nIHJvYnVzdCByZWdyZXNzaW9uLgoKV2Ugd2lsbCBtb2RlbCB0aGUgZGF0YSB3aXRoIGEgZGlmZmVyZW50IGdyb3VwIG1lYW4uIApUaGUgZ3JvdXAgaXMgaW5jb2RlZCBpbiB0aGUgdmFyaWFibGUgYGNvbmRpdGlvbmAgb2YgdGhlIGNvbERhdGEuIApXZSBjYW4gc3BlY2lmeSB0aGlzIG1vZGVsIGJ5IHVzaW5nIGEgZm9ybXVsYSB3aXRoIHRoZSBmYWN0b3IgY29uZGl0aW9uIGFzIGl0cyBwcmVkaWN0b3I6IApgZm9ybXVsYSA9IH5jb25kaXRpb25gLgoKTm90ZSwgdGhhdCBhIGZvcm11bGEgYWx3YXlzIHN0YXJ0cyB3aXRoIGEgc3ltYm9sICd+Jy4KCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQpwZSA8LSBtc3Fyb2Iob2JqZWN0ID0gcGUsIGkgPSAicHJvdGVpbiIsIGZvcm11bGEgPSB+Y29uZGl0aW9uKQpgYGAKCiMjIEluZmVyZW5jZQoKRmlyc3QsIHdlIGV4dHJhY3QgdGhlIHBhcmFtZXRlciBuYW1lcyBvZiB0aGUgbW9kZWwgYnkgbG9va2luZyBhdCB0aGUgZmlyc3QgbW9kZWwuIApUaGUgbW9kZWxzIGFyZSBzdG9yZWQgaW4gdGhlIHJvdyBkYXRhIG9mIHRoZSBhc3NheSB1bmRlciB0aGUgZGVmYXVsdCBuYW1lIG1zcXJvYk1vZGVscy4gCgpgYGB7cn0KZ2V0Q29lZihyb3dEYXRhKHBlW1sicHJvdGVpbiJdXSkkbXNxcm9iTW9kZWxzW1sxXV0pCmBgYAoKV2UgY2FuIGFsc28gZXhwbG9yZSB0aGUgZGVzaWduIG9mIHRoZSBtb2RlbCB0aGF0IHdlIHNwZWNpZmllZCB1c2luZyB0aGUgdGhlIHBhY2thZ2UgYEV4cGxvcmVNb2RlbE1hdHJpeGAgCgpgYGB7cn0KbGlicmFyeShFeHBsb3JlTW9kZWxNYXRyaXgpClZpc3VhbGl6ZURlc2lnbihjb2xEYXRhKHBlKSx+Y29uZGl0aW9uKSRwbG90bGlzdFtbMV1dCmBgYAoKU3Bpa2UtaW4gY29uZGl0aW9uIGBBYCBpcyB0aGUgcmVmZXJlbmNlIGNsYXNzLiBTbyB0aGUgbWVhbiBsb2cyIGV4cHJlc3Npb24KZm9yIHNhbXBsZXMgZnJvbSBjb25kaXRpb24gQSBpcyAnKEludGVyY2VwdCkuClRoZSBtZWFuIGxvZzIgZXhwcmVzc2lvbiBmb3Igc2FtcGxlcyBmcm9tIGNvbmRpdGlvbiBCIGlzICcoSW50ZXJjZXB0KStjb25kaXRpb25CJy4KSGVuY2UsIHRoZSBhdmVyYWdlIGxvZzIgZm9sZCBjaGFuZ2UgYmV0d2VlbiBjb25kaXRpb24gYiBhbmQKY29uZGl0aW9uIGEgaXMgbW9kZWxsZWQgdXNpbmcgdGhlIHBhcmFtZXRlciAnY29uZGl0aW9uQicuClRodXMsIHdlIGFzc2VzcyB0aGUgY29udHJhc3QgJ2NvbmRpdGlvbkIgPSAwJyB3aXRoIG91ciBzdGF0aXN0aWNhbCB0ZXN0LgoKYGBge3J9CkwgPC0gbWFrZUNvbnRyYXN0KCJjb25kaXRpb25CPTAiLCBwYXJhbWV0ZXJOYW1lcyA9IGMoImNvbmRpdGlvbkIiKSkKcGUgPC0gaHlwb3RoZXNpc1Rlc3Qob2JqZWN0ID0gcGUsIGkgPSAicHJvdGVpbiIsIGNvbnRyYXN0ID0gTCkKYGBgCgoKIyMgUGxvdHMKCiMjIyBWb2xjYW5vLXBsb3QKCgpgYGB7cix3YXJuaW5nPUZBTFNFfQp2b2xjYW5vIDwtIGdncGxvdChyb3dEYXRhKHBlW1sicHJvdGVpbiJdXSkkY29uZGl0aW9uQiwKICAgICAgICAgICAgICAgICAgYWVzKHggPSBsb2dGQywgeSA9IC1sb2cxMChwdmFsKSwgY29sb3IgPSBhZGpQdmFsIDwgMC4wNSkpICsKICBnZW9tX3BvaW50KGNleCA9IDIuNSkgKwogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBhbHBoYShjKCJibGFjayIsICJyZWQiKSwgMC41KSkgKyB0aGVtZV9taW5pbWFsKCkKdm9sY2FubwpgYGAKCk5vdGUsIHRoYXQgYHIgc3VtKHJvd0RhdGEocGVbWyJwcm90ZWluIl1dKSRjb25kaXRpb25CJGFkalB2YWwgPCAwLjA1LCBuYS5ybSA9IFRSVUUpYCBwcm90ZWlucyBhcmUgZm91bmQgdG8gYmUgZGlmZmVyZW50aWFsbHkgYWJ1bmRhbnQuCgojIyMgSGVhdG1hcAoKV2UgZmlyc3Qgc2VsZWN0IHRoZSBuYW1lcyBvZiB0aGUgcHJvdGVpbnMgdGhhdCB3ZXJlIGRlY2xhcmVkIHNpZ25maWNhbnQuCgpgYGB7cn0Kc2lnTmFtZXMgPC0gcm93RGF0YShwZVtbInByb3RlaW4iXV0pJGNvbmRpdGlvbkIgJT4lCiAgcm93bmFtZXNfdG9fY29sdW1uKCJwcm90ZWluIikgJT4lCiAgZmlsdGVyKGFkalB2YWw8MC4wNSkgJT4lCiAgcHVsbChwcm90ZWluKQpoZWF0bWFwKGFzc2F5KHBlW1sicHJvdGVpbiJdXSlbc2lnTmFtZXMsIF0pCmBgYAoKVGhlIG1ham9yaXR5IG9mIHRoZSBwcm90ZWlucyBhcmUgaW5kZWVkIFVQUyBwcm90ZWlucy4gCjEgeWVhc3QgcHJvdGVpbiBpcyByZXR1cm5lZC4gCk5vdGUsIHRoYXQgdGhlIHllYXN0IHByb3RlaW4gaW5kZWVkIHNob3dzIGV2aWRlbmNlIGZvciBkaWZmZXJlbnRpYWwgYWJ1bmRhbmNlLiAKCiMjIyBCb3hwbG90cwoKV2UgbWFrZSBib3hwbG90IG9mIHRoZSBsb2cyIEZDIGFuZCBzdHJhdGlmeSBhY2NvcmRpbmcgdG8gdGhlIHdoZXRoZXIgYSBwcm90ZWluIGlzIHNwaWtlZCBvciBub3QuCgpgYGB7cn0Kcm93RGF0YShwZVtbInByb3RlaW4iXV0pJGNvbmRpdGlvbkIgJT4lCiAgcm93bmFtZXNfdG9fY29sdW1uKHZhciA9ICJwcm90ZWluIikgJT4lCiAgZ2dwbG90KGFlcyh4PWdyZXBsKCJVUFMiLHByb3RlaW4pLHk9bG9nRkMpKSArCiAgZ2VvbV9ib3hwbG90KCkgKwogIHhsYWIoIlVQUyIpICsKICBnZW9tX3NlZ21lbnQoCiAgICB4ID0gMS41LAogICAgeGVuZCA9IDIuNSwKICAgIHkgPSBsb2cyKDAuNzQvMC4yNSksCiAgICB5ZW5kID0gbG9nMigwLjc0LzAuMjUpLAogICAgY29sb3VyPSJyZWQiKSArCiAgZ2VvbV9zZWdtZW50KAogICAgeCA9IDAuNSwKICAgIHhlbmQgPSAxLjUsCiAgICB5ID0gMCwKICAgIHllbmQgPSAwLAogICAgY29sb3VyPSJyZWQiKSArCiAgYW5ub3RhdGUoCiAgICAidGV4dCIsCiAgICB4ID0gYygxLDIpLAogICAgeSA9IGMoMCxsb2cyKDAuNzQvMC4yNSkpKy4xLAogICAgbGFiZWwgPSBjKAogICAgICAibG9nMiBGQyBFY29saSA9IDAiLAogICAgICBwYXN0ZTAoImxvZzIgRkMgVVBTID0gIixyb3VuZChsb2cyKDAuNzQvMC4yNSksMikpCiAgICAgICksCiAgICBjb2xvdXIgPSAicmVkIikKYGBgCgpXaGF0IGRvIHlvdSBvYnNlcnZlPwoKIyMjIERldGFpbCBwbG90cwoKV2UgZmlyc3QgZXh0cmFjdCB0aGUgbm9ybWFsaXplZCBwZXB0aWRlUmF3IGV4cHJlc3Npb24gdmFsdWVzIGZvciBhIHBhcnRpY3VsYXIgcHJvdGVpbi4gIAoKCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQpmb3IgKHByb3ROYW1lIGluIHNpZ05hbWVzKQp7CnBlUGxvdCA8LSBwZVtwcm90TmFtZSwgLCBjKCJwZXB0aWRlTm9ybSIsInByb3RlaW4iKV0KcGVQbG90RGYgPC0gZGF0YS5mcmFtZShsb25nRm9ybWF0KHBlUGxvdCkpCnBlUGxvdERmJGFzc2F5IDwtIGZhY3RvcihwZVBsb3REZiRhc3NheSwKICAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzID0gYygicGVwdGlkZU5vcm0iLCAicHJvdGVpbiIpKQpwZVBsb3REZiRjb25kaXRpb24gPC0gYXMuZmFjdG9yKGNvbERhdGEocGVQbG90KVtwZVBsb3REZiRjb2xuYW1lLCAiY29uZGl0aW9uIl0pCgojIHBsb3R0aW5nCnAxIDwtIGdncGxvdChkYXRhID0gcGVQbG90RGYsCiAgICAgICBhZXMoeCA9IGNvbG5hbWUsIHkgPSB2YWx1ZSwgZ3JvdXAgPSByb3duYW1lKSkgKwogICAgZ2VvbV9saW5lKCkgKyAKICAgIGdlb21fcG9pbnQoKSArICAKICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNzAsIGhqdXN0ID0gMSwgdmp1c3QgPSAwLjUpKSArCiAgICBmYWNldF9ncmlkKH5hc3NheSkgKyAKICAgIGdndGl0bGUocHJvdE5hbWUpCnByaW50KHAxKQoKIyBwbG90dGluZyAyCnAyIDwtIGdncGxvdChwZVBsb3REZiwgYWVzKHggPSBjb2xuYW1lLCB5ID0gdmFsdWUsIGZpbGwgPSBjb25kaXRpb24pKSArCiAgZ2VvbV9ib3hwbG90KG91dGxpZXIuc2hhcGUgPSBOQSkgKyAKICBnZW9tX3BvaW50KAogICAgcG9zaXRpb24gPSBwb3NpdGlvbl9qaXR0ZXIod2lkdGggPSAuMSksCiAgICBhZXMoc2hhcGUgPSByb3duYW1lKSkgKwogIHNjYWxlX3NoYXBlX21hbnVhbCh2YWx1ZXMgPSAxOm5yb3cocGVQbG90RGYpKSArCiAgbGFicyh0aXRsZSA9IHByb3ROYW1lLCB4ID0gInNhbXBsZSIsIHkgPSAicGVwdGlkZSBpbnRlbnNpdHkgKGxvZzIpIikgKyAKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDcwLCBoanVzdCA9IDEsIHZqdXN0ID0gMC41KSkgKwogIGZhY2V0X2dyaWQofmFzc2F5KQpwcmludChwMikKfQpgYGAKCk5vdGUsIHRoYXQgdGhlIHllYXN0IHByb3RlaW4gaXMgb25seSBjb3ZlcmVkIGJ5IDMgcGVwdGlkZXMuIApPbmx5IG9uZSBwZXB0aWRlIGlzIHBpY2tlZCB1cCBpbiBjb25kaXRpb24gQS4gClRoaXMgcGVwdGlkZSBpcyBhbHNvIG9ubHkgb25jZSBvYnNlcnZlZCBpbiBzcGlrZS1pbiBjb25kaXRpb24gQi4gClRoaXMgcHV0cyBhIGNvbnNpZGVyYWJsZSBidXJkZW4gdXBvbiB0aGUgaW5mZXJlbmNlIGFuZCBjb3VsZCBiZSBhdm9pZGVkIGJ5IG1vcmUgc3RyaW5nZW50IGZpbHRlcmluZy4gCgojIFNlc3Npb24gSW5mbwoKV2l0aCByZXNwZWN0IHRvIHJlcHJvZHVjaWJpbGl0eSwgaXQgaXMgaGlnaGx5IHJlY29tbWVuZGVkIHRvIGluY2x1ZGUgYSBzZXNzaW9uIGluZm8gaW4geW91ciBzY3JpcHQgc28gdGhhdCByZWFkZXJzIG9mIHlvdXIgb3V0cHV0IGNhbiBzZWUgeW91ciBwYXJ0aWN1bGFyIHNldHVwIG9mIFIuIAoKYGBge3J9CnNlc3Npb25JbmZvKCkKYGBgCg==