1. Estructuración de datos
1.1. Texto en formato tidy
Recordemos los datos que procesamos en la clase anterior para el caso práctico de análisis de noticias, valiéndonos de las herramientas presentes en la librería tidyverse.
library(tidyverse)
noticiasVelascoDF = readRDS("Caso1_Noticias/noticiasVelascoDF.RDS")
noticiasLarreaDF = readRDS("Caso1_Noticias/noticiasLarreaDF.RDS")
noticiasFreileDF = readRDS("Caso1_Noticias/noticiasFreileDF.RDS")
head(noticiasVelascoDF %>% select(Fecha, Diario, Titular, Link, Noticia))
En estos conjuntos de datos tenemos 5 columnas:
- La fecha en la que fue publicada la noticia
- El diario/página que publicó el titular
- El titular de la noticia
- El link a la noticia
- El cuerpo de la noticia como una cadena de texto
Sobre estas columnas trabajaremos para obtener datos de texto en formato tidy. De aquí en adelante el titular de cada noticia será el identificador de documento y por ende, trateremos cada noticia como un documento.
1.2. La librería tidytext
Tidytext es un librería que permite realizar mineo de texto con el uso de herramientas tidy. Estas herramientas facilitan la minería en ámbitos como la adecuación, limpieza y visualización de los datos.
A continuación mostraremos un flujo de adecuación de los datos texto utilizando distintas herramientas de las librerías tidytext y tidyverse.
Flujo de trabajo tidytext, Silge y Robinson (2017).
1.3. Tokenización: unnest_tokens
Al conjunto de datos visto anteriormente necesitamos convertirlo a un data.frame (tibble en el universo tidy) donde se tenga un token por documento por fila. Esto se logra como se muestra a continuación.
library(tidytext)
tidy_Velasco = noticiasVelascoDF %>% unnest_tokens(output = Token, input = Noticia)
tidy_Larrea = noticiasLarreaDF %>% unnest_tokens(output = Token, input = Noticia)
tidy_Freile = noticiasFreileDF %>% unnest_tokens(output = Token, input = Noticia)
Para realizar el seguimiento de nuestros ejemplos, escogeremos una noticia de cada uno de los conjuntos de datos.
- Juan Fernando Velasco: “Una moneda digital para evitar contagios del COVID-19 ofrece Juan Fernando Velasco en su plan de gobierno”
- Gustavo Larrea: “Gustavo Larrea: ‘Debemos lograr que este sea un país agroindustrial sólido’”
- Pedro José Freile: “Freile, un experto en contratación pública que busca el cambio”
ejemplo_Velasco = noticiasVelascoDF$Titular[1]
ejemplo_Larrea = noticiasLarreaDF$Titular[1]
ejemplo_Freile = noticiasFreileDF$Titular[2]
tidy_Velasco %>%
filter(Titular==ejemplo_Velasco) %>%
select(Diario, Titular, Token) %>%
head()
Las características por defecto de la función unnest tokens son:
- Tokenizar por palabras (n-gramas con \(n=1\)), aunque soporta también por caracteres, n-gramas, oraciones, tweets, párrafos y patrones basados en expresiones regulares (regex). Esto lo hace con la opción token.
- Cambiar las letras a minúsculas. Esto lo hace con la opción to_lower.
- Remueve signos de puntación.
Veamos algunos ejemplos:
noticiasLarreaDF %>%
filter(Titular==ejemplo_Larrea) %>%
unnest_tokens(output = Token, input = Noticia, token = 'sentences', to_lower = F) %>%
select(Token)
noticiasFreileDF %>%
filter(Titular==ejemplo_Freile) %>%
unnest_tokens(output = Token, input = Noticia, token = 'ngrams', n=3, to_lower = F) %>%
select(Token)
En esta tokenización podríamos querer también remover números o quitar signos de puntuación. Esto es alcanzable a través del uso de algunas funciones de la librería tm, como se muestra a continuación:
library(tm)
Loading required package: NLP
Attaching package: 㤼㸱NLP㤼㸲
The following object is masked from 㤼㸱package:ggplot2㤼㸲:
annotate
noticiasFreileDF %>%
filter(Titular==ejemplo_Freile) %>%
unnest_tokens(output = Token, input = Noticia, token = 'sentences', to_lower = F) %>%
mutate(Token = removePunctuation(Token)) %>%
mutate(Token = removeNumbers(Token)) %>%
select(Token)
Así mismo, podemos utilizar funciones de la librería stringr (ya cargada con tidyverse) y stringi para realizar cosas como quitar espacios extra, pasar las palabras a minúsculas/mayúsculas, remover acentos, etc.
library(stringi)
noticiasVelascoDF %>%
filter(Titular==ejemplo_Velasco) %>%
unnest_tokens(output = Token, input = Noticia, token = 'sentences', to_lower = F) %>%
mutate(Token = str_squish(string = Token)) %>%
mutate(Token = str_to_upper(Token, locale = 'es')) %>%
mutate(Token = stri_trans_general(Token, id = "Latin-ASCII")) %>%
select(Token)
1.4. Palabras vacías: anti_join y tm::removeWords
Una vez que los datos han sido tokenizados, podemos pasar a la parte de manipulación. En este análisis, y en muchos análisis de texto, puede ser útil quitar las llamadas ‘’palabras vacías’’ o ‘’stopwords’’. Estas son palabras que no son útiles para el análisis. Usualmente estas incluyen artículos, pronombres, preposiciones, etc.
La librería tidytext incluye un diccionario de stopwords cargado, pero solo para el idioma inglés. Para el español tenemos las siguientes opciones (aunque podrían existir otros recursos):
- Diccionario de stopwords incluidos en la librería tm.
- Diccionario de la librería stopwords de las fuentes ‘’stopwords-iso’’ y ‘’snowball’’.
- Diccionarios personalizados.
En nuestro caso, armaremos un diccionario de stopwords de las tres fuentes mencionadas como se muestra a continuación:
library(readxl)
stopwords_es_1 = read_excel("Diccionarios/Stopwords/CustomStopWords.xlsx")
names(stopwords_es_1) = c("Token","Fuente")
stopwords_es_2 = tibble(Token=tm::stopwords(kind = "es"), Fuente="tm")
stopwords_es = rbind(stopwords_es_1, stopwords_es_2)
stopwords_es = stopwords_es[!duplicated(stopwords_es$Token),]
remove(stopwords_es_1, stopwords_es_2)
stopwords_es[sample(nrow(stopwords_es),size = 10, replace = F),]
Si la tokenización realizada utiliza palabras como tokens, podemos remover las stopwords a través de:
tidy_Velasco = tidy_Velasco %>%
anti_join(stopwords_es)
Joining, by = "Token"
tidy_Larrea = tidy_Larrea %>%
anti_join(stopwords_es)
Joining, by = "Token"
tidy_Freile = tidy_Freile %>%
anti_join(stopwords_es)
Joining, by = "Token"
tidy_Velasco %>%
filter(Titular==ejemplo_Velasco) %>%
select(Token) %>%
head()
Si en cambio la tokenización utilizó otro tipo de tokens, podemos remover las stopwords como se muestra a continuación:
noticiasLarreaDF %>%
filter(Titular==ejemplo_Larrea) %>%
unnest_tokens(output = Token, input = Noticia, token = 'sentences', to_lower = F) %>%
mutate(Token = removeWords(Token, stopwords_es$Token)) %>%
select(Token)
1.5 Expresiones regulares (REGEX)
Una expresión regular es un patrón que describe un conjunto específico de cadenas de texto (strings) que tienen una estructura en común y son usadas para buscar coincidencias y/o reemplazarlas. Las expresiones regulares son en realidad el centro de las operaciones con cadenas de texto en todos los lenguajes de programación, aunque su sintaxis varía un poco en cada uno.
En R, podemos realizar estas transformaciones a través de la librería base y con la librería stringr. A continuación podemos ver algunas de estas tareas y sus funciones asociadas:
- Identificar o extraer coincidencias de texto: grep(), str_detect(), str_extract(), str_extract_all().
- Localizar un patrón dentro de una cadena de texto (i.e. obtener la posición del inicio y el final de la coincidencia): regexpr(), gregexpr(), str_locate(), str_locate_all().
- Reemplazar un patrón de texto: gsub(), str_replace(), str_replace_all().
- Dividir una cadena de texto en partes acorde a un patrón: strsplit(), str_split().
Dentro de las expresiones regulares usaremos además algunos cuantificadores y operadores:
* busca las coincidencias al menos cero veces.
+ busca las coincidencias al menos una vez.
? busca las coincidencias máximo una vez.
{n} busca las coincidencias exactamente n veces.
{n,} busca las coincidencias al menos n veces.
{n,m} busca las coincidencias entre n y m veces.
^ busca al principio de la cadena.
$ busca al final de la cadena.
[:digit:] o \d busca dígitos.
\D busca no-dígitos.
[:lower:] busca minúsculas.
[:upper:] busca mayúsculas.
[:alpha:] busca letras.
[:alnum:] busca caracteres alfanuméricos.
[:blank:] busca caracteres blancos como espacio y tabulador.
[:space:] busca caracteres de espacio como tabulador, nueva linea, etc.
[:punct:] busca caracteres de puntuación.
A continuación veremos algunos ejemplos utilizando nuestros datos.
# Extraemos una oración de ejemplo
oracion = substring(noticiasVelascoDF$Noticia[1],1,175)
oracion
[1] " Juan Fernando Velasco, aspirante presidencial del movimiento Construye (antes Ruptura), Lista 25, desarrollan su plan de Gobierno en tres ejes: Económico, Social y Seguridad."
# Reemplazar las letras 'a' minúscula o mayúscula por 'X'
gsub("a|A","X",oracion)
[1] " JuXn FernXndo VelXsco, XspirXnte presidenciXl del movimiento Construye (Xntes RupturX), ListX 25, desXrrollXn su plXn de Gobierno en tres ejes: Económico, SociXl y SeguridXd."
# Reemplazar los espacios por tabulador
gsub(" ","\t",oracion)
[1] "\tJuan\tFernando\tVelasco,\taspirante\tpresidencial\tdel\tmovimiento\tConstruye\t(antes\tRuptura),\tLista\t25,\tdesarrollan\tsu\tplan\tde\tGobierno\ten\ttres\tejes:\tEconómico,\tSocial\ty\tSeguridad."
# Reemplazar todas las letras por Y
str_replace_all(oracion,"[:alpha:]","Y")
[1] " YYYY YYYYYYYY YYYYYYY, YYYYYYYYY YYYYYYYYYYYY YYY YYYYYYYYYY YYYYYYYYY (YYYYY YYYYYYY), YYYYY 25, YYYYYYYYYYY YY YYYY YY YYYYYYYY YY YYYY YYYY: YYYYYYYYY, YYYYYY Y YYYYYYYYY."
# Reemplazar todas las letras mayúsculas por Z
str_replace_all(oracion,"[:upper:]","Z")
[1] " Zuan Zernando Zelasco, aspirante presidencial del movimiento Zonstruye (antes Zuptura), Zista 25, desarrollan su plan de Zobierno en tres ejes: Zconómico, Zocial y Zeguridad."
# Formamos un vector con las palabras de la oración
oracion_vector = str_split(oracion, " ")[[1]]
oracion_vector
[1] "" "Juan" "Fernando" "Velasco," "aspirante" "presidencial" "del"
[8] "movimiento" "Construye" "(antes" "Ruptura)," "Lista" "25," "desarrollan"
[15] "su" "plan" "de" "Gobierno" "en" "tres" "ejes:"
[22] "Económico," "Social" "y" "Seguridad."
# Extraer las palabras con 1 letra S.
grep("S{1}",oracion_vector, value = T)
[1] "Social" "Seguridad."
# Extraer las palabras que empiezan con a
grep("^a", oracion_vector, value = T)
[1] "aspirante"
Como fuente de consulta para ejemplos adicionales de este tema, tenemos la entrada de expresiones regulares en R en el blog de Albert Y. Kim.
2. Stemming y Lematización
Para fuente de consulta tenemos una buena referencia en la entrada de Text Mining en el blog de Danny Morris.
2.1. Stemming
El ‘’stemming’’ es un método para reducir una palabra a su raíz o morfema. Esto se hace con el objetivo de analizar variaciones de una palabra como una sola. Supongamos por ejemplo que tenemos el conjunto de palabras: {moderniza, moderno, moderna, modernos, modernización}. Lo que quisieramos es tener solo la raíz de estas palabras, que en último punto, son iguales. Al realizar el stemming sobre el conjunto mostrado obtendríamos: {modern, modern, modern, modern, modern}.
Actualmente existen varios algoritmos que realizan stemming, los cuales trabajan cortando el final o inicio de una palabra, tomando en cuenta la existencia de prefijos y sufijos que suelen ser comunes. En un gran número de casos este proceso suele ser exitoso, pero no siempre.
Para lograr esto en R utilizaremos la versión en español del algoritmo de Porter a través de la función wordStem de la librería SnowballC. Este algoritmo funciona acorde a 6 reglas:
- Un identificador de regla
- El sufijo a identificar
- El texto por el cual debe ser reemplazado al encontrar el sufijo
- El tamaño del sufijo
- El tamaño del texto de reemplazo
- El tamaño mínimo que debe tener la raíz resultante luego de aplicar la regla (esto es a los efectos de no procesar palabras demasiado pequeñas)
- Una función de validación (una función que verifica si se debe aplicar la regla una vez encontrado el sufijo)
library(SnowballC)
tidy_Velasco = tidy_Velasco %>%
mutate(stem = wordStem(Token, "spanish"))
tidy_Larrea = tidy_Larrea %>%
mutate(stem = wordStem(Token, "spanish"))
tidy_Freile = tidy_Freile %>%
mutate(stem = wordStem(Token, "spanish"))
tidy_Freile %>%
filter(Titular==ejemplo_Freile) %>%
select(Token, stem)
2.1. Lematización
La lematización es un proceso con los mismos objetivos del stemming, pero en lugar de obtener el morfema, obtendríamos la palabra raíz. Para el ejemplo mostrado esperaríamos el siguiente conjunto: {moderno, moderno, moderno, moderno, moderno}.
Así, la lematización realiza el análisis morfológico de las palabras, y para ello toma en cuenta diccionarios detallados a través de los cuales busca una forma de enlazar cada palabra a su lemma. Por ende, esta metodología está íntimamente ligada a la lingüística.
Las maneras más comunes de lematizar incluyen el uso de modelos probabilísticos, cadenas de markov o diccionarios. A continuación veremos cómo hacerlo en R con dos métodos:
# Función de lematización
library(rvest)
lematiza = function( frase ){
palabra = gsub( " ", "+", frase )
base.url = paste0(
"https://www.lenguaje.com/cgi-bin/lema.exe?edition_field=",
palabra,"&B1=Lematizar")
lemma = read_html(base.url, encoding = "latin1") %>%
html_node(css = "div div div div div li") %>%
html_text(trim = T)
lemma = ifelse(lemma=='RaÃz del sustantivo "suma".',frase,lemma)
return(lemma)
}
lematiza('comimos')
[1] "comer"
tidy_Freile %>%
tail(5) %>%
mutate(lemma_dict = sapply(Token,lematiza)) %>%
select(Token, stem, lemma_dict)
- Lematización pre-entrenada de la librería udpipe.
library(udpipe)
# udpipe::udpipe_download_model('spanish') # Descomentar al ejecutar por primera vez
model = udpipe_load_model(file = "spanish-gsd-ud-2.4-190531.udpipe")
tidy_Velasco_annotated = udpipe_annotate(model,
x = noticiasVelascoDF$Noticia,
doc_id = noticiasVelascoDF$Titular)
tidy_Velasco_annotated = as_tibble(tidy_Velasco_annotated)
names(tidy_Velasco_annotated)[6] = "Token"
tidy_Velasco_annotated = tidy_Velasco_annotated %>%
anti_join(stopwords_es) %>%
mutate(Token=removePunctuation(Token)) %>%
filter(Token!="")
tidy_Larrea_annotated = udpipe_annotate(model,
x = noticiasLarreaDF$Noticia,
doc_id = noticiasLarreaDF$Titular)
tidy_Larrea_annotated = as_tibble(tidy_Larrea_annotated)
names(tidy_Larrea_annotated)[6] = "Token"
tidy_Larrea_annotated = tidy_Larrea_annotated %>%
anti_join(stopwords_es) %>%
mutate(Token=removePunctuation(Token)) %>%
filter(Token!="")
Joining, by = "Token"
tidy_Freile_annotated = udpipe_annotate(model,
x = noticiasFreileDF$Noticia,
doc_id = noticiasFreileDF$Titular)
tidy_Freile_annotated = as_tibble(tidy_Freile_annotated)
names(tidy_Freile_annotated)[6] = "Token"
tidy_Freile_annotated = tidy_Freile_annotated %>%
# anti_join(stopwords_es) %>%
mutate(Token=removePunctuation(Token)) %>%
filter(Token!="")
El resultado de la lematización se verá como:
tidy_Velasco_annotated %>%
filter(doc_id==ejemplo_Velasco) %>%
select(Token,lemma, upos, feats)
La ventaja de la librería UDpipe yace en el hecho de que nuestro texto se encuentra ahora anotado. Así, como última tarea, revisaremos las frecuencias sobre las etiquetas POS:
tidy_Velasco_annotated %>%
count(upos) %>%
ggplot()+
geom_col(aes(x=reorder(upos,n),y=n,fill=upos))+
labs(x="Etiqueta POS", y="Frecuencia", title="Noticias: Juan Fernando Velasco")+
coord_flip()+
theme(legend.position = "none", text=element_text(size=18))

Y las 5 palabras más frecuentes (sin contar empates) para cada una de las 4 etiquetas más comunes:
tidy_Velasco_annotated %>%
filter(upos %in% c('NOUN','PROPN','VERB','ADJ')) %>%
count(upos, lemma) %>%
group_by(upos) %>%
slice_max(order_by = n, n = 5) %>%
ggplot()+
geom_col(aes(x=reorder_within(lemma,n,upos),y=n,fill=lemma))+
scale_x_reordered()+
labs(x="Lemma", y="Frecuencia", title="Noticias: Juan Fernando Velasco")+
facet_wrap(vars(upos), scales = "free", ncol = 2)+
coord_flip()+
theme(legend.position = "none", text=element_text(size=18))

Con miras a una próxima clase, guardamos los resultados de este primer análisis:
saveRDS(tidy_Velasco, "Caso1_Noticias/tidy_Velasco.RDS")
saveRDS(tidy_Larrea, "Caso1_Noticias/tidy_Larrea.RDS")
saveRDS(tidy_Freile, "Caso1_Noticias/tidy_Freile.RDS")
saveRDS(tidy_Velasco_annotated, "Caso1_Noticias/tidy_Velasco_annotated.RDS")
saveRDS(tidy_Larrea_annotated, "Caso1_Noticias/tidy_Larrea_annotated.RDS")
saveRDS(tidy_Freile_annotated, "Caso1_Noticias/tidy_Freile_annotated.RDS")
LS0tDQp0aXRsZTogIkFuw6FsaXNpcyBkZSBjb21wb3J0YW1pZW50byBlbiByZWRlcyBzb2NpYWxlcyB1c2FuZG8gUHJvY2VzYW1pZW50byBkZWwgTGVuZ3VhamUgTmF0dXJhbCINCnN1YnRpdGxlOiAnQ2Fww610dWxvIDM6IE9idGVuY2nDs24geSBwcm9jZXNhbWllbnRvIGRlIGRhdG9zIGRlIHRleHRvJw0KYXV0aG9yOiBIdWdvIFBvcnJhcw0Kb3V0cHV0OiANCiAgaHRtbF9ub3RlYm9vazoNCiAgICBjc3M6IEVzdGlsb3MuY3NzDQogICAgdG9jOiB0cnVlDQogICAgdG9jX2RlcHRoOiAyDQogICAgdG9jX2Zsb2F0Og0KICAgICAgY29sbGFwc2VkOiB0cnVlDQogICAgICBzbW9vdGhfc2Nyb2xsOiBmYWxzZQ0KYmlibGlvZ3JhcGh5OiBCaWJsaW9ncmFmaWEuYmliDQpjc2w6IGNlcGFsLnhtbA0Kbm9jaXRlOiB8IA0KICBAU2lsZ2UyMDE3LCBAU3RyYWthMjAxNw0KLS0tDQoNCiMgKioqUEFSVEUgMioqKg0KDQojICoqMS4gRXN0cnVjdHVyYWNpw7NuIGRlIGRhdG9zKioNCg0KIyMgKioxLjEuIFRleHRvIGVuIGZvcm1hdG8gdGlkeSoqDQoNCjxicj48L2JyPg0KPGNlbnRlcj48YT48aW1nIHdpZHRoPSI1MCUiIHNyYz0iZmlncy8wMl9UaWR5dmVyc2UucG5nIj48L2E+PC9jZW50ZXI+DQo8YnI+PC9icj4NCg0KUmVjb3JkZW1vcyBsb3MgZGF0b3MgcXVlIHByb2Nlc2Ftb3MgZW4gbGEgY2xhc2UgYW50ZXJpb3IgcGFyYSBlbCBjYXNvIHByw6FjdGljbyBkZSBhbsOhbGlzaXMgZGUgbm90aWNpYXMsIHZhbGnDqW5kb25vcyBkZSBsYXMgaGVycmFtaWVudGFzIHByZXNlbnRlcyBlbiBsYSBsaWJyZXLDrWEgKnRpZHl2ZXJzZSouDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpub3RpY2lhc1ZlbGFzY29ERiA9IHJlYWRSRFMoIkNhc28xX05vdGljaWFzL25vdGljaWFzVmVsYXNjb0RGLlJEUyIpDQpub3RpY2lhc0xhcnJlYURGID0gcmVhZFJEUygiQ2FzbzFfTm90aWNpYXMvbm90aWNpYXNMYXJyZWFERi5SRFMiKQ0Kbm90aWNpYXNGcmVpbGVERiA9IHJlYWRSRFMoIkNhc28xX05vdGljaWFzL25vdGljaWFzRnJlaWxlREYuUkRTIikNCmhlYWQobm90aWNpYXNWZWxhc2NvREYgJT4lIHNlbGVjdChGZWNoYSwgRGlhcmlvLCBUaXR1bGFyLCBMaW5rLCBOb3RpY2lhKSkNCmBgYA0KDQpFbiBlc3RvcyBjb25qdW50b3MgZGUgZGF0b3MgdGVuZW1vcyA1IGNvbHVtbmFzOg0KDQorIExhIGZlY2hhIGVuIGxhIHF1ZSBmdWUgcHVibGljYWRhIGxhIG5vdGljaWENCisgRWwgZGlhcmlvL3DDoWdpbmEgcXVlIHB1YmxpY8OzIGVsIHRpdHVsYXINCisgRWwgdGl0dWxhciBkZSBsYSBub3RpY2lhDQorIEVsIGxpbmsgYSBsYSBub3RpY2lhDQorIEVsIGN1ZXJwbyBkZSBsYSBub3RpY2lhIGNvbW8gdW5hIGNhZGVuYSBkZSB0ZXh0bw0KDQpTb2JyZSBlc3RhcyBjb2x1bW5hcyB0cmFiYWphcmVtb3MgcGFyYSBvYnRlbmVyIGRhdG9zIGRlIHRleHRvIGVuIGZvcm1hdG8gdGlkeS4gRGUgYXF1w60gZW4gYWRlbGFudGUgZWwgKip0aXR1bGFyKiogZGUgY2FkYSBub3RpY2lhIHNlcsOhIGVsICoqaWRlbnRpZmljYWRvciBkZSBkb2N1bWVudG8qKiB5IHBvciBlbmRlLCB0cmF0ZXJlbW9zIGNhZGEgKipub3RpY2lhKiogY29tbyB1biAqKmRvY3VtZW50byoqLg0KDQojIyAqKjEuMi4gTGEgbGlicmVyw61hIHRpZHl0ZXh0KioNCg0KPGJyPjwvYnI+DQo8Y2VudGVyPjxhPjxpbWcgd2lkdGg9IjIwJSIgc3JjPSJmaWdzLzAyX1RpZHl0ZXh0LnBuZyI+PC9hPjwvY2VudGVyPg0KPGJyPjwvYnI+DQoNCipUaWR5dGV4dCogZXMgdW4gbGlicmVyw61hIHF1ZSBwZXJtaXRlIHJlYWxpemFyIG1pbmVvIGRlIHRleHRvIGNvbiBlbCB1c28gZGUgaGVycmFtaWVudGFzIHRpZHkuIEVzdGFzIGhlcnJhbWllbnRhcyBmYWNpbGl0YW4gbGEgbWluZXLDrWEgZW4gw6FtYml0b3MgY29tbyBsYSBhZGVjdWFjacOzbiwgbGltcGllemEgeSB2aXN1YWxpemFjacOzbiBkZSBsb3MgZGF0b3MuDQoNCkEgY29udGludWFjacOzbiBtb3N0cmFyZW1vcyB1biBmbHVqbyBkZSBhZGVjdWFjacOzbiBkZSBsb3MgZGF0b3MgdGV4dG8gdXRpbGl6YW5kbyBkaXN0aW50YXMgaGVycmFtaWVudGFzIGRlIGxhcyBsaWJyZXLDrWFzICp0aWR5dGV4dCogeSAqdGlkeXZlcnNlKi4NCg0KPGJyPjwvYnI+DQo8Y2VudGVyPjxhPjxpbWcgd2lkdGg9IjkwJSIgc3JjPSJmaWdzLzAyX0ZsdWpvVGlkeTAxLnBuZyI+PC9hPg0KPHAgY2xhc3M9ImNhcHRpb24iPkZsdWpvIGRlIHRyYWJham8gdGlkeXRleHQsIFNpbGdlIHkgUm9iaW5zb24gKDIwMTcpLjwvcD48L2NlbnRlcj4NCjxicj48L2JyPg0KDQojIyAqKjEuMy4gVG9rZW5pemFjacOzbjogdW5uZXN0X3Rva2VucyoqDQoNCkFsIGNvbmp1bnRvIGRlIGRhdG9zIHZpc3RvIGFudGVyaW9ybWVudGUgbmVjZXNpdGFtb3MgY29udmVydGlybG8gYSB1biBkYXRhLmZyYW1lICh0aWJibGUgZW4gZWwgdW5pdmVyc28gdGlkeSkgZG9uZGUgc2UgdGVuZ2EgdW4gdG9rZW4gcG9yIGRvY3VtZW50byBwb3IgZmlsYS4gRXN0byBzZSBsb2dyYSBjb21vIHNlIG11ZXN0cmEgYSBjb250aW51YWNpw7NuLg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXRleHQpDQp0aWR5X1ZlbGFzY28gPSBub3RpY2lhc1ZlbGFzY29ERiAlPiUgdW5uZXN0X3Rva2VucyhvdXRwdXQgPSBUb2tlbiwgaW5wdXQgPSBOb3RpY2lhKQ0KdGlkeV9MYXJyZWEgPSBub3RpY2lhc0xhcnJlYURGICU+JSB1bm5lc3RfdG9rZW5zKG91dHB1dCA9IFRva2VuLCBpbnB1dCA9IE5vdGljaWEpDQp0aWR5X0ZyZWlsZSA9IG5vdGljaWFzRnJlaWxlREYgJT4lIHVubmVzdF90b2tlbnMob3V0cHV0ID0gVG9rZW4sIGlucHV0ID0gTm90aWNpYSkNCmBgYA0KDQpQYXJhIHJlYWxpemFyIGVsIHNlZ3VpbWllbnRvIGRlIG51ZXN0cm9zIGVqZW1wbG9zLCBlc2NvZ2VyZW1vcyB1bmEgbm90aWNpYSBkZSBjYWRhIHVubyBkZSBsb3MgY29uanVudG9zIGRlIGRhdG9zLg0KDQorIEp1YW4gRmVybmFuZG8gVmVsYXNjbzogIlVuYSBtb25lZGEgZGlnaXRhbCBwYXJhIGV2aXRhciBjb250YWdpb3MgZGVsIENPVklELTE5IG9mcmVjZSBKdWFuIEZlcm5hbmRvIFZlbGFzY28gZW4gc3UgcGxhbiBkZSBnb2JpZXJubyINCisgR3VzdGF2byBMYXJyZWE6ICJHdXN0YXZvIExhcnJlYTogJ0RlYmVtb3MgbG9ncmFyIHF1ZSBlc3RlIHNlYSB1biBwYcOtcyBhZ3JvaW5kdXN0cmlhbCBzw7NsaWRvJyINCisgUGVkcm8gSm9zw6kgRnJlaWxlOiAiRnJlaWxlLCB1biBleHBlcnRvIGVuIGNvbnRyYXRhY2nDs24gcMO6YmxpY2EgcXVlIGJ1c2NhIGVsIGNhbWJpbyINCg0KYGBge3J9DQplamVtcGxvX1ZlbGFzY28gPSBub3RpY2lhc1ZlbGFzY29ERiRUaXR1bGFyWzFdDQplamVtcGxvX0xhcnJlYSA9IG5vdGljaWFzTGFycmVhREYkVGl0dWxhclsxXQ0KZWplbXBsb19GcmVpbGUgPSBub3RpY2lhc0ZyZWlsZURGJFRpdHVsYXJbMl0NCnRpZHlfVmVsYXNjbyAlPiUgDQogICAgICAgZmlsdGVyKFRpdHVsYXI9PWVqZW1wbG9fVmVsYXNjbykgJT4lIA0KICAgICAgIHNlbGVjdChEaWFyaW8sIFRpdHVsYXIsIFRva2VuKSAlPiUgDQogICAgICAgaGVhZCgpDQpgYGANCg0KTGFzIGNhcmFjdGVyw61zdGljYXMgcG9yIGRlZmVjdG8gZGUgbGEgZnVuY2nDs24gdW5uZXN0IHRva2VucyBzb246DQoNCisgVG9rZW5pemFyIHBvciBwYWxhYnJhcyAobi1ncmFtYXMgY29uICRuPTEkKSwgYXVucXVlIHNvcG9ydGEgdGFtYmnDqW4gcG9yIGNhcmFjdGVyZXMsIG4tZ3JhbWFzLCBvcmFjaW9uZXMsIHR3ZWV0cywgcMOhcnJhZm9zIHkgcGF0cm9uZXMgYmFzYWRvcyBlbiBleHByZXNpb25lcyByZWd1bGFyZXMgKHJlZ2V4KS4gRXN0byBsbyBoYWNlIGNvbiBsYSBvcGNpw7NuICp0b2tlbiouDQorIENhbWJpYXIgbGFzIGxldHJhcyBhIG1pbsO6c2N1bGFzLiBFc3RvIGxvIGhhY2UgY29uIGxhIG9wY2nDs24gKnRvX2xvd2VyKi4NCisgUmVtdWV2ZSBzaWdub3MgZGUgcHVudGFjacOzbi4NCg0KVmVhbW9zIGFsZ3Vub3MgZWplbXBsb3M6DQoNCmBgYHtyfQ0Kbm90aWNpYXNMYXJyZWFERiAlPiUgDQogIGZpbHRlcihUaXR1bGFyPT1lamVtcGxvX0xhcnJlYSkgJT4lIA0KICB1bm5lc3RfdG9rZW5zKG91dHB1dCA9IFRva2VuLCBpbnB1dCA9IE5vdGljaWEsIHRva2VuID0gJ3NlbnRlbmNlcycsIHRvX2xvd2VyID0gRikgJT4lIA0KICBzZWxlY3QoVG9rZW4pDQpgYGANCg0KYGBge3J9DQpub3RpY2lhc0ZyZWlsZURGICU+JSANCiAgZmlsdGVyKFRpdHVsYXI9PWVqZW1wbG9fRnJlaWxlKSAlPiUgDQogIHVubmVzdF90b2tlbnMob3V0cHV0ID0gVG9rZW4sIGlucHV0ID0gTm90aWNpYSwgdG9rZW4gPSAnbmdyYW1zJywgbj0zLCB0b19sb3dlciA9IEYpICU+JSANCiAgc2VsZWN0KFRva2VuKQ0KYGBgDQoNCkVuIGVzdGEgdG9rZW5pemFjacOzbiBwb2Ryw61hbW9zIHF1ZXJlciB0YW1iacOpbiByZW1vdmVyIG7Dum1lcm9zIG8gcXVpdGFyIHNpZ25vcyBkZSBwdW50dWFjacOzbi4gRXN0byBlcyBhbGNhbnphYmxlIGEgdHJhdsOpcyBkZWwgdXNvIGRlIGFsZ3VuYXMgZnVuY2lvbmVzIGRlIGxhIGxpYnJlcsOtYSAqdG0qLCBjb21vIHNlIG11ZXN0cmEgYSBjb250aW51YWNpw7NuOg0KDQpgYGB7cn0NCmxpYnJhcnkodG0pDQpub3RpY2lhc0ZyZWlsZURGICU+JSANCiAgZmlsdGVyKFRpdHVsYXI9PWVqZW1wbG9fRnJlaWxlKSAlPiUgDQogIHVubmVzdF90b2tlbnMob3V0cHV0ID0gVG9rZW4sIGlucHV0ID0gTm90aWNpYSwgdG9rZW4gPSAnc2VudGVuY2VzJywgdG9fbG93ZXIgPSBGKSAlPiUgDQogIG11dGF0ZShUb2tlbiA9IHJlbW92ZVB1bmN0dWF0aW9uKFRva2VuKSkgJT4lDQogIG11dGF0ZShUb2tlbiA9IHJlbW92ZU51bWJlcnMoVG9rZW4pKSAlPiUgDQogIHNlbGVjdChUb2tlbikNCmBgYA0KDQpBc8OtIG1pc21vLCBwb2RlbW9zIHV0aWxpemFyIGZ1bmNpb25lcyBkZSBsYSBsaWJyZXLDrWEgKnN0cmluZ3IqICh5YSBjYXJnYWRhIGNvbiB0aWR5dmVyc2UpIHkgKnN0cmluZ2kqIHBhcmEgcmVhbGl6YXIgY29zYXMgY29tbyBxdWl0YXIgZXNwYWNpb3MgZXh0cmEsIHBhc2FyIGxhcyBwYWxhYnJhcyBhIG1pbsO6c2N1bGFzL21hecO6c2N1bGFzLCByZW1vdmVyIGFjZW50b3MsIGV0Yy4NCg0KYGBge3J9DQpsaWJyYXJ5KHN0cmluZ2kpDQpub3RpY2lhc1ZlbGFzY29ERiAlPiUgDQogIGZpbHRlcihUaXR1bGFyPT1lamVtcGxvX1ZlbGFzY28pICU+JSANCiAgdW5uZXN0X3Rva2VucyhvdXRwdXQgPSBUb2tlbiwgaW5wdXQgPSBOb3RpY2lhLCB0b2tlbiA9ICdzZW50ZW5jZXMnLCB0b19sb3dlciA9IEYpICU+JSANCiAgbXV0YXRlKFRva2VuID0gc3RyX3NxdWlzaChzdHJpbmcgPSBUb2tlbikpICU+JQ0KICBtdXRhdGUoVG9rZW4gPSBzdHJfdG9fdXBwZXIoVG9rZW4sIGxvY2FsZSA9ICdlcycpKSAlPiUgDQogIG11dGF0ZShUb2tlbiA9IHN0cmlfdHJhbnNfZ2VuZXJhbChUb2tlbiwgaWQgPSAiTGF0aW4tQVNDSUkiKSkgJT4lIA0KICBzZWxlY3QoVG9rZW4pDQpgYGANCg0KIyMgKioxLjQuIFBhbGFicmFzIHZhY8OtYXM6IGFudGlfam9pbiB5IHRtOjpyZW1vdmVXb3JkcyoqDQoNClVuYSB2ZXogcXVlIGxvcyBkYXRvcyBoYW4gc2lkbyB0b2tlbml6YWRvcywgcG9kZW1vcyBwYXNhciBhIGxhIHBhcnRlIGRlIG1hbmlwdWxhY2nDs24uIEVuIGVzdGUgYW7DoWxpc2lzLCB5IGVuIG11Y2hvcyBhbsOhbGlzaXMgZGUgdGV4dG8sIHB1ZWRlIHNlciDDunRpbCBxdWl0YXIgbGFzIGxsYW1hZGFzICcncGFsYWJyYXMgdmFjw61hcycnIG8gJydzdG9wd29yZHMnJy4gRXN0YXMgc29uIHBhbGFicmFzIHF1ZSAqKm5vKiogc29uIMO6dGlsZXMgcGFyYSBlbCBhbsOhbGlzaXMuIFVzdWFsbWVudGUgZXN0YXMgaW5jbHV5ZW4gYXJ0w61jdWxvcywgcHJvbm9tYnJlcywgcHJlcG9zaWNpb25lcywgZXRjLg0KDQpMYSBsaWJyZXLDrWEgdGlkeXRleHQgaW5jbHV5ZSB1biBkaWNjaW9uYXJpbyBkZSBzdG9wd29yZHMgY2FyZ2FkbywgcGVybyBzb2xvIHBhcmEgZWwgaWRpb21hIGluZ2zDqXMuIFBhcmEgZWwgZXNwYcOxb2wgdGVuZW1vcyBsYXMgc2lndWllbnRlcyBvcGNpb25lcyAoYXVucXVlIHBvZHLDrWFuIGV4aXN0aXIgb3Ryb3MgcmVjdXJzb3MpOg0KDQorIERpY2Npb25hcmlvIGRlIHN0b3B3b3JkcyBpbmNsdWlkb3MgZW4gbGEgbGlicmVyw61hIHRtLg0KKyBEaWNjaW9uYXJpbyBkZSBsYSBsaWJyZXLDrWEgc3RvcHdvcmRzIGRlIGxhcyBmdWVudGVzICcnc3RvcHdvcmRzLWlzbycnIHkgJydzbm93YmFsbCcnLg0KKyBEaWNjaW9uYXJpb3MgcGVyc29uYWxpemFkb3MuDQoNCkVuIG51ZXN0cm8gY2FzbywgYXJtYXJlbW9zIHVuIGRpY2Npb25hcmlvIGRlIHN0b3B3b3JkcyBkZSBsYXMgdHJlcyBmdWVudGVzIG1lbmNpb25hZGFzIGNvbW8gc2UgbXVlc3RyYSBhIGNvbnRpbnVhY2nDs246DQoNCmBgYHtyfQ0KbGlicmFyeShyZWFkeGwpDQpzdG9wd29yZHNfZXNfMSA9IHJlYWRfZXhjZWwoIkRpY2Npb25hcmlvcy9TdG9wd29yZHMvQ3VzdG9tU3RvcFdvcmRzLnhsc3giKQ0KbmFtZXMoc3RvcHdvcmRzX2VzXzEpID0gYygiVG9rZW4iLCJGdWVudGUiKQ0Kc3RvcHdvcmRzX2VzXzIgPSB0aWJibGUoVG9rZW49dG06OnN0b3B3b3JkcyhraW5kID0gImVzIiksIEZ1ZW50ZT0idG0iKQ0Kc3RvcHdvcmRzX2VzID0gcmJpbmQoc3RvcHdvcmRzX2VzXzEsIHN0b3B3b3Jkc19lc18yKQ0Kc3RvcHdvcmRzX2VzID0gc3RvcHdvcmRzX2VzWyFkdXBsaWNhdGVkKHN0b3B3b3Jkc19lcyRUb2tlbiksXQ0KcmVtb3ZlKHN0b3B3b3Jkc19lc18xLCBzdG9wd29yZHNfZXNfMikNCg0Kc3RvcHdvcmRzX2VzW3NhbXBsZShucm93KHN0b3B3b3Jkc19lcyksc2l6ZSA9IDEwLCByZXBsYWNlID0gRiksXQ0KYGBgDQoNClNpIGxhIHRva2VuaXphY2nDs24gcmVhbGl6YWRhIHV0aWxpemEgcGFsYWJyYXMgY29tbyB0b2tlbnMsIHBvZGVtb3MgcmVtb3ZlciBsYXMgc3RvcHdvcmRzIGEgdHJhdsOpcyBkZToNCg0KYGBge3J9DQp0aWR5X1ZlbGFzY28gPSB0aWR5X1ZlbGFzY28gJT4lDQogIGFudGlfam9pbihzdG9wd29yZHNfZXMpDQp0aWR5X0xhcnJlYSA9IHRpZHlfTGFycmVhICU+JQ0KICBhbnRpX2pvaW4oc3RvcHdvcmRzX2VzKQ0KdGlkeV9GcmVpbGUgPSB0aWR5X0ZyZWlsZSAlPiUNCiAgYW50aV9qb2luKHN0b3B3b3Jkc19lcykNCmBgYA0KDQpgYGB7cn0NCnRpZHlfVmVsYXNjbyAlPiUgDQogIGZpbHRlcihUaXR1bGFyPT1lamVtcGxvX1ZlbGFzY28pICU+JSANCiAgc2VsZWN0KFRva2VuKSAlPiUgDQogIGhlYWQoKQ0KYGBgDQoNClNpIGVuIGNhbWJpbyBsYSB0b2tlbml6YWNpw7NuIHV0aWxpesOzIG90cm8gdGlwbyBkZSB0b2tlbnMsIHBvZGVtb3MgcmVtb3ZlciBsYXMgc3RvcHdvcmRzIGNvbW8gc2UgbXVlc3RyYSBhIGNvbnRpbnVhY2nDs246DQoNCmBgYHtyfQ0Kbm90aWNpYXNMYXJyZWFERiAlPiUgDQogIGZpbHRlcihUaXR1bGFyPT1lamVtcGxvX0xhcnJlYSkgJT4lIA0KICB1bm5lc3RfdG9rZW5zKG91dHB1dCA9IFRva2VuLCBpbnB1dCA9IE5vdGljaWEsIHRva2VuID0gJ3NlbnRlbmNlcycsIHRvX2xvd2VyID0gRikgJT4lIA0KICBtdXRhdGUoVG9rZW4gPSByZW1vdmVXb3JkcyhUb2tlbiwgc3RvcHdvcmRzX2VzJFRva2VuKSkgJT4lIA0KICBzZWxlY3QoVG9rZW4pDQpgYGANCg0KIyMgKioxLjUgRXhwcmVzaW9uZXMgcmVndWxhcmVzIChSRUdFWCkqKg0KDQpVbmEgKmV4cHJlc2nDs24gcmVndWxhciogZXMgdW4gcGF0csOzbiBxdWUgZGVzY3JpYmUgdW4gY29uanVudG8gZXNwZWPDrWZpY28gZGUgY2FkZW5hcyBkZSB0ZXh0byAoKnN0cmluZ3MqKSBxdWUgdGllbmVuIHVuYSBlc3RydWN0dXJhIGVuIGNvbcO6biB5IHNvbiB1c2FkYXMgcGFyYSBidXNjYXIgY29pbmNpZGVuY2lhcyB5L28gcmVlbXBsYXphcmxhcy4gTGFzIGV4cHJlc2lvbmVzIHJlZ3VsYXJlcyBzb24gZW4gcmVhbGlkYWQgZWwgY2VudHJvIGRlIGxhcyBvcGVyYWNpb25lcyBjb24gY2FkZW5hcyBkZSB0ZXh0byBlbiB0b2RvcyBsb3MgbGVuZ3VhamVzIGRlIHByb2dyYW1hY2nDs24sIGF1bnF1ZSBzdSBzaW50YXhpcyB2YXLDrWEgdW4gcG9jbyBlbiBjYWRhIHVuby4NCg0KRW4gUiwgcG9kZW1vcyByZWFsaXphciBlc3RhcyB0cmFuc2Zvcm1hY2lvbmVzIGEgdHJhdsOpcyBkZSBsYSBsaWJyZXLDrWEgYmFzZSB5IGNvbiBsYSBsaWJyZXLDrWEgKnN0cmluZ3IqLiBBIGNvbnRpbnVhY2nDs24gcG9kZW1vcyB2ZXIgYWxndW5hcyBkZSBlc3RhcyB0YXJlYXMgeSBzdXMgZnVuY2lvbmVzIGFzb2NpYWRhczoNCg0KKyBJZGVudGlmaWNhciBvIGV4dHJhZXIgY29pbmNpZGVuY2lhcyBkZSB0ZXh0bzogKmdyZXAoKSosICpzdHJfZGV0ZWN0KCkqLCAqc3RyX2V4dHJhY3QoKSosICpzdHJfZXh0cmFjdF9hbGwoKSouDQorIExvY2FsaXphciB1biBwYXRyw7NuIGRlbnRybyBkZSB1bmEgY2FkZW5hIGRlIHRleHRvIChpLmUuIG9idGVuZXIgbGEgcG9zaWNpw7NuIGRlbCBpbmljaW8geSBlbCBmaW5hbCBkZSBsYSBjb2luY2lkZW5jaWEpOiAqcmVnZXhwcigpKiwgKmdyZWdleHByKCkqLCAqc3RyX2xvY2F0ZSgpKiwgKnN0cl9sb2NhdGVfYWxsKCkqLg0KKyBSZWVtcGxhemFyIHVuIHBhdHLDs24gZGUgdGV4dG86ICpnc3ViKCkqLCAqc3RyX3JlcGxhY2UoKSosICpzdHJfcmVwbGFjZV9hbGwoKSouDQorIERpdmlkaXIgdW5hIGNhZGVuYSBkZSB0ZXh0byBlbiBwYXJ0ZXMgYWNvcmRlIGEgdW4gcGF0csOzbjogKnN0cnNwbGl0KCkqLCAqc3RyX3NwbGl0KCkqLg0KDQpEZW50cm8gZGUgbGFzIGV4cHJlc2lvbmVzIHJlZ3VsYXJlcyB1c2FyZW1vcyBhZGVtw6FzIGFsZ3Vub3MgY3VhbnRpZmljYWRvcmVzIHkgb3BlcmFkb3JlczoNCg0KKyAqKlwqICoqIGJ1c2NhIGxhcyBjb2luY2lkZW5jaWFzIGFsIG1lbm9zIGNlcm8gdmVjZXMuDQorICoqKyoqIGJ1c2NhIGxhcyBjb2luY2lkZW5jaWFzIGFsIG1lbm9zIHVuYSB2ZXouDQorICoqPyoqIGJ1c2NhIGxhcyBjb2luY2lkZW5jaWFzIG3DoXhpbW8gdW5hIHZlei4NCisgKip7bn0qKiBidXNjYSBsYXMgY29pbmNpZGVuY2lhcyBleGFjdGFtZW50ZSBuIHZlY2VzLg0KKyAqKntuLH0qKiBidXNjYSBsYXMgY29pbmNpZGVuY2lhcyBhbCBtZW5vcyBuIHZlY2VzLg0KKyAqKntuLG19KiogYnVzY2EgbGFzIGNvaW5jaWRlbmNpYXMgZW50cmUgbiB5IG0gdmVjZXMuDQoNCisgKipeKiogYnVzY2EgYWwgcHJpbmNpcGlvIGRlIGxhIGNhZGVuYS4NCisgKiokKiogYnVzY2EgYWwgZmluYWwgZGUgbGEgY2FkZW5hLg0KDQorICoqWzpkaWdpdDpdKiogbyAqKlxcZCoqIGJ1c2NhIGTDrWdpdG9zLg0KKyAqKlxcRCoqIGJ1c2NhIG5vLWTDrWdpdG9zLg0KKyAqKls6bG93ZXI6XSoqIGJ1c2NhIG1pbsO6c2N1bGFzLg0KKyAqKls6dXBwZXI6XSoqIGJ1c2NhIG1hecO6c2N1bGFzLg0KKyAqKls6YWxwaGE6XSoqIGJ1c2NhIGxldHJhcy4NCisgKipbOmFsbnVtOl0qKiBidXNjYSBjYXJhY3RlcmVzIGFsZmFudW3DqXJpY29zLg0KKyAqKls6Ymxhbms6XSoqIGJ1c2NhIGNhcmFjdGVyZXMgYmxhbmNvcyBjb21vIGVzcGFjaW8geSB0YWJ1bGFkb3IuDQorICoqWzpzcGFjZTpdKiogYnVzY2EgY2FyYWN0ZXJlcyBkZSBlc3BhY2lvIGNvbW8gdGFidWxhZG9yLCBudWV2YSBsaW5lYSwgZXRjLg0KKyAqKls6cHVuY3Q6XSoqIGJ1c2NhIGNhcmFjdGVyZXMgZGUgcHVudHVhY2nDs24uDQoNCkEgY29udGludWFjacOzbiB2ZXJlbW9zIGFsZ3Vub3MgZWplbXBsb3MgdXRpbGl6YW5kbyBudWVzdHJvcyBkYXRvcy4NCg0KYGBge3J9DQojIEV4dHJhZW1vcyB1bmEgb3JhY2nDs24gZGUgZWplbXBsbw0Kb3JhY2lvbiA9IHN1YnN0cmluZyhub3RpY2lhc1ZlbGFzY29ERiROb3RpY2lhWzFdLDEsMTc1KQ0Kb3JhY2lvbg0KDQojIFJlZW1wbGF6YXIgbGFzIGxldHJhcyAnYScgbWluw7pzY3VsYSBvIG1hecO6c2N1bGEgcG9yICdYJw0KZ3N1YigiYXxBIiwiWCIsb3JhY2lvbikNCg0KIyBSZWVtcGxhemFyIGxvcyBlc3BhY2lvcyBwb3IgdGFidWxhZG9yDQpnc3ViKCIgIiwiXHQiLG9yYWNpb24pDQoNCiMgUmVlbXBsYXphciB0b2RhcyBsYXMgbGV0cmFzIHBvciBZDQpzdHJfcmVwbGFjZV9hbGwob3JhY2lvbiwiWzphbHBoYTpdIiwiWSIpDQoNCiMgUmVlbXBsYXphciB0b2RhcyBsYXMgbGV0cmFzIG1hecO6c2N1bGFzIHBvciBaDQpzdHJfcmVwbGFjZV9hbGwob3JhY2lvbiwiWzp1cHBlcjpdIiwiWiIpDQoNCiMgRm9ybWFtb3MgdW4gdmVjdG9yIGNvbiBsYXMgcGFsYWJyYXMgZGUgbGEgb3JhY2nDs24NCm9yYWNpb25fdmVjdG9yID0gc3RyX3NwbGl0KG9yYWNpb24sICIgIilbWzFdXQ0Kb3JhY2lvbl92ZWN0b3INCg0KIyBFeHRyYWVyIGxhcyBwYWxhYnJhcyBjb24gMSBsZXRyYSBTLiANCmdyZXAoIlN7MX0iLG9yYWNpb25fdmVjdG9yLCB2YWx1ZSA9IFQpDQoNCiMgRXh0cmFlciBsYXMgcGFsYWJyYXMgcXVlIGVtcGllemFuIGNvbiBhDQpncmVwKCJeYSIsIG9yYWNpb25fdmVjdG9yLCB2YWx1ZSA9IFQpDQpgYGANCg0KQ29tbyBmdWVudGUgZGUgY29uc3VsdGEgcGFyYSBlamVtcGxvcyBhZGljaW9uYWxlcyBkZSBlc3RlIHRlbWEsIHRlbmVtb3MgbGEgZW50cmFkYSBkZSBleHByZXNpb25lcyByZWd1bGFyZXMgZW4gUiBlbiBlbCBibG9nIGRlIFtBbGJlcnQgWS4gS2ltXShodHRwczovL3JzdHVkaW8tcHVicy1zdGF0aWMuczMuYW1hem9uYXdzLmNvbS83NDYwM183NmNkMTRkNTk4M2Y0NzQwOGZkZjBiMzIzNTUwYjg0Ni5odG1sKS4NCg0KIyAqKjIuIFN0ZW1taW5nIHkgTGVtYXRpemFjacOzbioqDQoNClBhcmEgZnVlbnRlIGRlIGNvbnN1bHRhIHRlbmVtb3MgdW5hIGJ1ZW5hIHJlZmVyZW5jaWEgZW4gbGEgZW50cmFkYSBkZSBUZXh0IE1pbmluZyBlbiBlbCBibG9nIGRlIFtEYW5ueSBNb3JyaXNdKGh0dHBzOi8vYWJuZGlzdHJvLmNvbS9wb3N0LzIwMTkvMDIvMTAvdGlkeS10ZXh0LW1pbmluZy1pbi1yLyNzdGVtbWluZykuDQoNCiMjICoqMi4xLiBTdGVtbWluZyoqDQoNCkVsIConJ3N0ZW1taW5nJycqIGVzIHVuIG3DqXRvZG8gcGFyYSByZWR1Y2lyIHVuYSBwYWxhYnJhIGEgc3UgcmHDrXogbyBtb3JmZW1hLiBFc3RvIHNlIGhhY2UgY29uIGVsIG9iamV0aXZvIGRlIGFuYWxpemFyIHZhcmlhY2lvbmVzIGRlIHVuYSBwYWxhYnJhIGNvbW8gdW5hIHNvbGEuIFN1cG9uZ2Ftb3MgcG9yIGVqZW1wbG8gcXVlIHRlbmVtb3MgZWwgY29uanVudG8gZGUgcGFsYWJyYXM6ICp7bW9kZXJuaXphLCBtb2Rlcm5vLCBtb2Rlcm5hLCBtb2Rlcm5vcywgbW9kZXJuaXphY2nDs259Ki4gTG8gcXVlIHF1aXNpZXJhbW9zIGVzIHRlbmVyIHNvbG8gbGEgcmHDrXogZGUgZXN0YXMgcGFsYWJyYXMsIHF1ZSBlbiDDumx0aW1vIHB1bnRvLCBzb24gaWd1YWxlcy4gQWwgcmVhbGl6YXIgZWwgc3RlbW1pbmcgc29icmUgZWwgY29uanVudG8gbW9zdHJhZG8gb2J0ZW5kcsOtYW1vczogKnttb2Rlcm4sIG1vZGVybiwgbW9kZXJuLCBtb2Rlcm4sIG1vZGVybn0qLg0KDQpBY3R1YWxtZW50ZSBleGlzdGVuIHZhcmlvcyBhbGdvcml0bW9zIHF1ZSByZWFsaXphbiBzdGVtbWluZywgbG9zIGN1YWxlcyB0cmFiYWphbiBjb3J0YW5kbyBlbCBmaW5hbCBvIGluaWNpbyBkZSB1bmEgcGFsYWJyYSwgdG9tYW5kbyBlbiBjdWVudGEgbGEgZXhpc3RlbmNpYSBkZSBwcmVmaWpvcyB5IHN1Zmlqb3MgcXVlIHN1ZWxlbiBzZXIgY29tdW5lcy4gRW4gdW4gZ3JhbiBuw7ptZXJvIGRlIGNhc29zIGVzdGUgcHJvY2VzbyBzdWVsZSBzZXIgZXhpdG9zbywgcGVybyBubyBzaWVtcHJlLg0KDQpQYXJhIGxvZ3JhciBlc3RvIGVuIFIgdXRpbGl6YXJlbW9zIGxhIHZlcnNpw7NuIGVuIGVzcGHDsW9sIGRlbCBhbGdvcml0bW8gZGUgUG9ydGVyIGEgdHJhdsOpcyBkZSBsYSBmdW5jacOzbiAqd29yZFN0ZW0qIGRlIGxhIGxpYnJlcsOtYSAqU25vd2JhbGxDKi4gRXN0ZSBhbGdvcml0bW8gZnVuY2lvbmEgYWNvcmRlIGEgNiByZWdsYXM6DQoNCjEuIFVuIGlkZW50aWZpY2Fkb3IgZGUgcmVnbGENCjIuIEVsIHN1ZmlqbyBhIGlkZW50aWZpY2FyDQozLiBFbCB0ZXh0byBwb3IgZWwgY3VhbCBkZWJlIHNlciByZWVtcGxhemFkbyBhbCBlbmNvbnRyYXIgZWwgc3VmaWpvDQo0LiBFbCB0YW1hw7FvIGRlbCBzdWZpam8NCjUuIEVsIHRhbWHDsW8gZGVsIHRleHRvIGRlIHJlZW1wbGF6bw0KNi4gRWwgdGFtYcOxbyBtw61uaW1vIHF1ZSBkZWJlIHRlbmVyIGxhIHJhw616IHJlc3VsdGFudGUgbHVlZ28gZGUgYXBsaWNhciBsYSByZWdsYSAoZXN0byBlcyBhIGxvcyBlZmVjdG9zIGRlIG5vIHByb2Nlc2FyIHBhbGFicmFzIGRlbWFzaWFkbyBwZXF1ZcOxYXMpDQo3LiBVbmEgZnVuY2nDs24gZGUgdmFsaWRhY2nDs24gKHVuYSBmdW5jacOzbiBxdWUgdmVyaWZpY2Egc2kgc2UgZGViZSBhcGxpY2FyIGxhIHJlZ2xhIHVuYSB2ZXogZW5jb250cmFkbyBlbCBzdWZpam8pDQoNCmBgYHtyfQ0KbGlicmFyeShTbm93YmFsbEMpDQp0aWR5X1ZlbGFzY28gPSB0aWR5X1ZlbGFzY28gJT4lIA0KICBtdXRhdGUoc3RlbSA9IHdvcmRTdGVtKFRva2VuLCAic3BhbmlzaCIpKQ0KdGlkeV9MYXJyZWEgPSB0aWR5X0xhcnJlYSAlPiUgDQogIG11dGF0ZShzdGVtID0gd29yZFN0ZW0oVG9rZW4sICJzcGFuaXNoIikpDQp0aWR5X0ZyZWlsZSA9IHRpZHlfRnJlaWxlICU+JSANCiAgbXV0YXRlKHN0ZW0gPSB3b3JkU3RlbShUb2tlbiwgInNwYW5pc2giKSkNCmBgYA0KDQpgYGB7cn0NCnRpZHlfRnJlaWxlICU+JSANCiAgZmlsdGVyKFRpdHVsYXI9PWVqZW1wbG9fRnJlaWxlKSAlPiUgDQogIHNlbGVjdChUb2tlbiwgc3RlbSkNCmBgYA0KDQojIyAqKjIuMS4gTGVtYXRpemFjacOzbioqDQoNCkxhIGxlbWF0aXphY2nDs24gZXMgdW4gcHJvY2VzbyBjb24gbG9zIG1pc21vcyBvYmpldGl2b3MgZGVsIHN0ZW1taW5nLCBwZXJvIGVuIGx1Z2FyIGRlIG9idGVuZXIgZWwgbW9yZmVtYSwgb2J0ZW5kcsOtYW1vcyBsYSBwYWxhYnJhIHJhw616LiBQYXJhIGVsIGVqZW1wbG8gbW9zdHJhZG8gZXNwZXJhcsOtYW1vcyBlbCBzaWd1aWVudGUgY29uanVudG86ICp7bW9kZXJubywgbW9kZXJubywgbW9kZXJubywgbW9kZXJubywgbW9kZXJub30qLiANCg0KQXPDrSwgbGEgbGVtYXRpemFjacOzbiByZWFsaXphIGVsIGFuw6FsaXNpcyBtb3Jmb2zDs2dpY28gZGUgbGFzIHBhbGFicmFzLCB5IHBhcmEgZWxsbyB0b21hIGVuIGN1ZW50YSBkaWNjaW9uYXJpb3MgZGV0YWxsYWRvcyBhIHRyYXbDqXMgZGUgbG9zIGN1YWxlcyBidXNjYSB1bmEgZm9ybWEgZGUgZW5sYXphciBjYWRhIHBhbGFicmEgYSBzdSAqbGVtbWEqLiBQb3IgZW5kZSwgZXN0YSBtZXRvZG9sb2fDrWEgZXN0w6Egw61udGltYW1lbnRlIGxpZ2FkYSBhIGxhIGxpbmfDvMOtc3RpY2EuIA0KDQpMYXMgbWFuZXJhcyBtw6FzIGNvbXVuZXMgZGUgbGVtYXRpemFyIGluY2x1eWVuIGVsIHVzbyBkZSBtb2RlbG9zIHByb2JhYmlsw61zdGljb3MsIGNhZGVuYXMgZGUgbWFya292IG8gZGljY2lvbmFyaW9zLiBBIGNvbnRpbnVhY2nDs24gdmVyZW1vcyBjw7NtbyBoYWNlcmxvIGVuIFIgY29uIGRvcyBtw6l0b2RvczoNCg0KKyBMZW1hdGl6YWNpw7NuIGEgdHJhdsOpcyBkZSBkaWNjaW9uYXJpb3MgKFtDYXJsb3MgR2lsIC0gRGF0YUFuYWx5dGljc10oaHR0cHM6Ly93d3cuZGF0YW5hbHl0aWNzLmNvbS8yMDExLzEyLzEzL3VuLWxlbWF0aXphZG9yLXBhcmEtZWwtZXNwYW5vbC1jb24tci0lYzIlYmZjdXRyZS0lYzIlYmZtZWpvcmFibGUvKSwgW2RpY2Npb25hcmlvIGxlbWF0aXphZG9yIG9ubGluZV0oaHR0cHM6Ly93d3cubGVuZ3VhamUuY29tL2NnaS1iaW4vbGVtYS5leGU/ZWRpdGlvbl9maWVsZD1zdW1hciZCMT1MZW1hdGl6YXIpKS4NCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQojIEZ1bmNpw7NuIGRlIGxlbWF0aXphY2nDs24NCmxpYnJhcnkocnZlc3QpDQpsZW1hdGl6YSA9IGZ1bmN0aW9uKCBmcmFzZSApew0KCXBhbGFicmEgPSBnc3ViKCAiICIsICIrIiwgZnJhc2UgKQ0KCWJhc2UudXJsID0gcGFzdGUwKCANCiAgICAgICAgICAgICAiaHR0cHM6Ly93d3cubGVuZ3VhamUuY29tL2NnaS1iaW4vbGVtYS5leGU/ZWRpdGlvbl9maWVsZD0iLCANCiAgICAgICAgICAgICBwYWxhYnJhLCImQjE9TGVtYXRpemFyIikNCglsZW1tYSA9IHJlYWRfaHRtbChiYXNlLnVybCwgZW5jb2RpbmcgPSAibGF0aW4xIikgJT4lIA0KCSAgaHRtbF9ub2RlKGNzcyA9ICJkaXYgZGl2IGRpdiBkaXYgZGl2IGxpIikgJT4lIA0KCSAgaHRtbF90ZXh0KHRyaW0gPSBUKSANCglsZW1tYSA9IGlmZWxzZShsZW1tYT09J1Jhw4PCrXogZGVsIHN1c3RhbnRpdm8gInN1bWEiLicsZnJhc2UsbGVtbWEpDQoJcmV0dXJuKGxlbW1hKQ0KfQ0KbGVtYXRpemEoJ2NvbWltb3MnKQ0KYGBgDQpgYGB7cn0NCnRpZHlfRnJlaWxlICU+JSANCiAgdGFpbCg1KSAlPiUgDQogIG11dGF0ZShsZW1tYV9kaWN0ID0gc2FwcGx5KFRva2VuLGxlbWF0aXphKSkgJT4lDQogIHNlbGVjdChUb2tlbiwgc3RlbSwgbGVtbWFfZGljdCkNCmBgYA0KDQorIExlbWF0aXphY2nDs24gcHJlLWVudHJlbmFkYSBkZSBsYSBsaWJyZXLDrWEgdWRwaXBlLg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodWRwaXBlKQ0KIyB1ZHBpcGU6OnVkcGlwZV9kb3dubG9hZF9tb2RlbCgnc3BhbmlzaCcpICMgRGVzY29tZW50YXIgYWwgZWplY3V0YXIgcG9yIHByaW1lcmEgdmV6DQptb2RlbCA9IHVkcGlwZV9sb2FkX21vZGVsKGZpbGUgPSAic3BhbmlzaC1nc2QtdWQtMi40LTE5MDUzMS51ZHBpcGUiKQ0KdGlkeV9WZWxhc2NvX2Fubm90YXRlZCA9IHVkcGlwZV9hbm5vdGF0ZShtb2RlbCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeCA9IG5vdGljaWFzVmVsYXNjb0RGJE5vdGljaWEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRvY19pZCA9IG5vdGljaWFzVmVsYXNjb0RGJFRpdHVsYXIpDQp0aWR5X1ZlbGFzY29fYW5ub3RhdGVkID0gYXNfdGliYmxlKHRpZHlfVmVsYXNjb19hbm5vdGF0ZWQpDQpuYW1lcyh0aWR5X1ZlbGFzY29fYW5ub3RhdGVkKVs2XSA9ICJUb2tlbiIgDQp0aWR5X1ZlbGFzY29fYW5ub3RhdGVkID0gdGlkeV9WZWxhc2NvX2Fubm90YXRlZCAlPiUgDQogIGFudGlfam9pbihzdG9wd29yZHNfZXMpICU+JSANCiAgbXV0YXRlKFRva2VuPXJlbW92ZVB1bmN0dWF0aW9uKFRva2VuKSkgJT4lIA0KICBmaWx0ZXIoVG9rZW4hPSIiKQ0KYGBgDQoNCmBgYHtyfQ0KdGlkeV9MYXJyZWFfYW5ub3RhdGVkID0gdWRwaXBlX2Fubm90YXRlKG1vZGVsLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB4ID0gbm90aWNpYXNMYXJyZWFERiROb3RpY2lhLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkb2NfaWQgPSBub3RpY2lhc0xhcnJlYURGJFRpdHVsYXIpDQp0aWR5X0xhcnJlYV9hbm5vdGF0ZWQgPSBhc190aWJibGUodGlkeV9MYXJyZWFfYW5ub3RhdGVkKQ0KbmFtZXModGlkeV9MYXJyZWFfYW5ub3RhdGVkKVs2XSA9ICJUb2tlbiIgDQp0aWR5X0xhcnJlYV9hbm5vdGF0ZWQgPSB0aWR5X0xhcnJlYV9hbm5vdGF0ZWQgJT4lIA0KICBhbnRpX2pvaW4oc3RvcHdvcmRzX2VzKSAlPiUgDQogIG11dGF0ZShUb2tlbj1yZW1vdmVQdW5jdHVhdGlvbihUb2tlbikpICU+JSANCiAgZmlsdGVyKFRva2VuIT0iIikNCmBgYA0KDQpgYGB7cn0NCnRpZHlfRnJlaWxlX2Fubm90YXRlZCA9IHVkcGlwZV9hbm5vdGF0ZShtb2RlbCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeCA9IG5vdGljaWFzRnJlaWxlREYkTm90aWNpYSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZG9jX2lkID0gbm90aWNpYXNGcmVpbGVERiRUaXR1bGFyKQ0KdGlkeV9GcmVpbGVfYW5ub3RhdGVkID0gYXNfdGliYmxlKHRpZHlfRnJlaWxlX2Fubm90YXRlZCkNCm5hbWVzKHRpZHlfRnJlaWxlX2Fubm90YXRlZClbNl0gPSAiVG9rZW4iIA0KdGlkeV9GcmVpbGVfYW5ub3RhdGVkID0gdGlkeV9GcmVpbGVfYW5ub3RhdGVkICU+JSANCiAgIyBhbnRpX2pvaW4oc3RvcHdvcmRzX2VzKSAlPiUgDQogIG11dGF0ZShUb2tlbj1yZW1vdmVQdW5jdHVhdGlvbihUb2tlbikpICU+JSANCiAgZmlsdGVyKFRva2VuIT0iIikNCmBgYA0KDQpFbCByZXN1bHRhZG8gZGUgbGEgbGVtYXRpemFjacOzbiBzZSB2ZXLDoSBjb21vOg0KDQpgYGB7cn0NCnRpZHlfVmVsYXNjb19hbm5vdGF0ZWQgJT4lDQogIGZpbHRlcihkb2NfaWQ9PWVqZW1wbG9fVmVsYXNjbykgJT4lIA0KICBzZWxlY3QoVG9rZW4sbGVtbWEsIHVwb3MsIGZlYXRzKQ0KYGBgDQoNCkxhIHZlbnRhamEgZGUgbGEgbGlicmVyw61hIFVEcGlwZSB5YWNlIGVuIGVsIGhlY2hvIGRlIHF1ZSBudWVzdHJvIHRleHRvIHNlIGVuY3VlbnRyYSBhaG9yYSAqKmFub3RhZG8qKi4gQXPDrSwgY29tbyDDumx0aW1hIHRhcmVhLCByZXZpc2FyZW1vcyBsYXMgZnJlY3VlbmNpYXMgc29icmUgbGFzIGV0aXF1ZXRhcyBQT1M6DQoNCmBgYHtyfQ0KdGlkeV9WZWxhc2NvX2Fubm90YXRlZCAlPiUgDQogIGNvdW50KHVwb3MpICU+JSANCiAgZ2dwbG90KCkrDQogIGdlb21fY29sKGFlcyh4PXJlb3JkZXIodXBvcyxuKSx5PW4sZmlsbD11cG9zKSkrDQogIGxhYnMoeD0iRXRpcXVldGEgUE9TIiwgeT0iRnJlY3VlbmNpYSIsIHRpdGxlPSJOb3RpY2lhczogSnVhbiBGZXJuYW5kbyBWZWxhc2NvIikrDQogIGNvb3JkX2ZsaXAoKSsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiLCB0ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE4KSkNCmBgYA0KDQpZIGxhcyA1IHBhbGFicmFzIG3DoXMgZnJlY3VlbnRlcyAoc2luIGNvbnRhciBlbXBhdGVzKSBwYXJhIGNhZGEgdW5hIGRlIGxhcyA0IGV0aXF1ZXRhcyBtw6FzIGNvbXVuZXM6DQoNCmBgYHtyfQ0KdGlkeV9WZWxhc2NvX2Fubm90YXRlZCAlPiUgDQogIGZpbHRlcih1cG9zICVpbiUgYygnTk9VTicsJ1BST1BOJywnVkVSQicsJ0FESicpKSAlPiUgDQogIGNvdW50KHVwb3MsIGxlbW1hKSAlPiUgDQogIGdyb3VwX2J5KHVwb3MpICU+JSANCiAgc2xpY2VfbWF4KG9yZGVyX2J5ID0gbiwgbiA9IDUpICU+JSANCiAgZ2dwbG90KCkrDQogIGdlb21fY29sKGFlcyh4PXJlb3JkZXJfd2l0aGluKGxlbW1hLG4sdXBvcykseT1uLGZpbGw9bGVtbWEpKSsNCiAgc2NhbGVfeF9yZW9yZGVyZWQoKSsNCiAgbGFicyh4PSJMZW1tYSIsIHk9IkZyZWN1ZW5jaWEiLCB0aXRsZT0iTm90aWNpYXM6IEp1YW4gRmVybmFuZG8gVmVsYXNjbyIpKw0KICBmYWNldF93cmFwKHZhcnModXBvcyksIHNjYWxlcyA9ICJmcmVlIiwgbmNvbCA9IDIpKw0KICBjb29yZF9mbGlwKCkrDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIiwgdGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xOCkpDQpgYGANCg0KQ29uIG1pcmFzIGEgdW5hIHByw7N4aW1hIGNsYXNlLCBndWFyZGFtb3MgbG9zIHJlc3VsdGFkb3MgZGUgZXN0ZSBwcmltZXIgYW7DoWxpc2lzOg0KDQpgYGB7cn0NCnNhdmVSRFModGlkeV9WZWxhc2NvLCAiQ2FzbzFfTm90aWNpYXMvdGlkeV9WZWxhc2NvLlJEUyIpDQpzYXZlUkRTKHRpZHlfTGFycmVhLCAiQ2FzbzFfTm90aWNpYXMvdGlkeV9MYXJyZWEuUkRTIikNCnNhdmVSRFModGlkeV9GcmVpbGUsICJDYXNvMV9Ob3RpY2lhcy90aWR5X0ZyZWlsZS5SRFMiKQ0Kc2F2ZVJEUyh0aWR5X1ZlbGFzY29fYW5ub3RhdGVkLCAiQ2FzbzFfTm90aWNpYXMvdGlkeV9WZWxhc2NvX2Fubm90YXRlZC5SRFMiKQ0Kc2F2ZVJEUyh0aWR5X0xhcnJlYV9hbm5vdGF0ZWQsICJDYXNvMV9Ob3RpY2lhcy90aWR5X0xhcnJlYV9hbm5vdGF0ZWQuUkRTIikNCnNhdmVSRFModGlkeV9GcmVpbGVfYW5ub3RhdGVkLCAiQ2FzbzFfTm90aWNpYXMvdGlkeV9GcmVpbGVfYW5ub3RhdGVkLlJEUyIpDQpgYGANCg0KDQojICoqMy4gQmlibGlvZ3JhZsOtYSoq