Axel Mantalian ()

01/12/2019

Importo librerías a utilizar

library(tidyverse)
library(broom)
library(ISLR)
library(GGally)
library(modelr)
library(pROC)
library(rlang)
library(caret)
set.seed(1992)

Preparación de los datos

Comenzamos leyendo el conjunto de datos de entrenamiento.

data_original <- read_csv('./datasets/titanic_complete_train.csv')

glimpse(data_original)
Observations: 891
Variables: 12
$ PassengerId <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24...
$ Survived    <dbl> 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1...
$ Pclass      <dbl> 3, 1, 3, 1, 3, 3, 1, 3, 3, 2, 3, 1, 3, 3, 3, 2, 3, 2, 3, 3, 2, 2, 3, 1, 3, 3, 3, 1, 3...
$ Name        <chr> "Braund, Mr. Owen Harris", "Cumings, Mrs. John Bradley (Florence Briggs Thayer)", "He...
$ Sex         <chr> "male", "female", "female", "female", "male", "male", "male", "male", "female", "fema...
$ Age         <dbl> 22.00000, 38.00000, 26.00000, 35.00000, 35.00000, 26.50759, 54.00000, 2.00000, 27.000...
$ SibSp       <dbl> 1, 1, 0, 1, 0, 0, 0, 3, 0, 1, 1, 0, 0, 1, 0, 0, 4, 0, 1, 0, 0, 0, 0, 0, 3, 1, 0, 3, 0...
$ Parch       <dbl> 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 0, 0, 5, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 5, 0, 2, 0...
$ Ticket      <chr> "A/5 21171", "PC 17599", "STON/O2. 3101282", "113803", "373450", "330877", "17463", "...
$ Fare        <dbl> 7.2500, 71.2833, 7.9250, 53.1000, 8.0500, 8.4583, 51.8625, 21.0750, 11.1333, 30.0708,...
$ Cabin       <chr> NA, "C85", NA, "C123", NA, NA, "E46", NA, NA, NA, "G6", "C103", NA, NA, NA, NA, NA, N...
$ Embarked    <chr> "S", "C", "S", "S", "S", "Q", "S", "S", "S", "C", "S", "S", "S", "S", "S", "S", "Q", ...

El dataset original consiste de 891 observaciones y 12 variables, de las cuales seleccionaremos las siguientes:

  • PassengerId: id del pasajero
  • Survived: 1 = sobrevivió, 0 = no sobrevivió
  • Pclass: clase del boleto. 1 = primera clase, 2 = segunda clase, 3 = tercera clase
  • Sex: sexo
  • Age: edad
  • SibSp: cantidad de hermanos / conyugues a bordo del barco
  • Parch: cantidad de padres / hijos a bordo del barco
  • Fare: tarifa
  • Embarked: puerto de embarcación. C = Cherbourg, Q = Queenstown, S = Southampton

Como transformación inicial convertimos a factor las variables Survived, Pclass y Embarked.

data <- data_original %>% 
  select(PassengerId,Survived,Pclass,Sex,Age,SibSp,Parch,Fare,Embarked) %>% 
  mutate_at(.vars = c('Survived','Pclass','Embarked'),
            .funs = as.factor)

data

A continuación exploraremos algunas de las variables mediante un gráfico de ggpairs, el cual permite analizarlas de a pares.

# Realizo el gráfico de GGpairs para las variables seleccionadas

data %>% 
  select(Survived,Pclass,Sex,Age,Fare) %>% 
  ggpairs(.,mapping = aes(colour=Survived), legend = c(1,1)) + 
  theme(axis.text.x = element_text(angle = 90, hjust = 1)) + 
  theme_bw() +
  theme(legend.position = "bottom")

En el gráfico podemos observar:

  • Vemos gráficamente, y posteriormente lo confirmaremos analíticamente, que hay una mayor cantidad de no sobrevivientes que de sobrevivientes. La relación es aproximadamente 60/40.
  • La variable de clase muestra una distribución aproximadamente uniforme para el grupo de los sobrevivientes, pero un marcado desbalanceo en cuanto a la tercera clase dentro de los no sobrevivientes. Esto nos da un indicio de que la mayoría de los no sobrevivientes pertenecían a la clase más baja (tercera clase).
  • De la totalidad de los hombres que viajaban a bordo del Titanic, más del 80% fallecieron. En el caso de las mujeres sucedió lo contrario, la gran mayoría sobrevivió a la tragedia.
  • En cuanto a las variables de edad y tarifa no se observan diferencias muy significativas entre el grupo de sobrevivientes y el de no sobrevivientes. Analizadas conjuntamente tienen un coeficiente de correlación de 0.118 lo cual es bastante bajo. De todas formas se puede ver en el boxplot entre fare y survived que la mediana de la tarifa abonada por los sobrevivientes es levemente mayor al de los que no sobrevivieron.

Como lo indica el gráfico de barras en la esquina superior izquierda, hay un desbalanceo entre las clases target del dataset. Específicamente, hay 549 casos de personas que no sobrevivieron (61.6%) contra 342 sobrevivientes (38.3%).

# Muestro la distribución original de las clases (Sobrevivientes vs No Sobrevivientes)

data %>% 
  group_by(Survived) %>% 
  summarise(cantidad_casos = n(),
            proporcion = cantidad_casos/nrow(data))

A continuación dividiremos el dataset en un conjunto de entrenamiento (70% de los datos) y otro conjunto de validación (30%). Verificamos que luego de la partición se haya mantenido la proporción de sobrevivientes y fallecidos.

# Divido el dataset en conjunto de entrenamiento (70% de los datos) y validación (30% de los datos)

train_validation <- data %>% resample_partition(c(train=0.7,test=0.3))

data.train <- train_validation$train %>% as_tibble()

data.validation <- train_validation$test %>% as_tibble()

# Verifico que se cumpla la división de 70-30

paste0("Tamaño total del dataset: ",nrow(data))
[1] "Tamaño total del dataset: 891"
paste0("Partición de entrenamiento (70%): ",nrow(data.train))
[1] "Partición de entrenamiento (70%): 623"
paste0("Partición de validación (30%): ",nrow(data.validation))
[1] "Partición de validación (30%): 268"
# Verifico distribución de clases en conjunto de entrenamiento

data.train %>% 
  group_by(Survived) %>% 
  summarise(cantidad_casos = n(),
            proporcion = cantidad_casos/nrow(data.train))

# Verifico distribución de clases en conjunto de validación

data.validation %>% 
  group_by(Survived) %>% 
  summarise(cantidad_casos = n(),
            proporcion = cantidad_casos/nrow(data.validation))

Predicciones

Genero un primer modelo en base al dataset de entrenamiento para clasificar a los pasajeros en sobrevivientes o no, basado en las variables Pclass, Sex y Age.

# Genero el modelo 1 basado en Pclass, Sex y Age.

m1 <- glm(data=data.train,formula = Survived ~ Pclass+Sex+Age, family = 'binomial')

#Imprimo el resumen del modelo

m1 %>% tidy()

El resultado de este primer modelo nos indica que todos sus coeficientes son significativos y además negativos. Los mismos reflejan que, comparado al caso base de una mujer de primera clase, en el resto de los casos el individuo tendrá menor probabilidad de supervivencia. Es decir, un hombre de la misma edad y clase de una mujer tendrá menor probabilidad de sobrevivir. Si además de ser hombre, también es de una clase más baja, sus chances disminuyen más aún. Finalmente, un incremento en la edad del pasajero contribuye también negativamente a su chance de sobrevivir.

A continuación analizaremos dos predicciones puntuales: en un caso será Jack, un hombre de 20 años y que pertenece a tercera clase. Y por el otro lado Rose, una mujer de 17 años que pertenece a primera clase. A la luz de lo observado hasta el momento, esperaremos que Rose tenga mayor probabilidad de sobrevivir a la tragedia que Jack.

# Definimos a los individuos Rose y Jack para el experimento de predicción
Rose <- data.frame(Pclass= as.factor(1),
           Sex='female',
           Age=17)

Jack <- data.frame(Pclass= as.factor(3),
           Sex='male',
           Age=20)

# Predecimos utilizando nuestro modelo_1

Rose.prob <- predict(m1,Rose,type = 'response')

Jack.prob <- predict(m1,Jack,type = 'response')

# Imprimimos los resultados

paste0("Probabilidad de supervivencia de Rose: ",round(Rose.prob,3))
[1] "Probabilidad de supervivencia de Rose: 0.952"
paste0("Probabilidad de supervivencia de Jack: ",round(Jack.prob,3))
[1] "Probabilidad de supervivencia de Jack: 0.125"

Tal como esperábamos, la probabilidad de sobrevivir de Rose (96.2%) supera ampliamente a la de Jack (0.12%), quien presentaba las cualidades de mayor riesgo de vida (hombre y tercera clase) en el contexto de este problema.

Generación de modelos

Vamos a generar tres nuevos modelos con distintas combinaciones de covariables para evaluar cuál de ellos realiza un mejor trabajo a la hora de predecir la probabilidad de supervivencia.

# Definimos la lista de fórmulas con las expresiones de todos los modelos. Se incluye m1 que era el modelo ya analizado en los puntos anteriores.

logit_formulas <- formulas(.response = ~Survived,
                           m1= ~Pclass+Sex+Age,
                           m2= ~Pclass+SibSp+Parch, 
                           m3= ~Sex+Age+Fare,  
                           m4= ~Pclass+Sex+Age+Embarked
                         )

# Generamos un dataframe de modelos

models <- data_frame(logit_formulas) %>% 
  mutate(models = names(logit_formulas), 
         expression = paste(logit_formulas),
         mod = map(logit_formulas, ~glm(.,family = 'binomial', data = data.train)),
         glance = map(mod,glance)) 
# Mostramos en detalle los coeficientes de cada modelo

models %>% 
  mutate(tidy = map(mod,tidy)) %>%
  unnest(tidy, .drop = TRUE) %>% 
  mutate(estimate=round(estimate,5),
         p.value=round(p.value,4)) %>% 
  select(models,expression,term,estimate,p.value)

Se puede observar que la gran mayoría de las covariables presentan coeficientes negativos y en algunos casos no resultaron significativos, como por ejemplo la variable SibSp del modelo m2. A continuación listaremos los modelos ordenados y seleccionaremos el modelo que mayor porcentaje de deviance explicada posea.

# Obtener las medidas de evaluacion de interes

models %>% 
  unnest(glance, .drop = TRUE) %>%
  mutate(perc_explained_dev = 1-deviance/null.deviance) %>% 
  select(models,deviance,perc_explained_dev,null.deviance) %>% 
  arrange(deviance)

Como observamos en la salida anterior, el modelo que seleccionaremos de acuerdo al criterio de mayor deviance explicada será m4.

De ahora en adelante trabajaremos con m4: Survived ~ Pclass + Sex + Age + Embarked

Evaluación del modelo

# Filtramos el dataframe de models para quedarnos con el modelo elegido (m4)

best.model <- models %>% 
              filter(models=='m4') %>% 
              select(mod)

# Generamos las predicciones utilizando m4

best.model.pred <- best.model %>% 
                   mutate(pred = map(mod,augment,type.predict='response')) %>% 
                   unnest(pred)

Ya teniendo las predicciones para cada observación, podemos generar la curva ROC del modelo m4 y calcular su área bajo la curva (AUC).

# Genero curva ROC

best.model.roc <- roc(response=best.model.pred$Survived,predictor = best.model.pred$.fitted)
Setting levels: control = 0, case = 1
Setting direction: controls < cases
# Grafico la curva ROC

ggroc(best.model.roc, size=1,color='red') + 
  geom_abline(slope = 1, intercept = 1, linetype='dashed') + 
  theme_bw() + 
  labs(title='Curva ROC - modelo m4')


# Imprimo el AUC del modelo

paste0("AUC modelo m4: ",round(best.model.roc$auc,3))
[1] "AUC modelo m4: 0.844"

Del gráfico obtenido de la curva ROC correspondiente al modelo seleccionado podemos decir que tiene una performance bastante mejor en comparación con un modelo basado en el azar. Esto se sustenta tanto gráficamente al mostrar una curva que se encuentra muy por arriba de la línea punteada (modelo azaroso), y también analíticamente donde obtuvimos un AUC = 0.852 > 0.5.

Como la curva ROC grafica el trade-off entre TPR (True Positive Rate) y FPR (False Positive Rate) para distintos puntos de corte, si por ejemplo se quisiera ganar en sensitividad para clasificar correctamente más observaciones positivas habrá que comprometer performance en especificidad, lo que derivaría en una mayor proporción de falsos positivos.

ggplot(best.model.pred, aes(x=Survived, y=.fitted, group=Survived,fill=factor(Survived))) +   geom_violin() +
  theme_bw() +
  guides(fill=FALSE) +
  labs(title='Violin plot', subtitle='Modelo m4', y='Predicted probability')

Por último en el violin plot lo que vemos es la concentración o densidad de las observaciones de acuerdo con su probabilidad estimada de supervivencia. Al separar los gráficos en su verdadera clasificación (azul para los sobrevivientes y rojo para los fallecidos) intentamos establecer si existe visiblemente un punto de corte para la probabilidad de supervivencia que nos permita separar lo mejor posible las observaciones en dichas categorías.

En este caso, si bien se nota una mayor concentración en probabilidades bajas para el caso de los fallecidos y de probabilidades altas en los sobrevivientes, no hay un punto de corte muy claro que podamos seleccionar desde este gráfico. Consecuentemente, realizaremos otros gráficos que tengan en cuenta métricas adicionales para realizar esta elección.

Elección del punto corte

Como mencionamos previamente nuestro objetivo ahora será el de elegir el punto de corte óptimo que nos permita clasificar a las observaciones con el menor error posible. Para realizar esta tarea nos valdremos ya no del conjunto de datos con el que entrenamos el modelo sino con la partición de validación.

Comenzamos generando predicciones sobre el conjunto de validación.

# Del dataset de validación selecciono sólo las variables que me interesan para la predicción

data.validation.pred <- data.validation %>% 
                        select(Pclass,Sex,Age,Embarked)
                        
# Genero predicciones

validation.pred <- best.model %>% 
                  mutate(pred = map(mod,augment,newdata=data.validation.pred,type.predict='response')) %>% 
                  unnest(pred) %>% 
                  bind_cols(data.validation)

Para elegir el punto de corte realizaremos un gráfico en el cual incluiremos simultáneamente las métricas ‘accuracy’, ‘sensitivity’, ‘specificity’, ‘precision’ y ‘recall’.

# Defino la funcion de métricas de predicción

prediction_metrics <- function(cutoff, predictions=validation.pred){
  table <- predictions %>% 
    mutate(predicted_class=if_else(.fitted>cutoff, 1, 0) %>% as.factor())
  
  confusionMatrix(table(table$predicted_class, table$Survived), positive = "1") %>%
    tidy() %>%
    select(term, estimate) %>%
    filter(term %in% c('accuracy', 'sensitivity', 'specificity', 'precision','recall')) %>%
    mutate(cutoff=cutoff)
  
}

# Genero secuencia de puntos de corte para pasarle a la función como parámetro

cutoffs = seq(
  min(validation.pred$.fitted),
  max(validation.pred$.fitted),
  0.005
)

# Genero dataframe con los distin
logit_pred= map_dfr(cutoffs, prediction_metrics)%>% mutate(term=as.factor(term))

ggplot(logit_pred, aes(cutoff,estimate, group=term, color=term)) + geom_line(size=1) +
  theme_bw() +
  labs(title= 'Accuracy, Sensitivity, Specificity, Recall y Precision', subtitle= 'Modelo m4', color="")

Analizando el gráfico podemos pensar en un buen punto de corte como el determinado por la intersección de las curvas de sensitividad y especificidad, ya que provee un buen trade-off entre dichas métricas. Pasado ese punto de intersección (o el más próximo a él que podamos encontrar), estaremos entrando en una zona de caída de la sensitivity o recall a expensas de un aumento en la specificity. En otras palabras, a medida que más aumente el punto de corte tendremos menos falsos positivos pero aumentarán los falsos negativos.

Pero sumado a esto hay que tener en cuenta dónde se posiciona la performance de nuestro modelo con ese punto de corte seleccionado en términos de accuracy, es decir, evaluando la proporcion de observaciones correctamente clasificadas sobre el total de las mismas. Es también en este punto de intersección elegido donde vemos que, si bien no es máxima, está en un nivel más que aceptable si consideramos la simplicidad del modelo que ajustamos. Si solamente nos guiáramos por la métrica de accuracy y buscáramos el punto donde ésta es máxima, y donde otra métrica como es la precisión (ratio entre los verdaderos caso positivos y la totalidad de las observaciones clasificadas como positivas) también tiene valores altos, habremos comprometido mucho en términos de recall donde la curva cae fuertemente a partir del cutoff ~ 0.65.

Calculando una nueva columna con la diferencia entre recall y specificity, y ordenando por ella considero las opciones que quedaron en el top 10 del listado. Al ser éstos puntos de corte muy próximos entre sí me quedo con el que mayor accuracy presenta que es el punto 0.38915448.

# Busco el punto de corte óptimo en las inmediaciones de la intersección entre recall y specificity

logit_pred %>% 
  spread(term,estimate) %>% 
  mutate(dif = recall - specificity) %>% 
  arrange(abs(dif),desc(accuracy))

# Guardo el punto de corte seleccionado

cutoff_seleccionado <- 0.38915448

Matriz de confusión


# Genreo la matriz de confusion para comparar los valores reales vs los predichos y de acuerdo al punto de corte seleccionado

tabla <- validation.pred %>% 
    mutate(predicted_class=if_else(.fitted>cutoff_seleccionado,"1", "0") %>% as.factor(),
           Survived= factor(Survived)) %>% select(predicted_class,Survived)

confusionMatrix(table(tabla$predicted_class, tabla$Survived), positive = "1") 
Confusion Matrix and Statistics

   
      0   1
  0 133  19
  1  34  82
                                          
               Accuracy : 0.8022          
                 95% CI : (0.7494, 0.8482)
    No Information Rate : 0.6231          
    P-Value [Acc > NIR] : 1.818e-10       
                                          
                  Kappa : 0.5909          
                                          
 Mcnemar's Test P-Value : 0.05447         
                                          
            Sensitivity : 0.8119          
            Specificity : 0.7964          
         Pos Pred Value : 0.7069          
         Neg Pred Value : 0.8750          
             Prevalence : 0.3769          
         Detection Rate : 0.3060          
   Detection Prevalence : 0.4328          
      Balanced Accuracy : 0.8041          
                                          
       'Positive' Class : 1               
                                          

La matriz de confusión obtenida a partir del conjunto de datos de validación (268 casos totales) es una manera de resumir el resultado del modelo.En su diagonal principal vemos la cantidad de casos correctamente clasificados, en particular 133 individuos del conjunto de validación que fallecieron fueron correctamente clasificados como tal; y 82 de los sobrevivientes fueron clasificados también correctamente con un 1 en su clase predicha.

Por otro lado, 19 de las personas que fueron clasificados como no sobrevivientes habían sobrevivido a la tragedia (falsos negativos) y finalmente hubieron 34 casos en los que se predijo que las personas sobrevivirían y realmente no lo habían hecho (falsos positivos).

Dataset de testeo

Para finalizar haremos predicciones sobre el conjunto de test, que contiene datos no utilizados hasta el momento ni para el entrenamiento ni para la validación del modelo. De esta forma tendremos una prueba fehaciente de la capacidad de generalización que posee el modelo.


# Leo el dataset de test

data.test <- read_csv('./datasets/titanic_complete_test.csv')


# Convierto a factor algunas variables

data.test <- data.test %>% 
  select(PassengerId,Survived,Pclass,Sex,Age,SibSp,Parch,Fare,Embarked) %>% 
  mutate_at(.vars = c('Survived','Pclass','Embarked'),
            .funs = as.factor)

glimpse(data.test)                           
Observations: 418
Variables: 9
$ PassengerId <dbl> 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, ...
$ Survived    <fct> 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0...
$ Pclass      <fct> 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 3, 1, 1, 2, 1, 2, 2, 3, 3, 3, 1, 3, 1, 1, 1, 3, 1, 3, 1...
$ Sex         <chr> "male", "female", "male", "male", "female", "male", "female", "male", "female", "male...
$ Age         <dbl> 34.50000, 47.00000, 62.00000, 27.00000, 22.00000, 14.00000, 30.00000, 26.00000, 18.00...
$ SibSp       <dbl> 0, 1, 0, 0, 1, 0, 0, 1, 0, 2, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0...
$ Parch       <dbl> 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 3, 0, 1, 0, 0...
$ Fare        <dbl> 7.8292, 7.0000, 9.6875, 8.6625, 12.2875, 9.2250, 7.6292, 29.0000, 7.2292, 24.1500, 7....
$ Embarked    <fct> Q, S, Q, S, S, S, Q, S, C, S, S, S, S, S, S, C, Q, C, S, C, C, S, S, C, C, S, C, C, S...

El conjunto de datos de test consiste de 418 observaciones.


# Del dataset de validación selecciono sólo las variables que me interesan para la predicción

data.test.pred <- data.test %>% 
                  select(Pclass,Sex,Age,Embarked)
                        
# Genero predicciones

test.pred <-  best.model %>% 
              mutate(pred = map(mod,augment,newdata=data.test.pred,type.predict='response')) %>% 
              unnest(pred) %>% 
              bind_cols(data.test) %>% 
              mutate(predicted_class = if_else(.fitted>cutoff_seleccionado,"1","0") %>%  as.factor())


# Armo la tabla con los valores predichos y reales

tabla.test <-   test.pred %>% 
                mutate(Survived= factor(Survived)) %>% 
                select(predicted_class,Survived)

# Genero matriz de confusión

confusionMatrix(table(tabla.test$predicted_class, tabla.test$Survived), positive = "1") 
Confusion Matrix and Statistics

   
      0   1
  0 190  38
  1  71 119
                                          
               Accuracy : 0.7392          
                 95% CI : (0.6943, 0.7807)
    No Information Rate : 0.6244          
    P-Value [Acc > NIR] : 4.341e-07       
                                          
                  Kappa : 0.4664          
                                          
 Mcnemar's Test P-Value : 0.002176        
                                          
            Sensitivity : 0.7580          
            Specificity : 0.7280          
         Pos Pred Value : 0.6263          
         Neg Pred Value : 0.8333          
             Prevalence : 0.3756          
         Detection Rate : 0.2847          
   Detection Prevalence : 0.4545          
      Balanced Accuracy : 0.7430          
                                          
       'Positive' Class : 1               
                                          

Como era esperable, la performance sobre el conjunto de test es levemente inferior con respecto al conjunto de validación dado que en este último se optimizó el punto de corte. De los 418 individuos pertenecientes al dataset de testing 309 fueron correctamente clasificados en sus categorías correspondientes, es decir esto nos da casi un ratio de 3 de cada 4 correctamente clasificados.

Con un accuracy del 74% considero que este es un buen modelo teniendo en cuenta la simplicidad del mismo, que solo utiliza 4 variables y no requirió de ningún tipo de hiperparametrización ni optimización compleja o costosa.

LS0tDQp0aXRsZTogIlRQIDM6IFJlZ3Jlc2nDs24gTG9nw61zdGljYSINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICB0aGVtZTogdW5pdGVkDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZmxvYXQ6IHllcw0KICAgIGRmX3ByaW50OiBwYWdlZA0KICAgIGNvZGVfZm9sZGluZzogbm9uZQ0KLS0tDQojIyMjIF9fQXhlbCBNYW50YWxpYW5fXyAoYXhlbC5tYW50YUBnbWFpbC5jb20pDQowMS8xMi8yMDE5DQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KA0KICBjb21tZW50ID0gIiM+IiwNCiAgY29sbGFwc2UgPSBUUlVFLA0KICAjY2FjaGUgPSBUUlVFLA0KICBvdXQud2lkdGggPSAiNzAlIiwNCiAgZmlnLmFsaWduID0gJ2NlbnRlcicsDQogIGZpZy53aWR0aCA9IDYsDQogIGZpZy5hc3AgPSAwLjYxOCwgICMgMSAvIHBoaQ0KICBmaWcuc2hvdyA9ICJob2xkIg0KKQ0KYGBgDQpJbXBvcnRvIGxpYnJlcsOtYXMgYSB1dGlsaXphcg0KYGBge3J9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoYnJvb20pDQpsaWJyYXJ5KElTTFIpDQpsaWJyYXJ5KEdHYWxseSkNCmxpYnJhcnkobW9kZWxyKQ0KbGlicmFyeShwUk9DKQ0KbGlicmFyeShybGFuZykNCmxpYnJhcnkoY2FyZXQpDQpzZXQuc2VlZCgxOTkyKQ0KYGBgDQojIFByZXBhcmFjacOzbiBkZSBsb3MgZGF0b3MNCg0KQ29tZW56YW1vcyBsZXllbmRvIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIGVudHJlbmFtaWVudG8uDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCmRhdGFfb3JpZ2luYWwgPC0gcmVhZF9jc3YoJy4vZGF0YXNldHMvdGl0YW5pY19jb21wbGV0ZV90cmFpbi5jc3YnKQ0KDQpnbGltcHNlKGRhdGFfb3JpZ2luYWwpDQpgYGANCkVsIGRhdGFzZXQgb3JpZ2luYWwgY29uc2lzdGUgZGUgODkxIG9ic2VydmFjaW9uZXMgeSAxMiB2YXJpYWJsZXMsIGRlIGxhcyBjdWFsZXMgc2VsZWNjaW9uYXJlbW9zIGxhcyBzaWd1aWVudGVzOg0KDQoqIF9fUGFzc2VuZ2VySWRfXzogaWQgZGVsIHBhc2FqZXJvDQoqIF9fU3Vydml2ZWRfXzogMSA9IHNvYnJldml2acOzLCAwID0gbm8gc29icmV2aXZpw7MgDQoqIF9fUGNsYXNzX186IGNsYXNlIGRlbCBib2xldG8uIDEgPSBwcmltZXJhIGNsYXNlLCAyID0gc2VndW5kYSBjbGFzZSwgMyA9IHRlcmNlcmEgY2xhc2UNCiogX19TZXhfXzogc2V4bw0KKiBfX0FnZV9fOiBlZGFkDQoqIF9fU2liU3BfXzogY2FudGlkYWQgZGUgaGVybWFub3MgLyBjb255dWd1ZXMgYSBib3JkbyBkZWwgYmFyY28NCiogX19QYXJjaF9fOiBjYW50aWRhZCBkZSBwYWRyZXMgLyBoaWpvcyBhIGJvcmRvIGRlbCBiYXJjbw0KKiBfX0ZhcmVfXzogdGFyaWZhDQoqIF9fRW1iYXJrZWRfXzogcHVlcnRvIGRlIGVtYmFyY2FjacOzbi4gQyA9IENoZXJib3VyZywgUSA9IFF1ZWVuc3Rvd24sIFMgPSBTb3V0aGFtcHRvbg0KDQpDb21vIHRyYW5zZm9ybWFjacOzbiBpbmljaWFsIGNvbnZlcnRpbW9zIGEgZmFjdG9yIGxhcyB2YXJpYWJsZXMgX18qU3Vydml2ZWQqX18sIF9fKlBjbGFzcypfXyB5IF9fKkVtYmFya2VkKl9fLg0KYGBge3J9DQpkYXRhIDwtIGRhdGFfb3JpZ2luYWwgJT4lIA0KICBzZWxlY3QoUGFzc2VuZ2VySWQsU3Vydml2ZWQsUGNsYXNzLFNleCxBZ2UsU2liU3AsUGFyY2gsRmFyZSxFbWJhcmtlZCkgJT4lIA0KICBtdXRhdGVfYXQoLnZhcnMgPSBjKCdTdXJ2aXZlZCcsJ1BjbGFzcycsJ0VtYmFya2VkJyksDQogICAgICAgICAgICAuZnVucyA9IGFzLmZhY3RvcikNCg0KZGF0YQ0KYGBgDQpBIGNvbnRpbnVhY2nDs24gZXhwbG9yYXJlbW9zIGFsZ3VuYXMgZGUgbGFzIHZhcmlhYmxlcyBtZWRpYW50ZSB1biBncsOhZmljbyBkZSBfZ2dwYWlyc18sIGVsIGN1YWwgcGVybWl0ZSBhbmFsaXphcmxhcyBkZSBhIHBhcmVzLg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGZpZy5oZWlnaHQ9OCxmaWcud2lkdGg9OH0NCiMgUmVhbGl6byBlbCBncsOhZmljbyBkZSBHR3BhaXJzIHBhcmEgbGFzIHZhcmlhYmxlcyBzZWxlY2Npb25hZGFzDQoNCmRhdGEgJT4lIA0KICBzZWxlY3QoU3Vydml2ZWQsUGNsYXNzLFNleCxBZ2UsRmFyZSkgJT4lIA0KICBnZ3BhaXJzKC4sbWFwcGluZyA9IGFlcyhjb2xvdXI9U3Vydml2ZWQpLCBsZWdlbmQgPSBjKDEsMSkpICsgDQogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSkpICsgDQogIHRoZW1lX2J3KCkgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIikNCg0KYGBgIA0KRW4gZWwgZ3LDoWZpY28gcG9kZW1vcyBvYnNlcnZhcjoNCg0KKiBWZW1vcyBncsOhZmljYW1lbnRlLCB5IHBvc3Rlcmlvcm1lbnRlIGxvIGNvbmZpcm1hcmVtb3MgYW5hbMOtdGljYW1lbnRlLCBxdWUgaGF5IHVuYSBtYXlvciBjYW50aWRhZCBkZSBubyBzb2JyZXZpdmllbnRlcyBxdWUgZGUgc29icmV2aXZpZW50ZXMuIExhIHJlbGFjacOzbiBlcyBhcHJveGltYWRhbWVudGUgNjAvNDAuDQoqIExhIHZhcmlhYmxlIGRlIGNsYXNlIG11ZXN0cmEgdW5hIGRpc3RyaWJ1Y2nDs24gYXByb3hpbWFkYW1lbnRlIHVuaWZvcm1lIHBhcmEgZWwgZ3J1cG8gZGUgbG9zIHNvYnJldml2aWVudGVzLCBwZXJvIHVuIG1hcmNhZG8gZGVzYmFsYW5jZW8gZW4gY3VhbnRvIGEgbGEgdGVyY2VyYSBjbGFzZSBkZW50cm8gZGUgbG9zIG5vIHNvYnJldml2aWVudGVzLiBFc3RvIG5vcyBkYSB1biBpbmRpY2lvIGRlIHF1ZSBsYSBtYXlvcsOtYSBkZSBsb3Mgbm8gc29icmV2aXZpZW50ZXMgcGVydGVuZWPDrWFuIGEgbGEgY2xhc2UgbcOhcyBiYWphICh0ZXJjZXJhIGNsYXNlKS4NCiogRGUgbGEgdG90YWxpZGFkIGRlIGxvcyBob21icmVzIHF1ZSB2aWFqYWJhbiBhIGJvcmRvIGRlbCBUaXRhbmljLCBtw6FzIGRlbCA4MCUgZmFsbGVjaWVyb24uIEVuIGVsIGNhc28gZGUgbGFzIG11amVyZXMgc3VjZWRpw7MgbG8gY29udHJhcmlvLCBsYSBncmFuIG1heW9yw61hIHNvYnJldml2acOzIGEgbGEgdHJhZ2VkaWEuDQoqIEVuIGN1YW50byBhIGxhcyB2YXJpYWJsZXMgZGUgX2VkYWRfIHkgX3RhcmlmYV8gbm8gc2Ugb2JzZXJ2YW4gZGlmZXJlbmNpYXMgbXV5IHNpZ25pZmljYXRpdmFzIGVudHJlIGVsIGdydXBvIGRlIHNvYnJldml2aWVudGVzIHkgZWwgZGUgbm8gc29icmV2aXZpZW50ZXMuIEFuYWxpemFkYXMgY29uanVudGFtZW50ZSB0aWVuZW4gdW4gY29lZmljaWVudGUgZGUgY29ycmVsYWNpw7NuIGRlIDAuMTE4IGxvIGN1YWwgZXMgYmFzdGFudGUgYmFqby4gRGUgdG9kYXMgZm9ybWFzIHNlIHB1ZWRlIHZlciBlbiBlbCBib3hwbG90IGVudHJlIF9mYXJlXyB5IF9zdXJ2aXZlZF8gcXVlIGxhIG1lZGlhbmEgZGUgbGEgdGFyaWZhIGFib25hZGEgcG9yIGxvcyBzb2JyZXZpdmllbnRlcyBlcyBsZXZlbWVudGUgbWF5b3IgYWwgZGUgbG9zIHF1ZSBubyBzb2JyZXZpdmllcm9uLg0KDQpDb21vIGxvIGluZGljYSBlbCBncsOhZmljbyBkZSBiYXJyYXMgZW4gbGEgZXNxdWluYSBzdXBlcmlvciBpenF1aWVyZGEsIGhheSB1biBkZXNiYWxhbmNlbyBlbnRyZSBsYXMgY2xhc2VzIHRhcmdldCBkZWwgZGF0YXNldC4gRXNwZWPDrWZpY2FtZW50ZSwgaGF5IDU0OSBjYXNvcyBkZSBwZXJzb25hcyBxdWUgbm8gc29icmV2aXZpZXJvbiAoNjEuNiUpIGNvbnRyYSAzNDIgc29icmV2aXZpZW50ZXMgKDM4LjMlKS4NCmBgYHtyfQ0KIyBNdWVzdHJvIGxhIGRpc3RyaWJ1Y2nDs24gb3JpZ2luYWwgZGUgbGFzIGNsYXNlcyAoU29icmV2aXZpZW50ZXMgdnMgTm8gU29icmV2aXZpZW50ZXMpDQoNCmRhdGEgJT4lIA0KICBncm91cF9ieShTdXJ2aXZlZCkgJT4lIA0KICBzdW1tYXJpc2UoY2FudGlkYWRfY2Fzb3MgPSBuKCksDQogICAgICAgICAgICBwcm9wb3JjaW9uID0gY2FudGlkYWRfY2Fzb3MvbnJvdyhkYXRhKSkNCmBgYA0KQSBjb250aW51YWNpw7NuIGRpdmlkaXJlbW9zIGVsIGRhdGFzZXQgZW4gdW4gY29uanVudG8gZGUgZW50cmVuYW1pZW50byAoNzAlIGRlIGxvcyBkYXRvcykgeSBvdHJvIGNvbmp1bnRvIGRlIHZhbGlkYWNpw7NuICgzMCUpLiBWZXJpZmljYW1vcyBxdWUgbHVlZ28gZGUgbGEgcGFydGljacOzbiBzZSBoYXlhIG1hbnRlbmlkbyBsYSBwcm9wb3JjacOzbiBkZSBzb2JyZXZpdmllbnRlcyB5IGZhbGxlY2lkb3MuDQpgYGB7cn0NCiMgRGl2aWRvIGVsIGRhdGFzZXQgZW4gY29uanVudG8gZGUgZW50cmVuYW1pZW50byAoNzAlIGRlIGxvcyBkYXRvcykgeSB2YWxpZGFjacOzbiAoMzAlIGRlIGxvcyBkYXRvcykNCg0KdHJhaW5fdmFsaWRhdGlvbiA8LSBkYXRhICU+JSByZXNhbXBsZV9wYXJ0aXRpb24oYyh0cmFpbj0wLjcsdGVzdD0wLjMpKQ0KDQpkYXRhLnRyYWluIDwtIHRyYWluX3ZhbGlkYXRpb24kdHJhaW4gJT4lIGFzX3RpYmJsZSgpDQoNCmRhdGEudmFsaWRhdGlvbiA8LSB0cmFpbl92YWxpZGF0aW9uJHRlc3QgJT4lIGFzX3RpYmJsZSgpDQoNCiMgVmVyaWZpY28gcXVlIHNlIGN1bXBsYSBsYSBkaXZpc2nDs24gZGUgNzAtMzANCg0KcGFzdGUwKCJUYW1hw7FvIHRvdGFsIGRlbCBkYXRhc2V0OiAiLG5yb3coZGF0YSkpDQoNCnBhc3RlMCgiUGFydGljacOzbiBkZSBlbnRyZW5hbWllbnRvICg3MCUpOiAiLG5yb3coZGF0YS50cmFpbikpDQoNCnBhc3RlMCgiUGFydGljacOzbiBkZSB2YWxpZGFjacOzbiAoMzAlKTogIixucm93KGRhdGEudmFsaWRhdGlvbikpDQoNCiMgVmVyaWZpY28gZGlzdHJpYnVjacOzbiBkZSBjbGFzZXMgZW4gY29uanVudG8gZGUgZW50cmVuYW1pZW50bw0KDQpkYXRhLnRyYWluICU+JSANCiAgZ3JvdXBfYnkoU3Vydml2ZWQpICU+JSANCiAgc3VtbWFyaXNlKGNhbnRpZGFkX2Nhc29zID0gbigpLA0KICAgICAgICAgICAgcHJvcG9yY2lvbiA9IGNhbnRpZGFkX2Nhc29zL25yb3coZGF0YS50cmFpbikpDQoNCiMgVmVyaWZpY28gZGlzdHJpYnVjacOzbiBkZSBjbGFzZXMgZW4gY29uanVudG8gZGUgdmFsaWRhY2nDs24NCg0KZGF0YS52YWxpZGF0aW9uICU+JSANCiAgZ3JvdXBfYnkoU3Vydml2ZWQpICU+JSANCiAgc3VtbWFyaXNlKGNhbnRpZGFkX2Nhc29zID0gbigpLA0KICAgICAgICAgICAgcHJvcG9yY2lvbiA9IGNhbnRpZGFkX2Nhc29zL25yb3coZGF0YS52YWxpZGF0aW9uKSkNCmBgYA0KDQojIFByZWRpY2Npb25lcw0KDQpHZW5lcm8gdW4gcHJpbWVyIG1vZGVsbyBlbiBiYXNlIGFsIGRhdGFzZXQgZGUgZW50cmVuYW1pZW50byBwYXJhIGNsYXNpZmljYXIgYSBsb3MgcGFzYWplcm9zIGVuIHNvYnJldml2aWVudGVzIG8gbm8sIGJhc2FkbyBlbiBsYXMgdmFyaWFibGVzIF9QY2xhc3NfLCBfU2V4XyB5IF9BZ2VfLg0KYGBge3J9DQojIEdlbmVybyBlbCBtb2RlbG8gMSBiYXNhZG8gZW4gUGNsYXNzLCBTZXggeSBBZ2UuDQoNCm0xIDwtIGdsbShkYXRhPWRhdGEudHJhaW4sZm9ybXVsYSA9IFN1cnZpdmVkIH4gUGNsYXNzK1NleCtBZ2UsIGZhbWlseSA9ICdiaW5vbWlhbCcpDQoNCiNJbXByaW1vIGVsIHJlc3VtZW4gZGVsIG1vZGVsbw0KDQptMSAlPiUgdGlkeSgpDQpgYGANCkVsIHJlc3VsdGFkbyBkZSBlc3RlIHByaW1lciBtb2RlbG8gbm9zIGluZGljYSBxdWUgdG9kb3Mgc3VzIGNvZWZpY2llbnRlcyBzb24gc2lnbmlmaWNhdGl2b3MgeSBhZGVtw6FzIG5lZ2F0aXZvcy4gTG9zIG1pc21vcyByZWZsZWphbiBxdWUsIGNvbXBhcmFkbyBhbCBjYXNvIGJhc2UgZGUgdW5hIG11amVyIGRlIHByaW1lcmEgY2xhc2UsIGVuIGVsIHJlc3RvIGRlIGxvcyBjYXNvcyBlbCBpbmRpdmlkdW8gdGVuZHLDoSBtZW5vciBwcm9iYWJpbGlkYWQgZGUgc3VwZXJ2aXZlbmNpYS4gRXMgZGVjaXIsIHVuIGhvbWJyZSBkZSBsYSBtaXNtYSBlZGFkIHkgY2xhc2UgZGUgdW5hIG11amVyIHRlbmRyw6EgbWVub3IgcHJvYmFiaWxpZGFkIGRlIHNvYnJldml2aXIuIA0KU2kgYWRlbcOhcyBkZSBzZXIgaG9tYnJlLCB0YW1iacOpbiBlcyBkZSB1bmEgY2xhc2UgbcOhcyBiYWphLCBzdXMgY2hhbmNlcyBkaXNtaW51eWVuIG3DoXMgYcO6bi4gDQpGaW5hbG1lbnRlLCB1biBpbmNyZW1lbnRvIGVuIGxhIGVkYWQgZGVsIHBhc2FqZXJvIGNvbnRyaWJ1eWUgdGFtYmnDqW4gbmVnYXRpdmFtZW50ZSBhIHN1IGNoYW5jZSBkZSBzb2JyZXZpdmlyLg0KDQpBIGNvbnRpbnVhY2nDs24gYW5hbGl6YXJlbW9zIGRvcyBwcmVkaWNjaW9uZXMgcHVudHVhbGVzOiBlbiB1biBjYXNvIHNlcsOhIEphY2ssIHVuIGhvbWJyZSBkZSAyMCBhw7FvcyB5IHF1ZSBwZXJ0ZW5lY2UgYSB0ZXJjZXJhIGNsYXNlLiBZIHBvciBlbCBvdHJvIGxhZG8gUm9zZSwgdW5hIG11amVyIGRlIDE3IGHDsW9zIHF1ZSBwZXJ0ZW5lY2UgYSBwcmltZXJhIGNsYXNlLiBBIGxhIGx1eiBkZSBsbyBvYnNlcnZhZG8gaGFzdGEgZWwgbW9tZW50bywgZXNwZXJhcmVtb3MgcXVlIFJvc2UgdGVuZ2EgbWF5b3IgcHJvYmFiaWxpZGFkIGRlIHNvYnJldml2aXIgYSBsYSB0cmFnZWRpYSBxdWUgSmFjay4NCmBgYHtyfQ0KIyBEZWZpbmltb3MgYSBsb3MgaW5kaXZpZHVvcyBSb3NlIHkgSmFjayBwYXJhIGVsIGV4cGVyaW1lbnRvIGRlIHByZWRpY2Npw7NuDQpSb3NlIDwtIGRhdGEuZnJhbWUoUGNsYXNzPSBhcy5mYWN0b3IoMSksDQogICAgICAgICAgIFNleD0nZmVtYWxlJywNCiAgICAgICAgICAgQWdlPTE3KQ0KDQpKYWNrIDwtIGRhdGEuZnJhbWUoUGNsYXNzPSBhcy5mYWN0b3IoMyksDQogICAgICAgICAgIFNleD0nbWFsZScsDQogICAgICAgICAgIEFnZT0yMCkNCg0KIyBQcmVkZWNpbW9zIHV0aWxpemFuZG8gbnVlc3RybyBtb2RlbG9fMQ0KDQpSb3NlLnByb2IgPC0gcHJlZGljdChtMSxSb3NlLHR5cGUgPSAncmVzcG9uc2UnKQ0KDQpKYWNrLnByb2IgPC0gcHJlZGljdChtMSxKYWNrLHR5cGUgPSAncmVzcG9uc2UnKQ0KDQojIEltcHJpbWltb3MgbG9zIHJlc3VsdGFkb3MNCg0KcGFzdGUwKCJQcm9iYWJpbGlkYWQgZGUgc3VwZXJ2aXZlbmNpYSBkZSBSb3NlOiAiLHJvdW5kKFJvc2UucHJvYiwzKSkNCg0KcGFzdGUwKCJQcm9iYWJpbGlkYWQgZGUgc3VwZXJ2aXZlbmNpYSBkZSBKYWNrOiAiLHJvdW5kKEphY2sucHJvYiwzKSkNCg0KYGBgDQpUYWwgY29tbyBlc3BlcsOhYmFtb3MsIGxhIHByb2JhYmlsaWRhZCBkZSBzb2JyZXZpdmlyIGRlIFJvc2UgKDk2LjIlKSBzdXBlcmEgYW1wbGlhbWVudGUgYSBsYSBkZSBKYWNrICgwLjEyJSksIHF1aWVuIHByZXNlbnRhYmEgbGFzIGN1YWxpZGFkZXMgZGUgbWF5b3Igcmllc2dvIGRlIHZpZGEgKGhvbWJyZSB5IHRlcmNlcmEgY2xhc2UpIGVuIGVsIGNvbnRleHRvIGRlIGVzdGUgcHJvYmxlbWEuDQoNCiMgR2VuZXJhY2nDs24gZGUgbW9kZWxvcw0KDQpWYW1vcyBhIGdlbmVyYXIgdHJlcyBudWV2b3MgbW9kZWxvcyBjb24gZGlzdGludGFzIGNvbWJpbmFjaW9uZXMgZGUgY292YXJpYWJsZXMgcGFyYSBldmFsdWFyIGN1w6FsIGRlIGVsbG9zIHJlYWxpemEgdW4gbWVqb3IgdHJhYmFqbyBhIGxhIGhvcmEgZGUgcHJlZGVjaXIgbGEgcHJvYmFiaWxpZGFkIGRlIHN1cGVydml2ZW5jaWEuDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCiMgRGVmaW5pbW9zIGxhIGxpc3RhIGRlIGbDs3JtdWxhcyBjb24gbGFzIGV4cHJlc2lvbmVzIGRlIHRvZG9zIGxvcyBtb2RlbG9zLiBTZSBpbmNsdXllIG0xIHF1ZSBlcmEgZWwgbW9kZWxvIHlhIGFuYWxpemFkbyBlbiBsb3MgcHVudG9zIGFudGVyaW9yZXMuDQoNCmxvZ2l0X2Zvcm11bGFzIDwtIGZvcm11bGFzKC5yZXNwb25zZSA9IH5TdXJ2aXZlZCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG0xPSB+UGNsYXNzK1NleCtBZ2UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBtMj0gflBjbGFzcytTaWJTcCtQYXJjaCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBtMz0gflNleCtBZ2UrRmFyZSwgIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgbTQ9IH5QY2xhc3MrU2V4K0FnZStFbWJhcmtlZA0KICAgICAgICAgICAgICAgICAgICAgICAgICkNCg0KIyBHZW5lcmFtb3MgdW4gZGF0YWZyYW1lIGRlIG1vZGVsb3MNCg0KbW9kZWxzIDwtIGRhdGFfZnJhbWUobG9naXRfZm9ybXVsYXMpICU+JSANCiAgbXV0YXRlKG1vZGVscyA9IG5hbWVzKGxvZ2l0X2Zvcm11bGFzKSwgDQogICAgICAgICBleHByZXNzaW9uID0gcGFzdGUobG9naXRfZm9ybXVsYXMpLA0KICAgICAgICAgbW9kID0gbWFwKGxvZ2l0X2Zvcm11bGFzLCB+Z2xtKC4sZmFtaWx5ID0gJ2Jpbm9taWFsJywgZGF0YSA9IGRhdGEudHJhaW4pKSwNCiAgICAgICAgIGdsYW5jZSA9IG1hcChtb2QsZ2xhbmNlKSkgDQpgYGANCg0KDQoNCmBgYHtyfQ0KIyBNb3N0cmFtb3MgZW4gZGV0YWxsZSBsb3MgY29lZmljaWVudGVzIGRlIGNhZGEgbW9kZWxvDQoNCm1vZGVscyAlPiUgDQogIG11dGF0ZSh0aWR5ID0gbWFwKG1vZCx0aWR5KSkgJT4lDQogIHVubmVzdCh0aWR5LCAuZHJvcCA9IFRSVUUpICU+JSANCiAgbXV0YXRlKGVzdGltYXRlPXJvdW5kKGVzdGltYXRlLDUpLA0KICAgICAgICAgcC52YWx1ZT1yb3VuZChwLnZhbHVlLDQpKSAlPiUgDQogIHNlbGVjdChtb2RlbHMsZXhwcmVzc2lvbix0ZXJtLGVzdGltYXRlLHAudmFsdWUpDQpgYGANClNlIHB1ZWRlIG9ic2VydmFyIHF1ZSBsYSBncmFuIG1heW9yw61hIGRlIGxhcyBjb3ZhcmlhYmxlcyBwcmVzZW50YW4gY29lZmljaWVudGVzIG5lZ2F0aXZvcyB5IGVuIGFsZ3Vub3MgY2Fzb3Mgbm8gcmVzdWx0YXJvbiBzaWduaWZpY2F0aXZvcywgY29tbyBwb3IgZWplbXBsbyBsYSB2YXJpYWJsZSBfX1NpYlNwX18gZGVsIG1vZGVsbyBtMi4gQSBjb250aW51YWNpw7NuIGxpc3RhcmVtb3MgbG9zIG1vZGVsb3Mgb3JkZW5hZG9zIHkgc2VsZWNjaW9uYXJlbW9zIGVsIG1vZGVsbyBxdWUgbWF5b3IgcG9yY2VudGFqZSBkZSBfZGV2aWFuY2VfIGV4cGxpY2FkYSBwb3NlYS4NCmBgYHtyfQ0KIyBPYnRlbmVyIGxhcyBtZWRpZGFzIGRlIGV2YWx1YWNpb24gZGUgaW50ZXJlcw0KDQptb2RlbHMgJT4lIA0KICB1bm5lc3QoZ2xhbmNlLCAuZHJvcCA9IFRSVUUpICU+JQ0KICBtdXRhdGUocGVyY19leHBsYWluZWRfZGV2ID0gMS1kZXZpYW5jZS9udWxsLmRldmlhbmNlKSAlPiUgDQogIHNlbGVjdChtb2RlbHMsZGV2aWFuY2UscGVyY19leHBsYWluZWRfZGV2LG51bGwuZGV2aWFuY2UpICU+JSANCiAgYXJyYW5nZShkZXZpYW5jZSkNCmBgYA0KQ29tbyBvYnNlcnZhbW9zIGVuIGxhIHNhbGlkYSBhbnRlcmlvciwgZWwgbW9kZWxvIHF1ZSBzZWxlY2Npb25hcmVtb3MgZGUgYWN1ZXJkbyBhbCBjcml0ZXJpbyBkZSBtYXlvciBfZGV2aWFuY2VfIGV4cGxpY2FkYSBzZXLDoSBtNC4gDQoNCkRlIGFob3JhIGVuIGFkZWxhbnRlIHRyYWJhamFyZW1vcyBjb24gbTQ6IFN1cnZpdmVkIH4gUGNsYXNzICsgU2V4ICsgQWdlICsgRW1iYXJrZWQNCg0KDQojIEV2YWx1YWNpw7NuIGRlbCBtb2RlbG8NCg0KYGBge3J9DQojIEZpbHRyYW1vcyBlbCBkYXRhZnJhbWUgZGUgbW9kZWxzIHBhcmEgcXVlZGFybm9zIGNvbiBlbCBtb2RlbG8gZWxlZ2lkbyAobTQpDQoNCmJlc3QubW9kZWwgPC0gbW9kZWxzICU+JSANCiAgICAgICAgICAgICAgZmlsdGVyKG1vZGVscz09J200JykgJT4lIA0KICAgICAgICAgICAgICBzZWxlY3QobW9kKQ0KDQojIEdlbmVyYW1vcyBsYXMgcHJlZGljY2lvbmVzIHV0aWxpemFuZG8gbTQNCg0KYmVzdC5tb2RlbC5wcmVkIDwtIGJlc3QubW9kZWwgJT4lIA0KICAgICAgICAgICAgICAgICAgIG11dGF0ZShwcmVkID0gbWFwKG1vZCxhdWdtZW50LHR5cGUucHJlZGljdD0ncmVzcG9uc2UnKSkgJT4lIA0KICAgICAgICAgICAgICAgICAgIHVubmVzdChwcmVkKQ0KYGBgDQpZYSB0ZW5pZW5kbyBsYXMgcHJlZGljY2lvbmVzIHBhcmEgY2FkYSBvYnNlcnZhY2nDs24sIHBvZGVtb3MgZ2VuZXJhciBsYSBjdXJ2YSBST0MgZGVsIG1vZGVsbyBtNCB5IGNhbGN1bGFyIHN1IMOhcmVhIGJham8gbGEgY3VydmEgKEFVQykuDQpgYGB7cn0NCiMgR2VuZXJvIGN1cnZhIFJPQw0KDQpiZXN0Lm1vZGVsLnJvYyA8LSByb2MocmVzcG9uc2U9YmVzdC5tb2RlbC5wcmVkJFN1cnZpdmVkLHByZWRpY3RvciA9IGJlc3QubW9kZWwucHJlZCQuZml0dGVkKQ0KDQojIEdyYWZpY28gbGEgY3VydmEgUk9DDQoNCmdncm9jKGJlc3QubW9kZWwucm9jLCBzaXplPTEsY29sb3I9J3JlZCcpICsgDQogIGdlb21fYWJsaW5lKHNsb3BlID0gMSwgaW50ZXJjZXB0ID0gMSwgbGluZXR5cGU9J2Rhc2hlZCcpICsgDQogIHRoZW1lX2J3KCkgKyANCiAgbGFicyh0aXRsZT0nQ3VydmEgUk9DIC0gbW9kZWxvIG00JykNCg0KIyBJbXByaW1vIGVsIEFVQyBkZWwgbW9kZWxvDQoNCnBhc3RlMCgiQVVDIG1vZGVsbyBtNDogIixyb3VuZChiZXN0Lm1vZGVsLnJvYyRhdWMsMykpDQpgYGANCkRlbCBncsOhZmljbyBvYnRlbmlkbyBkZSBsYSBjdXJ2YSBST0MgY29ycmVzcG9uZGllbnRlIGFsIG1vZGVsbyBzZWxlY2Npb25hZG8gcG9kZW1vcyBkZWNpciBxdWUgdGllbmUgdW5hIHBlcmZvcm1hbmNlIGJhc3RhbnRlIG1lam9yIGVuIGNvbXBhcmFjacOzbiBjb24gdW4gbW9kZWxvIGJhc2FkbyBlbiBlbCBhemFyLiBFc3RvIHNlIHN1c3RlbnRhIHRhbnRvIGdyw6FmaWNhbWVudGUgYWwgbW9zdHJhciB1bmEgY3VydmEgcXVlIHNlIGVuY3VlbnRyYSBtdXkgcG9yIGFycmliYSBkZSBsYSBsw61uZWEgcHVudGVhZGEgKG1vZGVsbyBhemFyb3NvKSwgeSB0YW1iacOpbiBhbmFsw610aWNhbWVudGUgZG9uZGUgb2J0dXZpbW9zIHVuIEFVQyA9IDAuODUyID4gMC41Lg0KDQpDb21vIGxhIGN1cnZhIFJPQyBncmFmaWNhIGVsIHRyYWRlLW9mZiBlbnRyZSBUUFIgKFRydWUgUG9zaXRpdmUgUmF0ZSkgeSBGUFIgKEZhbHNlIFBvc2l0aXZlIFJhdGUpIHBhcmEgZGlzdGludG9zIHB1bnRvcyBkZSBjb3J0ZSwgc2kgcG9yIGVqZW1wbG8gc2UgcXVpc2llcmEgZ2FuYXIgZW4gc2Vuc2l0aXZpZGFkIHBhcmEgY2xhc2lmaWNhciBjb3JyZWN0YW1lbnRlIG3DoXMgb2JzZXJ2YWNpb25lcyBwb3NpdGl2YXMgaGFicsOhIHF1ZSBjb21wcm9tZXRlciBwZXJmb3JtYW5jZSBlbiBlc3BlY2lmaWNpZGFkLCBsbyBxdWUgZGVyaXZhcsOtYSBlbiB1bmEgbWF5b3IgcHJvcG9yY2nDs24gZGUgZmFsc29zIHBvc2l0aXZvcy4gDQpgYGB7cn0NCmdncGxvdChiZXN0Lm1vZGVsLnByZWQsIGFlcyh4PVN1cnZpdmVkLCB5PS5maXR0ZWQsIGdyb3VwPVN1cnZpdmVkLGZpbGw9ZmFjdG9yKFN1cnZpdmVkKSkpICsgICBnZW9tX3Zpb2xpbigpICsNCiAgdGhlbWVfYncoKSArDQogIGd1aWRlcyhmaWxsPUZBTFNFKSArDQogIGxhYnModGl0bGU9J1Zpb2xpbiBwbG90Jywgc3VidGl0bGU9J01vZGVsbyBtNCcsIHk9J1ByZWRpY3RlZCBwcm9iYWJpbGl0eScpDQpgYGANClBvciDDumx0aW1vIGVuIGVsIHZpb2xpbiBwbG90IGxvIHF1ZSB2ZW1vcyBlcyBsYSBjb25jZW50cmFjacOzbiBvIGRlbnNpZGFkIGRlIGxhcyBvYnNlcnZhY2lvbmVzIGRlIGFjdWVyZG8gY29uIHN1IHByb2JhYmlsaWRhZCBlc3RpbWFkYSBkZSBzdXBlcnZpdmVuY2lhLiBBbCBzZXBhcmFyIGxvcyBncsOhZmljb3MgZW4gc3UgdmVyZGFkZXJhIGNsYXNpZmljYWNpw7NuIChhenVsIHBhcmEgbG9zIHNvYnJldml2aWVudGVzIHkgcm9qbyBwYXJhIGxvcyBmYWxsZWNpZG9zKSBpbnRlbnRhbW9zIGVzdGFibGVjZXIgc2kgZXhpc3RlIHZpc2libGVtZW50ZSB1biBwdW50byBkZSBjb3J0ZSBwYXJhIGxhIHByb2JhYmlsaWRhZCBkZSBzdXBlcnZpdmVuY2lhIHF1ZSBub3MgcGVybWl0YSBzZXBhcmFyIGxvIG1lam9yIHBvc2libGUgbGFzIG9ic2VydmFjaW9uZXMgZW4gZGljaGFzIGNhdGVnb3LDrWFzLiANCg0KRW4gZXN0ZSBjYXNvLCBzaSBiaWVuIHNlIG5vdGEgdW5hIG1heW9yIGNvbmNlbnRyYWNpw7NuIGVuIHByb2JhYmlsaWRhZGVzIGJhamFzIHBhcmEgZWwgY2FzbyBkZSBsb3MgZmFsbGVjaWRvcyB5IGRlIHByb2JhYmlsaWRhZGVzIGFsdGFzIGVuIGxvcyBzb2JyZXZpdmllbnRlcywgbm8gaGF5IHVuIHB1bnRvIGRlIGNvcnRlIG11eSBjbGFybyBxdWUgcG9kYW1vcyBzZWxlY2Npb25hciBkZXNkZSBlc3RlIGdyw6FmaWNvLiBDb25zZWN1ZW50ZW1lbnRlLCByZWFsaXphcmVtb3Mgb3Ryb3MgZ3LDoWZpY29zIHF1ZSB0ZW5nYW4gZW4gY3VlbnRhIG3DqXRyaWNhcyBhZGljaW9uYWxlcyBwYXJhIHJlYWxpemFyIGVzdGEgZWxlY2Npw7NuLiANCg0KIyBFbGVjY2nDs24gZGVsIHB1bnRvIGNvcnRlDQoNCkNvbW8gbWVuY2lvbmFtb3MgcHJldmlhbWVudGUgbnVlc3RybyBvYmpldGl2byBhaG9yYSBzZXLDoSBlbCBkZSBlbGVnaXIgZWwgcHVudG8gZGUgY29ydGUgw7NwdGltbyBxdWUgbm9zIHBlcm1pdGEgY2xhc2lmaWNhciBhIGxhcyBvYnNlcnZhY2lvbmVzIGNvbiBlbCBtZW5vciBlcnJvciBwb3NpYmxlLiBQYXJhIHJlYWxpemFyIGVzdGEgdGFyZWEgbm9zIHZhbGRyZW1vcyB5YSBubyBkZWwgY29uanVudG8gZGUgZGF0b3MgY29uIGVsIHF1ZSBlbnRyZW5hbW9zIGVsIG1vZGVsbyBzaW5vIGNvbiBsYSBwYXJ0aWNpw7NuIGRlIHZhbGlkYWNpw7NuLiANCg0KQ29tZW56YW1vcyBnZW5lcmFuZG8gcHJlZGljY2lvbmVzIHNvYnJlIGVsIGNvbmp1bnRvIGRlIHZhbGlkYWNpw7NuLg0KDQpgYGB7cn0NCiMgRGVsIGRhdGFzZXQgZGUgdmFsaWRhY2nDs24gc2VsZWNjaW9ubyBzw7NsbyBsYXMgdmFyaWFibGVzIHF1ZSBtZSBpbnRlcmVzYW4gcGFyYSBsYSBwcmVkaWNjacOzbg0KDQpkYXRhLnZhbGlkYXRpb24ucHJlZCA8LSBkYXRhLnZhbGlkYXRpb24gJT4lIA0KICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0KFBjbGFzcyxTZXgsQWdlLEVtYmFya2VkKQ0KICAgICAgICAgICAgICAgICAgICAgICAgDQojIEdlbmVybyBwcmVkaWNjaW9uZXMNCg0KdmFsaWRhdGlvbi5wcmVkIDwtIGJlc3QubW9kZWwgJT4lIA0KICAgICAgICAgICAgICAgICAgbXV0YXRlKHByZWQgPSBtYXAobW9kLGF1Z21lbnQsbmV3ZGF0YT1kYXRhLnZhbGlkYXRpb24ucHJlZCx0eXBlLnByZWRpY3Q9J3Jlc3BvbnNlJykpICU+JSANCiAgICAgICAgICAgICAgICAgIHVubmVzdChwcmVkKSAlPiUgDQogICAgICAgICAgICAgICAgICBiaW5kX2NvbHMoZGF0YS52YWxpZGF0aW9uKQ0KYGBgDQpQYXJhIGVsZWdpciBlbCBwdW50byBkZSBjb3J0ZSByZWFsaXphcmVtb3MgdW4gZ3LDoWZpY28gZW4gZWwgY3VhbCBpbmNsdWlyZW1vcyBzaW11bHTDoW5lYW1lbnRlIGxhcyBtw6l0cmljYXMgJ2FjY3VyYWN5JywgJ3NlbnNpdGl2aXR5JywgJ3NwZWNpZmljaXR5JywgJ3ByZWNpc2lvbicgeSAncmVjYWxsJy4NCg0KYGBge3J9DQojIERlZmlubyBsYSBmdW5jaW9uIGRlIG3DqXRyaWNhcyBkZSBwcmVkaWNjacOzbg0KDQpwcmVkaWN0aW9uX21ldHJpY3MgPC0gZnVuY3Rpb24oY3V0b2ZmLCBwcmVkaWN0aW9ucz12YWxpZGF0aW9uLnByZWQpew0KICB0YWJsZSA8LSBwcmVkaWN0aW9ucyAlPiUgDQogICAgbXV0YXRlKHByZWRpY3RlZF9jbGFzcz1pZl9lbHNlKC5maXR0ZWQ+Y3V0b2ZmLCAxLCAwKSAlPiUgYXMuZmFjdG9yKCkpDQogIA0KICBjb25mdXNpb25NYXRyaXgodGFibGUodGFibGUkcHJlZGljdGVkX2NsYXNzLCB0YWJsZSRTdXJ2aXZlZCksIHBvc2l0aXZlID0gIjEiKSAlPiUNCiAgICB0aWR5KCkgJT4lDQogICAgc2VsZWN0KHRlcm0sIGVzdGltYXRlKSAlPiUNCiAgICBmaWx0ZXIodGVybSAlaW4lIGMoJ2FjY3VyYWN5JywgJ3NlbnNpdGl2aXR5JywgJ3NwZWNpZmljaXR5JywgJ3ByZWNpc2lvbicsJ3JlY2FsbCcpKSAlPiUNCiAgICBtdXRhdGUoY3V0b2ZmPWN1dG9mZikNCiAgDQp9DQoNCiMgR2VuZXJvIHNlY3VlbmNpYSBkZSBwdW50b3MgZGUgY29ydGUgcGFyYSBwYXNhcmxlIGEgbGEgZnVuY2nDs24gY29tbyBwYXLDoW1ldHJvDQoNCmN1dG9mZnMgPSBzZXEoDQogIG1pbih2YWxpZGF0aW9uLnByZWQkLmZpdHRlZCksDQogIG1heCh2YWxpZGF0aW9uLnByZWQkLmZpdHRlZCksDQogIDAuMDA1DQopDQoNCiMgR2VuZXJvIGRhdGFmcmFtZSBjb24gbG9zIGRpc3Rpbg0KbG9naXRfcHJlZD0gbWFwX2RmcihjdXRvZmZzLCBwcmVkaWN0aW9uX21ldHJpY3MpJT4lIG11dGF0ZSh0ZXJtPWFzLmZhY3Rvcih0ZXJtKSkNCg0KZ2dwbG90KGxvZ2l0X3ByZWQsIGFlcyhjdXRvZmYsZXN0aW1hdGUsIGdyb3VwPXRlcm0sIGNvbG9yPXRlcm0pKSArIGdlb21fbGluZShzaXplPTEpICsNCiAgdGhlbWVfYncoKSArDQogIGxhYnModGl0bGU9ICdBY2N1cmFjeSwgU2Vuc2l0aXZpdHksIFNwZWNpZmljaXR5LCBSZWNhbGwgeSBQcmVjaXNpb24nLCBzdWJ0aXRsZT0gJ01vZGVsbyBtNCcsIGNvbG9yPSIiKQ0KDQpgYGANCg0KQW5hbGl6YW5kbyBlbCBncsOhZmljbyBwb2RlbW9zIHBlbnNhciBlbiB1biBidWVuIHB1bnRvIGRlIGNvcnRlIGNvbW8gZWwgZGV0ZXJtaW5hZG8gcG9yIGxhIGludGVyc2VjY2nDs24gZGUgbGFzIGN1cnZhcyBkZSBzZW5zaXRpdmlkYWQgeSBlc3BlY2lmaWNpZGFkLCB5YSBxdWUgcHJvdmVlIHVuIGJ1ZW4gdHJhZGUtb2ZmIGVudHJlIGRpY2hhcyBtw6l0cmljYXMuIFBhc2FkbyBlc2UgcHVudG8gZGUgaW50ZXJzZWNjacOzbiAobyBlbCBtw6FzIHByw7N4aW1vIGEgw6lsIHF1ZSBwb2RhbW9zIGVuY29udHJhciksIGVzdGFyZW1vcyBlbnRyYW5kbyBlbiB1bmEgem9uYSBkZSBjYcOtZGEgZGUgbGEgc2Vuc2l0aXZpdHkgbyByZWNhbGwgYSBleHBlbnNhcyBkZSB1biBhdW1lbnRvIGVuIGxhIHNwZWNpZmljaXR5LiBFbiBvdHJhcyBwYWxhYnJhcywgYSBtZWRpZGEgcXVlIG3DoXMgYXVtZW50ZSBlbCBwdW50byBkZSBjb3J0ZSB0ZW5kcmVtb3MgbWVub3MgZmFsc29zIHBvc2l0aXZvcyBwZXJvIGF1bWVudGFyw6FuIGxvcyBmYWxzb3MgbmVnYXRpdm9zLiANCg0KUGVybyBzdW1hZG8gYSBlc3RvIGhheSBxdWUgdGVuZXIgZW4gY3VlbnRhIGTDs25kZSBzZSBwb3NpY2lvbmEgbGEgcGVyZm9ybWFuY2UgZGUgbnVlc3RybyBtb2RlbG8gY29uIGVzZSBwdW50byBkZSBjb3J0ZSBzZWxlY2Npb25hZG8gZW4gdMOpcm1pbm9zIGRlIGFjY3VyYWN5LCBlcyBkZWNpciwgZXZhbHVhbmRvIGxhIHByb3BvcmNpb24gZGUgb2JzZXJ2YWNpb25lcyBjb3JyZWN0YW1lbnRlIGNsYXNpZmljYWRhcyBzb2JyZSBlbCB0b3RhbCBkZSBsYXMgbWlzbWFzLiBFcyB0YW1iacOpbiBlbiBlc3RlIHB1bnRvIGRlIGludGVyc2VjY2nDs24gZWxlZ2lkbyBkb25kZSB2ZW1vcyBxdWUsIHNpIGJpZW4gbm8gZXMgbcOheGltYSwgZXN0w6EgZW4gdW4gbml2ZWwgbcOhcyBxdWUgYWNlcHRhYmxlIHNpIGNvbnNpZGVyYW1vcyBsYSBzaW1wbGljaWRhZCBkZWwgbW9kZWxvIHF1ZSBhanVzdGFtb3MuIFNpIHNvbGFtZW50ZSBub3MgZ3Vpw6FyYW1vcyBwb3IgbGEgbcOpdHJpY2EgZGUgYWNjdXJhY3kgeSBidXNjw6FyYW1vcyBlbCBwdW50byBkb25kZSDDqXN0YSBlcyBtw6F4aW1hLCB5IGRvbmRlIG90cmEgbcOpdHJpY2EgY29tbyBlcyBsYSBwcmVjaXNpw7NuIChyYXRpbyBlbnRyZSBsb3MgdmVyZGFkZXJvcyBjYXNvIHBvc2l0aXZvcyB5IGxhIHRvdGFsaWRhZCBkZSBsYXMgb2JzZXJ2YWNpb25lcyBjbGFzaWZpY2FkYXMgY29tbyBwb3NpdGl2YXMpIHRhbWJpw6luIHRpZW5lIHZhbG9yZXMgYWx0b3MsIGhhYnJlbW9zIGNvbXByb21ldGlkbyBtdWNobyBlbiB0w6lybWlub3MgZGUgcmVjYWxsIGRvbmRlIGxhIGN1cnZhIGNhZSBmdWVydGVtZW50ZSBhIHBhcnRpciBkZWwgY3V0b2ZmIH4gMC42NS4NCg0KQ2FsY3VsYW5kbyB1bmEgbnVldmEgY29sdW1uYSBjb24gbGEgZGlmZXJlbmNpYSBlbnRyZSByZWNhbGwgeSBzcGVjaWZpY2l0eSwgeSBvcmRlbmFuZG8gcG9yIGVsbGEgY29uc2lkZXJvIGxhcyBvcGNpb25lcyBxdWUgcXVlZGFyb24gZW4gZWwgdG9wIDEwIGRlbCBsaXN0YWRvLiBBbCBzZXIgw6lzdG9zIHB1bnRvcyBkZSBjb3J0ZSBtdXkgcHLDs3hpbW9zIGVudHJlIHPDrSBtZSBxdWVkbyBjb24gZWwgcXVlIG1heW9yIGFjY3VyYWN5IHByZXNlbnRhIHF1ZSBlcyBlbCBwdW50byAwLjM4OTE1NDQ4Lg0KDQpgYGB7cn0NCiMgQnVzY28gZWwgcHVudG8gZGUgY29ydGUgw7NwdGltbyBlbiBsYXMgaW5tZWRpYWNpb25lcyBkZSBsYSBpbnRlcnNlY2Npw7NuIGVudHJlIHJlY2FsbCB5IHNwZWNpZmljaXR5DQoNCmxvZ2l0X3ByZWQgJT4lIA0KICBzcHJlYWQodGVybSxlc3RpbWF0ZSkgJT4lIA0KICBtdXRhdGUoZGlmID0gcmVjYWxsIC0gc3BlY2lmaWNpdHkpICU+JSANCiAgYXJyYW5nZShhYnMoZGlmKSxkZXNjKGFjY3VyYWN5KSkNCg0KIyBHdWFyZG8gZWwgcHVudG8gZGUgY29ydGUgc2VsZWNjaW9uYWRvDQoNCmN1dG9mZl9zZWxlY2Npb25hZG8gPC0gMC4zODkxNTQ0OA0KDQpgYGANCg0KIyBNYXRyaXogZGUgY29uZnVzacOzbg0KDQoNCmBgYHtyfQ0KDQojIEdlbnJlbyBsYSBtYXRyaXogZGUgY29uZnVzaW9uIHBhcmEgY29tcGFyYXIgbG9zIHZhbG9yZXMgcmVhbGVzIHZzIGxvcyBwcmVkaWNob3MgeSBkZSBhY3VlcmRvIGFsIHB1bnRvIGRlIGNvcnRlIHNlbGVjY2lvbmFkbw0KDQp0YWJsYSA8LSB2YWxpZGF0aW9uLnByZWQgJT4lIA0KICAgIG11dGF0ZShwcmVkaWN0ZWRfY2xhc3M9aWZfZWxzZSguZml0dGVkPmN1dG9mZl9zZWxlY2Npb25hZG8sIjEiLCAiMCIpICU+JSBhcy5mYWN0b3IoKSwNCiAgICAgICAgICAgU3Vydml2ZWQ9IGZhY3RvcihTdXJ2aXZlZCkpICU+JSBzZWxlY3QocHJlZGljdGVkX2NsYXNzLFN1cnZpdmVkKQ0KDQpjb25mdXNpb25NYXRyaXgodGFibGUodGFibGEkcHJlZGljdGVkX2NsYXNzLCB0YWJsYSRTdXJ2aXZlZCksIHBvc2l0aXZlID0gIjEiKSANCg0KDQpgYGANCg0KTGEgbWF0cml6IGRlIGNvbmZ1c2nDs24gb2J0ZW5pZGEgYSBwYXJ0aXIgZGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIHZhbGlkYWNpw7NuICgyNjggY2Fzb3MgdG90YWxlcykgZXMgdW5hIG1hbmVyYSBkZSByZXN1bWlyIGVsIHJlc3VsdGFkbyBkZWwgbW9kZWxvLkVuIHN1IGRpYWdvbmFsIHByaW5jaXBhbCB2ZW1vcyBsYSBjYW50aWRhZCBkZSBjYXNvcyBjb3JyZWN0YW1lbnRlIGNsYXNpZmljYWRvcywgZW4gcGFydGljdWxhciAxMzMgaW5kaXZpZHVvcyBkZWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gcXVlIGZhbGxlY2llcm9uIGZ1ZXJvbiBjb3JyZWN0YW1lbnRlIGNsYXNpZmljYWRvcyBjb21vIHRhbDsgeSA4MiBkZSBsb3Mgc29icmV2aXZpZW50ZXMgZnVlcm9uIGNsYXNpZmljYWRvcyB0YW1iacOpbiBjb3JyZWN0YW1lbnRlIGNvbiB1biAxIGVuIHN1IGNsYXNlIHByZWRpY2hhLiANCg0KUG9yIG90cm8gbGFkbywgMTkgZGUgbGFzIHBlcnNvbmFzIHF1ZSBmdWVyb24gY2xhc2lmaWNhZG9zIGNvbW8gbm8gc29icmV2aXZpZW50ZXMgaGFiw61hbiBzb2JyZXZpdmlkbyBhIGxhIHRyYWdlZGlhIChmYWxzb3MgbmVnYXRpdm9zKSB5IGZpbmFsbWVudGUgaHViaWVyb24gMzQgY2Fzb3MgZW4gbG9zIHF1ZSBzZSBwcmVkaWpvIHF1ZSBsYXMgcGVyc29uYXMgc29icmV2aXZpcsOtYW4geSByZWFsbWVudGUgbm8gbG8gaGFiw61hbiBoZWNobyAoZmFsc29zIHBvc2l0aXZvcykuDQoNCiMgRGF0YXNldCBkZSB0ZXN0ZW8NCg0KUGFyYSBmaW5hbGl6YXIgaGFyZW1vcyBwcmVkaWNjaW9uZXMgc29icmUgZWwgY29uanVudG8gZGUgdGVzdCwgcXVlIGNvbnRpZW5lIGRhdG9zIG5vIHV0aWxpemFkb3MgaGFzdGEgZWwgbW9tZW50byBuaSBwYXJhIGVsIGVudHJlbmFtaWVudG8gbmkgcGFyYSBsYSB2YWxpZGFjacOzbiBkZWwgbW9kZWxvLiBEZSBlc3RhIGZvcm1hIHRlbmRyZW1vcyB1bmEgcHJ1ZWJhIGZlaGFjaWVudGUgZGUgbGEgY2FwYWNpZGFkIGRlIGdlbmVyYWxpemFjacOzbiBxdWUgcG9zZWUgZWwgbW9kZWxvLiANCg0KYGBge3Isd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCg0KIyBMZW8gZWwgZGF0YXNldCBkZSB0ZXN0DQoNCmRhdGEudGVzdCA8LSByZWFkX2NzdignLi9kYXRhc2V0cy90aXRhbmljX2NvbXBsZXRlX3Rlc3QuY3N2JykNCg0KDQojIENvbnZpZXJ0byBhIGZhY3RvciBhbGd1bmFzIHZhcmlhYmxlcw0KDQpkYXRhLnRlc3QgPC0gZGF0YS50ZXN0ICU+JSANCiAgc2VsZWN0KFBhc3NlbmdlcklkLFN1cnZpdmVkLFBjbGFzcyxTZXgsQWdlLFNpYlNwLFBhcmNoLEZhcmUsRW1iYXJrZWQpICU+JSANCiAgbXV0YXRlX2F0KC52YXJzID0gYygnU3Vydml2ZWQnLCdQY2xhc3MnLCdFbWJhcmtlZCcpLA0KICAgICAgICAgICAgLmZ1bnMgPSBhcy5mYWN0b3IpDQoNCmdsaW1wc2UoZGF0YS50ZXN0KSAgICAgICAgICAgICAgICAgICAgICAgICAgIA0KYGBgDQpFbCBjb25qdW50byBkZSBkYXRvcyBkZSB0ZXN0IGNvbnNpc3RlIGRlIDQxOCBvYnNlcnZhY2lvbmVzLg0KDQoNCmBgYHtyfQ0KDQojIERlbCBkYXRhc2V0IGRlIHZhbGlkYWNpw7NuIHNlbGVjY2lvbm8gc8OzbG8gbGFzIHZhcmlhYmxlcyBxdWUgbWUgaW50ZXJlc2FuIHBhcmEgbGEgcHJlZGljY2nDs24NCg0KZGF0YS50ZXN0LnByZWQgPC0gZGF0YS50ZXN0ICU+JSANCiAgICAgICAgICAgICAgICAgIHNlbGVjdChQY2xhc3MsU2V4LEFnZSxFbWJhcmtlZCkNCiAgICAgICAgICAgICAgICAgICAgICAgIA0KIyBHZW5lcm8gcHJlZGljY2lvbmVzDQoNCnRlc3QucHJlZCA8LSAgYmVzdC5tb2RlbCAlPiUgDQogICAgICAgICAgICAgIG11dGF0ZShwcmVkID0gbWFwKG1vZCxhdWdtZW50LG5ld2RhdGE9ZGF0YS50ZXN0LnByZWQsdHlwZS5wcmVkaWN0PSdyZXNwb25zZScpKSAlPiUgDQogICAgICAgICAgICAgIHVubmVzdChwcmVkKSAlPiUgDQogICAgICAgICAgICAgIGJpbmRfY29scyhkYXRhLnRlc3QpICU+JSANCiAgICAgICAgICAgICAgbXV0YXRlKHByZWRpY3RlZF9jbGFzcyA9IGlmX2Vsc2UoLmZpdHRlZD5jdXRvZmZfc2VsZWNjaW9uYWRvLCIxIiwiMCIpICU+JSAgYXMuZmFjdG9yKCkpDQoNCg0KIyBBcm1vIGxhIHRhYmxhIGNvbiBsb3MgdmFsb3JlcyBwcmVkaWNob3MgeSByZWFsZXMNCg0KdGFibGEudGVzdCA8LSAgIHRlc3QucHJlZCAlPiUgDQogICAgICAgICAgICAgICAgbXV0YXRlKFN1cnZpdmVkPSBmYWN0b3IoU3Vydml2ZWQpKSAlPiUgDQogICAgICAgICAgICAgICAgc2VsZWN0KHByZWRpY3RlZF9jbGFzcyxTdXJ2aXZlZCkNCg0KIyBHZW5lcm8gbWF0cml6IGRlIGNvbmZ1c2nDs24NCg0KY29uZnVzaW9uTWF0cml4KHRhYmxlKHRhYmxhLnRlc3QkcHJlZGljdGVkX2NsYXNzLCB0YWJsYS50ZXN0JFN1cnZpdmVkKSwgcG9zaXRpdmUgPSAiMSIpIA0KDQpgYGANCg0KQ29tbyBlcmEgZXNwZXJhYmxlLCBsYSBwZXJmb3JtYW5jZSBzb2JyZSBlbCBjb25qdW50byBkZSB0ZXN0IGVzIGxldmVtZW50ZSBpbmZlcmlvciBjb24gcmVzcGVjdG8gYWwgY29uanVudG8gZGUgdmFsaWRhY2nDs24gZGFkbyBxdWUgZW4gZXN0ZSDDumx0aW1vIHNlIG9wdGltaXrDsyBlbCBwdW50byBkZSBjb3J0ZS4gRGUgbG9zIDQxOCBpbmRpdmlkdW9zIHBlcnRlbmVjaWVudGVzIGFsIGRhdGFzZXQgZGUgdGVzdGluZyAzMDkgZnVlcm9uIGNvcnJlY3RhbWVudGUgY2xhc2lmaWNhZG9zIGVuIHN1cyBjYXRlZ29yw61hcyBjb3JyZXNwb25kaWVudGVzLCBlcyBkZWNpciBlc3RvIG5vcyBkYSBjYXNpIHVuIHJhdGlvIGRlIDMgZGUgY2FkYSA0IGNvcnJlY3RhbWVudGUgY2xhc2lmaWNhZG9zLg0KDQpDb24gdW4gYWNjdXJhY3kgZGVsIDc0JSBjb25zaWRlcm8gcXVlIGVzdGUgZXMgdW4gYnVlbiBtb2RlbG8gdGVuaWVuZG8gZW4gY3VlbnRhIGxhIHNpbXBsaWNpZGFkIGRlbCBtaXNtbywgcXVlIHNvbG8gdXRpbGl6YSA0IHZhcmlhYmxlcyB5IG5vIHJlcXVpcmnDsyBkZSBuaW5nw7puIHRpcG8gZGUgaGlwZXJwYXJhbWV0cml6YWNpw7NuIG5pIG9wdGltaXphY2nDs24gY29tcGxlamEgbyBjb3N0b3NhLg0KDQoNCg==