Integrantes

Analisis RFM

“Este es un conjunto de datos transnacionales que contiene todas las transacciones que ocurrieron entre el 01/12/2010 y el 09/12/2011 para un comercio minorista en línea no registrado y con sede en el Reino Unido. La empresa vende principalmente regalos únicos para todas las ocasiones. Muchos clientes de la empresa son mayoristas”

Lectura de datos

df <- 
  readr::read_csv(file = 'data.csv', 
                  locale = readr::locale(encoding = 'UTF-8'))

dplyr::glimpse(df)
## Rows: 541,909
## Columns: 8
## $ InvoiceNo   <chr> "536365", "536365", "536365", "536365", "536365", "536365"~
## $ StockCode   <chr> "85123A", "71053", "84406B", "84029G", "84029E", "22752", ~
## $ Description <chr> "WHITE HANGING HEART T-LIGHT HOLDER", "WHITE METAL LANTERN~
## $ Quantity    <dbl> 6, 6, 8, 6, 6, 2, 6, 6, 6, 32, 6, 6, 8, 6, 6, 3, 2, 3, 3, ~
## $ InvoiceDate <chr> "12/1/2010 8:26", "12/1/2010 8:26", "12/1/2010 8:26", "12/~
## $ UnitPrice   <dbl> 2.55, 3.39, 2.75, 3.39, 3.39, 7.65, 4.25, 1.85, 1.85, 1.69~
## $ CustomerID  <dbl> 17850, 17850, 17850, 17850, 17850, 17850, 17850, 17850, 17~
## $ Country     <chr> "United Kingdom", "United Kingdom", "United Kingdom", "Uni~

Tratamiento de los datos

Se remueven las cantiades negativas y consumidores con NA asignados.

# Remocion de negativos y NA's
df1 <- 
  df %>% 
  dplyr::filter(Quantity > 0 , 
                UnitPrice > 0) %>% 
  tidyr::drop_na()

head(df1, n = 10)

Recodificacion de variables

df2 <- 
  df1 %>% 
  dplyr::mutate(InvoiceDate = as.Date(x = InvoiceDate, format = '%m/%d/%Y')) %>% 
  dplyr::mutate_if(is.character, as.factor) %>% 
  dplyr::mutate(total_dolar = Quantity*UnitPrice)

skimr::skim(df2)
Data summary
Name df2
Number of rows 397884
Number of columns 9
_______________________
Column type frequency:
Date 1
factor 4
numeric 4
________________________
Group variables None

Variable type: Date

skim_variable n_missing complete_rate min max median n_unique
InvoiceDate 0 1 2010-12-01 2011-12-09 2011-07-31 305

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
InvoiceNo 0 1 FALSE 18532 576: 542, 579: 533, 580: 529, 578: 442
StockCode 0 1 FALSE 3665 851: 2035, 224: 1723, 850: 1618, 848: 1408
Description 0 1 FALSE 3866 WHI: 2028, REG: 1723, JUM: 1618, ASS: 1408
Country 0 1 FALSE 37 Uni: 354321, Ger: 9040, Fra: 8341, EIR: 7236

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
Quantity 0 1 12.99 179.33 1 2.00 6.00 12.00 80995.00 ▇▁▁▁▁
UnitPrice 0 1 3.12 22.10 0 1.25 1.95 3.75 8142.75 ▇▁▁▁▁
CustomerID 0 1 15294.42 1713.14 12346 13969.00 15159.00 16795.00 18287.00 ▇▇▇▇▇
total_dolar 0 1 22.40 309.07 0 4.68 11.80 19.80 168469.60 ▇▁▁▁▁

Calculate RFM data set

df_RFM <- 
  df2 %>% 
  dplyr::group_by(CustomerID) %>% 
  dplyr::summarise(Recency = Sys.Date() - max(InvoiceDate), 
                   Recency = as.numeric(Recency), 
                   frequenci = dplyr::n_distinct(InvoiceNo), 
                   monitery = sum(total_dolar)/frequenci) %>% 
  dplyr::ungroup()

summary(df_RFM)

kableExtra::kable(head(df_RFM))
##    CustomerID       Recency       frequenci          monitery       
##  Min.   :12346   Min.   :3423   Min.   :  1.000   Min.   :    3.45  
##  1st Qu.:13813   1st Qu.:3440   1st Qu.:  1.000   1st Qu.:  178.62  
##  Median :15300   Median :3473   Median :  2.000   Median :  293.90  
##  Mean   :15300   Mean   :3515   Mean   :  4.272   Mean   :  419.17  
##  3rd Qu.:16779   3rd Qu.:3565   3rd Qu.:  5.000   3rd Qu.:  430.11  
##  Max.   :18287   Max.   :3796   Max.   :209.000   Max.   :84236.25
CustomerID Recency frequenci monitery
12346 3748 1 77183.6000
12347 3425 7 615.7143
12348 3498 4 449.3100
12349 3441 1 1757.5500
12350 3733 1 334.4000
12352 3459 8 313.2550

Recency

Recencia – Cuanto paso desde el ultimo consumo

df_RFM %>% 
  dplyr::filter(monitery < 7000, 
                frequenci < 200) %>% 
  ggplot2::ggplot() + 
  ggplot2::aes(Recency) + 
  ggplot2::geom_histogram(col = 'red', fill = 'skyblue', bins = 50) + 
  ggplot2::theme_minimal() + 
  ggplot2::theme(text = ggplot2::element_text(family = 'serif'))

Frequency

Frecuencia – Cuan frecuente fueron la compra?

df_RFM %>% 
  dplyr::filter(monitery < 7000, 
                frequenci < 200) %>% 
  ggplot2::ggplot() + 
  ggplot2::aes(frequenci) + 
  ggplot2::geom_histogram(col = 'red', fill = 'skyblue', bins = 50) + 
  ggplot2::theme_minimal() + 
  ggplot2::theme(text = ggplot2::element_text(family = 'serif'))

Monetary

Valor monetario – Cuanto gastaron?

df_RFM %>% 
  dplyr::filter(frequenci < 200) %>% 
  ggplot2::ggplot() + 
  ggplot2::aes(monitery) + 
  ggplot2::geom_histogram(col = 'red', fill = 'skyblue', bins = 50) + 
  ggplot2::theme_minimal() + 
  ggplot2::theme(text = ggplot2::element_text(family = 'serif'))

Ya que la variable cantidad monetaria es altamente insesgada, se usara una transformacion logaritmica para visualizarla mejor.

df_RFM$monitery <- log(df_RFM$monitery)

df_RFM %>% 
  dplyr::filter(frequenci < 200) %>% 
  ggplot2::ggplot() + 
  ggplot2::aes(monitery) + 
  ggplot2::geom_histogram(col = 'red', fill = 'skyblue', bins = 50) + 
  ggplot2::theme_minimal() + 
  ggplot2::theme(text = ggplot2::element_text(family = 'serif'))

RFM Analisis

rfm_result <- 
  rfm::rfm_table_customer(df_RFM, 
                          customer_id = CustomerID,
                          recency_days = Recency,
                          n_transactions = frequenci,
                          total_revenue = monitery)

Heat Map

rfm::rfm_heatmap(rfm_result)

Bar Chart

rfm::rfm_bar_chart(rfm_result)

Histogram

rfm::rfm_histograms(rfm_result)

Customers by Orders

rfm::rfm_order_dist(rfm_result)

Refinamiento del analisis

BDF %>% 
  ggplot2::ggplot() + 
  ggplot2::aes(x = cus_seg, fill = cus_seg) + 
  ggplot2::geom_bar() + 
  ggplot2::geom_text(
    ggplot2::aes(label = scales::percent(..count../sum(..count..))
                 ), 
    stat = 'count', position = position_dodge(1)
    ) + 
  ggplot2::labs(title = "Segmentacion de clientes", 
                x = "Segmento", 
                y = "Total del consumidor") + 
  ggplot2::coord_flip() + 
  ggplot2::theme(text = ggplot2::element_text(family = 'serif')) + 
  ggplot2::theme_minimal()

Analisis Cluster

df_RFM2 <- 
  df_RFM %>% 
  tibble::column_to_rownames(var = 'CustomerID') %>% 
  scale %>% 
  tibble::as_tibble()

summary(df_RFM2)
##     Recency          frequenci           monitery      
##  Min.   :-0.9205   Min.   :-0.42505   Min.   :-5.8832  
##  1st Qu.:-0.7505   1st Qu.:-0.42505   1st Qu.:-0.6153  
##  Median :-0.4205   Median :-0.29514   Median : 0.0493  
##  Mean   : 0.0000   Mean   : 0.00000   Mean   : 0.0000  
##  3rd Qu.: 0.4968   3rd Qu.: 0.09457   3rd Qu.: 0.5576  
##  Max.   : 2.8091   Max.   :26.59496   Max.   : 7.6012

Exploracion

# Histogramas y correlaciones:
# Para las correlaciones se probara la normalidad de las variables para aplicar 
# correlacion por Pearson, de no enontrarse normalidad se aplicara Spearman

# Ho: La variable sigue una distribucion normal
# H1: La variable no sigue una distribbucion normal
# Prueba de normalidad: Anderson Darling, 5% de significancia.

n.test <- apply(X = df_RFM2, MARGIN = 2, FUN = nortest::ad.test)
n.test
## $Recency
## 
##  Anderson-Darling normality test
## 
## data:  newX[, i]
## A = 300.15, p-value < 2.2e-16
## 
## 
## $frequenci
## 
##  Anderson-Darling normality test
## 
## data:  newX[, i]
## A = 660.15, p-value < 2.2e-16
## 
## 
## $monitery
## 
##  Anderson-Darling normality test
## 
## data:  newX[, i]
## A = 15.519, p-value < 2.2e-16
# En este caso, todas las variables rechazan la normalidad, se aplicara la 
# correlacion por spearman

PerformanceAnalytics::chart.Correlation(df_RFM2, method = c("spearman"))
## Warning in cor.test.default(as.numeric(x), as.numeric(y), method = method):
## Cannot compute exact p-value with ties

## Warning in cor.test.default(as.numeric(x), as.numeric(y), method = method):
## Cannot compute exact p-value with ties

## Warning in cor.test.default(as.numeric(x), as.numeric(y), method = method):
## Cannot compute exact p-value with ties

Definicion del cluster optimo

# Grafica de la comparativa de los clusteres usando el metodo silhouette
factoextra::fviz_nbclust(df_RFM2, kmeans, method = "silhouette")

# Grafico de los clusteres
res <- kmeans(df_RFM2, 2, nstart = 25)

Grafico de los clusteres

factoextra::fviz_cluster(res, data = df_RFM2,
             # palette = c("#E7B800", "#00AFBB"),
             geom = "point",
             ellipse.type = "convex", 
             ggtheme = theme_bw()
             )

Promedios por cluster y variables

df_RFM2 <- 
  df_RFM2 %>% 
  dplyr::mutate(cluster = as.factor(res$cluster)) %>% 
  tidyr::drop_na()

data_long <- 
  df_RFM2 %>% 
  tidyr::gather(key = 'var', 
                value = 'valor', c(1:3), 
                factor_key = TRUE)

ggplot(data_long, aes(as.factor(x = var), 
                      y = valor,group=cluster, colour = cluster)) + 
  stat_summary(fun = mean, geom="pointrange", size = 1)+
  stat_summary(geom="line") + labs(x="Atributos", y="Frecuencia",
                                   title="Clientes")

Conclusion:

  • A partir del analisis previo, decidimos elegir 2 clusteres a pesar de que los grupos se esten solapando.

Comportamiento de las variables por cluster

Resumen de las variables por cluster

df_RFM2 %>%
  dplyr::group_by(cluster) %>%
  dplyr::group_map(~ summary(.))
## [[1]]
##     Recency          frequenci          monitery      
##  Min.   :-0.9205   Min.   :-0.4250   Min.   :-3.4853  
##  1st Qu.:-0.8105   1st Qu.:-0.2951   1st Qu.:-0.4623  
##  Median :-0.6305   Median :-0.1652   Median : 0.1424  
##  Mean   :-0.5160   Mean   : 0.1210   Mean   : 0.1451  
##  3rd Qu.:-0.3106   3rd Qu.: 0.2245   3rd Qu.: 0.6472  
##  Max.   : 2.3291   Max.   :26.5950   Max.   : 7.6012  
## 
## [[2]]
##     Recency          frequenci          monitery      
##  Min.   :-0.5605   Min.   :-0.4250   Min.   :-5.8832  
##  1st Qu.: 0.9193   1st Qu.:-0.4250   1st Qu.:-1.0135  
##  Median : 1.4692   Median :-0.4250   Median :-0.4126  
##  Mean   : 1.4861   Mean   :-0.3485   Mean   :-0.4180  
##  3rd Qu.: 2.0392   3rd Qu.:-0.2951   3rd Qu.: 0.2233  
##  Max.   : 2.8091   Max.   : 3.8618   Max.   : 2.9900

Boxplot por cluster del monto

data_long %>% 
  dplyr::filter(var %in% 'monitery') %>% 
  dplyr::group_by(cluster) %>% 
  ggplot2::ggplot() + 
  ggplot2::aes(x = cluster, y = valor, fill = cluster) + 
  ggplot2::geom_boxplot() + 
  ggplot2::theme_minimal() + 
  ggplot2::theme(text = ggplot2::element_text(family = 'serif'))

Boxplot por cluster y variables

data_long %>%
  dplyr::group_by(cluster) %>% 
  ggplot2::ggplot() + 
  ggplot2::aes(x = cluster, y = valor, fill = cluster) + 
  ggplot2::geom_boxplot() + 
  ggplot2::theme_minimal() + 
  ggplot2::theme(text = ggplot2::element_text(family = 'serif')) + 
  ggplot2::facet_grid(var ~ ., scales = 'free')

Notas:

Etiquetado de los clusteres

  • Segmento 1: Clientes con mayores consumos.
  • Segmento 2: Clientes con menores consumos.
  • Se podria aplicar una campaña de descuentos a los clientes con menores consumos para tratar de generar engagment con ellos al considerarnos una compañia que se preocupa por su consumo e ingresos.

Conclusiones generales

LS0tDQp0aXRsZTogIlNlZ21lbnRhY2lvbiBkZSBjb25zdW1pZG9yZXMgdXNhbmRvIFJGTSINCmF1dGhvcjogJ0dydXBvIDUnDQpkYXRlOiAiYHIgZm9ybWF0KFN5cy50aW1lKCksICclZCAlQiwgJVknKWAiDQpvdXRwdXQ6IA0KICAgIGh0bWxfZG9jdW1lbnQ6DQogICAgICAgIGRmX3ByaW50OiBwYWdlZA0KICAgICAgICBmaWdfaGVpZ2h0OiA0DQogICAgICAgIGZpZ193aWR0aDogNw0KICAgICAgICB0b2M6IHRydWUNCiAgICAgICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgICAgICBjb2RlX2ZvbGRpbmc6ICJzaG93Ig0KLS0tDQoNCiMgSW50ZWdyYW50ZXMNCi0gQ2FybG8gVmVnYQ0KLSBKaGFtcGllciBUYXBpYQ0KLSBKYWlyIEd1aWxsZXJubw0KLSBWaWN0b3IgVmFsZXJhDQotIEFydHVybyBTYW5hYnJpYQ0KDQojIEFuYWxpc2lzIFJGTQ0KDQoiRXN0ZSBlcyB1biBjb25qdW50byBkZSBkYXRvcyB0cmFuc25hY2lvbmFsZXMgcXVlIGNvbnRpZW5lIHRvZGFzIGxhcyB0cmFuc2FjY2lvbmVzIHF1ZSBvY3Vycmllcm9uIGVudHJlIGVsIDAxLzEyLzIwMTAgeSBlbCAwOS8xMi8yMDExIHBhcmEgdW4gY29tZXJjaW8gbWlub3Jpc3RhIGVuIGzDrW5lYSBubyByZWdpc3RyYWRvIHkgY29uIHNlZGUgZW4gZWwgUmVpbm8gVW5pZG8uIExhIGVtcHJlc2EgdmVuZGUgcHJpbmNpcGFsbWVudGUgcmVnYWxvcyDDum5pY29zIHBhcmEgdG9kYXMgbGFzIG9jYXNpb25lcy4gTXVjaG9zIGNsaWVudGVzIGRlIGxhIGVtcHJlc2Egc29uIG1heW9yaXN0YXMgIg0KDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFLCBtZXNzYWdlPUYsIGVycm9yPUYsIHdhcm5pbmc9Rn0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkNCmBgYA0KDQpgYGB7ciBpbmNsdWRlPUZBTFNFfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHNraW1yKQ0KbGlicmFyeShkYXRhLnRhYmxlKQ0KbGlicmFyeShrbml0cikNCmxpYnJhcnkocm1hcmtkb3duKQ0KbGlicmFyeShwbG90bHkpDQpsaWJyYXJ5KHNxbGRmKQ0KDQpgYGANCg0KIyMgTGVjdHVyYSBkZSBkYXRvcw0KDQpgYGB7ciwgcmVzdWx0cz0nbWFya3VwJywgbWVzc2FnZT1GQUxTRX0NCg0KZGYgPC0gDQogIHJlYWRyOjpyZWFkX2NzdihmaWxlID0gJ2RhdGEuY3N2JywgDQogICAgICAgICAgICAgICAgICBsb2NhbGUgPSByZWFkcjo6bG9jYWxlKGVuY29kaW5nID0gJ1VURi04JykpDQoNCmRwbHlyOjpnbGltcHNlKGRmKQ0KDQpgYGANCg0KIyMgVHJhdGFtaWVudG8gZGUgbG9zIGRhdG9zDQpTZSByZW11ZXZlbiBsYXMgY2FudGlhZGVzIG5lZ2F0aXZhcyB5IGNvbnN1bWlkb3JlcyBjb24gTkEgYXNpZ25hZG9zLg0KDQpgYGB7cn0NCiMgUmVtb2Npb24gZGUgbmVnYXRpdm9zIHkgTkEncw0KZGYxIDwtIA0KICBkZiAlPiUgDQogIGRwbHlyOjpmaWx0ZXIoUXVhbnRpdHkgPiAwICwgDQogICAgICAgICAgICAgICAgVW5pdFByaWNlID4gMCkgJT4lIA0KICB0aWR5cjo6ZHJvcF9uYSgpDQoNCmhlYWQoZGYxLCBuID0gMTApDQoNCmBgYA0KDQojIyBSZWNvZGlmaWNhY2lvbiBkZSB2YXJpYWJsZXMNCg0KYGBge3J9DQpkZjIgPC0gDQogIGRmMSAlPiUgDQogIGRwbHlyOjptdXRhdGUoSW52b2ljZURhdGUgPSBhcy5EYXRlKHggPSBJbnZvaWNlRGF0ZSwgZm9ybWF0ID0gJyVtLyVkLyVZJykpICU+JSANCiAgZHBseXI6Om11dGF0ZV9pZihpcy5jaGFyYWN0ZXIsIGFzLmZhY3RvcikgJT4lIA0KICBkcGx5cjo6bXV0YXRlKHRvdGFsX2RvbGFyID0gUXVhbnRpdHkqVW5pdFByaWNlKQ0KDQpza2ltcjo6c2tpbShkZjIpDQoNCmBgYA0KDQojIyBDYWxjdWxhdGUgUkZNIGRhdGEgc2V0IHsudGFic2V0fQ0KDQpgYGB7ciByZXN1bHRzPSdob2xkJ30NCg0KZGZfUkZNIDwtIA0KICBkZjIgJT4lIA0KICBkcGx5cjo6Z3JvdXBfYnkoQ3VzdG9tZXJJRCkgJT4lIA0KICBkcGx5cjo6c3VtbWFyaXNlKFJlY2VuY3kgPSBTeXMuRGF0ZSgpIC0gbWF4KEludm9pY2VEYXRlKSwgDQogICAgICAgICAgICAgICAgICAgUmVjZW5jeSA9IGFzLm51bWVyaWMoUmVjZW5jeSksIA0KICAgICAgICAgICAgICAgICAgIGZyZXF1ZW5jaSA9IGRwbHlyOjpuX2Rpc3RpbmN0KEludm9pY2VObyksIA0KICAgICAgICAgICAgICAgICAgIG1vbml0ZXJ5ID0gc3VtKHRvdGFsX2RvbGFyKS9mcmVxdWVuY2kpICU+JSANCiAgZHBseXI6OnVuZ3JvdXAoKQ0KDQpzdW1tYXJ5KGRmX1JGTSkNCg0Ka2FibGVFeHRyYTo6a2FibGUoaGVhZChkZl9SRk0pKQ0KDQpgYGANCg0KIyMjIFJlY2VuY3kNClJlY2VuY2lhIOKAkyBDdWFudG8gcGFzbyBkZXNkZSBlbCB1bHRpbW8gY29uc3Vtbw0KDQpgYGB7cn0NCg0KZGZfUkZNICU+JSANCiAgZHBseXI6OmZpbHRlcihtb25pdGVyeSA8IDcwMDAsIA0KICAgICAgICAgICAgICAgIGZyZXF1ZW5jaSA8IDIwMCkgJT4lIA0KICBnZ3Bsb3QyOjpnZ3Bsb3QoKSArIA0KICBnZ3Bsb3QyOjphZXMoUmVjZW5jeSkgKyANCiAgZ2dwbG90Mjo6Z2VvbV9oaXN0b2dyYW0oY29sID0gJ3JlZCcsIGZpbGwgPSAnc2t5Ymx1ZScsIGJpbnMgPSA1MCkgKyANCiAgZ2dwbG90Mjo6dGhlbWVfbWluaW1hbCgpICsgDQogIGdncGxvdDI6OnRoZW1lKHRleHQgPSBnZ3Bsb3QyOjplbGVtZW50X3RleHQoZmFtaWx5ID0gJ3NlcmlmJykpDQoNCmBgYA0KDQojIyMgRnJlcXVlbmN5DQpGcmVjdWVuY2lhIOKAkyBDdWFuIGZyZWN1ZW50ZSBmdWVyb24gbGEgY29tcHJhPw0KDQpgYGB7cn0gDQoNCmRmX1JGTSAlPiUgDQogIGRwbHlyOjpmaWx0ZXIobW9uaXRlcnkgPCA3MDAwLCANCiAgICAgICAgICAgICAgICBmcmVxdWVuY2kgPCAyMDApICU+JSANCiAgZ2dwbG90Mjo6Z2dwbG90KCkgKyANCiAgZ2dwbG90Mjo6YWVzKGZyZXF1ZW5jaSkgKyANCiAgZ2dwbG90Mjo6Z2VvbV9oaXN0b2dyYW0oY29sID0gJ3JlZCcsIGZpbGwgPSAnc2t5Ymx1ZScsIGJpbnMgPSA1MCkgKyANCiAgZ2dwbG90Mjo6dGhlbWVfbWluaW1hbCgpICsgDQogIGdncGxvdDI6OnRoZW1lKHRleHQgPSBnZ3Bsb3QyOjplbGVtZW50X3RleHQoZmFtaWx5ID0gJ3NlcmlmJykpDQoNCmBgYA0KDQojIyMgTW9uZXRhcnkNClZhbG9yIG1vbmV0YXJpbyDigJMgQ3VhbnRvIGdhc3Rhcm9uPw0KDQpgYGB7cn0gDQoNCmRmX1JGTSAlPiUgDQogIGRwbHlyOjpmaWx0ZXIoZnJlcXVlbmNpIDwgMjAwKSAlPiUgDQogIGdncGxvdDI6OmdncGxvdCgpICsgDQogIGdncGxvdDI6OmFlcyhtb25pdGVyeSkgKyANCiAgZ2dwbG90Mjo6Z2VvbV9oaXN0b2dyYW0oY29sID0gJ3JlZCcsIGZpbGwgPSAnc2t5Ymx1ZScsIGJpbnMgPSA1MCkgKyANCiAgZ2dwbG90Mjo6dGhlbWVfbWluaW1hbCgpICsgDQogIGdncGxvdDI6OnRoZW1lKHRleHQgPSBnZ3Bsb3QyOjplbGVtZW50X3RleHQoZmFtaWx5ID0gJ3NlcmlmJykpDQoNCmBgYA0KDQpZYSBxdWUgbGEgdmFyaWFibGUgY2FudGlkYWQgbW9uZXRhcmlhIGVzIGFsdGFtZW50ZSBpbnNlc2dhZGEsIHNlIHVzYXJhIHVuYSANCnRyYW5zZm9ybWFjaW9uIGxvZ2FyaXRtaWNhIHBhcmEgdmlzdWFsaXphcmxhIG1lam9yLg0KDQpgYGB7cn0NCg0KZGZfUkZNJG1vbml0ZXJ5IDwtIGxvZyhkZl9SRk0kbW9uaXRlcnkpDQoNCmRmX1JGTSAlPiUgDQogIGRwbHlyOjpmaWx0ZXIoZnJlcXVlbmNpIDwgMjAwKSAlPiUgDQogIGdncGxvdDI6OmdncGxvdCgpICsgDQogIGdncGxvdDI6OmFlcyhtb25pdGVyeSkgKyANCiAgZ2dwbG90Mjo6Z2VvbV9oaXN0b2dyYW0oY29sID0gJ3JlZCcsIGZpbGwgPSAnc2t5Ymx1ZScsIGJpbnMgPSA1MCkgKyANCiAgZ2dwbG90Mjo6dGhlbWVfbWluaW1hbCgpICsgDQogIGdncGxvdDI6OnRoZW1lKHRleHQgPSBnZ3Bsb3QyOjplbGVtZW50X3RleHQoZmFtaWx5ID0gJ3NlcmlmJykpDQoNCmBgYA0KDQojIyBSRk0gQW5hbGlzaXMgey50YWJzZXR9DQoNCmBgYHtyfQ0KcmZtX3Jlc3VsdCA8LSANCiAgcmZtOjpyZm1fdGFibGVfY3VzdG9tZXIoZGZfUkZNLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgY3VzdG9tZXJfaWQgPSBDdXN0b21lcklELA0KICAgICAgICAgICAgICAgICAgICAgICAgICByZWNlbmN5X2RheXMgPSBSZWNlbmN5LA0KICAgICAgICAgICAgICAgICAgICAgICAgICBuX3RyYW5zYWN0aW9ucyA9IGZyZXF1ZW5jaSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgdG90YWxfcmV2ZW51ZSA9IG1vbml0ZXJ5KQ0KDQpgYGANCg0KIyMjIEhlYXQgTWFwDQpgYGB7cn0NCnJmbTo6cmZtX2hlYXRtYXAocmZtX3Jlc3VsdCkNCmBgYA0KDQojIyMgQmFyIENoYXJ0DQpgYGB7cn0NCnJmbTo6cmZtX2Jhcl9jaGFydChyZm1fcmVzdWx0KQ0KYGBgDQoNCiMjIyBIaXN0b2dyYW0NCmBgYHtyfQ0KcmZtOjpyZm1faGlzdG9ncmFtcyhyZm1fcmVzdWx0KQ0KYGBgDQoNCiMjIyBDdXN0b21lcnMgYnkgT3JkZXJzDQpgYGB7cn0NCnJmbTo6cmZtX29yZGVyX2Rpc3QocmZtX3Jlc3VsdCkNCg0KYGBgDQoNCiMjIFJlZmluYW1pZW50byBkZWwgYW5hbGlzaXMNCg0KYGBge3IgaW5jbHVkZT1GQUxTRX0NCg0KQkRGIDwtIGFzLmRhdGEuZnJhbWUocmZtX3Jlc3VsdCRyZm0pDQpCREYgPC0gDQogIHJlc2hhcGU6OnJlbmFtZShCREYsIA0KICAgICAgICAgICAgICAgICAgYyhyZWNlbmN5X3Njb3JlID0gInJmbV9yZWNlbmN5IiwgDQogICAgICAgICAgICAgICAgICAgIGZyZXF1ZW5jeV9zY29yZSA9ICJyZm1fZnJlcSIsIA0KICAgICAgICAgICAgICAgICAgICBtb25ldGFyeV9zY29yZSA9ICJyZm1fbW9uZXRhcnkiKSkNCmRwbHlyOjpnbGltcHNlKEJERikNCg0KQkRGIDwtIA0KICBzcWxkZjo6c3FsZGYoDQogICAgInNlbGVjdCBjdXN0b21lcl9pZCwgDQogICAgcmVjZW5jeV9kYXlzLHRyYW5zYWN0aW9uX2NvdW50LCANCiAgICBhbW91bnQscmZtX3JlY2VuY3ksIA0KICAgIHJmbV9mcmVxLHJmbV9tb25ldGFyeSxyZm1fc2NvcmUsIA0KICAgIGNhc2Ugd2hlbiAocmZtX3JlY2VuY3kgYmV0d2VlbiA0IGFuZCA1KSBhbmQgDQogICAgKHJmbV9mcmVxIGJldHdlZW4gNCBhbmQgNSBhbmQgcmZtX21vbmV0YXJ5IGJldHdlZW4gNCBhbmQgNSkgdGhlbiAnQ2FtcGVvbmVzJw0Kd2hlbiAocmZtX3JlY2VuY3kgYmV0d2VlbiAyIGFuZCA1KSBhbmQgKHJmbV9mcmVxIGJldHdlZW4gMiBhbmQgNSBhbmQgcmZtX21vbmV0YXJ5IGJldHdlZW4gMiBhbmQgNSkgdGhlbiAnQ2xpZW50ZXMgbGVhbGVzJw0Kd2hlbiAocmZtX3JlY2VuY3kgYmV0d2VlbiAzIGFuZCA1KSBhbmQgKHJmbV9mcmVxIGJldHdlZW4gMSBhbmQgMyBhbmQgcmZtX21vbmV0YXJ5IGJldHdlZW4gMSBhbmQgMykgdGhlbiAnTGVhbCBwb3RlbmNpYWwnDQp3aGVuIChyZm1fcmVjZW5jeSBiZXR3ZWVuIDQgYW5kIDUpIGFuZCAocmZtX2ZyZXEgYmV0d2VlbiAwIGFuZCAxIGFuZCByZm1fbW9uZXRhcnkgYmV0d2VlbiAwIGFuZCAxKSB0aGVuICdDbGllbnRlcyByZWNpZW50ZXMnDQp3aGVuIChyZm1fcmVjZW5jeSBiZXR3ZWVuIDMgYW5kIDQpIGFuZCAocmZtX2ZyZXEgYmV0d2VlbiAwIGFuZCAxIGFuZCByZm1fbW9uZXRhcnkgYmV0d2VlbiAwIGFuZCAxKSB0aGVuICdQcm9tZXRlZG9yJw0Kd2hlbiAocmZtX3JlY2VuY3kgYmV0d2VlbiAyIGFuZCAzKSBhbmQgKHJmbV9mcmVxIGJldHdlZW4gMiBhbmQgMyBhbmQgcmZtX21vbmV0YXJ5IGJldHdlZW4gMiBhbmQgMykgdGhlbiAnQ2xpZW50ZXMgcXVlIG5lY2VzaXRhbiBhdGVuY2lvbicNCndoZW4gKHJmbV9yZWNlbmN5IGJldHdlZW4gMiBhbmQgMykgYW5kIChyZm1fZnJlcSBiZXR3ZWVuIDAgYW5kIDIgYW5kIHJmbV9tb25ldGFyeSBiZXR3ZWVuIDAgYW5kIDIpIHRoZW4gJ0EgcHVudG8gZGUgZG9ybWlyJw0Kd2hlbiAocmZtX3JlY2VuY3kgYmV0d2VlbiAwIGFuZCAyKSBhbmQgKHJmbV9mcmVxIGJldHdlZW4gMiBhbmQgNSBhbmQgcmZtX21vbmV0YXJ5IGJldHdlZW4gMiBhbmQgNSkgdGhlbiAnRW4gcmllc2dvJw0Kd2hlbiAocmZtX3JlY2VuY3kgYmV0d2VlbiAwIGFuZCAxKSBhbmQgKHJmbV9mcmVxIGJldHdlZW4gNCBhbmQgNSBhbmQgcmZtX21vbmV0YXJ5IGJldHdlZW4gNCBhbmQgNSkgdGhlbiAnTm8gcHVlZG8gUGVyZGVybG9zJw0Kd2hlbiAocmZtX3JlY2VuY3kgYmV0d2VlbiAxIGFuZCAyKSBhbmQgKHJmbV9mcmVxIGJldHdlZW4gMSBhbmQgMiBhbmQgcmZtX21vbmV0YXJ5IGJldHdlZW4gMSBhbmQgMikgdGhlbiAnSGliZXJuYW5kbycNCndoZW4gKHJmbV9yZWNlbmN5IGJldHdlZW4gMCBhbmQgMikgYW5kIChyZm1fZnJlcSBiZXR3ZWVuIDAgYW5kIDIgYW5kIHJmbV9tb25ldGFyeSBiZXR3ZWVuIDAgYW5kIDIpIHRoZW4gJ1BlcmRpZG8nDQplbHNlICdvdGhlcicgZW5kIGFzIGN1c19zZWcNCmZyb20gQkRGIikNCg0KYGBgDQoNCmBgYHtyfQ0KDQpCREYgJT4lIA0KICBnZ3Bsb3QyOjpnZ3Bsb3QoKSArIA0KICBnZ3Bsb3QyOjphZXMoeCA9IGN1c19zZWcsIGZpbGwgPSBjdXNfc2VnKSArIA0KICBnZ3Bsb3QyOjpnZW9tX2JhcigpICsgDQogIGdncGxvdDI6Omdlb21fdGV4dCgNCiAgICBnZ3Bsb3QyOjphZXMobGFiZWwgPSBzY2FsZXM6OnBlcmNlbnQoLi5jb3VudC4uL3N1bSguLmNvdW50Li4pKQ0KICAgICAgICAgICAgICAgICApLCANCiAgICBzdGF0ID0gJ2NvdW50JywgcG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgxKQ0KICAgICkgKyANCiAgZ2dwbG90Mjo6bGFicyh0aXRsZSA9ICJTZWdtZW50YWNpb24gZGUgY2xpZW50ZXMiLCANCiAgICAgICAgICAgICAgICB4ID0gIlNlZ21lbnRvIiwgDQogICAgICAgICAgICAgICAgeSA9ICJUb3RhbCBkZWwgY29uc3VtaWRvciIpICsgDQogIGdncGxvdDI6OmNvb3JkX2ZsaXAoKSArIA0KICBnZ3Bsb3QyOjp0aGVtZSh0ZXh0ID0gZ2dwbG90Mjo6ZWxlbWVudF90ZXh0KGZhbWlseSA9ICdzZXJpZicpKSArIA0KICBnZ3Bsb3QyOjp0aGVtZV9taW5pbWFsKCkNCg0KYGBgDQoNCiMgQW5hbGlzaXMgQ2x1c3Rlcg0KDQpgYGB7cn0NCg0KZGZfUkZNMiA8LSANCiAgZGZfUkZNICU+JSANCiAgdGliYmxlOjpjb2x1bW5fdG9fcm93bmFtZXModmFyID0gJ0N1c3RvbWVySUQnKSAlPiUgDQogIHNjYWxlICU+JSANCiAgdGliYmxlOjphc190aWJibGUoKQ0KDQpzdW1tYXJ5KGRmX1JGTTIpDQoNCmBgYA0KDQojIyBFeHBsb3JhY2lvbg0KDQpgYGB7cn0NCiMgSGlzdG9ncmFtYXMgeSBjb3JyZWxhY2lvbmVzOg0KIyBQYXJhIGxhcyBjb3JyZWxhY2lvbmVzIHNlIHByb2JhcmEgbGEgbm9ybWFsaWRhZCBkZSBsYXMgdmFyaWFibGVzIHBhcmEgYXBsaWNhciANCiMgY29ycmVsYWNpb24gcG9yIFBlYXJzb24sIGRlIG5vIGVub250cmFyc2Ugbm9ybWFsaWRhZCBzZSBhcGxpY2FyYSBTcGVhcm1hbg0KDQojIEhvOiBMYSB2YXJpYWJsZSBzaWd1ZSB1bmEgZGlzdHJpYnVjaW9uIG5vcm1hbA0KIyBIMTogTGEgdmFyaWFibGUgbm8gc2lndWUgdW5hIGRpc3RyaWJidWNpb24gbm9ybWFsDQojIFBydWViYSBkZSBub3JtYWxpZGFkOiBBbmRlcnNvbiBEYXJsaW5nLCA1JSBkZSBzaWduaWZpY2FuY2lhLg0KDQpuLnRlc3QgPC0gYXBwbHkoWCA9IGRmX1JGTTIsIE1BUkdJTiA9IDIsIEZVTiA9IG5vcnRlc3Q6OmFkLnRlc3QpDQpuLnRlc3QNCg0KIyBFbiBlc3RlIGNhc28sIHRvZGFzIGxhcyB2YXJpYWJsZXMgcmVjaGF6YW4gbGEgbm9ybWFsaWRhZCwgc2UgYXBsaWNhcmEgbGEgDQojIGNvcnJlbGFjaW9uIHBvciBzcGVhcm1hbg0KDQpQZXJmb3JtYW5jZUFuYWx5dGljczo6Y2hhcnQuQ29ycmVsYXRpb24oZGZfUkZNMiwgbWV0aG9kID0gYygic3BlYXJtYW4iKSkNCg0KYGBgDQoNCiMjIERlZmluaWNpb24gZGVsIGNsdXN0ZXIgb3B0aW1vDQoNCmBgYHtyICwgY2FjaGU9VFJVRX0NCg0KIyBHcmFmaWNhIGRlIGxhIGNvbXBhcmF0aXZhIGRlIGxvcyBjbHVzdGVyZXMgdXNhbmRvIGVsIG1ldG9kbyBzaWxob3VldHRlDQpmYWN0b2V4dHJhOjpmdml6X25iY2x1c3QoZGZfUkZNMiwga21lYW5zLCBtZXRob2QgPSAic2lsaG91ZXR0ZSIpDQoNCiMgR3JhZmljbyBkZSBsb3MgY2x1c3RlcmVzDQpyZXMgPC0ga21lYW5zKGRmX1JGTTIsIDIsIG5zdGFydCA9IDI1KQ0KDQpgYGANCg0KIyMgR3JhZmljbyBkZSBsb3MgY2x1c3RlcmVzDQoNCmBgYHtyfQ0KDQpmYWN0b2V4dHJhOjpmdml6X2NsdXN0ZXIocmVzLCBkYXRhID0gZGZfUkZNMiwNCiAgICAgICAgICAgICAjIHBhbGV0dGUgPSBjKCIjRTdCODAwIiwgIiMwMEFGQkIiKSwNCiAgICAgICAgICAgICBnZW9tID0gInBvaW50IiwNCiAgICAgICAgICAgICBlbGxpcHNlLnR5cGUgPSAiY29udmV4IiwgDQogICAgICAgICAgICAgZ2d0aGVtZSA9IHRoZW1lX2J3KCkNCiAgICAgICAgICAgICApDQpgYGANCg0KIyMgUHJvbWVkaW9zIHBvciBjbHVzdGVyIHkgdmFyaWFibGVzDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KDQpkZl9SRk0yIDwtIA0KICBkZl9SRk0yICU+JSANCiAgZHBseXI6Om11dGF0ZShjbHVzdGVyID0gYXMuZmFjdG9yKHJlcyRjbHVzdGVyKSkgJT4lIA0KICB0aWR5cjo6ZHJvcF9uYSgpDQoNCmRhdGFfbG9uZyA8LSANCiAgZGZfUkZNMiAlPiUgDQogIHRpZHlyOjpnYXRoZXIoa2V5ID0gJ3ZhcicsIA0KICAgICAgICAgICAgICAgIHZhbHVlID0gJ3ZhbG9yJywgYygxOjMpLCANCiAgICAgICAgICAgICAgICBmYWN0b3Jfa2V5ID0gVFJVRSkNCg0KZ2dwbG90KGRhdGFfbG9uZywgYWVzKGFzLmZhY3Rvcih4ID0gdmFyKSwgDQogICAgICAgICAgICAgICAgICAgICAgeSA9IHZhbG9yLGdyb3VwPWNsdXN0ZXIsIGNvbG91ciA9IGNsdXN0ZXIpKSArIA0KICBzdGF0X3N1bW1hcnkoZnVuID0gbWVhbiwgZ2VvbT0icG9pbnRyYW5nZSIsIHNpemUgPSAxKSsNCiAgc3RhdF9zdW1tYXJ5KGdlb209ImxpbmUiKSArIGxhYnMoeD0iQXRyaWJ1dG9zIiwgeT0iRnJlY3VlbmNpYSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRpdGxlPSJDbGllbnRlcyIpDQoNCmBgYA0KDQoNCkNvbmNsdXNpb246DQoNCi0gQSBwYXJ0aXIgZGVsIGFuYWxpc2lzIHByZXZpbywgZGVjaWRpbW9zIGVsZWdpciAyIGNsdXN0ZXJlcyBhIHBlc2FyIGRlIHF1ZSANCmxvcyBncnVwb3Mgc2UgZXN0ZW4gc29sYXBhbmRvLg0KDQojIyBDb21wb3J0YW1pZW50byBkZSBsYXMgdmFyaWFibGVzIHBvciBjbHVzdGVyIHsudGFic2V0fQ0KDQojIyMgUmVzdW1lbiBkZSBsYXMgdmFyaWFibGVzIHBvciBjbHVzdGVyDQpgYGB7cn0NCg0KZGZfUkZNMiAlPiUNCiAgZHBseXI6Omdyb3VwX2J5KGNsdXN0ZXIpICU+JQ0KICBkcGx5cjo6Z3JvdXBfbWFwKH4gc3VtbWFyeSguKSkNCg0KYGBgDQoNCiMjIyBCb3hwbG90IHBvciBjbHVzdGVyIGRlbCBtb250bw0KYGBge3J9DQoNCmRhdGFfbG9uZyAlPiUgDQogIGRwbHlyOjpmaWx0ZXIodmFyICVpbiUgJ21vbml0ZXJ5JykgJT4lIA0KICBkcGx5cjo6Z3JvdXBfYnkoY2x1c3RlcikgJT4lIA0KICBnZ3Bsb3QyOjpnZ3Bsb3QoKSArIA0KICBnZ3Bsb3QyOjphZXMoeCA9IGNsdXN0ZXIsIHkgPSB2YWxvciwgZmlsbCA9IGNsdXN0ZXIpICsgDQogIGdncGxvdDI6Omdlb21fYm94cGxvdCgpICsgDQogIGdncGxvdDI6OnRoZW1lX21pbmltYWwoKSArIA0KICBnZ3Bsb3QyOjp0aGVtZSh0ZXh0ID0gZ2dwbG90Mjo6ZWxlbWVudF90ZXh0KGZhbWlseSA9ICdzZXJpZicpKQ0KDQpgYGANCg0KIyMjIEJveHBsb3QgcG9yIGNsdXN0ZXIgeSB2YXJpYWJsZXMNCmBgYHtyfQ0KDQpkYXRhX2xvbmcgJT4lDQogIGRwbHlyOjpncm91cF9ieShjbHVzdGVyKSAlPiUgDQogIGdncGxvdDI6OmdncGxvdCgpICsgDQogIGdncGxvdDI6OmFlcyh4ID0gY2x1c3RlciwgeSA9IHZhbG9yLCBmaWxsID0gY2x1c3RlcikgKyANCiAgZ2dwbG90Mjo6Z2VvbV9ib3hwbG90KCkgKyANCiAgZ2dwbG90Mjo6dGhlbWVfbWluaW1hbCgpICsgDQogIGdncGxvdDI6OnRoZW1lKHRleHQgPSBnZ3Bsb3QyOjplbGVtZW50X3RleHQoZmFtaWx5ID0gJ3NlcmlmJykpICsgDQogIGdncGxvdDI6OmZhY2V0X2dyaWQodmFyIH4gLiwgc2NhbGVzID0gJ2ZyZWUnKQ0KDQpgYGANCg0KDQpOb3RhczoNCg0KIyMgRXRpcXVldGFkbyBkZSBsb3MgY2x1c3RlcmVzDQoNCi0gU2VnbWVudG8gMTogQ2xpZW50ZXMgY29uIG1heW9yZXMgY29uc3Vtb3MuDQotIFNlZ21lbnRvIDI6IENsaWVudGVzIGNvbiBtZW5vcmVzIGNvbnN1bW9zLg0KLSBTZSBwb2RyaWEgYXBsaWNhciB1bmEgY2FtcGHDsWEgZGUgZGVzY3VlbnRvcyBhIGxvcyBjbGllbnRlcyBjb24gbWVub3Jlcw0KICAgIGNvbnN1bW9zIHBhcmEgdHJhdGFyIGRlIGdlbmVyYXIgZW5nYWdtZW50IGNvbiBlbGxvcyBhbCBjb25zaWRlcmFybm9zIHVuYSANCiAgICBjb21wYcOxaWEgcXVlIHNlIHByZW9jdXBhIHBvciBzdSBjb25zdW1vIGUgaW5ncmVzb3MuDQoNCiMgQ29uY2x1c2lvbmVzIGdlbmVyYWxlcw0KDQotIENsaWVudGVzIDU1NToNCiAgKiBRdWnDqW5lcyBzb246IENsaWVudGVzIGFsdGFtZW50ZSBjb21wcm9tZXRpZG9zIHF1ZSBoYW4gY29tcHJhZG8gbG8gbcOhcyANCnJlY2llbnRlLCBjb24gbWF5b3IgZnJlY3VlbmNpYSwgeSBoYW4gZ2VuZXJhZG8gbGEgbWF5b3IgY2FudGlkYWQgZGUgaW5ncmVzb3MuDQoNCiAgKiBQYXJhIGxvcyBjbGllbnRlcyBtZWpvciBwdW50dWFkb3MgbHVlZ28gZGVsIGFuYWxpc2lzDQpwbGFudGVhbW9zIGRhcmxlcyB1biB1cGdyYWRlIGEgY2xpZW50ZXMgcHJlbWl1bSwNCmFjY2VkaWVuZG8gYSBkZWxpdmVyeSBncmF0aXMsIHByb21vY2lvbmVzIHVuaWNhcywNCmxvIGN1YWwgbG9zIGhhcsOhIHNlbnRpcnNlIGNvbW9kb3Mgc2lndWllbmRvcyBjb24gZWwNCm1pc21vIG5pdmVsIGRlIGNvbnN1bW8sIGZyZWN1ZW5jaWEgeSBtb250byBkZSBjb21wcmFzLA0KYXBhcnRlIGRlIGluZm9ybWFjacOzbiBlc3BlY2lhbCBkZSB1c29zIGRlIGxvcyBwcm9kdWN0b3MgY29tcHJhZG9zLg0KDQotIENsaWVudGVzIFhYMTogDQogICogUXVpw6luZXMgc29uOiBMb3MgY2xpZW50ZXMgcXVlIGhhbiBnZW5lcmFkbyBtw6FzIGluZ3Jlc29zIGVuIHR1IHRpZW5kYS4NCg0KICAqIFNlIHBsYW50ZWEgYnJpbmRhcmxlcyB1biBwbGFuIGRlIGZpZGVsaXphY2nDs24gZG9uZGUNCnRlbmdhbiBuaXZlbGVzIHBvciBtb250b3MgZGUgY29uc3VtbyBlbiB1biBsYXBzbyANCmJpbWVzdHJhbCwgZG9uZGUgY3VhbmRvIHBhc2VuIGxhIHZhbGxhIGFsIHNpZ3VpZW50ZQ0Kbml2ZWwgZGVzYmxvcXVlZW4gYmVuZWZpY2lvcyBjb21vIHNvcG9ydGUgZXNwZWNpYWxpemFkbywNCmRlbGl2ZXJ5IGdyYXRpcyBlbiBhbGdvcyBwcm9kdWN0b3MgeSBkZXNjdWVudG9zIGVzcGVjaWFsZXMuDQoNCi0gQ2xpZW50ZXMgWDEzLCBYMTQ6DQogICogUXVpw6luZXMgc29uOiBDbGllbnRlcyBxdWUgc3VlbGVuIHZvbHZlciwgcGVybyBubyBnYXN0YW4gbXVjaG8uDQoNCiAgKiBFbiBlc3RlIHNlZ21lbnRvIHlhIGhlbW9zIGNvbnNlZ3VpZG8gY3JlYXIgZmlkZWxpZGFkLiBDb25jw6ludHJhdGUgZW4gYXVtZW50YXIgDQpsYSBtb25ldGl6YWNpw7NuIGEgdHJhdsOpcyBkZSByZWNvbWVuZGFjaW9uZXMgZGUgcHJvZHVjdG9zIGVuIGNvbXByYXMgcGFzYWRhcyBlIA0KaW5jZW50aXZvcyByZWxhY2lvbmFkb3MgY29uIGxvcyB1bWJyYWxlcyBkZSBnYXN0by4gDQpQbGFudGVhbW9zIGFwbGljYXIgYnJpbmRhcmxlIHBvciBzdXMgY29tcHJhcyBvcGNpb25lcyBhIHNvcnRlb3MgcGFyYSBpbmNlbnRpdmFyDQplbCBnYXN0byBtYW50ZW5pZW5kbyBsYSBmcmVjdWVuY2lhIHkgYXVtZW50YW5kbyBlbCBtb250byBwYXJhIHZvbHZlcmxvcw0KY2xpZW50ZXMgaWRlYWxlcy4NCg0KLSBDbGllbnRlcyA0NFg6IA0KICAqIFF1acOpbmVzIHNvbjogR3JhbmRlcyBjbGllbnRlcyBkZWwgcGFzYWRvIHF1ZSBubyBoYW4gY29tcHJhZG8gZW4gdW4gdGllbXBvLg0KDQogICogU2UgY3JlYXIgdW4gcGxhbiBkZSBlbmN1ZXN0YXMgcHVkaWVuZG8gb2J0ZW5lciBkYXRvcyBlbCBwb3JxdWUgZGUgc3UgZGlzbWludWNpw7NuDQpkZSBjb25zdW1vIHBhcmEgZW4gYmFzZSBhIGVzdG8gZ2VuZXJhciBkYXRvcyB5IG9wb3J0dW5pZGFkZXMgZGUgbWVqb3JhDQpvIHF1aXrDoXMgY3JlYWNpw7NuIGRlIG51ZXZhcyBjYW1wYcOxYXMu