Importamos datos y librerías

Primero, importamos librerias

library(readr) # Leer datos csv
library(dplyr) # Manipulación de datos
library(ggcorrplot) # Grafico de correlaciones
library(caret) # Entrenamiento y evaluacion del modelo
library(plotly) # Graficos dinamicos
library(ggplot2) # Graficos generales
library(skimr) # Resumenes estadisticos
library(neuralnet) # Modelado redes neuronales
library(knitr) # Tabla de resultado mas elegantes
library(tidyverse) # Incluye dplyr, tidyr (pivot_wider), etc.
library(gt)  # Para crear tablas bonitas
library(scales) # Para formatear porcentajes

Segundo, importamos base de datos

CrediCars <- read_delim("~/Library/Mobile Documents/com~apple~CloudDocs/Cursos/Decision Tree, Random Forest and Gradient Busting in R/Seccion 7 (Neural Networks)/CrediCars.csv", 
    delim = ";", escape_double = FALSE, trim_ws = TRUE)

En el presente cuaderno se busca reproducir de forma clara y explicativa tanto la teoría como el código necesario para implementar una red neuronal en R. El objetivo es brindar una guía práctica que combine fundamentos conceptuales con una ejecución paso a paso del modelo, de manera que sea comprensible tanto para quienes inician en el tema como para quienes desean profundizar en el uso de redes neuronales dentro del entorno de R.

Para estructurar este proceso se utilizará la metodología CRISP-DM (Cross Industry Standard Process for Data Mining), un enfoque ampliamente adoptado para proyectos de análisis de datos. Esta metodología consta de seis fases:

  1. Comprensión del negocio: se define el objetivo del modelo, en este caso, predecir el comportamiento de clientes (por ejemplo, probabilidad de incumplimiento).

  2. Comprensión de los datos: se exploran las variables disponibles y su relación con el problema de negocio.

  3. Preparación de los datos: se limpian, transforman y normalizan los datos para que sean adecuados para el modelo.

  4. Modelado: se construye y entrena la red neuronal utilizando funciones específicas de R.

  5. Evaluación: se analizan métricas de desempeño para validar la calidad del modelo.

  6. Despliegue: se explican posibles usos del modelo una vez validado, ya sea en entorno de producción o análisis estratégico.

1) Comprensión del negocio

Los datos disponibles corresponden a solicitudes de crédito de clientes de una entidad llamada CrediCars. Cada fila representa a un cliente y contiene información tanto demográfica como financiera, así como indicadores sobre el estado de pago de sus créditos. El objetivo del modelo es predecir el riesgo de incumplimiento de pagos, utilizando como variable dependiente el campo DefaulterFlag, que indica si el cliente ha caído en mora alguna vez (1) o no (0). Adicionalmente, existe una versión categorizada más detallada: Defaulter Type, que clasifica el tipo de incumplimiento en tres niveles de gravedad.

Las variables predictoras incluyen:

  • Demográficas y educativas: edad (AGE), sexo (SEXCODE), número de dependientes (NOOFDEPE), nivel educativo (QUALHSCQUAL_PG).

  • Financieras: ingreso mensual (MTHINCTH), plazo del crédito en años (TENORYR), fracción de cuota inicial (DWNPMFR), y sincronización entre fecha de pago y fecha de salario (SALDATFR).

  • Indicadores de activos y comportamiento: si posee electrodomésticos (FRICODEWASHCODE), si entregó cheques posfechados (FULLPDC), tipo de ocupación (PROFBUS).

  • Variables de localización: región (REGION) y sucursal (BRANCH) donde se aprobó el crédito.

Esta base de datos permite entender patrones que podrían influir en el comportamiento de pago, facilitando la creación de un modelo de red neuronal que anticipe la probabilidad de incumplimiento de un cliente al momento de aprobar un crédito.

Variable Descripción
ID ID Number of the Client
Contract_Status Contract Status
Start_Date Contract Start Date
AGE Age of Borrower
NOOFDEPE Number of Dependents
MTHINCTH Monthly Income
SALDATFR Salary date fraction (1 = 31st, 0.5 = 15th)
TENORYR Tenor in Years
DWNPMFR Fraction of loan in down payment
PROFBUS Business = 1, Professional = 0
QUALHSC High school qualification (flag)
QUAL_PG Post-graduate qualification (flag)
SEXCODE Male = 1, Female = 0
FULLPDC Gave post-dated checks in full (flag)
FRICODE Owns refrigerator (flag)
WASHCODE Owns washing machine (flag)
REGION Region where loan is approved
BRANCH Branch where loan is approved
DefaulterFlag 1 if customer delayed at least once, 0 otherwise
Defaulter_Type 0 = no delay, 1 = delay <90 days, 2 = delay >90 days

Así mismo, es importante mencionar que la perdida par el negocio en los errores del modelo son:

  • Falsos positivos $4.200
  • Falsos negativos $3.500

2) Comprensión de los datos

Primero, vemos un resumen descriptivo de como están los datos

# Renombramos datos como buena practica para la minipulación
df <- CrediCars

# Resumen general
skim(df)
── Data Summary ────────────────────────
                           Values
Name                       df    
Number of rows             28906 
Number of columns          20    
_______________________          
Column type frequency:           
  character                4     
  numeric                  16    
________________________         
Group variables            None  

Segundo convertimos tipo de datos para la visualización de los datos

# Convertir variables categóricas
df <- df %>%
  mutate(
    DefaulterFlag = as.factor(DefaulterFlag),
    PROFBUS = as.factor(PROFBUS),
    SEXCODE = as.factor(SEXCODE),
    QUALHSC = as.factor(QUALHSC),
    QUAL_PG = as.factor(QUAL_PG),
    FULLPDC = as.factor(FULLPDC),
    FRICODE = as.factor(FRICODE),
    WASHCODE = as.factor(WASHCODE)
  )

Vemos distribución de las edad

# 📊 Histograma de edad
ggplot(df, aes(x = AGE)) +
  geom_histogram(binwidth = 5, fill = "steelblue", color = "white") +
  labs(title = "Distribución de Edad", x = "Edad", y = "Frecuencia") +
  theme_minimal()

Vemos distribución del ingreso según el incumplimiento

# 📦 Boxplot ingreso mensual por estado de mora
ggplot(df, aes(x = DefaulterFlag, y = MTHINCTH, fill = DefaulterFlag)) +
  geom_boxplot() +
  labs(title = "Ingreso mensual según incumplimiento", x = "Incumplimiento", y = "Ingreso mensual") +
  scale_fill_manual(values = c("lightgreen", "salmon")) +
  theme_minimal()

Vemos numero de dependientes en un gráfico de barras

# 📊 Gráfico de barras: número de dependientes
ggplot(df, aes(x = as.factor(NOOFDEPE))) +
  geom_bar(fill = "darkorange") +
  labs(title = "Número de Dependientes", x = "Dependientes", y = "Cantidad") +
  theme_minimal()

Vemos gráfico dinámico de la relación entre edad e ingreso mensual, separando por incumplimiento

# ⚡ Gráfico dinámico: Edad vs Ingreso mensual según mora
ggplot(df, aes(x = AGE, y = MTHINCTH, color = DefaulterFlag)) +
  geom_point(alpha = 0.6) +
  labs(title = "Edad vs Ingreso mensual", x = "Edad", y = "Ingreso mensual", color = "Incumplimiento") +
  theme_minimal()

NA
NA

Vemos una tabla cruzada (tabla de contingencia) de incumplimiento según el tipo de cliente






# Crear tabla cruzada con proporciones
tabla_etiquetada <- df %>%
  mutate(PROFBUS = factor(PROFBUS, labels = c("Profesional", "Empresario")),
         DefaulterFlag = factor(DefaulterFlag, labels = c("No Incumple", "Incumple"))) %>%
  count(PROFBUS, DefaulterFlag) %>%
  group_by(PROFBUS) %>%
  mutate(Proporcion = n / sum(n)) %>%
  ungroup() %>%
  select(PROFBUS, DefaulterFlag, Proporcion) %>%
  pivot_wider(names_from = DefaulterFlag, values_from = Proporcion) %>%
  mutate(across(where(is.numeric), ~ scales::percent(.x, accuracy = 0.1)))

# Mostrar con gt
tabla_etiquetada %>%
  gt() %>%
  tab_header(title = "Proporción de Incumplimiento por Tipo de Ocupación") %>%
  cols_label(
    PROFBUS = "Tipo de Cliente",
    `No Incumple` = "No Incumple (%)",
    `Incumple` = "Incumple (%)"
  )
Proporción de Incumplimiento por Tipo de Ocupación
Tipo de Cliente No Incumple (%) Incumple (%)
Profesional 28.6% 71.4%
Empresario 29.9% 70.1%
NA

3) Preparación de los datos

3.1) Limpieza de datos

Primero, ajustamos el tipo de datos garantizando que Start_Date este en formato fecha

CrediCars <-  CrediCars %>% 
  mutate(Start_Date = as.Date(Start_Date,"%d/%m/%Y"))

Segundo, creamos funcion para el manejo de datos faltantes:

# Create the function FillNAs
# Assign a "fill" value according to the data type

FillNAs<-function(data_frame){ 
  fill_number<-0 
  fill_factor<-"NA_filled" 
  fill_character<-"NA_filled"
  fill_date<-as.Date("1900-01-01")
  
  
  
  # Make a loop in the columns of the data frame and according to the 
  # data type, fill the respective value and create a surrogate column
  
  for (i in 1 : ncol(data_frame)){
    if (class(data_frame[,i]) %in% c("numeric","integer")) { 
      if (any(is.na(data_frame[,i]))){
        data_frame[,paste0(colnames(data_frame)[i],"_filledNA")]<- as.factor(ifelse(is.na(data_frame[,i]),"1","0"))
        data_frame[is.na(data_frame[,i]),i]<-fill_number
      }
    } else
      if (class(data_frame[,i]) %in% c("factor")) { 
        if (any(is.na(data_frame[,i]))){
          data_frame[,i]<-as.character(data_frame[,i]) 
          data_frame[,paste0(colnames(data_frame)[i],"_filledNA")]<-as.factor(ifelse(is.na(data_frame[,i]),"1","0")) 
          data_frame[is.na(data_frame[,i]),i]<-fill_factor
          data_frame[,i]<-as.factor(data_frame[,i])
          
        }
      } else {
        if (class(data_frame[,i]) %in% c("character")) { 
          if (any(is.na(data_frame[,i]))){
            data_frame[,paste0(colnames(data_frame)[i],"_filledNA")]<- as.factor(ifelse(is.na(data_frame[,i]),"1","0"))
            data_frame[is.na(data_frame[,i]),i]<-afill_character
          }
        } else {
          if (class(data_frame[,i]) %in% c("Date")) {
            if (any(is.na(data_frame[,i]))){ 
              data_frame[,paste0(colnames(data_frame)[i],"_filledNA")]<-as.factor(ifelse(is.na(data_frame[,i]),"1","0")) 
              data_frame[is.na(data_frame[,i]),i]<-fill_date
            }
          }
        }
      }
  }
  return(data_frame)
}

Este código en R define una función llamada FillNAs cuyo propósito es llenar los valores faltantes (NA) de un data.frame con un valor predeterminado según el tipo de dato de cada columna. Además, genera una columna adicional para cada variable con NA, que indica con “1” dónde hubo un valor faltante y con “0” dónde no lo hubo. Esto es útil para mantener trazabilidad de los datos imputados (técnica conocida como creación de indicadores de imputación).

Paso a paso:

  1. Inicialización de valores por defecto:

    • Se definen los valores con los que se reemplazarán los NA:

      • Números (numéricos e enteros): se reemplazarán por 0.

      • Factores y caracteres: se reemplazan con la cadena "NA_filled".

      • Fechas (Date): se reemplazan por la fecha "1900-01-01".

  2. Iteración sobre las columnas:

    • La función recorre cada columna del data.frame usando un bucle for.
  3. Revisión del tipo de datos:

    • Utiliza class(data_frame[, i]) para verificar el tipo de dato de la columna:

      • Si es numérico o entero, y contiene valores NA, los reemplaza con 0 y crea una nueva columna con el mismo nombre más el sufijo "_filledNA" que indica con "1" dónde había NA.

      • Si es factor, convierte la columna a carácter temporalmente, realiza el mismo procedimiento de imputación y luego la convierte de nuevo a factor.

      • Si es carácter, hace lo mismo que con los factores, pero usando directamente el valor de texto "NA_filled" (aunque aquí hay un error que se menciona abajo).

      • Si es fecha, los NA se reemplazan por "1900-01-01" y también se crea la columna indicadora.

  4. Retorno del resultado:

    • La función devuelve el data.frame con los NA reemplazados y las columnas indicadoras añadidas.

Tercero, aplicamos la función al conjunto de datos previamente cargado

# apply the function FillNAs to the CrediCars dataframe
CrediCarsNA <-FillNAs(as.data.frame(CrediCars))

Cuarto, eliminamos las columnas que no son de nuestro interes

CrediCars <-  CrediCars %>% 
  select(-ContractID,-Contract_Status,-Start_Date)

Quinto, cambiamos datos categoricos a numericos

Para ello, visualizamos los valores categoricos que desemos cambiar por valores numericos:

# By regions
unique(CrediCars$Region)
[1] "BB" "DD" "EE" "GG" "FF" "CC" "AA" "HH"
# By branchs
unique(CrediCars$Branch)
 [1] "N"   "M"   "A"   "D"   "C"   "E"   "B"   "FFF" "G"   "H"   "I"   "K"   "L"  
[14] "J"  

Aplicamos case_when de para la modificacion


# Para las variables de region
CrediCars <- CrediCars %>%
  mutate(Region = case_when(
    Region == "BB" ~ 1,
    Region == "DD" ~ 2,
    Region == "EE" ~ 3,
    Region == "GG" ~ 4,
    Region == "FF" ~ 5,
    Region == "CC" ~ 6,
    Region == "AA" ~ 7,
    Region == "HH" ~ 8,
    TRUE ~ NA_real_  # por si hay valores que no están en la lista
  ))


# Para las variables de marca
CrediCars <- CrediCars %>%
  mutate(Branch = case_when(
    Branch == "N"   ~ 1,
    Branch == "M"   ~ 2,
    Branch == "A"   ~ 3,
    Branch == "D"   ~ 4,
    Branch == "C"   ~ 5,
    Branch == "E"   ~ 6,
    Branch == "B"   ~ 7,
    Branch == "FFF" ~ 8,
    Branch == "G"   ~ 9,
    Branch == "H"   ~ 10,
    Branch == "I"   ~ 11,
    Branch == "K"   ~ 12,
    Branch == "L"   ~ 13,
    Branch == "J"   ~ 14,
    TRUE ~ NA_real_  # opcional: para valores no contemplados
  ))

Finalmente nos asegurmaos de que esten en tipo numerico

# Transformamos tipo de variables
CrediCars <- CrediCars %>% 
  mutate(
    Region = as.numeric(Region),
    Branch = as.numeric(Branch)
  )

# Validamos
typeof(CrediCars$Branch)
[1] "double"
typeof(CrediCars$Region)
[1] "double"

3.2) Preparación de los datos

Primero, analizamos matriz de correlación con el fin de determinar si tenemos multicolinealidad en nuestros datos

# Calcular matriz de correlación
correlation <- cor(CrediCars, use = "complete.obs")

# Graficar
ggcorrplot(correlation, 
           hc.order = TRUE, 
           type = "lower",
           lab = TRUE, 
           lab_size = 3, 
           method = "circle",
           colors = c("red", "white", "blue"),
           title = "Matriz de Correlación")

Las variables que presentan una correlación mayor a 0.4 o menor a -0.4 podrían ser candidatas para ser eliminadas del modelo, pero esta decisión no debe basarse únicamente en criterios cuantitativos. Es necesario hacer un análisis cualitativo para determinar si realmente tiene sentido excluirlas, evaluando su relevancia y función dentro del contexto del problema.

En el análisis realizado se identificó que las variables TENORYR y DWNPMFR están altamente correlacionadas con un valor de -0.56. Al comprobar que ambas son relevantes respecto a la variable dependiente, se consideró si existía una razón cualitativa que explicara la multicolinealidad entre ellas (por ejemplo, si una depende de la otra). Como no se encontró tal relación y ambas aportan valor al modelo, se decidió conservarlas en la versión preliminar del mismo.

Segundo, normalizamos los datos ya que es mejor para redes neuronales porque ayuda a que todas las entradas estén en un mismo rango (como 0 a 1), lo que mejora la velocidad de aprendizaje, evita que algunas neuronas dominen a otras y reduce el riesgo de que el modelo se quede atascado durante el entrenamiento.

Formula de la normalización:

\[ x_{\text{norm}} = \frac{x - \min(x)}{\max(x) - \min(x)} \]

La decisión entre normalizar o escalar depende del tipo de algoritmo que se esté utilizando y de la naturaleza de los datos. La normalización (o Min-Max scaling) transforma las variables para que todas estén dentro de un rango fijo, usualmente entre 0 y 1. Este método es ideal cuando se usan algoritmos que se basan en distancias absolutas, como K-means, K-Nearest Neighbors (KNN), redes neuronales o métodos de clustering, ya que evita que variables con mayores magnitudes dominen el análisis. Además, es útil cuando los datos no siguen una distribución normal y se desea conservar la forma original de la distribución.

Formula de la estandarización:

\[ x_{\text{std}} = \frac{x - \mu}{\sigma} \]

Por otro lado, la escalación o estandarización transforma las variables para que tengan una media de 0 y una desviación estándar de 1, lo que es particularmente útil en modelos que asumen normalidad o linealidad, como la regresión lineal, la regresión logística, el análisis de componentes principales (PCA) o los modelos SVM lineales. Este enfoque es preferido cuando se busca comparar variables con diferentes unidades o rangos, y también ayuda a mitigar la influencia de valores extremos moderados. En resumen, se recomienda escalar para modelos estadísticos clásicos y normalizar para modelos basados en distancias o redes neuronales.

Aplicamos normalizacion de rangos con la libreria caret :

# Crear el modelo de normalización (Min-Max)
modelo_norm <- preProcess(CrediCars, method = "range")

# Aplicar la normalización a todo el dataset
CrediCars_normalizado <- predict(modelo_norm, CrediCars)

# Visualizamos los primeros 5
head(CrediCars_normalizado,5)

Tercero, separamos en datos de entrenamientos y de prueba. Para ello usamos el paquete caret que facilita esta separación:

# Establecer semilla para reproducibilidad
set.seed(2021)

# Crear partición estratificada (80% entrenamiento, 20% prueba)
partition_index <- createDataPartition(y = CrediCars_normalizado$DefaulterFlag, 
                                       p = 0.8, list = FALSE)

En el código se usa set.seed(2021) para fijar la semilla del generador aleatorio, lo que asegura que los resultados de la partición sean reproducibles cada vez que se ejecute el código. Luego, con createDataPartition(), se crea una partición estratificada de los datos, es decir, manteniendo la proporción de clases de la variable DefaulterFlag, que es la variable objetivo del modelo. El parámetro p = 0.8 indica que el 80 % de los datos se usará para entrenamiento, mientras que el 20 % restante quedará para prueba. Finalmente, list = FALSE hace que la salida sea un vector plano de índices en lugar de una lista, lo cual facilita su uso con slice() o subsetting.

Ya con los datos separados ahora indicamos cuales observaciones son de entrenamiento y cuales son de prueba:

# Dividir el dataset usando dplyr::slice

# Separar datos de entrenamients
training <- CrediCars_normalizado %>%
  slice(partition_index)

# Separar datos de prueba
testing  <- CrediCars_normalizado %>% 
  slice(-partition_index)

En esta parte del código, la función slice() de dplyr se utiliza para extraer filas específicas de un data frame según sus índices. Primero, CrediCars.normal %>% slice(partition_index) selecciona las filas cuyos números de fila están en partition_index, es decir, el 80 % de los datos elegidos aleatoriamente para entrenamiento. Luego, slice(-partition_index) selecciona todas las filas que no están en esa partición, es decir, el 20 % restante, que se destina al conjunto de prueba. Así, slice() permite dividir el conjunto de datos de forma clara y controlada, usando los índices generados previamente.

4) Modelado

Para el modelado de redes neuronales usamos el paquete neuralnet. Este paquete es una herramienta de R diseñada para entrenar redes neuronales artificiales mediante algoritmos como backpropagation, resilient backpropagation (con o sin retroceso de pesos) y una versión globalmente convergente modificada. Permite una configuración flexible mediante la elección personalizada de funciones de error y activación, e incluye funcionalidades como el cálculo de pesos generalizados y la estimación de intervalos de confianza para los pesos. Además, ofrece métodos para visualizar la red y sus pesos, realizar predicciones y evaluar el rendimiento del modelo, siendo útil tanto para clasificación binaria como multiclase, y adaptable a funciones de activación personalizadas.

4.1) Creamos modelo

model1 = neuralnet(DefaulterFlag~.-DefaulterType, data=training,
                   hidden=2, err.fct = "sse", threshold = 0.05,
                    linear.output = TRUE)

Este código en R entrena una red neuronal utilizando el paquete neuralnet para predecir la variable DefaulterFlag a partir de todas las demás variables del conjunto de datos training, excepto DefaulterType. La red tiene una sola capa oculta con 2 neuronas (hidden=2), utiliza la suma de errores cuadrados como función de error (err.fct = "sse"), y detiene el entrenamiento cuando el gradiente del error cae por debajo de 0.05 (threshold = 0.05). Además, se especifica que la salida debe ser lineal (linear.output = TRUE), lo cual es común en problemas de regresión. En resumen, este modelo busca predecir una variable continua o binaria sin aplicar una función de activación en la capa de salida

# Convertir la matriz a data.frame
result_df <- as.data.frame(model1$result.matrix)

# Mostrar como tabla
kable(result_df, caption = "Resumen de Resultados del Modelo")
Resumen de Resultados del Modelo
V1
error 2033.8313283
reached.threshold 0.0490273
steps 44374.0000000
Intercept.to.1layhid1 1.4232370
AGE.to.1layhid1 -0.9780690
NOOFDEPE.to.1layhid1 0.3295564
MTHINCTH.to.1layhid1 -0.0176417
SALDATFR.to.1layhid1 -0.0774310
TENORYR.to.1layhid1 4.9622208
DWNPMFR.to.1layhid1 -1.5818042
PROFBUS.to.1layhid1 0.3770548
QUALHSC.to.1layhid1 0.0908092
QUAL_PG.to.1layhid1 -0.4115170
SEXCODE.to.1layhid1 0.3285056
FULLPDC.to.1layhid1 -1.7139877
FRICODE.to.1layhid1 -0.2364627
WASHCODE.to.1layhid1 -0.1344690
Region.to.1layhid1 -0.9702726
Branch.to.1layhid1 0.8087999
Intercept.to.1layhid2 93.7919507
AGE.to.1layhid2 -4.6278093
NOOFDEPE.to.1layhid2 12.2001037
MTHINCTH.to.1layhid2 3.4027806
SALDATFR.to.1layhid2 -50.2386063
TENORYR.to.1layhid2 -3.3219125
DWNPMFR.to.1layhid2 2.4514990
PROFBUS.to.1layhid2 -4.0501572
QUALHSC.to.1layhid2 1.5697012
QUAL_PG.to.1layhid2 -2.0938727
SEXCODE.to.1layhid2 1.4829946
FULLPDC.to.1layhid2 -5.8699206
FRICODE.to.1layhid2 -0.1702011
WASHCODE.to.1layhid2 -2.1323500
Region.to.1layhid2 -73.7419229
Branch.to.1layhid2 -7.4387363
Intercept.to.DefaulterFlag -0.0287451
1layhid1.to.DefaulterFlag 0.8000771
1layhid2.to.DefaulterFlag 0.1727926
NA

🧠 Estructura general

  • error: el valor del error final del modelo después del entrenamiento. En este caso es 2033.83, lo que indica qué tan lejos están las predicciones del modelo respecto a los valores reales.

  • reached.threshold: el valor mínimo del gradiente del error alcanzado. Si es menor al umbral que se definio (threshold = 0.05), el entrenamiento se detiene

  • steps: número de iteraciones que tomó entrenar la red. Aquí fueron 44374 pasos.

🔗 Pesos de la red neuronal

Los siguientes valores representan los pesos aprendidos por la red entre las capas:

  • Intercept.to.1layhid1: peso del sesgo (bias) hacia la primera neurona oculta.

  • AGE.to.1layhid1: peso de la variable AGE hacia la primera neurona oculta.

  • ...to.1layhid2: pesos hacia la segunda neurona oculta.

  • 1layhid1.to.DefaulterFlag: peso de la primera neurona oculta hacia la neurona de salida (DefaulterFlag).

  • Intercept.to.DefaulterFlag: sesgo hacia la neurona de salida.

🧾 ¿Cómo interpretarlo?

Cada peso indica la influencia de una variable sobre una neurona. Por ejemplo:

  • Un peso positivo como TENORYR.to.1layhid1 = 3.87 sugiere que a mayor TENORYR, mayor activación en esa neurona.

  • Un peso negativo como PROFBUS.to.1layhid1 = -0.63 indica una relación inversa.sos hacia la segunda neurona oculta. 1layhid1.to.DefaulterFlag: peso de la primera neurona oculta hacia la neurona de salida (DefaulterFlag). Intercept.to.DefaulterFlag: sesgo hacia la neurona de salida.

plot(model1)

🧠 Resumen del Gráfico de la Red Neuronal

El gráfico representa visualmente cómo tu modelo de red neuronal procesa la información para predecir la variable objetivo DefaulterFlag.

🔹 Entradas (Input Layer)

Cada nodo de entrada representa una variable del conjunto de datos, como AGE, NOOFDEPDE, MTHINCTH, etc. Estas son las características que el modelo utiliza para hacer predicciones.

🔹 Sesgos (Bias)

Los valores constantes como 1 conectados a las neuronas ocultas y de salida representan los términos de sesgo. Estos permiten que las neuronas se activen incluso si todas las entradas son cero, mejorando la flexibilidad del modelo.

🔹 Capa Oculta (Hidden Layer)

Contiene neuronas que combinan las entradas con sus respectivos pesos. Cada conexión tiene un peso que indica la influencia de una variable sobre esa neurona. Pesos positivos refuerzan la señal; negativos la reducen.

🔹 Salida (Output Layer)

La neurona de salida produce el valor final de predicción (DefaulterFlag), combinando las señales de las neuronas ocultas.

🔹 Pesos

Los números en las conexiones (como 2.1224, -0.3680, etc.) son los pesos aprendidos durante el entrenamiento. Indican la fuerza y dirección de la influencia entre nodos.

4.2) Predecimos con el modelo creado

Primero, usamos el modelo entrenado (model1) para predecir los valores de la variable objetivo (DefaulterFlag) sobre el conjunto de datos de prueba (testing):

prediction = compute(model1, testing)

Segundo, obtener los valores reales desde el conjunto original :


real.defaulter <- CrediCars %>%
 slice(-partition_index,) %>% # Excluimos el indice de las filas de entramientos y solo tomamos las de prubea  (la "," indica que son filas)
 pull(DefaulterFlag) # Extraer variable como vector

Tercero, extraer las predicciones normalizadas

pred.defaulter.norm <- prediction$net.result
head(pred.defaulter.norm)
          [,1]
[1,] 0.7366943
[2,] 0.4415514
[3,] 0.9274547
[4,] 0.7971660
[5,] 0.8857299
[6,] 0.8295557

Cuarto, Denormalizar (opcional, ilustrativo). Dado que los valores ya están entre 0 y 1, no necesitas desnormalizar. Sin embaergo, de forma ilustrativa se convierte el vector de predicciones normalizadas pred.defaulter.norm en un vector numérico y lo multiplica por el valor máximo de real.defaulter. Intenta reescalar las predicciones, pero lo hace incorrectamente al usar una fórmula equivocada.

pred.defaulter <- pred.defaulter.norm %>%
  as.vector() %>%
  `*`(diff(range(real.defaulter)) + min(real.defaulter))

head(pred.defaulter)
[1] 0.7366943 0.4415514 0.9274547 0.7971660 0.8857299 0.8295557

Quinto, Crear un data frame con los resultados


DefaulterFlag <- tibble(
 real = real.defaulter,
 prediction = pred.defaulter
)

head(DefaulterFlag)

Sexto, establecemos un umbral que de la probabilidad de la predicción que minimice las perdidas de falsos positivos y falsos negativos, teniendo en cuenta los costos asociados de $4.200 para falsos positivos y $3.500 para falsos negativos. Para ello calculamos el umbral que minimice la opción de falsos negativo y falsos positivos, que a su vez minimices los costos que tendría el error del modelo


# Costos definidos
cost_fp <- 4200  # Costo de falso positivo
cost_fn <- 3500  # Costo de falso negativo

# Función para calcular el costo total dado un umbral
calcular_costo <- function(umbral, data) {
  pred_clasificada <- ifelse(data$prediction >= umbral, 1, 0)
  fp <- sum(pred_clasificada == 1 & data$real == 0)
  fn <- sum(pred_clasificada == 0 & data$real == 1)
  return(fp * cost_fp + fn * cost_fn)
}

# Secuencia de umbrales
umbrales <- seq(0, 1, by = 0.001)

# Calcular el costo para cada umbral
costos <- sapply(umbrales, calcular_costo, data = DefaulterFlag)

# Crear un dataframe para graficar
df_costos <- data.frame(umbral = umbrales, costo = costos)

# Encontrar el umbral óptimo
umbral_optimo <- df_costos$umbral[which.min(df_costos$costo)]
costo_minimo <- min(df_costos$costo)

# Mostrar resultados
cat("Umbral óptimo:", round(umbral_optimo, 3), "\n")
Umbral óptimo: 0.527 
cat("Costo mínimo total:", costo_minimo, "\n")
Costo mínimo total: 6112400 
# Graficar con ggplot2
ggplot(df_costos, aes(x = umbral, y = costo)) +
  geom_line(color = "steelblue", size = 1) +
  geom_vline(xintercept = umbral_optimo, color = "red", linetype = "dashed") +
  geom_hline(yintercept = costo_minimo, color = "darkgreen", linetype = "dotted") +
  annotate("text", x = umbral_optimo, y = costo_minimo + 10000,
           label = paste0("Umbral óptimo = ", round(umbral_optimo, 3)),
           color = "red", hjust = -0.1) +
  annotate("text", x = 0.05, y = costo_minimo,
           label = paste0("Costo mínimo = $", format(costo_minimo, big.mark = ",")),
           color = "darkgreen", vjust = -1) +
  labs(title = "Costo total vs. Umbral de predicción",
       x = "Umbral de clasificación",
       y = "Costo total") +
  theme_minimal()

5) Evaluación

Calculamos la matriz de confusión

📌 Interpretación de métricas de clasificación

Métrica ¿Qué evalúa? Valor agregado
Accuracy Proporción de predicciones correctas sobre el total. Útil como visión general del desempeño, pero puede ser engañosa si hay desbalance entre clases.
Kappa Acuerdo entre predicción y realidad, ajustado por el azar. Evalúa si el modelo realmente aporta valor más allá de una clasificación aleatoria.
Precision De los casos predichos como positivos, ¿cuántos lo eran realmente? Minimiza falsos positivos. Clave si actuar sobre un falso positivo tiene un costo (ej. rechazar crédito).
Recall (Sensibilidad) De los casos realmente positivos, ¿cuántos fueron detectados por el modelo? Minimiza falsos negativos. Fundamental si omitir un caso positivo es riesgoso (ej. no detectar fraude).
F1 Score Promedio armónico entre precisión y recall. Resume el balance entre precisión y recall. Ideal cuando hay desbalance de clases.
Specificity De los casos negativos reales, ¿cuántos fueron correctamente clasificados? Evalúa la capacidad del modelo para no etiquetar erróneamente a los negativos.
Balanced Accuracy Promedio entre sensibilidad y especificidad. Corrige el sesgo de accuracy en datasets desbalanceados.

# Clasificar según el umbral
DefaulterFlag <- DefaulterFlag %>%
  mutate(predicted_class = ifelse(prediction >= umbral_optimo, 1, 0),
         real = as.factor(real),
         predicted_class = as.factor(predicted_class))

# Crear matriz de confusión
matriz <- confusionMatrix(data = DefaulterFlag$predicted_class, reference = DefaulterFlag$real, positive = "1")

# Mostrar métricas
print(matriz)
Confusion Matrix and Statistics

          Reference
Prediction    0    1
         0  589  454
         1 1077 3661
                                          
               Accuracy : 0.7352          
                 95% CI : (0.7236, 0.7465)
    No Information Rate : 0.7118          
    P-Value [Acc > NIR] : 4.135e-05       
                                          
                  Kappa : 0.2737          
                                          
 Mcnemar's Test P-Value : < 2.2e-16       
                                          
            Sensitivity : 0.8897          
            Specificity : 0.3535          
         Pos Pred Value : 0.7727          
         Neg Pred Value : 0.5647          
             Prevalence : 0.7118          
         Detection Rate : 0.6333          
   Detection Prevalence : 0.8196          
      Balanced Accuracy : 0.6216          
                                          
       'Positive' Class : 1               
                                          
# Extraer tabla para graficar
tabla <- as.data.frame(matriz$table)
colnames(tabla) <- c("Real", "Predicho", "Frecuencia")

# Graficar matriz de confusión
ggplot(tabla, aes(x = Predicho, y = Real, fill = Frecuencia)) +
  geom_tile(color = "white") +
  geom_text(aes(label = Frecuencia), size = 6) +
  scale_fill_gradient(low = "lightblue", high = "steelblue") +
  labs(title = "Matriz de Confusión", x = "Predicción", y = "Valor Real") +
  theme_minimal()

NA
NA

Ahora vemos las métricas de forma explicita

# Accuracy
accuracy <- matriz$overall["Accuracy"]

# Kappa (medida de concordancia)
kappa <- matriz$overall["Kappa"]

# Métricas por clase
precision <- matriz$byClass["Precision"]
recall <- matriz$byClass["Recall"]  # también conocido como Sensitivity
f1 <- matriz$byClass["F1"]

# También podemos extraer otras métricas útiles:
specificity <- matriz$byClass["Specificity"]
balanced_accuracy <- matriz$byClass["Balanced Accuracy"]

metricas <- data.frame(
  Accuracy = accuracy,
  Kappa = kappa,
  Precision = precision,
  Recall = recall,
  F1_Score = f1,
  Specificity = specificity,
  Balanced_Accuracy = balanced_accuracy
)

print(metricas)
NA
NA

📊 Evaluación del Modelo de Clasificación

Se evaluó el desempeño del modelo utilizando métricas estándar de clasificación binaria. A continuación se presentan los resultados obtenidos:

Métrica Valor Interpretación
Accuracy 0.735 El 73.5% de las predicciones totales fueron correctas. Útil si las clases están balanceadas.
Kappa 0.274 Mide el acuerdo entre predicción y realidad, ajustado por azar. Un valor bajo indica que el modelo no mejora mucho sobre el azar.
Precision 0.773 De todas las veces que el modelo predijo “defaulter” (1), acertó el 77.3%. Importante si los falsos positivos son costosos.
Recall (Sensibilidad) 0.890 De todos los verdaderos “defaulters”, el modelo identificó correctamente el 89%. Ideal si es más grave no detectar un defaulter.
F1 Score 0.827 Promedio armónico entre precisión y recall. Buen resumen cuando hay desbalance de clases.
Specificity 0.354 Solo el 35.4% de los “no defaulters” fueron correctamente identificados. El modelo tiene dificultad para detectar correctamente los negativos.
Balanced Accuracy 0.622 Promedio entre sensibilidad y especificidad. Útil cuando las clases están desbalanceadas.

🧠 Conclusiones

  • El modelo muestra alta capacidad para identificar correctamente a los clientes con riesgo de incumplimiento (recall alto), lo cual es deseable en contextos de riesgo crediticio o cumplimiento normativo.

  • Sin embargo, la baja especificidad indica una alta tasa de falsos positivos, lo que podría llevar a rechazar clientes que en realidad no representan riesgo.

  • El F1 Score de 0.827 refleja un buen equilibrio entre precisión y sensibilidad.

  • El índice Kappa de 0.274 sugiere que el modelo tiene margen de mejora respecto a una clasificación aleatoria.

6) Despliegue

El modelo de clasificación evaluado muestra un desempeño aceptable en términos generales, con una precisión (accuracy) del 73.5% y un F1 Score de 0.827, lo que indica un buen equilibrio entre la capacidad de identificar correctamente los casos positivos (defaulters) y evitar falsos positivos. La sensibilidad (recall) es particularmente alta (0.89), lo que sugiere que el modelo es eficaz para detectar la mayoría de los casos relevantes, un aspecto crítico en contextos como el análisis de riesgo o cumplimiento normativo.

Sin embargo, la especificidad es baja (0.35), lo que implica que el modelo tiene dificultades para identificar correctamente los casos negativos (no defaulters). Esto puede traducirse en una tasa elevada de falsos positivos, lo cual podría impactar negativamente en decisiones operativas, como rechazar solicitudes de clientes que en realidad no representan riesgo. El índice Kappa (0.27) también sugiere que el modelo tiene margen de mejora respecto a una clasificación aleatoria.

En cuanto al uso de redes neuronales, estas son recomendables cuando se trabaja con grandes volúmenes de datos, relaciones no lineales complejas o múltiples variables altamente correlacionadas. Son especialmente útiles en tareas como procesamiento de lenguaje natural, visión por computadora o series temporales complejas. En el contexto de modelos de riesgo o clasificación de clientes, pueden ser útiles si se dispone de datos históricos ricos y variados, como transacciones, comportamiento digital o interacciones multicanal.

No obstante, las redes neuronales no siempre son la mejor opción. Si el conjunto de datos es pequeño, si se requiere interpretabilidad clara para auditorías o reguladores, o si el modelo debe ser fácilmente explicable para usuarios de negocio, entonces modelos más simples como árboles de decisión, regresión logística o modelos basados en reglas pueden ser preferibles. En estos casos, la transparencia y la facilidad de implementación pueden pesar más que una ligera mejora en la precisión.

LS0tCnRpdGxlOiAiQW5hbMOtdGljYSBkZSBuZWdvY2lvcyB1c2FuZG8gcmVkZXMgbmV1cm9uYWxlcyIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlICAgICAgICAgICMgQWN0aXZhIHRhYmxhIGRlIGNvbnRlbmlkbwogICAgdG9jX2RlcHRoOiAzICAgICAgICMgUHJvZnVuZGlkYWQgZGUgbGEgVE9DIChwb3IgZGVmZWN0byBlcyAzKQogICAgdG9jX2Zsb2F0OiB0cnVlICAgICMgVGFibGEgZGUgY29udGVuaWRvIGZsb3RhbnRlCiAgICAjbnVtYmVyX3NlY3Rpb25zOiB0cnVlICAjIE51bWVyYSBzZWNjaW9uZXMKICAgIGZpZ193aWR0aDogNyAgICAgICAjIEFuY2hvIHByZWRldGVybWluYWRvIGRlIGZpZ3VyYXMgKGVuIHB1bGdhZGFzKQogICAgZmlnX2hlaWdodDogNSAgICAgICMgQWx0byBwcmVkZXRlcm1pbmFkbyBkZSBmaWd1cmFzIChlbiBwdWxnYWRhcykKICAgICN0aGVtZTogdW5pdGVkICAgICAgIyBUZW1hIHZpc3VhbCBkZSBCb290c3RyYXAgKG9wY2lvbmFsKQogICAgaGlnaGxpZ2h0OiB0YW5nbyAgICAjIFRlbWEgZGUgcmVzYWx0YWRvIGRlIGPDs2RpZ28KLS0tCgojICoqSW1wb3J0YW1vcyBkYXRvcyB5IGxpYnJlcsOtYXMqKgoKKipQcmltZXJvKiosIGltcG9ydGFtb3MgbGlicmVyaWFzCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KHJlYWRyKSAjIExlZXIgZGF0b3MgY3N2CmxpYnJhcnkoZHBseXIpICMgTWFuaXB1bGFjacOzbiBkZSBkYXRvcwpsaWJyYXJ5KGdnY29ycnBsb3QpICMgR3JhZmljbyBkZSBjb3JyZWxhY2lvbmVzCmxpYnJhcnkoY2FyZXQpICMgRW50cmVuYW1pZW50byB5IGV2YWx1YWNpb24gZGVsIG1vZGVsbwpsaWJyYXJ5KHBsb3RseSkgIyBHcmFmaWNvcyBkaW5hbWljb3MKbGlicmFyeShnZ3Bsb3QyKSAjIEdyYWZpY29zIGdlbmVyYWxlcwpsaWJyYXJ5KHNraW1yKSAjIFJlc3VtZW5lcyBlc3RhZGlzdGljb3MKbGlicmFyeShuZXVyYWxuZXQpICMgTW9kZWxhZG8gcmVkZXMgbmV1cm9uYWxlcwpsaWJyYXJ5KGtuaXRyKSAjIFRhYmxhIGRlIHJlc3VsdGFkbyBtYXMgZWxlZ2FudGVzCmxpYnJhcnkodGlkeXZlcnNlKSAjIEluY2x1eWUgZHBseXIsIHRpZHlyIChwaXZvdF93aWRlciksIGV0Yy4KbGlicmFyeShndCkgICMgUGFyYSBjcmVhciB0YWJsYXMgYm9uaXRhcwpsaWJyYXJ5KHNjYWxlcykgIyBQYXJhIGZvcm1hdGVhciBwb3JjZW50YWplcwoKCmBgYAoKKipTZWd1bmRvKiosIGltcG9ydGFtb3MgYmFzZSBkZSBkYXRvcwoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KQ3JlZGlDYXJzIDwtIHJlYWRfZGVsaW0oIn4vTGlicmFyeS9Nb2JpbGUgRG9jdW1lbnRzL2NvbX5hcHBsZX5DbG91ZERvY3MvQ3Vyc29zL0RlY2lzaW9uIFRyZWUsIFJhbmRvbSBGb3Jlc3QgYW5kIEdyYWRpZW50IEJ1c3RpbmcgaW4gUi9TZWNjaW9uIDcgKE5ldXJhbCBOZXR3b3JrcykvQ3JlZGlDYXJzLmNzdiIsIAogICAgZGVsaW0gPSAiOyIsIGVzY2FwZV9kb3VibGUgPSBGQUxTRSwgdHJpbV93cyA9IFRSVUUpCmBgYAoKRW4gZWwgcHJlc2VudGUgY3VhZGVybm8gc2UgYnVzY2EgcmVwcm9kdWNpciBkZSBmb3JtYSBjbGFyYSB5IGV4cGxpY2F0aXZhIHRhbnRvIGxhIHRlb3LDrWEgY29tbyBlbCBjw7NkaWdvIG5lY2VzYXJpbyBwYXJhIGltcGxlbWVudGFyIHVuYcKgKipyZWQgbmV1cm9uYWwgZW4gUioqLiBFbCBvYmpldGl2byBlcyBicmluZGFyIHVuYSBndcOtYSBwcsOhY3RpY2EgcXVlIGNvbWJpbmUgZnVuZGFtZW50b3MgY29uY2VwdHVhbGVzIGNvbiB1bmEgZWplY3VjacOzbiBwYXNvIGEgcGFzbyBkZWwgbW9kZWxvLCBkZSBtYW5lcmEgcXVlIHNlYSBjb21wcmVuc2libGUgdGFudG8gcGFyYSBxdWllbmVzIGluaWNpYW4gZW4gZWwgdGVtYSBjb21vIHBhcmEgcXVpZW5lcyBkZXNlYW4gcHJvZnVuZGl6YXIgZW4gZWwgdXNvIGRlIHJlZGVzIG5ldXJvbmFsZXMgZGVudHJvIGRlbCBlbnRvcm5vIGRlIFIuCgpQYXJhIGVzdHJ1Y3R1cmFyIGVzdGUgcHJvY2VzbyBzZSB1dGlsaXphcsOhIGxhwqAqKm1ldG9kb2xvZ8OtYSBDUklTUC1ETSAoQ3Jvc3MgSW5kdXN0cnkgU3RhbmRhcmQgUHJvY2VzcyBmb3IgRGF0YSBNaW5pbmcpKiosIHVuIGVuZm9xdWUgYW1wbGlhbWVudGUgYWRvcHRhZG8gcGFyYSBwcm95ZWN0b3MgZGUgYW7DoWxpc2lzIGRlIGRhdG9zLiBFc3RhIG1ldG9kb2xvZ8OtYSBjb25zdGEgZGUgc2VpcyBmYXNlczoKCjEuICAqKkNvbXByZW5zacOzbiBkZWwgbmVnb2NpbyoqOiBzZSBkZWZpbmUgZWwgb2JqZXRpdm8gZGVsIG1vZGVsbywgZW4gZXN0ZSBjYXNvLCBwcmVkZWNpciBlbCBjb21wb3J0YW1pZW50byBkZSBjbGllbnRlcyAocG9yIGVqZW1wbG8sIHByb2JhYmlsaWRhZCBkZSBpbmN1bXBsaW1pZW50bykuCgoyLiAgKipDb21wcmVuc2nDs24gZGUgbG9zIGRhdG9zKio6IHNlIGV4cGxvcmFuIGxhcyB2YXJpYWJsZXMgZGlzcG9uaWJsZXMgeSBzdSByZWxhY2nDs24gY29uIGVsIHByb2JsZW1hIGRlIG5lZ29jaW8uCgozLiAgKipQcmVwYXJhY2nDs24gZGUgbG9zIGRhdG9zKio6IHNlIGxpbXBpYW4sIHRyYW5zZm9ybWFuIHkgbm9ybWFsaXphbiBsb3MgZGF0b3MgcGFyYSBxdWUgc2VhbiBhZGVjdWFkb3MgcGFyYSBlbCBtb2RlbG8uCgo0LiAgKipNb2RlbGFkbyoqOiBzZSBjb25zdHJ1eWUgeSBlbnRyZW5hIGxhIHJlZCBuZXVyb25hbCB1dGlsaXphbmRvIGZ1bmNpb25lcyBlc3BlY8OtZmljYXMgZGUgUi4KCjUuICAqKkV2YWx1YWNpw7NuKio6IHNlIGFuYWxpemFuIG3DqXRyaWNhcyBkZSBkZXNlbXBlw7FvIHBhcmEgdmFsaWRhciBsYSBjYWxpZGFkIGRlbCBtb2RlbG8uCgo2LiAgKipEZXNwbGllZ3VlKio6IHNlIGV4cGxpY2FuIHBvc2libGVzIHVzb3MgZGVsIG1vZGVsbyB1bmEgdmV6IHZhbGlkYWRvLCB5YSBzZWEgZW4gZW50b3JubyBkZSBwcm9kdWNjacOzbiBvIGFuw6FsaXNpcyBlc3RyYXTDqWdpY28uCgohW10oaW1hZ2VzL2NsaXBib2FyZC0xNjI5NDY5MTMucG5nKQoKIyAxKSAqKkNvbXByZW5zacOzbiBkZWwgbmVnb2NpbyoqCgpMb3MgZGF0b3MgZGlzcG9uaWJsZXMgY29ycmVzcG9uZGVuIGEgc29saWNpdHVkZXMgZGUgY3LDqWRpdG8gZGUgY2xpZW50ZXMgZGUgdW5hIGVudGlkYWQgbGxhbWFkYcKgKkNyZWRpQ2FycyouIENhZGEgZmlsYSByZXByZXNlbnRhIGEgdW4gY2xpZW50ZSB5IGNvbnRpZW5lIGluZm9ybWFjacOzbiB0YW50byBkZW1vZ3LDoWZpY2EgY29tbyBmaW5hbmNpZXJhLCBhc8OtIGNvbW8gaW5kaWNhZG9yZXMgc29icmUgZWwgZXN0YWRvIGRlIHBhZ28gZGUgc3VzIGNyw6lkaXRvcy4gRWwgb2JqZXRpdm8gZGVsIG1vZGVsbyBlcyBwcmVkZWNpciBlbMKgKipyaWVzZ28gZGUgaW5jdW1wbGltaWVudG8gZGUgcGFnb3MqKiwgdXRpbGl6YW5kbyBjb21vIHZhcmlhYmxlIGRlcGVuZGllbnRlIGVsIGNhbXBvwqAqKmBEZWZhdWx0ZXJGbGFnYCoqLCBxdWUgaW5kaWNhIHNpIGVsIGNsaWVudGUgaGEgY2HDrWRvIGVuIG1vcmEgYWxndW5hIHZleiAoMSkgbyBubyAoMCkuIEFkaWNpb25hbG1lbnRlLCBleGlzdGUgdW5hIHZlcnNpw7NuIGNhdGVnb3JpemFkYSBtw6FzIGRldGFsbGFkYTrCoCoqYERlZmF1bHRlciBUeXBlYCoqLCBxdWUgY2xhc2lmaWNhIGVsIHRpcG8gZGUgaW5jdW1wbGltaWVudG8gZW4gdHJlcyBuaXZlbGVzIGRlIGdyYXZlZGFkLgoKTGFzIHZhcmlhYmxlcyBwcmVkaWN0b3JhcyBpbmNsdXllbjoKCi0gICAqKkRlbW9ncsOhZmljYXMgeSBlZHVjYXRpdmFzKio6IGVkYWQgKGBBR0VgKSwgc2V4byAoYFNFWENPREVgKSwgbsO6bWVybyBkZSBkZXBlbmRpZW50ZXMgKGBOT09GREVQRWApLCBuaXZlbCBlZHVjYXRpdm8gKGBRVUFMSFNDYCzCoGBRVUFMX1BHYCkuCgotICAgKipGaW5hbmNpZXJhcyoqOiBpbmdyZXNvIG1lbnN1YWwgKGBNVEhJTkNUSGApLCBwbGF6byBkZWwgY3LDqWRpdG8gZW4gYcOxb3MgKGBURU5PUllSYCksIGZyYWNjacOzbiBkZSBjdW90YSBpbmljaWFsIChgRFdOUE1GUmApLCB5IHNpbmNyb25pemFjacOzbiBlbnRyZSBmZWNoYSBkZSBwYWdvIHkgZmVjaGEgZGUgc2FsYXJpbyAoYFNBTERBVEZSYCkuCgotICAgKipJbmRpY2Fkb3JlcyBkZSBhY3Rpdm9zIHkgY29tcG9ydGFtaWVudG8qKjogc2kgcG9zZWUgZWxlY3Ryb2RvbcOpc3RpY29zIChgRlJJQ09ERWAswqBgV0FTSENPREVgKSwgc2kgZW50cmVnw7MgY2hlcXVlcyBwb3NmZWNoYWRvcyAoYEZVTExQRENgKSwgdGlwbyBkZSBvY3VwYWNpw7NuIChgUFJPRkJVU2ApLgoKLSAgICoqVmFyaWFibGVzIGRlIGxvY2FsaXphY2nDs24qKjogcmVnacOzbiAoYFJFR0lPTmApIHkgc3VjdXJzYWwgKGBCUkFOQ0hgKSBkb25kZSBzZSBhcHJvYsOzIGVsIGNyw6lkaXRvLgoKRXN0YSBiYXNlIGRlIGRhdG9zIHBlcm1pdGUgZW50ZW5kZXIgcGF0cm9uZXMgcXVlIHBvZHLDrWFuIGluZmx1aXIgZW4gZWwgY29tcG9ydGFtaWVudG8gZGUgcGFnbywgZmFjaWxpdGFuZG8gbGEgY3JlYWNpw7NuIGRlIHVuIG1vZGVsbyBkZSByZWQgbmV1cm9uYWwgcXVlIGFudGljaXBlIGxhIHByb2JhYmlsaWRhZCBkZSBpbmN1bXBsaW1pZW50byBkZSB1biBjbGllbnRlIGFsIG1vbWVudG8gZGUgYXByb2JhciB1biBjcsOpZGl0by4KCnwgVmFyaWFibGUgICAgICAgIHwgRGVzY3JpcGNpw7NuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwtLS0tLS0tLS0tLS0tLS0tLXwtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLXwKfCBJRCAgICAgICAgICAgICAgfCBJRCBOdW1iZXIgb2YgdGhlIENsaWVudCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IENvbnRyYWN0X1N0YXR1cyB8IENvbnRyYWN0IFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgU3RhcnRfRGF0ZSAgICAgIHwgQ29udHJhY3QgU3RhcnQgRGF0ZSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBBR0UgICAgICAgICAgICAgfCBBZ2Ugb2YgQm9ycm93ZXIgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IE5PT0ZERVBFICAgICAgICB8IE51bWJlciBvZiBEZXBlbmRlbnRzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgTVRISU5DVEggICAgICAgIHwgTW9udGhseSBJbmNvbWUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBTQUxEQVRGUiAgICAgICAgfCBTYWxhcnkgZGF0ZSBmcmFjdGlvbiAoMSA9IDMxc3QsIDAuNSA9IDE1dGgpICAgICAgICAgICAgfAp8IFRFTk9SWVIgICAgICAgICB8IFRlbm9yIGluIFllYXJzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgRFdOUE1GUiAgICAgICAgIHwgRnJhY3Rpb24gb2YgbG9hbiBpbiBkb3duIHBheW1lbnQgICAgICAgICAgICAgICAgICAgICAgIHwKfCBQUk9GQlVTICAgICAgICAgfCBCdXNpbmVzcyA9IDEsIFByb2Zlc3Npb25hbCA9IDAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IFFVQUxIU0MgICAgICAgICB8IEhpZ2ggc2Nob29sIHF1YWxpZmljYXRpb24gKGZsYWcpICAgICAgICAgICAgICAgICAgICAgICB8CnwgUVVBTF9QRyAgICAgICAgIHwgUG9zdC1ncmFkdWF0ZSBxdWFsaWZpY2F0aW9uIChmbGFnKSAgICAgICAgICAgICAgICAgICAgIHwKfCBTRVhDT0RFICAgICAgICAgfCBNYWxlID0gMSwgRmVtYWxlID0gMCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IEZVTExQREMgICAgICAgICB8IEdhdmUgcG9zdC1kYXRlZCBjaGVja3MgaW4gZnVsbCAoZmxhZykgICAgICAgICAgICAgICAgICB8CnwgRlJJQ09ERSAgICAgICAgIHwgT3ducyByZWZyaWdlcmF0b3IgKGZsYWcpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBXQVNIQ09ERSAgICAgICAgfCBPd25zIHdhc2hpbmcgbWFjaGluZSAoZmxhZykgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IFJFR0lPTiAgICAgICAgICB8IFJlZ2lvbiB3aGVyZSBsb2FuIGlzIGFwcHJvdmVkICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgQlJBTkNIICAgICAgICAgIHwgQnJhbmNoIHdoZXJlIGxvYW4gaXMgYXBwcm92ZWQgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBEZWZhdWx0ZXJGbGFnICAgfCAxIGlmIGN1c3RvbWVyIGRlbGF5ZWQgYXQgbGVhc3Qgb25jZSwgMCBvdGhlcndpc2UgICAgICAgfAp8IERlZmF1bHRlcl9UeXBlICB8IDAgPSBubyBkZWxheSwgMSA9IGRlbGF5IFw8OTAgZGF5cywgMiA9IGRlbGF5IFw+OTAgZGF5cyB8CgpBc8OtIG1pc21vLCBlcyBpbXBvcnRhbnRlIG1lbmNpb25hciBxdWUgbGEgcGVyZGlkYSBwYXIgZWwgbmVnb2NpbyBlbiBsb3MgZXJyb3JlcyBkZWwgbW9kZWxvIHNvbjoKCi0gICBGYWxzb3MgcG9zaXRpdm9zIFwkNC4yMDAKLSAgIEZhbHNvcyBuZWdhdGl2b3MgXCQzLjUwMAoKIyAqKjIpIENvbXByZW5zacOzbiBkZSBsb3MgZGF0b3MqKgoKKipQcmltZXJvKiosIHZlbW9zIHVuIHJlc3VtZW4gZGVzY3JpcHRpdm8gZGUgY29tbyBlc3TDoW4gbG9zIGRhdG9zCgpgYGB7cn0KIyBSZW5vbWJyYW1vcyBkYXRvcyBjb21vIGJ1ZW5hIHByYWN0aWNhIHBhcmEgbGEgbWluaXB1bGFjacOzbgpkZiA8LSBDcmVkaUNhcnMKCiMgUmVzdW1lbiBnZW5lcmFsCnNraW0oZGYpCmBgYAoKKipTZWd1bmRvKiogY29udmVydGltb3MgdGlwbyBkZSBkYXRvcyBwYXJhIGxhIHZpc3VhbGl6YWNpw7NuIGRlIGxvcyBkYXRvcwoKYGBge3J9CiMgQ29udmVydGlyIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMKZGYgPC0gZGYgJT4lCiAgbXV0YXRlKAogICAgRGVmYXVsdGVyRmxhZyA9IGFzLmZhY3RvcihEZWZhdWx0ZXJGbGFnKSwKICAgIFBST0ZCVVMgPSBhcy5mYWN0b3IoUFJPRkJVUyksCiAgICBTRVhDT0RFID0gYXMuZmFjdG9yKFNFWENPREUpLAogICAgUVVBTEhTQyA9IGFzLmZhY3RvcihRVUFMSFNDKSwKICAgIFFVQUxfUEcgPSBhcy5mYWN0b3IoUVVBTF9QRyksCiAgICBGVUxMUERDID0gYXMuZmFjdG9yKEZVTExQREMpLAogICAgRlJJQ09ERSA9IGFzLmZhY3RvcihGUklDT0RFKSwKICAgIFdBU0hDT0RFID0gYXMuZmFjdG9yKFdBU0hDT0RFKQogICkKYGBgCgpWZW1vcyBkaXN0cmlidWNpw7NuIGRlIGxhcyBlZGFkCgpgYGB7cn0KIyDwn5OKIEhpc3RvZ3JhbWEgZGUgZWRhZApnZ3Bsb3QoZGYsIGFlcyh4ID0gQUdFKSkgKwogIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gNSwgZmlsbCA9ICJzdGVlbGJsdWUiLCBjb2xvciA9ICJ3aGl0ZSIpICsKICBsYWJzKHRpdGxlID0gIkRpc3RyaWJ1Y2nDs24gZGUgRWRhZCIsIHggPSAiRWRhZCIsIHkgPSAiRnJlY3VlbmNpYSIpICsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgpWZW1vcyBkaXN0cmlidWNpw7NuIGRlbCBpbmdyZXNvIHNlZ8O6biBlbCBpbmN1bXBsaW1pZW50bwoKYGBge3J9CiMg8J+TpiBCb3hwbG90IGluZ3Jlc28gbWVuc3VhbCBwb3IgZXN0YWRvIGRlIG1vcmEKZ2dwbG90KGRmLCBhZXMoeCA9IERlZmF1bHRlckZsYWcsIHkgPSBNVEhJTkNUSCwgZmlsbCA9IERlZmF1bHRlckZsYWcpKSArCiAgZ2VvbV9ib3hwbG90KCkgKwogIGxhYnModGl0bGUgPSAiSW5ncmVzbyBtZW5zdWFsIHNlZ8O6biBpbmN1bXBsaW1pZW50byIsIHggPSAiSW5jdW1wbGltaWVudG8iLCB5ID0gIkluZ3Jlc28gbWVuc3VhbCIpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjKCJsaWdodGdyZWVuIiwgInNhbG1vbiIpKSArCiAgdGhlbWVfbWluaW1hbCgpCmBgYAoKVmVtb3MgbnVtZXJvIGRlIGRlcGVuZGllbnRlcyBlbiB1biBncsOhZmljbyBkZSBiYXJyYXMKCmBgYHtyfQojIPCfk4ogR3LDoWZpY28gZGUgYmFycmFzOiBuw7ptZXJvIGRlIGRlcGVuZGllbnRlcwpnZ3Bsb3QoZGYsIGFlcyh4ID0gYXMuZmFjdG9yKE5PT0ZERVBFKSkpICsKICBnZW9tX2JhcihmaWxsID0gImRhcmtvcmFuZ2UiKSArCiAgbGFicyh0aXRsZSA9ICJOw7ptZXJvIGRlIERlcGVuZGllbnRlcyIsIHggPSAiRGVwZW5kaWVudGVzIiwgeSA9ICJDYW50aWRhZCIpICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKVmVtb3MgZ3LDoWZpY28gZGluw6FtaWNvIGRlIGxhIHJlbGFjacOzbiBlbnRyZSBlZGFkIGUgaW5ncmVzbyBtZW5zdWFsLCBzZXBhcmFuZG8gcG9yIGluY3VtcGxpbWllbnRvCgpgYGB7cn0KIyDimqEgR3LDoWZpY28gZGluw6FtaWNvOiBFZGFkIHZzIEluZ3Jlc28gbWVuc3VhbCBzZWfDum4gbW9yYQpnZ3Bsb3QoZGYsIGFlcyh4ID0gQUdFLCB5ID0gTVRISU5DVEgsIGNvbG9yID0gRGVmYXVsdGVyRmxhZykpICsKICBnZW9tX3BvaW50KGFscGhhID0gMC42KSArCiAgbGFicyh0aXRsZSA9ICJFZGFkIHZzIEluZ3Jlc28gbWVuc3VhbCIsIHggPSAiRWRhZCIsIHkgPSAiSW5ncmVzbyBtZW5zdWFsIiwgY29sb3IgPSAiSW5jdW1wbGltaWVudG8iKSArCiAgdGhlbWVfbWluaW1hbCgpCgoKYGBgCgpWZW1vcyB1bmEgdGFibGEgY3J1emFkYSAodGFibGEgZGUgY29udGluZ2VuY2lhKSBkZSBpbmN1bXBsaW1pZW50byBzZWfDum4gZWwgdGlwbyBkZSBjbGllbnRlCgpgYGB7cn0KCgoKCgojIENyZWFyIHRhYmxhIGNydXphZGEgY29uIHByb3BvcmNpb25lcwp0YWJsYV9ldGlxdWV0YWRhIDwtIGRmICU+JQogIG11dGF0ZShQUk9GQlVTID0gZmFjdG9yKFBST0ZCVVMsIGxhYmVscyA9IGMoIlByb2Zlc2lvbmFsIiwgIkVtcHJlc2FyaW8iKSksCiAgICAgICAgIERlZmF1bHRlckZsYWcgPSBmYWN0b3IoRGVmYXVsdGVyRmxhZywgbGFiZWxzID0gYygiTm8gSW5jdW1wbGUiLCAiSW5jdW1wbGUiKSkpICU+JQogIGNvdW50KFBST0ZCVVMsIERlZmF1bHRlckZsYWcpICU+JQogIGdyb3VwX2J5KFBST0ZCVVMpICU+JQogIG11dGF0ZShQcm9wb3JjaW9uID0gbiAvIHN1bShuKSkgJT4lCiAgdW5ncm91cCgpICU+JQogIHNlbGVjdChQUk9GQlVTLCBEZWZhdWx0ZXJGbGFnLCBQcm9wb3JjaW9uKSAlPiUKICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gRGVmYXVsdGVyRmxhZywgdmFsdWVzX2Zyb20gPSBQcm9wb3JjaW9uKSAlPiUKICBtdXRhdGUoYWNyb3NzKHdoZXJlKGlzLm51bWVyaWMpLCB+IHNjYWxlczo6cGVyY2VudCgueCwgYWNjdXJhY3kgPSAwLjEpKSkKCiMgTW9zdHJhciBjb24gZ3QKdGFibGFfZXRpcXVldGFkYSAlPiUKICBndCgpICU+JQogIHRhYl9oZWFkZXIodGl0bGUgPSAiUHJvcG9yY2nDs24gZGUgSW5jdW1wbGltaWVudG8gcG9yIFRpcG8gZGUgT2N1cGFjacOzbiIpICU+JQogIGNvbHNfbGFiZWwoCiAgICBQUk9GQlVTID0gIlRpcG8gZGUgQ2xpZW50ZSIsCiAgICBgTm8gSW5jdW1wbGVgID0gIk5vIEluY3VtcGxlICglKSIsCiAgICBgSW5jdW1wbGVgID0gIkluY3VtcGxlICglKSIKICApCgpgYGAKCiMgMykgKipQcmVwYXJhY2nDs24gZGUgbG9zIGRhdG9zKioKCiMjIDMuMSkgTGltcGllemEgZGUgZGF0b3MKCioqUHJpbWVybyoqLCBhanVzdGFtb3MgZWwgdGlwbyBkZSBkYXRvcyBnYXJhbnRpemFuZG8gcXVlIGBTdGFydF9EYXRlYCBlc3RlIGVuIGZvcm1hdG8gZmVjaGEKCmBgYHtyfQpDcmVkaUNhcnMgPC0gIENyZWRpQ2FycyAlPiUgCiAgbXV0YXRlKFN0YXJ0X0RhdGUgPSBhcy5EYXRlKFN0YXJ0X0RhdGUsIiVkLyVtLyVZIikpCmBgYAoKKipTZWd1bmRvKiosIGNyZWFtb3MgZnVuY2lvbiBwYXJhIGVsIG1hbmVqbyBkZSBkYXRvcyBmYWx0YW50ZXM6CgpgYGB7cn0KIyBDcmVhdGUgdGhlIGZ1bmN0aW9uIEZpbGxOQXMKIyBBc3NpZ24gYSAiZmlsbCIgdmFsdWUgYWNjb3JkaW5nIHRvIHRoZSBkYXRhIHR5cGUKCkZpbGxOQXM8LWZ1bmN0aW9uKGRhdGFfZnJhbWUpeyAKICBmaWxsX251bWJlcjwtMCAKICBmaWxsX2ZhY3RvcjwtIk5BX2ZpbGxlZCIgCiAgZmlsbF9jaGFyYWN0ZXI8LSJOQV9maWxsZWQiCiAgZmlsbF9kYXRlPC1hcy5EYXRlKCIxOTAwLTAxLTAxIikKICAKICAKICAKICAjIE1ha2UgYSBsb29wIGluIHRoZSBjb2x1bW5zIG9mIHRoZSBkYXRhIGZyYW1lIGFuZCBhY2NvcmRpbmcgdG8gdGhlIAogICMgZGF0YSB0eXBlLCBmaWxsIHRoZSByZXNwZWN0aXZlIHZhbHVlIGFuZCBjcmVhdGUgYSBzdXJyb2dhdGUgY29sdW1uCiAgCiAgZm9yIChpIGluIDEgOiBuY29sKGRhdGFfZnJhbWUpKXsKICAgIGlmIChjbGFzcyhkYXRhX2ZyYW1lWyxpXSkgJWluJSBjKCJudW1lcmljIiwiaW50ZWdlciIpKSB7IAogICAgICBpZiAoYW55KGlzLm5hKGRhdGFfZnJhbWVbLGldKSkpewogICAgICAgIGRhdGFfZnJhbWVbLHBhc3RlMChjb2xuYW1lcyhkYXRhX2ZyYW1lKVtpXSwiX2ZpbGxlZE5BIildPC0gYXMuZmFjdG9yKGlmZWxzZShpcy5uYShkYXRhX2ZyYW1lWyxpXSksIjEiLCIwIikpCiAgICAgICAgZGF0YV9mcmFtZVtpcy5uYShkYXRhX2ZyYW1lWyxpXSksaV08LWZpbGxfbnVtYmVyCiAgICAgIH0KICAgIH0gZWxzZQogICAgICBpZiAoY2xhc3MoZGF0YV9mcmFtZVssaV0pICVpbiUgYygiZmFjdG9yIikpIHsgCiAgICAgICAgaWYgKGFueShpcy5uYShkYXRhX2ZyYW1lWyxpXSkpKXsKICAgICAgICAgIGRhdGFfZnJhbWVbLGldPC1hcy5jaGFyYWN0ZXIoZGF0YV9mcmFtZVssaV0pIAogICAgICAgICAgZGF0YV9mcmFtZVsscGFzdGUwKGNvbG5hbWVzKGRhdGFfZnJhbWUpW2ldLCJfZmlsbGVkTkEiKV08LWFzLmZhY3RvcihpZmVsc2UoaXMubmEoZGF0YV9mcmFtZVssaV0pLCIxIiwiMCIpKSAKICAgICAgICAgIGRhdGFfZnJhbWVbaXMubmEoZGF0YV9mcmFtZVssaV0pLGldPC1maWxsX2ZhY3RvcgogICAgICAgICAgZGF0YV9mcmFtZVssaV08LWFzLmZhY3RvcihkYXRhX2ZyYW1lWyxpXSkKICAgICAgICAgIAogICAgICAgIH0KICAgICAgfSBlbHNlIHsKICAgICAgICBpZiAoY2xhc3MoZGF0YV9mcmFtZVssaV0pICVpbiUgYygiY2hhcmFjdGVyIikpIHsgCiAgICAgICAgICBpZiAoYW55KGlzLm5hKGRhdGFfZnJhbWVbLGldKSkpewogICAgICAgICAgICBkYXRhX2ZyYW1lWyxwYXN0ZTAoY29sbmFtZXMoZGF0YV9mcmFtZSlbaV0sIl9maWxsZWROQSIpXTwtIGFzLmZhY3RvcihpZmVsc2UoaXMubmEoZGF0YV9mcmFtZVssaV0pLCIxIiwiMCIpKQogICAgICAgICAgICBkYXRhX2ZyYW1lW2lzLm5hKGRhdGFfZnJhbWVbLGldKSxpXTwtYWZpbGxfY2hhcmFjdGVyCiAgICAgICAgICB9CiAgICAgICAgfSBlbHNlIHsKICAgICAgICAgIGlmIChjbGFzcyhkYXRhX2ZyYW1lWyxpXSkgJWluJSBjKCJEYXRlIikpIHsKICAgICAgICAgICAgaWYgKGFueShpcy5uYShkYXRhX2ZyYW1lWyxpXSkpKXsgCiAgICAgICAgICAgICAgZGF0YV9mcmFtZVsscGFzdGUwKGNvbG5hbWVzKGRhdGFfZnJhbWUpW2ldLCJfZmlsbGVkTkEiKV08LWFzLmZhY3RvcihpZmVsc2UoaXMubmEoZGF0YV9mcmFtZVssaV0pLCIxIiwiMCIpKSAKICAgICAgICAgICAgICBkYXRhX2ZyYW1lW2lzLm5hKGRhdGFfZnJhbWVbLGldKSxpXTwtZmlsbF9kYXRlCiAgICAgICAgICAgIH0KICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0KICB9CiAgcmV0dXJuKGRhdGFfZnJhbWUpCn0KCmBgYAoKRXN0ZSBjw7NkaWdvIGVuIFIgZGVmaW5lIHVuYSBmdW5jacOzbiBsbGFtYWRhwqBgRmlsbE5Bc2DCoGN1eW8gcHJvcMOzc2l0byBlc8KgKipsbGVuYXIgbG9zIHZhbG9yZXMgZmFsdGFudGVzIChOQSkgZGUgdW7CoGBkYXRhLmZyYW1lYCoqwqBjb24gdW4gdmFsb3IgcHJlZGV0ZXJtaW5hZG8gc2Vnw7puIGVsIHRpcG8gZGUgZGF0byBkZSBjYWRhIGNvbHVtbmEuIEFkZW3DoXMswqAqKmdlbmVyYSB1bmEgY29sdW1uYSBhZGljaW9uYWwgcGFyYSBjYWRhIHZhcmlhYmxlIGNvbiBOQSoqLCBxdWUgaW5kaWNhIGNvbiAiMSIgZMOzbmRlIGh1Ym8gdW4gdmFsb3IgZmFsdGFudGUgeSBjb24gIjAiIGTDs25kZSBubyBsbyBodWJvLiBFc3RvIGVzIMO6dGlsIHBhcmEgbWFudGVuZXIgdHJhemFiaWxpZGFkIGRlIGxvcyBkYXRvcyBpbXB1dGFkb3MgKHTDqWNuaWNhIGNvbm9jaWRhIGNvbW8gY3JlYWNpw7NuIGRlwqAqaW5kaWNhZG9yZXMgZGUgaW1wdXRhY2nDs24qKS4KClBhc28gYSBwYXNvOgoKMS4gIEluaWNpYWxpemFjacOzbiBkZSB2YWxvcmVzIHBvciBkZWZlY3RvOgoKICAgIC0gICBTZSBkZWZpbmVuIGxvcyB2YWxvcmVzIGNvbiBsb3MgcXVlIHNlIHJlZW1wbGF6YXLDoW4gbG9zwqBgTkFgOgoKICAgICAgICAtICAgTsO6bWVyb3MgKG51bcOpcmljb3MgZSBlbnRlcm9zKTogc2UgcmVlbXBsYXphcsOhbiBwb3LCoGAwYC4KCiAgICAgICAgLSAgIEZhY3RvcmVzIHkgY2FyYWN0ZXJlczogc2UgcmVlbXBsYXphbiBjb24gbGEgY2FkZW5hwqBgIk5BX2ZpbGxlZCJgLgoKICAgICAgICAtICAgRmVjaGFzIChgRGF0ZWApOiBzZSByZWVtcGxhemFuIHBvciBsYSBmZWNoYcKgYCIxOTAwLTAxLTAxImAuCgoyLiAgSXRlcmFjacOzbiBzb2JyZSBsYXMgY29sdW1uYXM6CgogICAgLSAgIExhIGZ1bmNpw7NuIHJlY29ycmUgY2FkYSBjb2x1bW5hIGRlbMKgYGRhdGEuZnJhbWVgwqB1c2FuZG8gdW4gYnVjbGXCoGBmb3JgLgoKMy4gIFJldmlzacOzbiBkZWwgdGlwbyBkZSBkYXRvczoKCiAgICAtICAgVXRpbGl6YcKgYGNsYXNzKGRhdGFfZnJhbWVbLCBpXSlgwqBwYXJhIHZlcmlmaWNhciBlbCB0aXBvIGRlIGRhdG8gZGUgbGEgY29sdW1uYToKCiAgICAgICAgLSAgIFNpIGVzwqBudW3DqXJpY2/CoG/CoGVudGVybywgeSBjb250aWVuZSB2YWxvcmVzwqBgTkFgLCBsb3MgcmVlbXBsYXphIGNvbsKgYDBgwqB5IGNyZWEgdW5hIG51ZXZhIGNvbHVtbmEgY29uIGVsIG1pc21vIG5vbWJyZSBtw6FzIGVsIHN1Zmlqb8KgYCJfZmlsbGVkTkEiYMKgcXVlIGluZGljYSBjb27CoGAiMSJgwqBkw7NuZGUgaGFiw61hwqBgTkFgLgoKICAgICAgICAtICAgU2kgZXPCoGZhY3RvciwgY29udmllcnRlIGxhIGNvbHVtbmEgYcKgY2Fyw6FjdGVyIHRlbXBvcmFsbWVudGUsIHJlYWxpemEgZWwgbWlzbW8gcHJvY2VkaW1pZW50byBkZSBpbXB1dGFjacOzbiB5IGx1ZWdvIGxhIGNvbnZpZXJ0ZSBkZSBudWV2byBhIGZhY3Rvci4KCiAgICAgICAgLSAgIFNpIGVzwqBjYXLDoWN0ZXIsIGhhY2UgbG8gbWlzbW8gcXVlIGNvbiBsb3MgZmFjdG9yZXMsIHBlcm8gdXNhbmRvIGRpcmVjdGFtZW50ZSBlbCB2YWxvciBkZSB0ZXh0b8KgYCJOQV9maWxsZWQiYMKgKGF1bnF1ZSBhcXXDrSBoYXkgdW4gZXJyb3IgcXVlIHNlIG1lbmNpb25hIGFiYWpvKS4KCiAgICAgICAgLSAgIFNpIGVzwqBmZWNoYSwgbG9zwqBgTkFgwqBzZSByZWVtcGxhemFuIHBvcsKgYCIxOTAwLTAxLTAxImDCoHkgdGFtYmnDqW4gc2UgY3JlYSBsYSBjb2x1bW5hIGluZGljYWRvcmEuCgo0LiAgUmV0b3JubyBkZWwgcmVzdWx0YWRvOgoKICAgIC0gICBMYSBmdW5jacOzbiBkZXZ1ZWx2ZSBlbMKgYGRhdGEuZnJhbWVgwqBjb24gbG9zwqBgTkFgwqByZWVtcGxhemFkb3MgeSBsYXMgY29sdW1uYXMgaW5kaWNhZG9yYXMgYcOxYWRpZGFzLgoKKipUZXJjZXJvKiosIGFwbGljYW1vcyBsYSBmdW5jacOzbiBhbCBjb25qdW50byBkZSBkYXRvcyBwcmV2aWFtZW50ZSBjYXJnYWRvCgpgYGB7cn0KIyBhcHBseSB0aGUgZnVuY3Rpb24gRmlsbE5BcyB0byB0aGUgQ3JlZGlDYXJzIGRhdGFmcmFtZQpDcmVkaUNhcnNOQSA8LUZpbGxOQXMoYXMuZGF0YS5mcmFtZShDcmVkaUNhcnMpKQoKYGBgCgoqKkN1YXJ0byoqLCBlbGltaW5hbW9zIGxhcyBjb2x1bW5hcyBxdWUgbm8gc29uIGRlIG51ZXN0cm8gaW50ZXJlcwoKYGBge3J9CkNyZWRpQ2FycyA8LSAgQ3JlZGlDYXJzICU+JSAKICBzZWxlY3QoLUNvbnRyYWN0SUQsLUNvbnRyYWN0X1N0YXR1cywtU3RhcnRfRGF0ZSkKYGBgCgoqKlF1aW50byoqLCBjYW1iaWFtb3MgZGF0b3MgY2F0ZWdvcmljb3MgYSBudW1lcmljb3MKClBhcmEgZWxsbywgdmlzdWFsaXphbW9zIGxvcyB2YWxvcmVzIGNhdGVnb3JpY29zIHF1ZSBkZXNlbW9zIGNhbWJpYXIgcG9yIHZhbG9yZXMgbnVtZXJpY29zOgoKYGBge3J9CiMgQnkgcmVnaW9ucwp1bmlxdWUoQ3JlZGlDYXJzJFJlZ2lvbikKCiMgQnkgYnJhbmNocwp1bmlxdWUoQ3JlZGlDYXJzJEJyYW5jaCkKCmBgYAoKQXBsaWNhbW9zIGNhc2Vfd2hlbiBkZSBwYXJhIGxhIG1vZGlmaWNhY2lvbgoKYGBge3J9CgojIFBhcmEgbGFzIHZhcmlhYmxlcyBkZSByZWdpb24KQ3JlZGlDYXJzIDwtIENyZWRpQ2FycyAlPiUKICBtdXRhdGUoUmVnaW9uID0gY2FzZV93aGVuKAogICAgUmVnaW9uID09ICJCQiIgfiAxLAogICAgUmVnaW9uID09ICJERCIgfiAyLAogICAgUmVnaW9uID09ICJFRSIgfiAzLAogICAgUmVnaW9uID09ICJHRyIgfiA0LAogICAgUmVnaW9uID09ICJGRiIgfiA1LAogICAgUmVnaW9uID09ICJDQyIgfiA2LAogICAgUmVnaW9uID09ICJBQSIgfiA3LAogICAgUmVnaW9uID09ICJISCIgfiA4LAogICAgVFJVRSB+IE5BX3JlYWxfICAjIHBvciBzaSBoYXkgdmFsb3JlcyBxdWUgbm8gZXN0w6FuIGVuIGxhIGxpc3RhCiAgKSkKCgojIFBhcmEgbGFzIHZhcmlhYmxlcyBkZSBtYXJjYQpDcmVkaUNhcnMgPC0gQ3JlZGlDYXJzICU+JQogIG11dGF0ZShCcmFuY2ggPSBjYXNlX3doZW4oCiAgICBCcmFuY2ggPT0gIk4iICAgfiAxLAogICAgQnJhbmNoID09ICJNIiAgIH4gMiwKICAgIEJyYW5jaCA9PSAiQSIgICB+IDMsCiAgICBCcmFuY2ggPT0gIkQiICAgfiA0LAogICAgQnJhbmNoID09ICJDIiAgIH4gNSwKICAgIEJyYW5jaCA9PSAiRSIgICB+IDYsCiAgICBCcmFuY2ggPT0gIkIiICAgfiA3LAogICAgQnJhbmNoID09ICJGRkYiIH4gOCwKICAgIEJyYW5jaCA9PSAiRyIgICB+IDksCiAgICBCcmFuY2ggPT0gIkgiICAgfiAxMCwKICAgIEJyYW5jaCA9PSAiSSIgICB+IDExLAogICAgQnJhbmNoID09ICJLIiAgIH4gMTIsCiAgICBCcmFuY2ggPT0gIkwiICAgfiAxMywKICAgIEJyYW5jaCA9PSAiSiIgICB+IDE0LAogICAgVFJVRSB+IE5BX3JlYWxfICAjIG9wY2lvbmFsOiBwYXJhIHZhbG9yZXMgbm8gY29udGVtcGxhZG9zCiAgKSkKYGBgCgpGaW5hbG1lbnRlIG5vcyBhc2VndXJtYW9zIGRlIHF1ZSBlc3RlbiBlbiB0aXBvIG51bWVyaWNvCgpgYGB7cn0KIyBUcmFuc2Zvcm1hbW9zIHRpcG8gZGUgdmFyaWFibGVzCkNyZWRpQ2FycyA8LSBDcmVkaUNhcnMgJT4lIAogIG11dGF0ZSgKICAgIFJlZ2lvbiA9IGFzLm51bWVyaWMoUmVnaW9uKSwKICAgIEJyYW5jaCA9IGFzLm51bWVyaWMoQnJhbmNoKQogICkKCiMgVmFsaWRhbW9zCnR5cGVvZihDcmVkaUNhcnMkQnJhbmNoKQp0eXBlb2YoQ3JlZGlDYXJzJFJlZ2lvbikKCmBgYAoKIyMgMy4yKSBQcmVwYXJhY2nDs24gZGUgbG9zIGRhdG9zCgoqKlByaW1lcm8qKiwgYW5hbGl6YW1vcyBtYXRyaXogZGUgY29ycmVsYWNpw7NuIGNvbiBlbCBmaW4gZGUgZGV0ZXJtaW5hciBzaSB0ZW5lbW9zIG11bHRpY29saW5lYWxpZGFkIGVuIG51ZXN0cm9zIGRhdG9zCgpgYGB7ciBmaWcuaGVpZ2h0PTEwLCBmaWcud2lkdGg9MTB9CiMgQ2FsY3VsYXIgbWF0cml6IGRlIGNvcnJlbGFjacOzbgpjb3JyZWxhdGlvbiA8LSBjb3IoQ3JlZGlDYXJzLCB1c2UgPSAiY29tcGxldGUub2JzIikKCiMgR3JhZmljYXIKZ2djb3JycGxvdChjb3JyZWxhdGlvbiwgCiAgICAgICAgICAgaGMub3JkZXIgPSBUUlVFLCAKICAgICAgICAgICB0eXBlID0gImxvd2VyIiwKICAgICAgICAgICBsYWIgPSBUUlVFLCAKICAgICAgICAgICBsYWJfc2l6ZSA9IDMsIAogICAgICAgICAgIG1ldGhvZCA9ICJjaXJjbGUiLAogICAgICAgICAgIGNvbG9ycyA9IGMoInJlZCIsICJ3aGl0ZSIsICJibHVlIiksCiAgICAgICAgICAgdGl0bGUgPSAiTWF0cml6IGRlIENvcnJlbGFjacOzbiIpCmBgYAoKTGFzIHZhcmlhYmxlcyBxdWUgcHJlc2VudGFuIHVuYSBjb3JyZWxhY2nDs24gbWF5b3IgYSAwLjQgbyBtZW5vciBhIC0wLjQgcG9kcsOtYW4gc2VyIGNhbmRpZGF0YXMgcGFyYSBzZXIgZWxpbWluYWRhcyBkZWwgbW9kZWxvLCBwZXJvIGVzdGEgZGVjaXNpw7NuIG5vIGRlYmUgYmFzYXJzZSDDum5pY2FtZW50ZSBlbiBjcml0ZXJpb3MgY3VhbnRpdGF0aXZvcy4gRXMgbmVjZXNhcmlvIGhhY2VyIHVuIGFuw6FsaXNpcyBjdWFsaXRhdGl2byBwYXJhIGRldGVybWluYXIgc2kgcmVhbG1lbnRlIHRpZW5lIHNlbnRpZG8gZXhjbHVpcmxhcywgZXZhbHVhbmRvIHN1IHJlbGV2YW5jaWEgeSBmdW5jacOzbiBkZW50cm8gZGVsIGNvbnRleHRvIGRlbCBwcm9ibGVtYS4KCkVuIGVsIGFuw6FsaXNpcyByZWFsaXphZG8gc2UgaWRlbnRpZmljw7MgcXVlIGxhcyB2YXJpYWJsZXMgVEVOT1JZUiB5IERXTlBNRlIgZXN0w6FuIGFsdGFtZW50ZSBjb3JyZWxhY2lvbmFkYXMgY29uIHVuIHZhbG9yIGRlIC0wLjU2LiBBbCBjb21wcm9iYXIgcXVlIGFtYmFzIHNvbiByZWxldmFudGVzIHJlc3BlY3RvIGEgbGEgdmFyaWFibGUgZGVwZW5kaWVudGUsIHNlIGNvbnNpZGVyw7Mgc2kgZXhpc3TDrWEgdW5hIHJhesOzbiBjdWFsaXRhdGl2YSBxdWUgZXhwbGljYXJhIGxhIG11bHRpY29saW5lYWxpZGFkIGVudHJlIGVsbGFzIChwb3IgZWplbXBsbywgc2kgdW5hIGRlcGVuZGUgZGUgbGEgb3RyYSkuIENvbW8gbm8gc2UgZW5jb250csOzIHRhbCByZWxhY2nDs24geSBhbWJhcyBhcG9ydGFuIHZhbG9yIGFsIG1vZGVsbywgc2UgZGVjaWRpw7MgY29uc2VydmFybGFzIGVuIGxhIHZlcnNpw7NuIHByZWxpbWluYXIgZGVsIG1pc21vLgoKKipTZWd1bmRvKiosIG5vcm1hbGl6YW1vcyBsb3MgZGF0b3MgeWEgcXVlIGVzIG1lam9yIHBhcmEgcmVkZXMgbmV1cm9uYWxlcyBwb3JxdWUgYXl1ZGEgYSBxdWUgdG9kYXMgbGFzIGVudHJhZGFzIGVzdMOpbiBlbiB1biBtaXNtbyByYW5nbyAoY29tbyAwIGEgMSksIGxvIHF1ZSBtZWpvcmEgbGEgdmVsb2NpZGFkIGRlIGFwcmVuZGl6YWplLCBldml0YSBxdWUgYWxndW5hcyBuZXVyb25hcyBkb21pbmVuIGEgb3RyYXMgeSByZWR1Y2UgZWwgcmllc2dvIGRlIHF1ZSBlbCBtb2RlbG8gc2UgcXVlZGUgYXRhc2NhZG8gZHVyYW50ZSBlbCBlbnRyZW5hbWllbnRvLgoKRm9ybXVsYSBkZSBsYSBub3JtYWxpemFjacOzbjoKCiQkCnhfe1x0ZXh0e25vcm19fSA9IFxmcmFje3ggLSBcbWluKHgpfXtcbWF4KHgpIC0gXG1pbih4KX0KJCQKCkxhIGRlY2lzacOzbiBlbnRyZSBub3JtYWxpemFyIG8gZXNjYWxhciBkZXBlbmRlIGRlbCB0aXBvIGRlIGFsZ29yaXRtbyBxdWUgc2UgZXN0w6kgdXRpbGl6YW5kbyB5IGRlIGxhIG5hdHVyYWxlemEgZGUgbG9zIGRhdG9zLiBMYSBub3JtYWxpemFjacOzbiAobyBNaW4tTWF4IHNjYWxpbmcpIHRyYW5zZm9ybWEgbGFzIHZhcmlhYmxlcyBwYXJhIHF1ZSB0b2RhcyBlc3TDqW4gZGVudHJvIGRlIHVuIHJhbmdvIGZpam8sIHVzdWFsbWVudGUgZW50cmUgMCB5IDEuIEVzdGUgbcOpdG9kbyBlcyBpZGVhbCBjdWFuZG8gc2UgdXNhbiBhbGdvcml0bW9zIHF1ZSBzZSBiYXNhbiBlbiBkaXN0YW5jaWFzIGFic29sdXRhcywgY29tbyBLLW1lYW5zLCBLLU5lYXJlc3QgTmVpZ2hib3JzIChLTk4pLCByZWRlcyBuZXVyb25hbGVzIG8gbcOpdG9kb3MgZGUgY2x1c3RlcmluZywgeWEgcXVlIGV2aXRhIHF1ZSB2YXJpYWJsZXMgY29uIG1heW9yZXMgbWFnbml0dWRlcyBkb21pbmVuIGVsIGFuw6FsaXNpcy4gQWRlbcOhcywgZXMgw7p0aWwgY3VhbmRvIGxvcyBkYXRvcyBubyBzaWd1ZW4gdW5hIGRpc3RyaWJ1Y2nDs24gbm9ybWFsIHkgc2UgZGVzZWEgY29uc2VydmFyIGxhIGZvcm1hIG9yaWdpbmFsIGRlIGxhIGRpc3RyaWJ1Y2nDs24uCgpGb3JtdWxhIGRlIGxhIGVzdGFuZGFyaXphY2nDs246CgokJAp4X3tcdGV4dHtzdGR9fSA9IFxmcmFje3ggLSBcbXV9e1xzaWdtYX0KJCQKClBvciBvdHJvIGxhZG8sIGxhIGVzY2FsYWNpw7NuIG8gZXN0YW5kYXJpemFjacOzbiB0cmFuc2Zvcm1hIGxhcyB2YXJpYWJsZXMgcGFyYSBxdWUgdGVuZ2FuIHVuYSBtZWRpYSBkZSAwIHkgdW5hIGRlc3ZpYWNpw7NuIGVzdMOhbmRhciBkZSAxLCBsbyBxdWUgZXMgcGFydGljdWxhcm1lbnRlIMO6dGlsIGVuIG1vZGVsb3MgcXVlIGFzdW1lbiBub3JtYWxpZGFkIG8gbGluZWFsaWRhZCwgY29tbyBsYSByZWdyZXNpw7NuIGxpbmVhbCwgbGEgcmVncmVzacOzbiBsb2fDrXN0aWNhLCBlbCBhbsOhbGlzaXMgZGUgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMgKFBDQSkgbyBsb3MgbW9kZWxvcyBTVk0gbGluZWFsZXMuIEVzdGUgZW5mb3F1ZSBlcyBwcmVmZXJpZG8gY3VhbmRvIHNlIGJ1c2NhIGNvbXBhcmFyIHZhcmlhYmxlcyBjb24gZGlmZXJlbnRlcyB1bmlkYWRlcyBvIHJhbmdvcywgeSB0YW1iacOpbiBheXVkYSBhIG1pdGlnYXIgbGEgaW5mbHVlbmNpYSBkZSB2YWxvcmVzIGV4dHJlbW9zIG1vZGVyYWRvcy4gRW4gcmVzdW1lbiwgc2UgcmVjb21pZW5kYSBlc2NhbGFyIHBhcmEgbW9kZWxvcyBlc3RhZMOtc3RpY29zIGNsw6FzaWNvcyB5IG5vcm1hbGl6YXIgcGFyYSBtb2RlbG9zIGJhc2Fkb3MgZW4gZGlzdGFuY2lhcyBvIHJlZGVzIG5ldXJvbmFsZXMuCgpBcGxpY2Ftb3Mgbm9ybWFsaXphY2lvbiBkZSByYW5nb3MgY29uIGxhIGxpYnJlcmlhIGBjYXJldGAgOgoKYGBge3J9CiMgQ3JlYXIgZWwgbW9kZWxvIGRlIG5vcm1hbGl6YWNpw7NuIChNaW4tTWF4KQptb2RlbG9fbm9ybSA8LSBwcmVQcm9jZXNzKENyZWRpQ2FycywgbWV0aG9kID0gInJhbmdlIikKCiMgQXBsaWNhciBsYSBub3JtYWxpemFjacOzbiBhIHRvZG8gZWwgZGF0YXNldApDcmVkaUNhcnNfbm9ybWFsaXphZG8gPC0gcHJlZGljdChtb2RlbG9fbm9ybSwgQ3JlZGlDYXJzKQoKIyBWaXN1YWxpemFtb3MgbG9zIHByaW1lcm9zIDUKaGVhZChDcmVkaUNhcnNfbm9ybWFsaXphZG8sNSkKYGBgCgoqKlRlcmNlcm8qKiwgc2VwYXJhbW9zIGVuIGRhdG9zIGRlIGVudHJlbmFtaWVudG9zIHkgZGUgcHJ1ZWJhLiBQYXJhIGVsbG8gdXNhbW9zIGVsIHBhcXVldGUgYGNhcmV0YCBxdWUgZmFjaWxpdGEgZXN0YSBzZXBhcmFjacOzbjoKCmBgYHtyfQojIEVzdGFibGVjZXIgc2VtaWxsYSBwYXJhIHJlcHJvZHVjaWJpbGlkYWQKc2V0LnNlZWQoMjAyMSkKCiMgQ3JlYXIgcGFydGljacOzbiBlc3RyYXRpZmljYWRhICg4MCUgZW50cmVuYW1pZW50bywgMjAlIHBydWViYSkKcGFydGl0aW9uX2luZGV4IDwtIGNyZWF0ZURhdGFQYXJ0aXRpb24oeSA9IENyZWRpQ2Fyc19ub3JtYWxpemFkbyREZWZhdWx0ZXJGbGFnLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcCA9IDAuOCwgbGlzdCA9IEZBTFNFKQpgYGAKCkVuIGVsIGPDs2RpZ28gc2UgdXNhwqBgc2V0LnNlZWQoMjAyMSlgwqBwYXJhIGZpamFyIGxhIHNlbWlsbGEgZGVsIGdlbmVyYWRvciBhbGVhdG9yaW8sIGxvIHF1ZSBhc2VndXJhIHF1ZSBsb3MgcmVzdWx0YWRvcyBkZSBsYSBwYXJ0aWNpw7NuIHNlYW7CoHJlcHJvZHVjaWJsZXPCoGNhZGEgdmV6IHF1ZSBzZSBlamVjdXRlIGVsIGPDs2RpZ28uIEx1ZWdvLCBjb27CoGBjcmVhdGVEYXRhUGFydGl0aW9uKClgLCBzZSBjcmVhIHVuYSBwYXJ0aWNpw7NuwqBlc3RyYXRpZmljYWRhwqBkZSBsb3MgZGF0b3MsIGVzIGRlY2lyLCBtYW50ZW5pZW5kbyBsYSBwcm9wb3JjacOzbiBkZSBjbGFzZXMgZGUgbGEgdmFyaWFibGXCoGBEZWZhdWx0ZXJGbGFnYCwgcXVlIGVzIGxhIHZhcmlhYmxlIG9iamV0aXZvIGRlbCBtb2RlbG8uIEVsIHBhcsOhbWV0cm/CoGBwID0gMC44YMKgaW5kaWNhIHF1ZSBlbMKgODDigK8lIGRlIGxvcyBkYXRvcyBzZSB1c2Fyw6EgcGFyYSBlbnRyZW5hbWllbnRvLCBtaWVudHJhcyBxdWUgZWwgMjDigK8lIHJlc3RhbnRlIHF1ZWRhcsOhIHBhcmEgcHJ1ZWJhLiBGaW5hbG1lbnRlLMKgYGxpc3QgPSBGQUxTRWDCoGhhY2UgcXVlIGxhIHNhbGlkYSBzZWEgdW4gdmVjdG9yIHBsYW5vIGRlIMOtbmRpY2VzIGVuIGx1Z2FyIGRlIHVuYSBsaXN0YSwgbG8gY3VhbCBmYWNpbGl0YSBzdSB1c28gY29uwqBgc2xpY2UoKWDCoG8gc3Vic2V0dGluZy4KCllhIGNvbiBsb3MgZGF0b3Mgc2VwYXJhZG9zIGFob3JhIGluZGljYW1vcyBjdWFsZXMgb2JzZXJ2YWNpb25lcyBzb24gZGUgZW50cmVuYW1pZW50byB5IGN1YWxlcyBzb24gZGUgcHJ1ZWJhOgoKYGBge3J9CiMgRGl2aWRpciBlbCBkYXRhc2V0IHVzYW5kbyBkcGx5cjo6c2xpY2UKCiMgU2VwYXJhciBkYXRvcyBkZSBlbnRyZW5hbWllbnRzCnRyYWluaW5nIDwtIENyZWRpQ2Fyc19ub3JtYWxpemFkbyAlPiUKICBzbGljZShwYXJ0aXRpb25faW5kZXgpCgojIFNlcGFyYXIgZGF0b3MgZGUgcHJ1ZWJhCnRlc3RpbmcgIDwtIENyZWRpQ2Fyc19ub3JtYWxpemFkbyAlPiUgCiAgc2xpY2UoLXBhcnRpdGlvbl9pbmRleCkKYGBgCgpFbiBlc3RhIHBhcnRlIGRlbCBjw7NkaWdvLCBsYSBmdW5jacOzbsKgYHNsaWNlKClgwqBkZcKgYGRwbHlyYMKgc2UgdXRpbGl6YSBwYXJhwqBleHRyYWVyIGZpbGFzIGVzcGVjw61maWNhc8KgZGUgdW4gZGF0YSBmcmFtZSBzZWfDum4gc3VzIMOtbmRpY2VzLiBQcmltZXJvLMKgYENyZWRpQ2Fycy5ub3JtYWwgJT4lIHNsaWNlKHBhcnRpdGlvbl9pbmRleClgwqBzZWxlY2Npb25hIGxhcyBmaWxhcyBjdXlvcyBuw7ptZXJvcyBkZSBmaWxhIGVzdMOhbiBlbsKgYHBhcnRpdGlvbl9pbmRleGAsIGVzIGRlY2lyLCBlbMKgODDigK8lIGRlIGxvcyBkYXRvcyBlbGVnaWRvcyBhbGVhdG9yaWFtZW50ZcKgcGFyYSBlbnRyZW5hbWllbnRvLiBMdWVnbyzCoGBzbGljZSgtcGFydGl0aW9uX2luZGV4KWDCoHNlbGVjY2lvbmHCoHRvZGFzIGxhcyBmaWxhcyBxdWUgbm8gZXN0w6FuwqBlbiBlc2EgcGFydGljacOzbiwgZXMgZGVjaXIsIGVswqAyMOKAryUgcmVzdGFudGUsIHF1ZSBzZSBkZXN0aW5hIGFsIGNvbmp1bnRvIGRlIHBydWViYS4gQXPDrSzCoGBzbGljZSgpYMKgcGVybWl0ZSBkaXZpZGlyIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIGZvcm1hIGNsYXJhIHkgY29udHJvbGFkYSwgdXNhbmRvIGxvcyDDrW5kaWNlcyBnZW5lcmFkb3MgcHJldmlhbWVudGUuCgojIDQpICoqTW9kZWxhZG8qKgoKUGFyYSBlbCBtb2RlbGFkbyBkZSByZWRlcyBuZXVyb25hbGVzIHVzYW1vcyBlbCBwYXF1ZXRlIFtuZXVyYWxuZXRdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9uZXVyYWxuZXQvbmV1cmFsbmV0LnBkZikuIEVzdGUgcGFxdWV0ZSBlcyB1bmEgaGVycmFtaWVudGEgZGUgUiBkaXNlw7FhZGEgcGFyYSBlbnRyZW5hciByZWRlcyBuZXVyb25hbGVzIGFydGlmaWNpYWxlcyBtZWRpYW50ZSBhbGdvcml0bW9zIGNvbW8gYmFja3Byb3BhZ2F0aW9uLCByZXNpbGllbnQgYmFja3Byb3BhZ2F0aW9uIChjb24gbyBzaW4gcmV0cm9jZXNvIGRlIHBlc29zKSB5IHVuYSB2ZXJzacOzbiBnbG9iYWxtZW50ZSBjb252ZXJnZW50ZSBtb2RpZmljYWRhLiBQZXJtaXRlIHVuYSBjb25maWd1cmFjacOzbiBmbGV4aWJsZSBtZWRpYW50ZSBsYSBlbGVjY2nDs24gcGVyc29uYWxpemFkYSBkZSBmdW5jaW9uZXMgZGUgZXJyb3IgeSBhY3RpdmFjacOzbiwgZSBpbmNsdXllIGZ1bmNpb25hbGlkYWRlcyBjb21vIGVsIGPDoWxjdWxvIGRlIHBlc29zIGdlbmVyYWxpemFkb3MgeSBsYSBlc3RpbWFjacOzbiBkZSBpbnRlcnZhbG9zIGRlIGNvbmZpYW56YSBwYXJhIGxvcyBwZXNvcy4gQWRlbcOhcywgb2ZyZWNlIG3DqXRvZG9zIHBhcmEgdmlzdWFsaXphciBsYSByZWQgeSBzdXMgcGVzb3MsIHJlYWxpemFyIHByZWRpY2Npb25lcyB5IGV2YWx1YXIgZWwgcmVuZGltaWVudG8gZGVsIG1vZGVsbywgc2llbmRvIMO6dGlsIHRhbnRvIHBhcmEgY2xhc2lmaWNhY2nDs24gYmluYXJpYSBjb21vIG11bHRpY2xhc2UsIHkgYWRhcHRhYmxlIGEgZnVuY2lvbmVzIGRlIGFjdGl2YWNpw7NuIHBlcnNvbmFsaXphZGFzLgoKIyMgNC4xKSBDcmVhbW9zIG1vZGVsbwoKYGBge3J9Cm1vZGVsMSA9IG5ldXJhbG5ldChEZWZhdWx0ZXJGbGFnfi4tRGVmYXVsdGVyVHlwZSwgZGF0YT10cmFpbmluZywKICAgICAgICAgICAgICAgICAgIGhpZGRlbj0yLCBlcnIuZmN0ID0gInNzZSIsIHRocmVzaG9sZCA9IDAuMDUsCiAgICAgICAgICAgICAgICAgICAgbGluZWFyLm91dHB1dCA9IFRSVUUpCmBgYAoKRXN0ZSBjw7NkaWdvIGVuIFIgZW50cmVuYSB1bmEgcmVkIG5ldXJvbmFsIHV0aWxpemFuZG8gZWwgcGFxdWV0ZSBgbmV1cmFsbmV0YCBwYXJhIHByZWRlY2lyIGxhIHZhcmlhYmxlIGBEZWZhdWx0ZXJGbGFnYCBhIHBhcnRpciBkZSB0b2RhcyBsYXMgZGVtw6FzIHZhcmlhYmxlcyBkZWwgY29uanVudG8gZGUgZGF0b3MgYHRyYWluaW5nYCwgZXhjZXB0byBgRGVmYXVsdGVyVHlwZWAuIExhIHJlZCB0aWVuZSB1bmEgc29sYSBjYXBhIG9jdWx0YSBjb24gMiBuZXVyb25hcyAoYGhpZGRlbj0yYCksIHV0aWxpemEgbGEgc3VtYSBkZSBlcnJvcmVzIGN1YWRyYWRvcyBjb21vIGZ1bmNpw7NuIGRlIGVycm9yIChgZXJyLmZjdCA9ICJzc2UiYCksIHkgZGV0aWVuZSBlbCBlbnRyZW5hbWllbnRvIGN1YW5kbyBlbCBncmFkaWVudGUgZGVsIGVycm9yIGNhZSBwb3IgZGViYWpvIGRlIDAuMDUgKGB0aHJlc2hvbGQgPSAwLjA1YCkuIEFkZW3DoXMsIHNlIGVzcGVjaWZpY2EgcXVlIGxhIHNhbGlkYSBkZWJlIHNlciBsaW5lYWwgKGBsaW5lYXIub3V0cHV0ID0gVFJVRWApLCBsbyBjdWFsIGVzIGNvbcO6biBlbiBwcm9ibGVtYXMgZGUgcmVncmVzacOzbi4gRW4gcmVzdW1lbiwgZXN0ZSBtb2RlbG8gYnVzY2EgcHJlZGVjaXIgdW5hIHZhcmlhYmxlIGNvbnRpbnVhIG8gYmluYXJpYSBzaW4gYXBsaWNhciB1bmEgZnVuY2nDs24gZGUgYWN0aXZhY2nDs24gZW4gbGEgY2FwYSBkZSBzYWxpZGEKCmBgYHtyfQojIENvbnZlcnRpciBsYSBtYXRyaXogYSBkYXRhLmZyYW1lCnJlc3VsdF9kZiA8LSBhcy5kYXRhLmZyYW1lKG1vZGVsMSRyZXN1bHQubWF0cml4KQoKIyBNb3N0cmFyIGNvbW8gdGFibGEKa2FibGUocmVzdWx0X2RmLCBjYXB0aW9uID0gIlJlc3VtZW4gZGUgUmVzdWx0YWRvcyBkZWwgTW9kZWxvIikKCmBgYAoKKirwn6egIEVzdHJ1Y3R1cmEgZ2VuZXJhbCoqCgotICAgKiplcnJvcioqOiBlbCB2YWxvciBkZWwgZXJyb3IgZmluYWwgZGVsIG1vZGVsbyBkZXNwdcOpcyBkZWwgZW50cmVuYW1pZW50by4gRW4gZXN0ZSBjYXNvIGVzIGAyMDMzLjgzYCwgbG8gcXVlIGluZGljYSBxdcOpIHRhbiBsZWpvcyBlc3TDoW4gbGFzIHByZWRpY2Npb25lcyBkZWwgbW9kZWxvIHJlc3BlY3RvIGEgbG9zIHZhbG9yZXMgcmVhbGVzLgoKLSAgICoqcmVhY2hlZC50aHJlc2hvbGQqKjogZWwgdmFsb3IgbcOtbmltbyBkZWwgZ3JhZGllbnRlIGRlbCBlcnJvciBhbGNhbnphZG8uIFNpIGVzIG1lbm9yIGFsIHVtYnJhbCBxdWUgc2UgZGVmaW5pbyAoYHRocmVzaG9sZCA9IDAuMDVgKSwgZWwgZW50cmVuYW1pZW50byBzZSBkZXRpZW5lCgotICAgKipzdGVwcyoqOiBuw7ptZXJvIGRlIGl0ZXJhY2lvbmVzIHF1ZSB0b23DsyBlbnRyZW5hciBsYSByZWQuIEFxdcOtIGZ1ZXJvbiBgNDQzNzRgIHBhc29zLgoKKirwn5SXIFBlc29zIGRlIGxhIHJlZCBuZXVyb25hbCoqCgpMb3Mgc2lndWllbnRlcyB2YWxvcmVzIHJlcHJlc2VudGFuIGxvcyAqKnBlc29zIGFwcmVuZGlkb3MqKiBwb3IgbGEgcmVkIGVudHJlIGxhcyBjYXBhczoKCi0gICBgSW50ZXJjZXB0LnRvLjFsYXloaWQxYDogcGVzbyBkZWwgc2VzZ28gKGJpYXMpIGhhY2lhIGxhIHByaW1lcmEgbmV1cm9uYSBvY3VsdGEuCgotICAgYEFHRS50by4xbGF5aGlkMWA6IHBlc28gZGUgbGEgdmFyaWFibGUgYEFHRWAgaGFjaWEgbGEgcHJpbWVyYSBuZXVyb25hIG9jdWx0YS4KCi0gICBgLi4udG8uMWxheWhpZDJgOiBwZXNvcyBoYWNpYSBsYSBzZWd1bmRhIG5ldXJvbmEgb2N1bHRhLgoKLSAgIGAxbGF5aGlkMS50by5EZWZhdWx0ZXJGbGFnYDogcGVzbyBkZSBsYSBwcmltZXJhIG5ldXJvbmEgb2N1bHRhIGhhY2lhIGxhIG5ldXJvbmEgZGUgc2FsaWRhIChgRGVmYXVsdGVyRmxhZ2ApLgoKLSAgIGBJbnRlcmNlcHQudG8uRGVmYXVsdGVyRmxhZ2A6IHNlc2dvIGhhY2lhIGxhIG5ldXJvbmEgZGUgc2FsaWRhLgoKKirwn6e+IMK/Q8OzbW8gaW50ZXJwcmV0YXJsbz8qKgoKQ2FkYSBwZXNvIGluZGljYSBsYSBpbmZsdWVuY2lhIGRlIHVuYSB2YXJpYWJsZSBzb2JyZSB1bmEgbmV1cm9uYS4gUG9yIGVqZW1wbG86CgotICAgVW4gcGVzbyBwb3NpdGl2byBjb21vIGBURU5PUllSLnRvLjFsYXloaWQxID0gMy44N2Agc3VnaWVyZSBxdWUgYSBtYXlvciBgVEVOT1JZUmAsIG1heW9yIGFjdGl2YWNpw7NuIGVuIGVzYSBuZXVyb25hLgoKLSAgIFVuIHBlc28gbmVnYXRpdm8gY29tbyBgUFJPRkJVUy50by4xbGF5aGlkMSA9IC0wLjYzYCBpbmRpY2EgdW5hIHJlbGFjacOzbiBpbnZlcnNhLnNvcyBoYWNpYSBsYSBzZWd1bmRhIG5ldXJvbmEgb2N1bHRhLiAxbGF5aGlkMS50by5EZWZhdWx0ZXJGbGFnOiBwZXNvIGRlIGxhIHByaW1lcmEgbmV1cm9uYSBvY3VsdGEgaGFjaWEgbGEgbmV1cm9uYSBkZSBzYWxpZGEgKERlZmF1bHRlckZsYWcpLiBJbnRlcmNlcHQudG8uRGVmYXVsdGVyRmxhZzogc2VzZ28gaGFjaWEgbGEgbmV1cm9uYSBkZSBzYWxpZGEuCgpgYGB7ciBmaWcuaGVpZ2h0PTEwLCBmaWcud2lkdGg9MTV9CnBsb3QobW9kZWwxKQoKYGBgCgoqKvCfp6AgUmVzdW1lbiBkZWwgR3LDoWZpY28gZGUgbGEgUmVkIE5ldXJvbmFsKioKCkVsIGdyw6FmaWNvIHJlcHJlc2VudGEgdmlzdWFsbWVudGUgY8OzbW8gdHUgbW9kZWxvIGRlIHJlZCBuZXVyb25hbCBwcm9jZXNhIGxhIGluZm9ybWFjacOzbiBwYXJhIHByZWRlY2lyIGxhIHZhcmlhYmxlIG9iamV0aXZvIGBEZWZhdWx0ZXJGbGFnYC4KCioq8J+UuSBFbnRyYWRhcyAoSW5wdXQgTGF5ZXIpKioKCkNhZGEgbm9kbyBkZSBlbnRyYWRhIHJlcHJlc2VudGEgdW5hIHZhcmlhYmxlIGRlbCBjb25qdW50byBkZSBkYXRvcywgY29tbyBgQUdFYCwgYE5PT0ZERVBERWAsIGBNVEhJTkNUSGAsIGV0Yy4gRXN0YXMgc29uIGxhcyBjYXJhY3RlcsOtc3RpY2FzIHF1ZSBlbCBtb2RlbG8gdXRpbGl6YSBwYXJhIGhhY2VyIHByZWRpY2Npb25lcy4KCioq8J+UuSBTZXNnb3MgKEJpYXMpKioKCkxvcyB2YWxvcmVzIGNvbnN0YW50ZXMgY29tbyBgMWAgY29uZWN0YWRvcyBhIGxhcyBuZXVyb25hcyBvY3VsdGFzIHkgZGUgc2FsaWRhIHJlcHJlc2VudGFuIGxvcyAqKnTDqXJtaW5vcyBkZSBzZXNnbyoqLiBFc3RvcyBwZXJtaXRlbiBxdWUgbGFzIG5ldXJvbmFzIHNlIGFjdGl2ZW4gaW5jbHVzbyBzaSB0b2RhcyBsYXMgZW50cmFkYXMgc29uIGNlcm8sIG1lam9yYW5kbyBsYSBmbGV4aWJpbGlkYWQgZGVsIG1vZGVsby4KCioq8J+UuSBDYXBhIE9jdWx0YSAoSGlkZGVuIExheWVyKSoqCgpDb250aWVuZSBuZXVyb25hcyBxdWUgY29tYmluYW4gbGFzIGVudHJhZGFzIGNvbiBzdXMgcmVzcGVjdGl2b3MgcGVzb3MuIENhZGEgY29uZXhpw7NuIHRpZW5lIHVuIHBlc28gcXVlIGluZGljYSBsYSBpbmZsdWVuY2lhIGRlIHVuYSB2YXJpYWJsZSBzb2JyZSBlc2EgbmV1cm9uYS4gUGVzb3MgcG9zaXRpdm9zIHJlZnVlcnphbiBsYSBzZcOxYWw7IG5lZ2F0aXZvcyBsYSByZWR1Y2VuLgoKKirwn5S5IFNhbGlkYSAoT3V0cHV0IExheWVyKSoqCgpMYSBuZXVyb25hIGRlIHNhbGlkYSBwcm9kdWNlIGVsIHZhbG9yIGZpbmFsIGRlIHByZWRpY2Npw7NuIChgRGVmYXVsdGVyRmxhZ2ApLCBjb21iaW5hbmRvIGxhcyBzZcOxYWxlcyBkZSBsYXMgbmV1cm9uYXMgb2N1bHRhcy4KCioq8J+UuSBQZXNvcyoqCgpMb3MgbsO6bWVyb3MgZW4gbGFzIGNvbmV4aW9uZXMgKGNvbW8gYDIuMTIyNGAsIGAtMC4zNjgwYCwgZXRjLikgc29uIGxvcyAqKnBlc29zIGFwcmVuZGlkb3MqKiBkdXJhbnRlIGVsIGVudHJlbmFtaWVudG8uIEluZGljYW4gbGEgZnVlcnphIHkgZGlyZWNjacOzbiBkZSBsYSBpbmZsdWVuY2lhIGVudHJlIG5vZG9zLgoKIyMgNC4yKSBQcmVkZWNpbW9zIGNvbiBlbCBtb2RlbG8gY3JlYWRvCgoqKlByaW1lcm8qKiwgdXNhbW9zIGVsIG1vZGVsbyBlbnRyZW5hZG8gKGBtb2RlbDFgKSBwYXJhIHByZWRlY2lyIGxvcyB2YWxvcmVzIGRlIGxhIHZhcmlhYmxlIG9iamV0aXZvIChgRGVmYXVsdGVyRmxhZ2ApIHNvYnJlIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIHBydWViYSAoYHRlc3RpbmdgKToKCmBgYHtyfQpwcmVkaWN0aW9uID0gY29tcHV0ZShtb2RlbDEsIHRlc3RpbmcpCgpgYGAKCioqU2VndW5kbyoqLCBvYnRlbmVyIGxvcyB2YWxvcmVzIHJlYWxlcyBkZXNkZSBlbCBjb25qdW50byBvcmlnaW5hbCA6CgpgYGB7cn0KCnJlYWwuZGVmYXVsdGVyIDwtIENyZWRpQ2FycyAlPiUKIHNsaWNlKC1wYXJ0aXRpb25faW5kZXgsKSAlPiUgIyBFeGNsdWltb3MgZWwgaW5kaWNlIGRlIGxhcyBmaWxhcyBkZSBlbnRyYW1pZW50b3MgeSBzb2xvIHRvbWFtb3MgbGFzIGRlIHBydWJlYSAgKGxhICIsIiBpbmRpY2EgcXVlIHNvbiBmaWxhcykKIHB1bGwoRGVmYXVsdGVyRmxhZykgIyBFeHRyYWVyIHZhcmlhYmxlIGNvbW8gdmVjdG9yCgpgYGAKCioqVGVyY2VybyoqLCBleHRyYWVyIGxhcyBwcmVkaWNjaW9uZXMgbm9ybWFsaXphZGFzCgpgYGB7cn0KcHJlZC5kZWZhdWx0ZXIubm9ybSA8LSBwcmVkaWN0aW9uJG5ldC5yZXN1bHQKaGVhZChwcmVkLmRlZmF1bHRlci5ub3JtKQoKYGBgCgoqKkN1YXJ0byoqLCBEZW5vcm1hbGl6YXIgKG9wY2lvbmFsLCBpbHVzdHJhdGl2bykuIERhZG8gcXVlIGxvcyB2YWxvcmVzIHlhIGVzdMOhbiBlbnRyZSAwIHkgMSwgbm8gbmVjZXNpdGFzIGRlc25vcm1hbGl6YXIuIFNpbiBlbWJhZXJnbywgZGUgZm9ybWEgaWx1c3RyYXRpdmEgc2UgY29udmllcnRlIGVsIHZlY3RvciBkZSBwcmVkaWNjaW9uZXMgbm9ybWFsaXphZGFzIHByZWQuZGVmYXVsdGVyLm5vcm0gZW4gdW4gdmVjdG9yIG51bcOpcmljbyB5IGxvIG11bHRpcGxpY2EgcG9yIGVsIHZhbG9yIG3DoXhpbW8gZGUgcmVhbC5kZWZhdWx0ZXIuIEludGVudGEgcmVlc2NhbGFyIGxhcyBwcmVkaWNjaW9uZXMsIHBlcm8gbG8gaGFjZSBpbmNvcnJlY3RhbWVudGUgYWwgdXNhciB1bmEgZsOzcm11bGEgZXF1aXZvY2FkYS4KCmBgYHtyfQpwcmVkLmRlZmF1bHRlciA8LSBwcmVkLmRlZmF1bHRlci5ub3JtICU+JQogIGFzLnZlY3RvcigpICU+JQogIGAqYChkaWZmKHJhbmdlKHJlYWwuZGVmYXVsdGVyKSkgKyBtaW4ocmVhbC5kZWZhdWx0ZXIpKQoKaGVhZChwcmVkLmRlZmF1bHRlcikKYGBgCgoqKlF1aW50byoqLCBDcmVhciB1biBkYXRhIGZyYW1lIGNvbiBsb3MgcmVzdWx0YWRvcwoKYGBge3J9CgpEZWZhdWx0ZXJGbGFnIDwtIHRpYmJsZSgKIHJlYWwgPSByZWFsLmRlZmF1bHRlciwKIHByZWRpY3Rpb24gPSBwcmVkLmRlZmF1bHRlcgopCgpoZWFkKERlZmF1bHRlckZsYWcpCmBgYAoKKipTZXh0byoqLCBlc3RhYmxlY2Vtb3MgdW4gdW1icmFsIHF1ZSBkZSBsYSBwcm9iYWJpbGlkYWQgZGUgbGEgcHJlZGljY2nDs24gcXVlIG1pbmltaWNlIGxhcyBwZXJkaWRhcyBkZSBmYWxzb3MgcG9zaXRpdm9zIHkgZmFsc29zIG5lZ2F0aXZvcywgdGVuaWVuZG8gZW4gY3VlbnRhIGxvcyBjb3N0b3MgYXNvY2lhZG9zIGRlIFwkNC4yMDAgcGFyYSBmYWxzb3MgcG9zaXRpdm9zIHkgXCQzLjUwMCBwYXJhIGZhbHNvcyBuZWdhdGl2b3MuIFBhcmEgZWxsbyBjYWxjdWxhbW9zIGVsIHVtYnJhbCBxdWUgbWluaW1pY2UgbGEgb3BjacOzbiBkZSBmYWxzb3MgbmVnYXRpdm8geSBmYWxzb3MgcG9zaXRpdm9zLCBxdWUgYSBzdSB2ZXogbWluaW1pY2VzIGxvcyBjb3N0b3MgcXVlIHRlbmRyw61hIGVsIGVycm9yIGRlbCBtb2RlbG8KCmBgYHtyfQoKIyBDb3N0b3MgZGVmaW5pZG9zCmNvc3RfZnAgPC0gNDIwMCAgIyBDb3N0byBkZSBmYWxzbyBwb3NpdGl2bwpjb3N0X2ZuIDwtIDM1MDAgICMgQ29zdG8gZGUgZmFsc28gbmVnYXRpdm8KCiMgRnVuY2nDs24gcGFyYSBjYWxjdWxhciBlbCBjb3N0byB0b3RhbCBkYWRvIHVuIHVtYnJhbApjYWxjdWxhcl9jb3N0byA8LSBmdW5jdGlvbih1bWJyYWwsIGRhdGEpIHsKICBwcmVkX2NsYXNpZmljYWRhIDwtIGlmZWxzZShkYXRhJHByZWRpY3Rpb24gPj0gdW1icmFsLCAxLCAwKQogIGZwIDwtIHN1bShwcmVkX2NsYXNpZmljYWRhID09IDEgJiBkYXRhJHJlYWwgPT0gMCkKICBmbiA8LSBzdW0ocHJlZF9jbGFzaWZpY2FkYSA9PSAwICYgZGF0YSRyZWFsID09IDEpCiAgcmV0dXJuKGZwICogY29zdF9mcCArIGZuICogY29zdF9mbikKfQoKIyBTZWN1ZW5jaWEgZGUgdW1icmFsZXMKdW1icmFsZXMgPC0gc2VxKDAsIDEsIGJ5ID0gMC4wMDEpCgojIENhbGN1bGFyIGVsIGNvc3RvIHBhcmEgY2FkYSB1bWJyYWwKY29zdG9zIDwtIHNhcHBseSh1bWJyYWxlcywgY2FsY3VsYXJfY29zdG8sIGRhdGEgPSBEZWZhdWx0ZXJGbGFnKQoKIyBDcmVhciB1biBkYXRhZnJhbWUgcGFyYSBncmFmaWNhcgpkZl9jb3N0b3MgPC0gZGF0YS5mcmFtZSh1bWJyYWwgPSB1bWJyYWxlcywgY29zdG8gPSBjb3N0b3MpCgojIEVuY29udHJhciBlbCB1bWJyYWwgw7NwdGltbwp1bWJyYWxfb3B0aW1vIDwtIGRmX2Nvc3RvcyR1bWJyYWxbd2hpY2gubWluKGRmX2Nvc3RvcyRjb3N0byldCmNvc3RvX21pbmltbyA8LSBtaW4oZGZfY29zdG9zJGNvc3RvKQoKIyBNb3N0cmFyIHJlc3VsdGFkb3MKY2F0KCJVbWJyYWwgw7NwdGltbzoiLCByb3VuZCh1bWJyYWxfb3B0aW1vLCAzKSwgIlxuIikKY2F0KCJDb3N0byBtw61uaW1vIHRvdGFsOiIsIGNvc3RvX21pbmltbywgIlxuIikKCgoKCiMgR3JhZmljYXIgY29uIGdncGxvdDIKZ2dwbG90KGRmX2Nvc3RvcywgYWVzKHggPSB1bWJyYWwsIHkgPSBjb3N0bykpICsKICBnZW9tX2xpbmUoY29sb3IgPSAic3RlZWxibHVlIiwgc2l6ZSA9IDEpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSB1bWJyYWxfb3B0aW1vLCBjb2xvciA9ICJyZWQiLCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gY29zdG9fbWluaW1vLCBjb2xvciA9ICJkYXJrZ3JlZW4iLCBsaW5ldHlwZSA9ICJkb3R0ZWQiKSArCiAgYW5ub3RhdGUoInRleHQiLCB4ID0gdW1icmFsX29wdGltbywgeSA9IGNvc3RvX21pbmltbyArIDEwMDAwLAogICAgICAgICAgIGxhYmVsID0gcGFzdGUwKCJVbWJyYWwgw7NwdGltbyA9ICIsIHJvdW5kKHVtYnJhbF9vcHRpbW8sIDMpKSwKICAgICAgICAgICBjb2xvciA9ICJyZWQiLCBoanVzdCA9IC0wLjEpICsKICBhbm5vdGF0ZSgidGV4dCIsIHggPSAwLjA1LCB5ID0gY29zdG9fbWluaW1vLAogICAgICAgICAgIGxhYmVsID0gcGFzdGUwKCJDb3N0byBtw61uaW1vID0gJCIsIGZvcm1hdChjb3N0b19taW5pbW8sIGJpZy5tYXJrID0gIiwiKSksCiAgICAgICAgICAgY29sb3IgPSAiZGFya2dyZWVuIiwgdmp1c3QgPSAtMSkgKwogIGxhYnModGl0bGUgPSAiQ29zdG8gdG90YWwgdnMuIFVtYnJhbCBkZSBwcmVkaWNjacOzbiIsCiAgICAgICB4ID0gIlVtYnJhbCBkZSBjbGFzaWZpY2FjacOzbiIsCiAgICAgICB5ID0gIkNvc3RvIHRvdGFsIikgKwogIHRoZW1lX21pbmltYWwoKQoKYGBgCgojIDUpICoqRXZhbHVhY2nDs24qKgoKKipDKiphbGN1bGFtb3MgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24KCioq8J+TjCBJbnRlcnByZXRhY2nDs24gZGUgbcOpdHJpY2FzIGRlIGNsYXNpZmljYWNpw7NuKioKCnwgKipNw6l0cmljYSoqIHwgKirCv1F1w6kgZXZhbMO6YT8qKiB8ICoqVmFsb3IgYWdyZWdhZG8qKiB8Cnw6LS0tfDotLS18Oi0tLXwKfCAqKkFjY3VyYWN5KiogfCBQcm9wb3JjacOzbiBkZSBwcmVkaWNjaW9uZXMgY29ycmVjdGFzIHNvYnJlIGVsIHRvdGFsLiB8IMOadGlsIGNvbW8gdmlzacOzbiBnZW5lcmFsIGRlbCBkZXNlbXBlw7FvLCBwZXJvIHB1ZWRlIHNlciBlbmdhw7Fvc2Egc2kgaGF5IGRlc2JhbGFuY2UgZW50cmUgY2xhc2VzLiB8CnwgKipLYXBwYSoqIHwgQWN1ZXJkbyBlbnRyZSBwcmVkaWNjacOzbiB5IHJlYWxpZGFkLCBhanVzdGFkbyBwb3IgZWwgYXphci4gfCBFdmFsw7phIHNpIGVsIG1vZGVsbyByZWFsbWVudGUgYXBvcnRhIHZhbG9yIG3DoXMgYWxsw6EgZGUgdW5hIGNsYXNpZmljYWNpw7NuIGFsZWF0b3JpYS4gfAp8ICoqUHJlY2lzaW9uKiogfCBEZSBsb3MgY2Fzb3MgcHJlZGljaG9zIGNvbW8gcG9zaXRpdm9zLCDCv2N1w6FudG9zIGxvIGVyYW4gcmVhbG1lbnRlPyB8IE1pbmltaXphIGZhbHNvcyBwb3NpdGl2b3MuIENsYXZlIHNpIGFjdHVhciBzb2JyZSB1biBmYWxzbyBwb3NpdGl2byB0aWVuZSB1biBjb3N0byAoZWouIHJlY2hhemFyIGNyw6lkaXRvKS4gfAp8ICoqUmVjYWxsIChTZW5zaWJpbGlkYWQpKiogfCBEZSBsb3MgY2Fzb3MgcmVhbG1lbnRlIHBvc2l0aXZvcywgwr9jdcOhbnRvcyBmdWVyb24gZGV0ZWN0YWRvcyBwb3IgZWwgbW9kZWxvPyB8IE1pbmltaXphIGZhbHNvcyBuZWdhdGl2b3MuIEZ1bmRhbWVudGFsIHNpIG9taXRpciB1biBjYXNvIHBvc2l0aXZvIGVzIHJpZXNnb3NvIChlai4gbm8gZGV0ZWN0YXIgZnJhdWRlKS4gfAp8ICoqRjEgU2NvcmUqKiB8IFByb21lZGlvIGFybcOzbmljbyBlbnRyZSBwcmVjaXNpw7NuIHkgcmVjYWxsLiB8IFJlc3VtZSBlbCBiYWxhbmNlIGVudHJlIHByZWNpc2nDs24geSByZWNhbGwuIElkZWFsIGN1YW5kbyBoYXkgZGVzYmFsYW5jZSBkZSBjbGFzZXMuIHwKfCAqKlNwZWNpZmljaXR5KiogfCBEZSBsb3MgY2Fzb3MgbmVnYXRpdm9zIHJlYWxlcywgwr9jdcOhbnRvcyBmdWVyb24gY29ycmVjdGFtZW50ZSBjbGFzaWZpY2Fkb3M/IHwgRXZhbMO6YSBsYSBjYXBhY2lkYWQgZGVsIG1vZGVsbyBwYXJhIG5vIGV0aXF1ZXRhciBlcnLDs25lYW1lbnRlIGEgbG9zIG5lZ2F0aXZvcy4gfAp8ICoqQmFsYW5jZWQgQWNjdXJhY3kqKiB8IFByb21lZGlvIGVudHJlIHNlbnNpYmlsaWRhZCB5IGVzcGVjaWZpY2lkYWQuIHwgQ29ycmlnZSBlbCBzZXNnbyBkZSBhY2N1cmFjeSBlbiBkYXRhc2V0cyBkZXNiYWxhbmNlYWRvcy4gfAoKYGBge3J9CgojIENsYXNpZmljYXIgc2Vnw7puIGVsIHVtYnJhbApEZWZhdWx0ZXJGbGFnIDwtIERlZmF1bHRlckZsYWcgJT4lCiAgbXV0YXRlKHByZWRpY3RlZF9jbGFzcyA9IGlmZWxzZShwcmVkaWN0aW9uID49IHVtYnJhbF9vcHRpbW8sIDEsIDApLAogICAgICAgICByZWFsID0gYXMuZmFjdG9yKHJlYWwpLAogICAgICAgICBwcmVkaWN0ZWRfY2xhc3MgPSBhcy5mYWN0b3IocHJlZGljdGVkX2NsYXNzKSkKCiMgQ3JlYXIgbWF0cml6IGRlIGNvbmZ1c2nDs24KbWF0cml6IDwtIGNvbmZ1c2lvbk1hdHJpeChkYXRhID0gRGVmYXVsdGVyRmxhZyRwcmVkaWN0ZWRfY2xhc3MsIHJlZmVyZW5jZSA9IERlZmF1bHRlckZsYWckcmVhbCwgcG9zaXRpdmUgPSAiMSIpCgojIE1vc3RyYXIgbcOpdHJpY2FzCnByaW50KG1hdHJpeikKCiMgRXh0cmFlciB0YWJsYSBwYXJhIGdyYWZpY2FyCnRhYmxhIDwtIGFzLmRhdGEuZnJhbWUobWF0cml6JHRhYmxlKQpjb2xuYW1lcyh0YWJsYSkgPC0gYygiUmVhbCIsICJQcmVkaWNobyIsICJGcmVjdWVuY2lhIikKCiMgR3JhZmljYXIgbWF0cml6IGRlIGNvbmZ1c2nDs24KZ2dwbG90KHRhYmxhLCBhZXMoeCA9IFByZWRpY2hvLCB5ID0gUmVhbCwgZmlsbCA9IEZyZWN1ZW5jaWEpKSArCiAgZ2VvbV90aWxlKGNvbG9yID0gIndoaXRlIikgKwogIGdlb21fdGV4dChhZXMobGFiZWwgPSBGcmVjdWVuY2lhKSwgc2l6ZSA9IDYpICsKICBzY2FsZV9maWxsX2dyYWRpZW50KGxvdyA9ICJsaWdodGJsdWUiLCBoaWdoID0gInN0ZWVsYmx1ZSIpICsKICBsYWJzKHRpdGxlID0gIk1hdHJpeiBkZSBDb25mdXNpw7NuIiwgeCA9ICJQcmVkaWNjacOzbiIsIHkgPSAiVmFsb3IgUmVhbCIpICsKICB0aGVtZV9taW5pbWFsKCkKCgpgYGAKCkFob3JhIHZlbW9zIGxhcyBtw6l0cmljYXMgZGUgZm9ybWEgZXhwbGljaXRhCgpgYGB7cn0KIyBBY2N1cmFjeQphY2N1cmFjeSA8LSBtYXRyaXokb3ZlcmFsbFsiQWNjdXJhY3kiXQoKIyBLYXBwYSAobWVkaWRhIGRlIGNvbmNvcmRhbmNpYSkKa2FwcGEgPC0gbWF0cml6JG92ZXJhbGxbIkthcHBhIl0KCiMgTcOpdHJpY2FzIHBvciBjbGFzZQpwcmVjaXNpb24gPC0gbWF0cml6JGJ5Q2xhc3NbIlByZWNpc2lvbiJdCnJlY2FsbCA8LSBtYXRyaXokYnlDbGFzc1siUmVjYWxsIl0gICMgdGFtYmnDqW4gY29ub2NpZG8gY29tbyBTZW5zaXRpdml0eQpmMSA8LSBtYXRyaXokYnlDbGFzc1siRjEiXQoKIyBUYW1iacOpbiBwb2RlbW9zIGV4dHJhZXIgb3RyYXMgbcOpdHJpY2FzIMO6dGlsZXM6CnNwZWNpZmljaXR5IDwtIG1hdHJpeiRieUNsYXNzWyJTcGVjaWZpY2l0eSJdCmJhbGFuY2VkX2FjY3VyYWN5IDwtIG1hdHJpeiRieUNsYXNzWyJCYWxhbmNlZCBBY2N1cmFjeSJdCgptZXRyaWNhcyA8LSBkYXRhLmZyYW1lKAogIEFjY3VyYWN5ID0gYWNjdXJhY3ksCiAgS2FwcGEgPSBrYXBwYSwKICBQcmVjaXNpb24gPSBwcmVjaXNpb24sCiAgUmVjYWxsID0gcmVjYWxsLAogIEYxX1Njb3JlID0gZjEsCiAgU3BlY2lmaWNpdHkgPSBzcGVjaWZpY2l0eSwKICBCYWxhbmNlZF9BY2N1cmFjeSA9IGJhbGFuY2VkX2FjY3VyYWN5CikKCnByaW50KG1ldHJpY2FzKQoKCmBgYAoKKirwn5OKIEV2YWx1YWNpw7NuIGRlbCBNb2RlbG8gZGUgQ2xhc2lmaWNhY2nDs24qKgoKU2UgZXZhbHXDsyBlbCBkZXNlbXBlw7FvIGRlbCBtb2RlbG8gdXRpbGl6YW5kbyBtw6l0cmljYXMgZXN0w6FuZGFyIGRlIGNsYXNpZmljYWNpw7NuIGJpbmFyaWEuIEEgY29udGludWFjacOzbiBzZSBwcmVzZW50YW4gbG9zIHJlc3VsdGFkb3Mgb2J0ZW5pZG9zOgoKfCAqKk3DqXRyaWNhKiogfCAqKlZhbG9yKiogfCAqKkludGVycHJldGFjacOzbioqIHwKfDotLS18Oi0tLXw6LS0tfAp8ICoqQWNjdXJhY3kqKiB8IDAuNzM1IHwgRWwgNzMuNSUgZGUgbGFzIHByZWRpY2Npb25lcyB0b3RhbGVzIGZ1ZXJvbiBjb3JyZWN0YXMuIMOadGlsIHNpIGxhcyBjbGFzZXMgZXN0w6FuIGJhbGFuY2VhZGFzLiB8CnwgKipLYXBwYSoqIHwgMC4yNzQgfCBNaWRlIGVsIGFjdWVyZG8gZW50cmUgcHJlZGljY2nDs24geSByZWFsaWRhZCwgYWp1c3RhZG8gcG9yIGF6YXIuIFVuIHZhbG9yIGJham8gaW5kaWNhIHF1ZSBlbCBtb2RlbG8gbm8gbWVqb3JhIG11Y2hvIHNvYnJlIGVsIGF6YXIuIHwKfCAqKlByZWNpc2lvbioqIHwgMC43NzMgfCBEZSB0b2RhcyBsYXMgdmVjZXMgcXVlIGVsIG1vZGVsbyBwcmVkaWpvIOKAnGRlZmF1bHRlcuKAnSAoMSksIGFjZXJ0w7MgZWwgNzcuMyUuIEltcG9ydGFudGUgc2kgbG9zIGZhbHNvcyBwb3NpdGl2b3Mgc29uIGNvc3Rvc29zLiB8CnwgKipSZWNhbGwgKFNlbnNpYmlsaWRhZCkqKiB8IDAuODkwIHwgRGUgdG9kb3MgbG9zIHZlcmRhZGVyb3Mg4oCcZGVmYXVsdGVyc+KAnSwgZWwgbW9kZWxvIGlkZW50aWZpY8OzIGNvcnJlY3RhbWVudGUgZWwgODklLiBJZGVhbCBzaSBlcyBtw6FzIGdyYXZlIG5vIGRldGVjdGFyIHVuIGRlZmF1bHRlci4gfAp8ICoqRjEgU2NvcmUqKiB8IDAuODI3IHwgUHJvbWVkaW8gYXJtw7NuaWNvIGVudHJlIHByZWNpc2nDs24geSByZWNhbGwuIEJ1ZW4gcmVzdW1lbiBjdWFuZG8gaGF5IGRlc2JhbGFuY2UgZGUgY2xhc2VzLiB8CnwgKipTcGVjaWZpY2l0eSoqIHwgMC4zNTQgfCBTb2xvIGVsIDM1LjQlIGRlIGxvcyDigJxubyBkZWZhdWx0ZXJz4oCdIGZ1ZXJvbiBjb3JyZWN0YW1lbnRlIGlkZW50aWZpY2Fkb3MuIEVsIG1vZGVsbyB0aWVuZSBkaWZpY3VsdGFkIHBhcmEgZGV0ZWN0YXIgY29ycmVjdGFtZW50ZSBsb3MgbmVnYXRpdm9zLiB8CnwgKipCYWxhbmNlZCBBY2N1cmFjeSoqIHwgMC42MjIgfCBQcm9tZWRpbyBlbnRyZSBzZW5zaWJpbGlkYWQgeSBlc3BlY2lmaWNpZGFkLiDDmnRpbCBjdWFuZG8gbGFzIGNsYXNlcyBlc3TDoW4gZGVzYmFsYW5jZWFkYXMuIHwKCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKKirwn6egIENvbmNsdXNpb25lcyoqCgotICAgRWwgbW9kZWxvIG11ZXN0cmEgKiphbHRhIGNhcGFjaWRhZCBwYXJhIGlkZW50aWZpY2FyIGNvcnJlY3RhbWVudGUgYSBsb3MgY2xpZW50ZXMgY29uIHJpZXNnbyBkZSBpbmN1bXBsaW1pZW50byoqIChyZWNhbGwgYWx0byksIGxvIGN1YWwgZXMgZGVzZWFibGUgZW4gY29udGV4dG9zIGRlIHJpZXNnbyBjcmVkaXRpY2lvIG8gY3VtcGxpbWllbnRvIG5vcm1hdGl2by4KCi0gICBTaW4gZW1iYXJnbywgKipsYSBiYWphIGVzcGVjaWZpY2lkYWQgaW5kaWNhIHVuYSBhbHRhIHRhc2EgZGUgZmFsc29zIHBvc2l0aXZvcyoqLCBsbyBxdWUgcG9kcsOtYSBsbGV2YXIgYSByZWNoYXphciBjbGllbnRlcyBxdWUgZW4gcmVhbGlkYWQgbm8gcmVwcmVzZW50YW4gcmllc2dvLgoKLSAgIEVsICoqRjEgU2NvcmUgZGUgMC44MjcqKiByZWZsZWphIHVuIGJ1ZW4gZXF1aWxpYnJpbyBlbnRyZSBwcmVjaXNpw7NuIHkgc2Vuc2liaWxpZGFkLgoKLSAgIEVsICoqw61uZGljZSBLYXBwYSBkZSAwLjI3NCoqIHN1Z2llcmUgcXVlIGVsIG1vZGVsbyB0aWVuZSBtYXJnZW4gZGUgbWVqb3JhIHJlc3BlY3RvIGEgdW5hIGNsYXNpZmljYWNpw7NuIGFsZWF0b3JpYS4KCiMgNikgKipEZXNwbGllZ3VlKioKCkVsIG1vZGVsbyBkZSBjbGFzaWZpY2FjacOzbiBldmFsdWFkbyBtdWVzdHJhIHVuIGRlc2VtcGXDsW8gYWNlcHRhYmxlIGVuIHTDqXJtaW5vcyBnZW5lcmFsZXMsIGNvbiB1bmEgcHJlY2lzacOzbiAoYWNjdXJhY3kpIGRlbCA3My41JSB5IHVuIEYxIFNjb3JlIGRlIDAuODI3LCBsbyBxdWUgaW5kaWNhIHVuIGJ1ZW4gZXF1aWxpYnJpbyBlbnRyZSBsYSBjYXBhY2lkYWQgZGUgaWRlbnRpZmljYXIgY29ycmVjdGFtZW50ZSBsb3MgY2Fzb3MgcG9zaXRpdm9zIChkZWZhdWx0ZXJzKSB5IGV2aXRhciBmYWxzb3MgcG9zaXRpdm9zLiBMYSBzZW5zaWJpbGlkYWQgKHJlY2FsbCkgZXMgcGFydGljdWxhcm1lbnRlIGFsdGEgKDAuODkpLCBsbyBxdWUgc3VnaWVyZSBxdWUgZWwgbW9kZWxvIGVzIGVmaWNheiBwYXJhIGRldGVjdGFyIGxhIG1heW9yw61hIGRlIGxvcyBjYXNvcyByZWxldmFudGVzLCB1biBhc3BlY3RvIGNyw610aWNvIGVuIGNvbnRleHRvcyBjb21vIGVsIGFuw6FsaXNpcyBkZSByaWVzZ28gbyBjdW1wbGltaWVudG8gbm9ybWF0aXZvLgoKU2luIGVtYmFyZ28sIGxhIGVzcGVjaWZpY2lkYWQgZXMgYmFqYSAoMC4zNSksIGxvIHF1ZSBpbXBsaWNhIHF1ZSBlbCBtb2RlbG8gdGllbmUgZGlmaWN1bHRhZGVzIHBhcmEgaWRlbnRpZmljYXIgY29ycmVjdGFtZW50ZSBsb3MgY2Fzb3MgbmVnYXRpdm9zIChubyBkZWZhdWx0ZXJzKS4gRXN0byBwdWVkZSB0cmFkdWNpcnNlIGVuIHVuYSB0YXNhIGVsZXZhZGEgZGUgZmFsc29zIHBvc2l0aXZvcywgbG8gY3VhbCBwb2Ryw61hIGltcGFjdGFyIG5lZ2F0aXZhbWVudGUgZW4gZGVjaXNpb25lcyBvcGVyYXRpdmFzLCBjb21vIHJlY2hhemFyIHNvbGljaXR1ZGVzIGRlIGNsaWVudGVzIHF1ZSBlbiByZWFsaWRhZCBubyByZXByZXNlbnRhbiByaWVzZ28uIEVsIMOtbmRpY2UgS2FwcGEgKDAuMjcpIHRhbWJpw6luIHN1Z2llcmUgcXVlIGVsIG1vZGVsbyB0aWVuZSBtYXJnZW4gZGUgbWVqb3JhIHJlc3BlY3RvIGEgdW5hIGNsYXNpZmljYWNpw7NuIGFsZWF0b3JpYS4KCkVuIGN1YW50byBhbCB1c28gZGUgcmVkZXMgbmV1cm9uYWxlcywgZXN0YXMgc29uIHJlY29tZW5kYWJsZXMgY3VhbmRvIHNlIHRyYWJhamEgY29uIGdyYW5kZXMgdm9sw7ptZW5lcyBkZSBkYXRvcywgcmVsYWNpb25lcyBubyBsaW5lYWxlcyBjb21wbGVqYXMgbyBtw7psdGlwbGVzIHZhcmlhYmxlcyBhbHRhbWVudGUgY29ycmVsYWNpb25hZGFzLiBTb24gZXNwZWNpYWxtZW50ZSDDunRpbGVzIGVuIHRhcmVhcyBjb21vIHByb2Nlc2FtaWVudG8gZGUgbGVuZ3VhamUgbmF0dXJhbCwgdmlzacOzbiBwb3IgY29tcHV0YWRvcmEgbyBzZXJpZXMgdGVtcG9yYWxlcyBjb21wbGVqYXMuIEVuIGVsIGNvbnRleHRvIGRlIG1vZGVsb3MgZGUgcmllc2dvIG8gY2xhc2lmaWNhY2nDs24gZGUgY2xpZW50ZXMsIHB1ZWRlbiBzZXIgw7p0aWxlcyBzaSBzZSBkaXNwb25lIGRlIGRhdG9zIGhpc3TDs3JpY29zIHJpY29zIHkgdmFyaWFkb3MsIGNvbW8gdHJhbnNhY2Npb25lcywgY29tcG9ydGFtaWVudG8gZGlnaXRhbCBvIGludGVyYWNjaW9uZXMgbXVsdGljYW5hbC4KCk5vIG9ic3RhbnRlLCBsYXMgcmVkZXMgbmV1cm9uYWxlcyBubyBzaWVtcHJlIHNvbiBsYSBtZWpvciBvcGNpw7NuLiBTaSBlbCBjb25qdW50byBkZSBkYXRvcyBlcyBwZXF1ZcOxbywgc2kgc2UgcmVxdWllcmUgaW50ZXJwcmV0YWJpbGlkYWQgY2xhcmEgcGFyYSBhdWRpdG9yw61hcyBvIHJlZ3VsYWRvcmVzLCBvIHNpIGVsIG1vZGVsbyBkZWJlIHNlciBmw6FjaWxtZW50ZSBleHBsaWNhYmxlIHBhcmEgdXN1YXJpb3MgZGUgbmVnb2NpbywgZW50b25jZXMgbW9kZWxvcyBtw6FzIHNpbXBsZXMgY29tbyDDoXJib2xlcyBkZSBkZWNpc2nDs24sIHJlZ3Jlc2nDs24gbG9nw61zdGljYSBvIG1vZGVsb3MgYmFzYWRvcyBlbiByZWdsYXMgcHVlZGVuIHNlciBwcmVmZXJpYmxlcy4gRW4gZXN0b3MgY2Fzb3MsIGxhIHRyYW5zcGFyZW5jaWEgeSBsYSBmYWNpbGlkYWQgZGUgaW1wbGVtZW50YWNpw7NuIHB1ZWRlbiBwZXNhciBtw6FzIHF1ZSB1bmEgbGlnZXJhIG1lam9yYSBlbiBsYSBwcmVjaXNpw7NuLgo=