1 Introduction

Instacart is an app for on-demand grocery shopping with same-day delivery service. Instacart uses a crowdsourced marketplace model, akin to that of Uber or Lyft.

The Instacart shopping process is as follows. First, an app user places their grocery order through the app. Then, a locally crowdsourced “shopper” is notified of the order, goes to a nearby store, buys the groceries, and delivers them to the user.

There are three ways that Instacart generates revenue: delivery fees, membership fees, and mark-ups on in-store prices.

1.1 Research Goal & Objective

The main objective of the competition is to predict what will the user will buy in the next order, given all data of prior orders.

3 Dataset Preparation

3.1 Data Source

Last year, Instacart released a public dataset, “The Instacart Online Grocery Shopping Dataset 2017”. The dataset contains over 3 million anonymized grocery orders from more than 200,000 Instacart users. This analysis will make use of this datasets.

Data source can be downloaded here: https://www.kaggle.com/c/instacart-market-basket-analysis/data

3.2 R Libraries Used

Here are the R libraries used in this analysis.

library(knitr)      # web widget
library(tidyverse)  # data manipulation
library(data.table) # fast file reading
library(caret)      # rocr analysis
library(ROCR)       # rocr analysis
library(kableExtra) # nice table html formating 
library(gridExtra)  # arranging ggplot in grid

3.3 Import Datasets

setwd('./datasets')
aisles      = fread('aisles.csv',      stringsAsFactors = TRUE)
departments = fread('departments.csv', stringsAsFactors = TRUE)
products    = fread('products.csv',    stringsAsFactors = TRUE)
orders      = fread('orders.csv',      stringsAsFactors = TRUE)
order_products_train = fread('order_products__train.csv')
order_products_prior = fread('order_products__prior.csv')
|--------------------------------------------------|
|==================================================|

3.4 Data Dictionary

The dataset for this competition is a relational set of files describing customers’ orders over time. They are anonymized and contains a sample of over 3 million grocery orders from more than 200,000 Instacart users. For each user, Instacart provided between 4 and 100 of their orders, with the sequence of products purchased in each order, the week and hour of day the order was placed, and a relative measure of time between orders.

Total six datasets were imported. Follwing section will explore each datasets in further detail. These datasets were sourced from an existing Kaggle competition.

orders (3.4m rows, 206k users):

  • order_id: order identifier
  • user_id: customer identifier
  • eval_set: which evaluation set this order belongs in (see SET described below)
  • order_number: the order sequence number for this user (1 = first, n = nth)
  • order_dow: the day of the week the order was placed on
  • order_hour_of_day: the hour of the day the order was placed on
  • days_since_prior: days since the last order, capped at 30 (with NAs for order_number = 1)

products (50k rows):

  • product_id: product identifier
  • product_name: name of the product
  • aisle_id: foreign key
  • department_id: foreign key

aisles (134 rows):

  • aisle_id: aisle identifier
  • aisle: the name of the aisle

deptartments (21 rows):

  • department_id: department identifier
  • department: the name of the department

order_products__SET (30m+ rows):

  • order_id: foreign key
  • product_id: foreign key
  • add_to_cart_order: order in which each product was added to cart
  • reordered: 1 if this product has been ordered by this user in the past, 0 otherwise

where SET is one of the four following evaluation sets (eval_set in orders):

  • "prior": orders prior to that users most recent order (~3.2m orders)
  • "train": training data supplied to participants (~131k orders)
  • "test": test data reserved for machine learning competitions (~75k orders)

3.5 Understanding Datasets

3.5.1 Aisles

There are 134 ailes in this dataset. Here are few sample names of the ailes.

paste(sort(head(aisles$aisle)), collapse=', ')
[1] "energy granola bars, instant foods, marinades meat preparation, other, prepared soups salads, specialty cheeses"

3.5.2 Departments

There are 21 departments in this dataset.Names of all deparments are listed below in aphabetically ordered.

paste(sort(departments$department), collapse = ', ')
[1] "alcohol, babies, bakery, beverages, breakfast, bulk, canned goods, dairy eggs, deli, dry goods pasta, frozen, household, international, meat seafood, missing, other, pantry, personal care, pets, produce, snacks"

3.5.3 Products

There are 49,688 products in the catalogue within 134 aisles and 21 departments.

Sample products are as below.

products %>% head %>% kable %>% 
  kable_styling(bootstrap_options = c("striped", "hover"))
product_id product_name aisle_id department_id
1 Chocolate Sandwich Cookies 61 19
2 All-Seasons Salt 104 13
3 Robust Golden Unsweetened Oolong Tea 94 7
4 Smart Ones Classic Favorites Mini Rigatoni With Vodka Cream Sauce 38 1
5 Green Chile Anytime Sauce 5 13
6 Dry Nose Oil 11 11

3.5.4 Departments And Its Relevant Products

Products dataframe is related to Deparments.

We shall see sample of 3 products for few deparments.

left_join(departments, products) %>% select(department, product_name ) %>%
  group_by(department) %>%
  sample_n(3) %>%
  summarise(three_examples_product=paste(product_name, collapse=' / ')) %>% 
  sample_n(5) %>% kable %>% kable_styling(bootstrap_options = c("striped", "hover"))
department three_examples_product
beverages Peach Iced Tea Mix Packets / Cayenne Lemonade Made With Raw Tristate Honey / Mango Lemonade Sparkling Water
alcohol The Cutrer Chardonnay / Bud Light Beer Cans / Jamaican Style Lager
meat seafood Country Mild Premium Pork Sausage / Ground Beef 85% Lean / Grass Fed Ground Beef Patties
personal care Very Volumizing Pomegranate Shampoo / Radiant Super Unscented Tampons / Wild Cherry Throat Drops
international Organic Ramen Noodles / Borscht / Lo Mein Egg Noodles

3.5.5 Aisles And Its Relevant Products

Products dataframe is also related to aisles. Each aisle relates to multiple products. By joining both aisles and products dataframe, we have an idea what type of prodcuts for each ailes.

Example below shows 3 samples products of for few aisles.

left_join(aisles, products) %>%
  select(aisle, product_name ) %>%  group_by(aisle) %>%
  sample_n(3) %>% 
  summarise(three_examples_product=paste(product_name, collapse=' || ')) %>% 
  sample_n(5) %>% kable %>% kable_styling(bootstrap_options = c("striped", "hover"))
aisle three_examples_product
frozen dessert Naked Coconut Organic Coconut Bliss || Smooth & Creamy Cherry Original Cream Cheesecake || Outshine Fruit & Veggie Bars Tangerine Medley
nuts seeds dried fruit Organic Dried Cranberries Apple Sweetened || Ginger Rescue Strong Chewable Ginger Tablets || Sesame Sticks
beauty Intense i-Color Eye liner - For Hazel Eyes, Black Pearl || Cinnamon Cassia Oil || Cushioned Nail Board
ice cream toppings The Ultimate Caramel Suace || Classic Waffle Cones || Gluten Free Sugar Cones
food storage Oven Bags || Linking Bag Clip || No Stick Heavy Duty Foil

3.5.6 Orders

There are over 3 millions observations in orders dataset. Each row represent an unique order.

3.5.6.1 Train Eval_Set

Let’s analyse the construct of one user. For example, user_id 1 had made 10 prior orders (order number from 1 to 10), last order is a train (eval_set). Note that the first order (order_number 1) does not have value for day_since_prior_order, as it is the first order without prior records.

This also means <user_id, product_id> made up the key for prediction.

orders %>% filter (user_id==1) %>% 
  kable %>%kable_styling(bootstrap_options = c("striped", "hover"))
order_id user_id eval_set order_number order_dow order_hour_of_day days_since_prior_order
2539329 1 prior 1 2 8 NA
2398795 1 prior 2 3 7 15
473747 1 prior 3 3 12 21
2254736 1 prior 4 4 7 29
431534 1 prior 5 4 15 28
3367565 1 prior 6 2 7 19
550135 1 prior 7 1 9 20
3108588 1 prior 8 1 14 14
2295261 1 prior 9 1 16 0
2550362 1 prior 10 4 8 30
1187899 1 train 11 4 8 14

3.5.6.2 Test Eval_Set

Let’s analyse another construct of orders. User_id 3 had made 12 orders before the final order labeled as test (eval_set) order. From the data we know that order_number is being recycled for each user.

Instacart did not provide us the basket content for test order. This is in fact the target for prediction.

orders %>% filter (user_id==3) %>% 
  kable %>%kable_styling(bootstrap_options = c("striped", "hover"))
order_id user_id eval_set order_number order_dow order_hour_of_day days_since_prior_order
1374495 3 prior 1 1 14 NA
444309 3 prior 2 3 19 9
3002854 3 prior 3 3 16 21
2037211 3 prior 4 2 18 20
2710558 3 prior 5 0 17 12
1972919 3 prior 6 0 16 7
1839752 3 prior 7 0 15 7
3225766 3 prior 8 0 17 7
3160850 3 prior 9 0 16 7
676467 3 prior 10 3 16 17
521107 3 prior 11 0 18 11
1402502 3 prior 12 1 15 15
2774568 3 test 13 5 15 11

3.5.7 Order_Product

Each order contain multiple products purchased by user. Instacart had cleanly categorized the orders into ‘train’ and ‘prior’ in SINGLE order dataset.

However, the detail of each orders are splitted into two datsets:
- order_product_train: contain only detail product items of last order
- order_product_prior: contain detail product items of all prior orders

3.5.8 Order_Product_Train

order_product_train/prior dataframe tells us which products were purchased at each order; for both train and prior order.

For example, we know user_id 1 in the LAST ORDER (order_id 1187899) purchased 10 unique products by quering order_product_train with the relevant order_id.

order_products_train %>% filter (order_id == 1187899) %>% 
  kable %>% kable_styling(bootstrap_options = c("striped", "hover"))
order_id product_id add_to_cart_order reordered
1187899 196 1 1
1187899 25133 2 1
1187899 38928 3 1
1187899 26405 4 1
1187899 39657 5 1
1187899 10258 6 1
1187899 13032 7 1
1187899 26088 8 1
1187899 27845 9 0
1187899 49235 10 1
1187899 46149 11 1

3.5.9 Order_Product_Train_Prior

Similary, detail items for a PRIOR ORDER (example order_id: 2550362) can be retireved by quering diffren dataset order_product_prior.

order_products_prior %>% filter (order_id == 2550362) %>% 
  kable %>% kable_styling(bootstrap_options = c("striped", "hover"))
order_id product_id add_to_cart_order reordered
2550362 196 1 1
2550362 46149 2 1
2550362 39657 3 0
2550362 38928 4 0
2550362 25133 5 1
2550362 10258 6 1
2550362 35951 7 0
2550362 13032 8 1
2550362 12427 9 1

3.5.10 Users

Ther is no dedicated dataframe for users. However, we can derive number of unique users from order dataframe. By grouping the user_id and eval_set column, we found that there are 75,000 test users, 131,209 train users.

orders %>% filter(eval_set %in% c('train','test') ) %>%
  count(eval_set) %>%
  mutate(percentage=n/sum(n)) %>%
  kable %>% kable_styling(bootstrap_options = c("striped", "hover"))
eval_set n percentage
test 75000 0.3637087
train 131209 0.6362913

4 Exploratory Data Analysis

In this section, we shall try to understand the buying behaviour by asking some interesting quesitons.

To reduce our coding steps, we construct a reusable dataframe combining all details from orders and its products. This dataframe will contain rows for prior orders and products only (excluding last order which is labeled as ‘train’).

4.1 Orders

4.1.1 How Many Orders ?

Most users made few orders. The number of orders a users made decrease significally along the order numbers. Maximum orders any users had made is 99.

tmp = users_orders_products_ %>% group_by(user_id) %>% summarize(n_orders = max(order_number))
tmp %>% ggplot(aes(x=as.factor(n_orders))) + geom_bar() +
    ylab('Count of Users') +
    xlab('Number of Orders Made By Users') +
    theme(
      axis.text.x  = element_text (size = 6.0, angle = (90), hjust = 1, vjust = 0.5)
    )

4.1.2 How Soon Until Next Order ?

It is very obvious that most users made their orders weekly (every 7 days) and monthly (every 30 days). See the peak of day 7 and day 30 in the chart below.

tmp = users_orders_products_ %>% 
  filter(order_number>1) %>% # days_since_prior is NA for first order, need to filter out
  group_by(order_id) %>% 
    summarize(n_orders = max(days_since_prior_order))
tmp %>% ggplot(aes(x=as.factor(n_orders))) + geom_bar() + ylab('Count of Orders') + xlab('Days Since Prior For Each Order')

4.2 Orders_Products

4.2.4 Products Ordered Day Pattern

We can see that both Day 0 and Day 1 stands out to be the most busy shopping day for instacart. This means that day of order made may influence the basket size.

users_orders_products_ %>%
  group_by(order_dow) %>%
    summarize(count = n()) %>%
  mutate(percentage=count/sum(count)) %>%
  ggplot (aes(x=as.factor(order_dow), y=percentage, fill=as.factor(order_dow))) + 
    geom_col()+ ylab('Percentage of Orders') + ggtitle('Daily Orders')

When we zoom into daily orders, we notice that top ten products contributes between 7% to 8% of daily orders. It is interesting to see that Limes are part of top ten for Day 0 and Day 6, but not other days. Whereas Organic Whole Milk doesn’t make it to top ten for Day 0. Organic Respberries does not make it to top 10 of Day 6. This means that there is a chance of predictability based on the day order is made.

users_orders_products_%>%
  group_by(order_dow, product_name) %>%
    summarize(n=n()) %>%
  mutate(percentage=n/sum(n)) %>%
  top_n(10, wt=n) %>%
  ggplot (aes(x=as.factor(order_dow), y=percentage, fill=product_name)) + 
    geom_col() + ylab('Proprtion of Orders In A Day') + ggtitle('Daily Top 10 Products Ordered') +
    theme(legend.position="bottom",legend.direction="horizontal")

4.2.5 Products Ordered Hour Pattern

Morning to afternoon are the peak shopping hours for instacart customers. The hour order made influences basket size.

users_orders_products_ %>%
  group_by(order_hour_of_day) %>%
    summarize(count = n()) %>%
  mutate(percentage=count/sum(count)) %>%
  ggplot (aes(x=as.factor(order_hour_of_day), y=percentage)) + 
    geom_col()+ ylab('Percentage of Orders') + ggtitle('Hourly Orders')

In the grocery, there are close to 50,000 products. When we zoom into hourly purchases, we noticed that top 10 products managed to score betwen 6% to 8% of hourly sales. Every hour has slightly diffrent combination of top 10 products (combination out of 12 products). That means certain products are predictable for ordering irregardless of the hour of order.

It is interesting to know that, similar to daily top 10 products, the Organic Wholemilk and Limes is missing as top 10 from some hours.

users_orders_products_ %>%
  group_by(order_hour_of_day, product_name) %>%
    summarize(n=n()) %>%
  mutate(percentage=n/sum(n)) %>%
  top_n(10, wt=n) %>%
  ggplot (aes(x=as.factor(order_hour_of_day), y=percentage, fill=product_name)) + 
    geom_col() + ylab('Proprtion of Orders In A Hour') + ggtitle('Hourly Top 10 Products Ordered') +
    theme(legend.position="bottom",legend.direction="horizontal")

4.3 Basket Analysis

4.3.1 Basket Size Distribution

Number of items in all orders range from 1 to 145. The histogram below is highly skewed towards small basket size. Majority of users purchased 5 items in their orders.

tmp = users_orders_products_ %>%
  group_by(order_id)  %>%
  summarize( basket_size=n(), 
             reordered_items = sum(reordered)) %>%
  group_by(basket_size) %>%
  summarize(n=n(), avg_reordered_items =mean(reordered_items)) %>%
  arrange(basket_size)
  
tmp %>% ggplot(aes(x=as.factor(basket_size))) +
    geom_col(aes(y=n)) +
    ylab('Order Count') +
    xlab('Number of Items in Basket') +
    ggtitle('Basket Size Distribution') +
    theme(
      axis.text.x  = element_text (size = 6.0, angle = (90), hjust = 1, vjust = 0.5)
    )

4.4 Re-Ordered Analysis

Analyzing the re-ordered products is the most important part of the EDA. This is becasue insights from this analysis can help to develop intuition for furhter feature engineering that will make the prediction more meaningful.

4.4.1 Average Re-ordered Items In Basket Distribution

tmp %>% ggplot (aes(x=as.factor(basket_size))) + 
    geom_point(aes(y=avg_reordered_items), color='red') +
    ylab('Avg Number of Re-Ordered Items') +  
    xlab('Number of Items in Basket') +
    ggtitle('Reorder Rate by Basket Size') +
    theme(
      axis.text.x  = element_text (size = 6.0, angle = (90), hjust = 1, vjust = 0.5)
    ) +
    geom_abline(intercept = 0, slope = 1, color='blue')

4.4.2 Product Reorder Ratio

One of the tricker things to predict in the Instacart dataset is the incidence of orders without reordered products. Plotting the proportion of this incidence across the training sample (a snapshot of 131K+ users) provides some inspiration.

4.4.3 How Many Products Were Reordered

Among all product purchases, 41% of products are reordered. The reordered rate is particularly high on top 10 products. As shown in chart below, top ten popular products has reordered rate is around 70% to 85%; higher than the overall ratio of 41%.

## overall all products reordered rate
tmp1 = users_orders_products_ %>%
  filter(order_number >1) %>% # exclude first order, which will never have reordered
  count(reordered) %>%
  mutate(ratio=n/sum(n))
  
p1 = tmp1 %>% 
  ggplot (aes(x='',y=ratio, fill=as.factor(reordered))) + geom_col(width=1) + ylab('Product Reordered Ratio') +
  coord_polar(theta='y', start=0) + 
  scale_fill_brewer(palette="Dark2") +
  theme(axis.title.y = element_blank())
## top10 products and its reordered rate
tmp2 = users_orders_products_ %>%
  count(product_id) %>% # filter only top 10 products for reorder analysis
  top_n(n=10) %>%
  left_join(users_orders_products_) %>%  # now find out their reordered rate
  group_by(product_id, product_name) %>%
    summarize(
      reordered_rate = sum(reordered,na.rm=T)/n()
    ) %>% 
  select(product_id, product_name, reordered_rate) %>%
  arrange(desc(reordered_rate))
p2 = tmp2 %>% ggplot (aes(x=reorder(product_name, reordered_rate), y=reordered_rate)) + 
  ggtitle('Top 10 Products Sold and Their Reordering Rate') +
  geom_col() + scale_y_continuous(limits = c(0,1), breaks = seq(0,1,0.1)) + coord_flip()
grid.arrange(p1, p2, ncol = 2)

4.4.4 Reordering vs Days Since Prior Order

We understand from order analysis earlier that most users place their orders every 7 and 30 days. However, from reorder ration perspective, day 7 and day 30 has high contrast whereby day 7 orders has high reorder ratio and day 30 has lowest reordering ratio.

tmp = users_orders_products_ %>%
  filter(order_number>1) %>%
  group_by(days_since_prior_order, order_id) %>%
    summarize(
      contain_reordered = max(reordered)
    ) %>%
    summarize( 
      reordered_orders = sum(contain_reordered),
      n = n()+1
    ) %>%
  mutate(non_reorder_ratio= 1-(reordered_orders/n))
tmp %>% ggplot (aes(x=days_since_prior_order, y=non_reorder_ratio)) + geom_point() + geom_line() +
    ggtitle('Orders NOT Containing Reordered Products over Days since Prior Order')

4.4.5 Reordering vs Hour Of Order

tmp = users_orders_products_ %>%
  filter(order_number>1) %>%
  group_by(order_hour_of_day, order_id) %>%
    summarize(
      contain_reordered = max(reordered)
    ) %>%
    summarize( 
      reordered_orders = sum(contain_reordered),
      n = n()
    ) %>%
  mutate(non_reorder_ratio= 1-(reordered_orders/n))
tmp %>% ggplot (aes(x=order_hour_of_day, y=non_reorder_ratio)) + geom_point() + geom_line() +
  ggtitle('Non Reorder Ratio over Time of Order Placed')

4.4.6 Reordering vs Day Of Order

tmp = users_orders_products_ %>%
  filter(order_number>1) %>%
  group_by(order_dow, order_id) %>%
    summarize(
      contain_reordered = max(reordered)
    ) %>%
    summarize( 
      reordered_orders = sum(contain_reordered),
      n = n()+1
    ) %>%
  mutate(non_reorder_ratio= 1-(reordered_orders/n))
tmp %>% ggplot (aes(x=order_dow, y=non_reorder_ratio)) + geom_point() + geom_line() +
  ggtitle('Non Reorder Ratio over Day of Purchase')

4.4.7 Reordering vs Day Of Order

Intuitively, we can think of the more regular a buyer is, the person tend to repeat ordering the same products.

tmp = users_orders_products_ %>%
  filter(order_number>1) %>%
  group_by(user_id, order_id) %>%
    summarize(
      contain_reordered = max(reordered)
    ) %>%
    summarize( 
      reordered_orders = sum(contain_reordered),
      total_orders_per_user=n()
    ) %>%
    group_by(total_orders_per_user) %>%
    summarize(
      reorders = sum(reordered_orders),
      total = sum(total_orders_per_user)) %>%
    mutate(non_ratio= 1-(reorders/total))
  
tmp %>% ggplot (aes(x=total_orders_per_user, y=non_ratio)) + geom_point() + geom_line() +
  ggtitle('Non Reorder Ratio over Day of Purchase')

5 Predictive Analysis

5.1 Type of Prediction

The objective is to predict what product will the customer purchase in the next basket. It require probability estimation of each product that bad been purchased before, that to be purchased before.This is a classification problem, as well as a regression of probability of repurchases.

For this analysis, we shall use two Naive models (handcrafted baseline) and one Machine Learning Logistic regression will be used for Machine Learning approach for its speed and simplicity; to demonstrate the feasibility to producing a better outcome then baseline.

5.1.1 Train/Test Dataset Splitting

Instacart did not provide us test order detail, therefore we shall use the train users for both trainng and testing. We achieve this by splitting the train users and its related orders and products into train dataset and train dataset, at 70%/30% split (by number of users). That means our train/test dataset will contain approximately 91846 / 39,363 users.

For this analysis, we will not be submitting to Kaggle.

# update this variable for changing split ratio
train_proportion = 0.7
# build list of all users ID
tmp = orders %>% filter(eval_set=='train') %>% distinct(user_id)
# 70/30 split
set.seed(12345)
train.rows = sample( 1:nrow(tmp), train_proportion * nrow(tmp) )
train.users = tmp[train.rows,]  # select training rows, list of train users
test.users  = tmp[-train.rows,] # select testing rows, list of test users
cat("Total Rows in Training Users: ", length(train.users),
    "\nTotal Rows in Testing Users: ", length(test.users),
    "\nTrain/Test Split % : ", 100*length(train.users)/(length(test.users)+length(train.users)),
    " / ", 100*length(test.users)/(length(test.users)+length(train.users)))
Total Rows in Training Users:  91846 
Total Rows in Testing Users:  39363 
Train/Test Split % :  69.99977  /  30.00023

5.1.2 Training Data Construct

The data frame used for training should contain the below columns and features:

key

  • This is unique pair of user_id and product_id from orders
  • The keys should be constructed from all user_id-product_id pair that includes all prior and test/train rows

`actual

  • This is the response variable with value of 1 or 0 for each unique key
  • The value is 1 when the product is purchased in the last order (train or test set of orders)
  • The value is 0 when the product is not purchased in the train or test set, but was bought in prior set

other features

From exploratory discovery, features that could contribute to the prediction should be populated into the construct. Feature engineering will happen in the later stage.

Let’s proceed to create the basic training construct. This won’t be used for prediction until feature engineering is completed in later stage.

# list of products in the final order, this make up the label
construct1 = orders %>%    
  filter(user_id %in% train.users, eval_set=='train') %>% 
  left_join(order_products_train) %>%
  distinct(user_id, product_id) %>%
  mutate(actual=1)  #training label
# list of products each users had bought before in prior orders
construct2 = orders %>%   
  filter(user_id %in% train.users, eval_set=='prior') %>% 
  left_join(order_products_prior) %>%
  distinct(user_id,product_id)
# Training Construct
train.construct = left_join(construct2,construct1) %>%
  mutate(key=paste(user_id,product_id,sep="-")) %>%  # key
  select(key, user_id, product_id, actual) %>%
  arrange(user_id, product_id) %>%
  replace_na(list(actual = 0)) # proudcts not in last order, but exist in prior order
#  drop_na # remove proudcts not in historical but appear in last order
rm(list=c('construct1','construct2'))
head(train.construct,50)

5.1.3 Testing Data Construct

Similar approach to training data construct, here we frame the testing data for evaluate our model built with training data.

# list of products in the final order, this make up the label
construct1 = orders %>%    
  filter(user_id %in% test.users, eval_set=='train') %>% 
  left_join(order_products_train) %>%
  distinct(user_id, product_id) %>%
  mutate(actual=1)  #training label

# list of products each users had bought before in prior orders
construct2 = orders %>%   
  filter(user_id %in% test.users, eval_set=='prior') %>% 
  left_join(order_products_prior) %>%
  distinct(user_id,product_id)

# Training Construct
test.construct = left_join(construct2,construct1) %>%
  mutate(key=paste(user_id,product_id,sep="-")) %>%  # key
  select(key, user_id, product_id, actual) %>%
  arrange(user_id, product_id) %>%
  replace_na(list(actual = 0)) # proudcts not in last order, but exist in prior order
#  drop_na # remove proudcts not in historical but appear in last order

rm(list=c('construct1','construct2'))
head(test.construct,50)

5.2 Model Evaluation & Optimization

Instacart has close to 50k products in their catalogue. As the maximum number of items ordered by a user is just a fraction of the 50k available product. This means by simply predicting nothing is purchased in the next basket, we would yeild close to 100% accuracy.

Due to the highly imbalance dataset, Instacart require F1 Score as the competition scoring, instead of accuracy.

To evaluate the performance of the model, we had created a custom function to build a confusion matrix and derive other binary classification metrics.

## Custom Function For Binary Class Performance Evaluation
binclass_eval = function (actual, predict) {
  cm = table(as.integer(actual), as.integer(predict), dnn=c('Actual','Predicted'))
  ac = (cm['1','1']+cm['0','0'])/(cm['0','1'] + cm['1','0'] + cm['1','1'] + cm['0','0'])
  pr = cm['1','1']/(cm['0','1'] + cm['1','1'])
  rc = cm['1','1']/(cm['1','0'] + cm['1','1'])
  fs = 2* pr*rc/(pr+rc)
  list(cm=cm, recall=rc, precision=pr, fscore=fs, accuracy=ac)
}

If the prediction is based on probability, we shall build a function to discover cutoff that optimize various performance metrics.

### Cutoff Threshold Optimization
optimize_cutoff = function (actual, probability) {
  rocr.pred = prediction(predictions = probability, labels = actual)
  rocr.metrics = data.frame(
      cutoff   = rocr.pred@cutoffs[[1]],
      accuracy = (rocr.pred@tp[[1]] + rocr.pred@tn[[1]]) / 
                   (rocr.pred@tp[[1]] + rocr.pred@tn[[1]] + rocr.pred@fp[[1]] + rocr.pred@fn[[1]]),
      tpr = rocr.pred@tp[[1]] / (rocr.pred@tp[[1]] + rocr.pred@fn[[1]]),
      fpr = rocr.pred@fp[[1]] / (rocr.pred@fp[[1]] + rocr.pred@tn[[1]]),
      ppv = rocr.pred@tp[[1]] / (rocr.pred@tp[[1]] + rocr.pred@fp[[1]])
  )
  rocr.metrics$fscore = 2 * (rocr.metrics$tpr * rocr.metrics$ppv) / (rocr.metrics$tpr + rocr.metrics$ppv)
  rocr.metrics$tpr_fpr = rocr.metrics$tpr / rocr.metrics$fpr
  
  ## Discovery the optimal threshold for various metrics
  rocr.best = rbind(
    best.accuracy = c(max = max(rocr.metrics$accuracy, na.rm = TRUE), cutoff=rocr.metrics$cutoff[which.max(rocr.metrics$accuracy)]),
    best.ppv = c(max = max(rocr.metrics$ppv, na.rm = TRUE), cutoff = rocr.metrics$cutoff[which.max(rocr.metrics$ppv)]),
    best.recall = c(max = max(rocr.metrics$tpr, na.rm = TRUE), cutoff = rocr.metrics$cutoff[which.max(rocr.metrics$tpr)]),
    best.fscore = c(max = max(rocr.metrics$fscore, na.rm = TRUE), cutoff = rocr.metrics$cutoff[which.max(rocr.metrics$fscore)]),
    best.tpr_fpr = c(max = max(rocr.metrics$tpr_fpr, na.rm = TRUE), cutoff = rocr.metrics$cutoff[which.max(rocr.metrics$tpr_fpr)])
  )
  
  list(metrics = rocr.metrics, best = rocr.best)
}

5.3 Model 1 : Naive Prediction

5.3.1 Build The Model

With intension to make this a baseline model, We simply predict the basket based on user last order.

m1.train.data = users_orders_products_ %>%
  filter(user_id %in% train.users) %>%
  group_by(user_id) %>%
  top_n(n=1, wt=order_number)  %>% #last order has the higher order_number
  select(user_id, product_id) %>% 
  mutate (predicted=1)  %>%        #predict based on last ordered, therefore 1
  full_join(train.construct) %>%  # join with train construct for items not predicted but in final order
  select(user_id, product_id, actual, predicted) %>%
  replace_na(list(predicted = 0))
head(m1.train.data,25)

5.3.2 Confusion Matrix

m1.eval = binclass_eval(m1.train.data$actual, m1.train.data$predicted)
m1.eval$cm
      Predicted
Actual       0       1
     0 4672615  688814
     1  315266  265703

5.3.3 Model Performance

The result shows only 0.3460833 F1 Score.

cat("Accuracy:  ", m1.eval$accuracy,
    "\nPrecision: ", m1.eval$precision,
    "\nRecall:    ", m1.eval$recall,
    "\nFScore:    ", m1.eval$fscore)
Accuracy:   0.8310312 
Precision:  0.2783638 
Recall:     0.4573445 
FScore:     0.3460833

5.4 Model 2 : Smarter Naive Prediction (Baseline)

In this model, we predict products in the basket by estimating their frequency of repurchased. This way we get a ratio to indicate probability of re-purchases. We use ROCR package to estimate the best cutoff point (at which above this cutoff we shall predict for re-order) that give us the optimum F1 score.

5.4.1 Build The Model

## Build Model
m2.train.data = users_orders_products_ %>%
  filter(user_id %in% train.users) %>%
  group_by(user_id) %>%
    mutate(total_orders = max(order_number)) %>%  # total number of orders made previously
  ungroup %>% 
  select(user_id, order_id, product_id, total_orders) %>%
  group_by(user_id, product_id) %>%
    summarize(predicted=n()/max(total_orders)) %>%
  select(user_id, product_id, predicted) %>%
  full_join(train.construct) %>%  # join with train construct for items not predicted but in final order
  select(user_id, product_id, actual, predicted) %>%
  replace_na(list(predicted = 0))
head(m2.train.data,20)

5.4.2 Optimize Cutoff

We see that in order to maximize F1 Score, we need to set the cutoff threshold to 0.3368, which is the next step.

### Threshold Optimization
m2.rocr = optimize_cutoff(actual = m2.train.data$actual, probability = m2.train.data$predicted)
kable(m2.rocr$best) %>% kable_styling(bootstrap_options = c("striped"))
max cutoff
best.accuracy 0.9061271 0.6710526
best.ppv 0.6850580 0.8600000
best.recall 1.0000000 0.0101010
best.fscore 0.3753548 0.3367347
best.tpr_fpr 20.0735607 0.8600000

5.4.3 Confusion Matrix

Let’s set the cutoff to 0.3367347 as discovered in previous step.

m2.eval = binclass_eval(m2.train.data$actual, m2.train.data$predicted>0.3367347)
m2.eval$cm
      Predicted
Actual       0       1
     0 5023825  337604
     1  368744  212225

5.4.4 Model Performance

We are getting slightly better F1 Score (0.3753544) compare to previous naive model. We shall use this as the BASELINE.

cat("Accuracy:  ", m2.eval$accuracy,
    "\nPrecision: ", m2.eval$precision,
    "\nRecall:    ", m2.eval$recall,
    "\nFScore:    ", m2.eval$fscore)
Accuracy:   0.8811342 
Precision:  0.3859836 
Recall:     0.3652949 
FScore:     0.3753544

5.5 Machine Learning Framing

We construct all the products that users had purchased in the last 3 orders, then use machine learning classification to predict will each of the product be purchased again. We shall use decision tree and logistic regression for this prediction.

5.5.1 Feature Engineering

5.5.1.1 Order Features

These are original features provided by Instacart. Although there are no other features engineered specifically to describe Order, thse features are being used to generate other features in the following sections.

orders
- order_dow
- order_hour_of_day
- days_since_prior_order
- reordered

5.5.1.2 User Features

We create five features which is unique to each individual user. These are the features that desribe the user.

users
- u_n_orders: Number of Orders Per User
- u_avg_priors: Average waiting days between orders per User
- u_avg_hod: Average Order Placing Hour Per User
- u_avg_dow: Average Order Placing Day Per User
- u_avg_order_size: Average Size of Basket (items in order) Per User

#### user features
users_ = users_orders_products_ %>%
  group_by(user_id,order_id) %>%
    mutate(u_o_size = ifelse(row_number()==1, max(add_to_cart_order),0) ) %>%
  group_by(user_id) %>%
    summarize(
      u_n_orders = max(order_number),
      u_avg_priors = mean(days_since_prior_order,na.rm=TRUE),
      u_avg_hod = mean(order_hour_of_day),
      u_avg_dow = mean(order_dow),
      u_avg_order_size = sum(u_o_size)/max(order_number)
    ) %>% 
  arrange(user_id)

head(users_)

5.5.1.3 Product Features

We create two product specific features.

products

  • avg_product_order_dow: Average of product order_dow
  • avg_product_order_hod: Average of product order_hour_of_day
products_ = users_orders_products_ %>%
  group_by(product_id) %>%
  summarize( 
    p_avg_dow = mean(order_dow),
    p_avg_hod = mean(order_hour_of_day)
  ) %>% arrange(product_id)

head(products_)

5.5.1.4 User-Product Features

We shall introduce product related features that are user-product specifc

  • up_n_reordered : how many times a user reorderedthis product
  • up_avg_priors : Average number of days in between before a user purchase this product
  • up_avg_hod : Average hour a user purchase this product
  • up_avg_dow : Average day of week a user purchase this product
  • up_avg_rank : Average add to cart number a user select this product

5.5.2 Construct Training Data

We shall combined training construct table with the new engineered features to form the training data. Categorical data which are merely names or identification will be removed since they should not contribute to prediction.

After this step, the trianing data is ready for machine learning algorithm of choice.

m3.train.data = users_orders_products_ %>%
  filter(user_id %in% train.users) %>%
  left_join(user_products_) %>% 
  left_join(products_) %>%
  #left_join(users_)  #user_products_ already contain user specific features
  full_join(train.construct, by=c('user_id','product_id')) %>%
  arrange(user_id, product_id) %>%
  select(-c('key','user_id','order_id', 'product_id', 'product_name', 'department_id', 'aisle_id', 'department','aisle', 'days_since_prior_order')) 

glimpse(m3.train.data)

5.5.3 Construct Testing Data

m3.test.data = users_orders_products_ %>%
  filter(user_id %in% test.users) %>%
  left_join(user_products_) %>% 
  left_join(products_) %>%
  #left_join(users_)  #user_products_ already contain user specific features
  full_join(test.construct, by=c('user_id','product_id')) %>%
  arrange(user_id, product_id) %>%
  select(-c('key','user_id','order_id', 'product_id', 'product_name', 'department_id', 'aisle_id', 'department','aisle', 'days_since_prior_order')) 

glimpse(m3.test.data)

5.6 Model 3 : Logistic Regression

5.6.1 Model Trainng

m3.fit = glm(actual ~ ., family = binomial, data = m3.train.data)

5.6.2 Training Data Performance

5.6.2.1 Prediction

m3.predict = predict(m3.fit, type = 'response', newdata = m3.train.data)

5.6.2.2 Optimize Cutoff

max cutoff
best.accuracy 0.8221607 0.5246629
best.ppv 1.0000000 0.9895952
best.recall 1.0000000 0.0092867
best.fscore 0.5388467 0.2233115
best.tpr_fpr Inf 0.9895952

5.6.2.3 Confusion Matrix

m3.eval = binclass_eval(m3.train.data$actual, m3.predict>0.2233115)
m3.eval$cm
      Predicted
Actual       0       1
     0 9191429 2312354
     1 1022350 1948260

5.6.2.4 Model Evaluation

Logistic regression produce F1 Score of 0.5388937 with training data, a much better compared to Model 1 and Model 2. We shall proceed test the model on unknown data, the test data.

cat("Accuracy:  ",   m3.eval$accuracy,
    "\nPrecision: ", m3.eval$precision,
    "\nRecall:    ", m3.eval$recall,
    "\nFScore:    ", m3.eval$fscore)
Accuracy:   0.7696136 
Precision:  0.4572721 
Recall:     0.6558451 
FScore:     0.5388465
rocr.pred = prediction(m3.predict, m3.train.data$actual)
rocr.perf = performance(rocr.pred, measure = "tpr", x.measure = "fpr")
rocr.auc = as.numeric(performance(rocr.pred, "auc")@y.values)
plot(rocr.perf,
    lwd = 3, colorize = TRUE,
    print.cutoffs.at = seq(0, 1, by = 0.1),
    text.adj = c(-0.2, 1.7),
    main = 'ROC Curve')
mtext(paste('auc : ', round(rocr.auc, 5)))
abline(0, 1, col = "red", lty = 2)

5.6.3 Test Data Performance

5.6.3.1 Prediction

m3.predict.test = predict(m3.fit, type = 'response', newdata = m3.test.data)

5.6.3.2 Optimize Cutoff

### Threshold Optimization
m3.rocr.test = optimize_cutoff(actual = m3.test.data$actual, probability = m3.predict.test)
kable(m3.rocr.test$best) %>% kable_styling(bootstrap_options = c("striped"))
max cutoff
best.accuracy 0.8221624 0.5217251
best.ppv 1.0000000 0.9861198
best.recall 1.0000000 0.0100454
best.fscore 0.5405047 0.2266508
best.tpr_fpr Inf 0.9861198

5.6.3.3 Confusion Matrix

m3.eval.test = binclass_eval(m3.test.data$actual, m3.predict.test>0.2233115)
m3.eval.test$cm
      Predicted
Actual       0       1
     0 3921528  978677
     1  435702  831691

5.6.3.4 Model Evaluation

Logistic regression produce F1 Score of 0.5388937 with training data, a much better compared to Model 1 and Model 2. We shall proceed test the model on unknown data, the test data.

We acheived F1 Score of 0.5405588, slightly higher than training data.

cat("Accuracy:  ",   m3.eval.test$accuracy,
    "\nPrecision: ", m3.eval.test$precision,
    "\nRecall:    ", m3.eval.test$recall,
    "\nFScore:    ", m3.eval.test$fscore)
Accuracy:   0.7706759 
Precision:  0.4594044 
Recall:     0.6562219 
FScore:     0.540452
rm(list=c('m3.fit','m3.predict', 'm3.rocr'))

5.6.3.5 ROC

rocr.auc
[1] 0.8090974
plot(rocr.perf,
     lwd = 3, colorize = TRUE,
     print.cutoffs.at = seq(0, 1, by = 0.1),
     text.adj = c(-0.2, 1.7),
     main = 'ROC Curve')
mtext(paste('auc : ', round(rocr.auc, 5)))
Error: memory exhausted (limit reached?)
abline(0, 1, col = "red", lty = 2)
Error: memory exhausted (limit reached?)

6 Analysis & Recommendations

Technical Challenges

LS0tDQp0aXRsZTogIkluc3RhY2FydCBNYXJrZXQgQmFza2V0IEFuYWx5c2lzIg0KYXV0aG9yOiAiWW9uZyBLZWggU29vbiINCmRhdGU6ICIxOCBOb3ZlbWJlciAyMDE4Ig0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOg0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6IGNlcnVsZWFuDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6IDQNCiAgd29yZF9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogJzQnDQogIGh0bWxfZG9jdW1lbnQ6DQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6ICc0Jw0KLS0tDQoNCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+DQoNCmJvZHl7IC8qIE5vcm1hbCAgKi8NCiAgICAgIGZvbnQtc2l6ZTogMTJweDsNCiAgfQ0KdGQgeyAgLyogVGFibGUgICovDQogIGZvbnQtc2l6ZTogMTJweDsNCn0NCmgxLnRpdGxlIHsNCiAgZm9udC1zaXplOiAzOHB4Ow0KICBjb2xvcjogRGFya1JlZDsNCn0NCmgxIHsgLyogSGVhZGVyIDEgKi8NCiAgZm9udC1zaXplOiAyNHB4Ow0KICBjb2xvcjogRGFya0JsdWU7DQp9DQpoMiB7IC8qIEhlYWRlciAyICovDQogIGZvbnQtc2l6ZTogMjBweDsNCiAgY29sb3I6IERhcmtCbHVlOw0KfQ0KaDMgeyAvKiBIZWFkZXIgMyAqLw0KICBmb250LXNpemU6IDE2cHg7DQojICBmb250LWZhbWlseTogIlRpbWVzIE5ldyBSb21hbiIsIFRpbWVzLCBzZXJpZjsNCiAgY29sb3I6IERhcmtCbHVlOw0KfQ0KaDQgeyAvKiBIZWFkZXIgNCAqLw0KICBmb250LXNpemU6IDE0cHg7DQogIGNvbG9yOiBEYXJrQmx1ZTsNCn0NCmNvZGUucnsgLyogQ29kZSBibG9jayAqLw0KICAgIGZvbnQtc2l6ZTogMTJweDsNCn0NCnByZSB7IC8qIENvZGUgYmxvY2sgLSBkZXRlcm1pbmVzIGNvZGUgc3BhY2luZyBiZXR3ZWVuIGxpbmVzICovDQogICAgZm9udC1zaXplOiAxMnB4Ow0KfQ0KPC9zdHlsZT4NCg0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCiNrbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobz1UUlVFLCBmaWcuaGVpZ2h0PTMuNSwgZmlnLndpZHRoPTkuMiwgcmVzdWx0cz0naG9sZCcsIHdhcm5pbmc9RkFMU0UsIGZpZy5zaG93PSdob2xkJywgbWVzc2FnZT1GQUxTRSkgDQpgYGANCg0KIyBJbnRyb2R1Y3Rpb24NCg0KSW5zdGFjYXJ0IGlzIGFuIGFwcCBmb3Igb24tZGVtYW5kIGdyb2Nlcnkgc2hvcHBpbmcgd2l0aCBzYW1lLWRheSBkZWxpdmVyeSBzZXJ2aWNlLiAgSW5zdGFjYXJ0IHVzZXMgYSBjcm93ZHNvdXJjZWQgbWFya2V0cGxhY2UgbW9kZWwsIGFraW4gdG8gdGhhdCBvZiBVYmVyIG9yIEx5ZnQuIA0KIA0KVGhlIEluc3RhY2FydCBzaG9wcGluZyBwcm9jZXNzIGlzIGFzIGZvbGxvd3MuICBGaXJzdCwgYW4gYXBwIHVzZXIgcGxhY2VzIHRoZWlyIGdyb2Nlcnkgb3JkZXIgdGhyb3VnaCB0aGUgYXBwLiAgVGhlbiwgYSBsb2NhbGx5IGNyb3dkc291cmNlZCAic2hvcHBlciIgaXMgbm90aWZpZWQgb2YgdGhlIG9yZGVyLCBnb2VzIHRvIGEgbmVhcmJ5IHN0b3JlLCBidXlzIHRoZSBncm9jZXJpZXMsIGFuZCBkZWxpdmVycyB0aGVtIHRvIHRoZSB1c2VyLiAgIA0KIA0KVGhlcmUgYXJlIHRocmVlIHdheXMgdGhhdCBJbnN0YWNhcnQgZ2VuZXJhdGVzIHJldmVudWU6IGRlbGl2ZXJ5IGZlZXMsIG1lbWJlcnNoaXAgZmVlcywgYW5kIG1hcmstdXBzIG9uIGluLXN0b3JlIHByaWNlcy4NCg0KIyMgUmVzZWFyY2ggR29hbCAmIE9iamVjdGl2ZQ0KDQpUaGUgbWFpbiBvYmplY3RpdmUgb2YgdGhlIGNvbXBldGl0aW9uIGlzIHRvIHByZWRpY3Qgd2hhdCB3aWxsIHRoZSB1c2VyIHdpbGwgYnV5IGluIHRoZSBuZXh0IG9yZGVyLCBnaXZlbiBhbGwgZGF0YSBvZiBwcmlvciBvcmRlcnMuIA0KDQoNCiMgUmVsYXRlZCBXb3Jrcw0KDQoxLiAqKjJuZCBQbGFjZSBXaW5uZXIgSW50ZXJ2aWV3IGJ5IEthenVraSBPbm9kZXJhKiouICAgIA0KICAgIEthenVkaSBkZXZlbG9wZWQgdHdvIG1vZGVscyB1c2luZyBYR0Jvb3N0LCBvbmUgdG8gcHJlZGljdCB3aGF0IHVzZXIgd2lsbCBwdXJjaGFzZSBpbiB0aGUgbmV4dCBvcmRlciwgdGhlIG90aGVyIHByZWRpY3QgaWYgdGhlIHVzZXIgcHVyY2hhc2VkIG5vdGhpbmcgZnJvbSBoaXN0b3JpY2FsIHByb2R1Y3QuIFRoZSBvdXRwdXQgZnJvbSBib3RoIG1vZGVscyBhcmUgZnVydGhlciBwcm9jZXNzZWQgYnkgYW4gT3B0aW16aWVyIGZ1bmN0aW9uIGZvciBGMVNjb3JlIE9wdGltaXphdGlvbi4gSGlzIGZpbmFsIHJlc3VsdCBpcyAqKkYxIFNjb3JlIG9mIDAuNDA3KiogIA0KDQogICAgaHR0cDovL2Jsb2cua2FnZ2xlLmNvbS8yMDE3LzA5LzIxL2luc3RhY2FydC1tYXJrZXQtYmFza2V0LWFuYWx5c2lzLXdpbm5lcnMtaW50ZXJ2aWV3LTJuZC1wbGFjZS1rYXp1a2ktb25vZGVyYS8NCg0KMi4gKipEaW1pdHJpIExpbmRlIEJsb2cqKiAgDQogICAgVGhlIGF1dGhvciBoYWQgcHJvdmlkZWQgaGlzIEVEQSBhbmQgUHJlZGljdGlvbiB3b3JrIGluIEp1cHl0ZXIgTm90ZWJvb2sgKHdyaXRldG4gaW4gUHl0aG9uKS4gRGltaXRyaSBoYWQgZXhwbG9yZWQgdmFyaW91cyBmZWF0dXJlIGVuZ2luZWVyaW5nIGFuZCB0aHJlZSBhbGdvcml0aG1zLCAgTG9naXN0aWMgUmVncmVzc2lvbiwgR3JhZGllbnQgQm9vc3QgYW5kIEFkYUJvb3N0LiBGb3IgZWFjaCBhbGdvcml0aG0sIERpbWl0cmkgZGl2aWRlZCBoaXMgZGF0YSBpbnRvIHR3byBncm91cHMsIGZpcnN0IHdpdGggdXNlcnMgbGVzcyB0aGFuIDEwIG9yZGVycyBhbmQgc2Vjb25kIG1vcmUgdGhhbiAxMCBvcmRlcnMuIEhlIGJ1aWx0IHR3byBtb2RlbHMgZm9yIGVhY2ggYWxnb3JpdGhtIG9uIHRoaXMgcHVycG9zZS4gUmVzdWx0cyBhcmUgY29tcGFyZWQgYW5kIExvZ2lzdGljIHJlZ3Jlc3Npb24gRjEgc2NvcmVkIGFyb3VuZCAwLjI5Lg0KDQogICAgaHR0cHM6Ly9naXRodWIuY29tL2RsaW5kZS9EU0ktUHJvamVjdHMvYmxvYi9tYXN0ZXIvSW5zdGFjYXJ0LUNhcHN0b25lLVByb2plY3QtTm90ZWJvb2suaXB5bmINCg0KMy4gKiozcmQgUGxhY2UgU29sdXRpb24gYnkgU2p2KiogIA0KICAgIEEgc29sdXRpb24gdGhhdCBkZW1vbnN0cmF0ZWQgdHdvIGxheWVycyBvZiBOZXVyYWwgTmV0d29ya3MuIFRoZSBhdXRob3IgdXNlZCAxMkdQVSBhbmQgNjRHQiBvZiBSQU0uICANCg0KICAgIGh0dHBzOi8vZ2l0aHViLmNvbS9zanZhc3F1ZXovaW5zdGFjYXJ0LWJhc2tldC1wcmVkaWN0aW9uDQoNCjQuICoqSW5zdGFjYXJ0IE1hcmtldCBCYXNrZXQgQW5hbHlzaXMgU3VtbWFyeSBieSBNZWl5aSoqICANCiAgICBBIHZlcnkgbmVhdGx5IG9yZ2FuaXplZCByZXZpZXcgb2YgdGhlIGRhdGEsIG9iamVjdCBhbmQgYXBwcm9hY2ggdGFrZW4gZm9yIHRoZSBwcmVkaWN0aW9uLiBBbHRob3VnaCB0aGUgYXV0aG9yIG9ubHkgbWFrZSBwIHRvIDE0JSwgdGhlIGFydGljbGUgaXMgY2xlYW4gYW5kIGV4cGxhaW5zIHRoZSBjb25zdHJ1Y3Qgb2YgdGhlIHByb2JsZW0gY2xlYXJseS4gVGhlIGF1dGhvciBhbHNvIG5vdGVkIGFsbCBvdGhlciBsaW5rcyByZWxldmFudCB0byB0aGlzIGRhdGFzZXQuICANCg0KICAgIGh0dHBzOi8vbWVpeWlwYW4uY29tLzIwMTcvMDkvMTYvaW5zdGFjYXJ0Lw0KDQo1LiAqKkEgU3R1ZGVudCBGaW5hbCBQcm9qZWN0IFJlcG9ydCBGcm9tIFN0YW5kZm9yZCBVbml2ZXJzaXR5KiogIA0KICAgIEluIHRoaXMgcGFwZXIsIHRoZSBhdXRob3JzIGNvbXBhcmVkIHRoZSBwZXJmb3JtYW5jZSB1c2luZyBOZXVyYWwgTmV0LCBTVk0sIExvZ2lzdGljIGFuZCBvdGhlciBhbGdvcml0aG1zLiBJbiBhZGRpdG9uLCB0aGUgYXV0aG9ycyBkZXNjcmliZWQgdGhlIHRoZW9yeSBvZiBBc3NvY2lhdGlvbiBSdWxlcyBmb3IgdGhlIHByb2R1Y3RzLiBIb3dldmVyLCBpdCB3YXNuJ3QgY2xlYXIgaG93IEFzc29jaWF0aW9uIFJ1bGVzIGNhbiBiZSBhcHBsaWVkIGluIHRoaXMgZGF0YXNldC4NCg0KICAgIGh0dHA6Ly9jczIyOS5zdGFuZm9yZC5lZHUvcHJvajIwMTcvZmluYWwtcmVwb3J0cy81MjQwMzM3LnBkZg0KDQojIERhdGFzZXQgUHJlcGFyYXRpb24NCg0KIyMgRGF0YSBTb3VyY2UNCg0KTGFzdCB5ZWFyLCBJbnN0YWNhcnQgcmVsZWFzZWQgYSBwdWJsaWMgZGF0YXNldCwgKioiVGhlIEluc3RhY2FydCBPbmxpbmUgR3JvY2VyeSBTaG9wcGluZyBEYXRhc2V0IDIwMTciKiouIFRoZSBkYXRhc2V0IGNvbnRhaW5zIG92ZXIgMyBtaWxsaW9uIGFub255bWl6ZWQgZ3JvY2VyeSBvcmRlcnMgZnJvbSBtb3JlIHRoYW4gMjAwLDAwMCBJbnN0YWNhcnQgdXNlcnMuIFRoaXMgYW5hbHlzaXMgd2lsbCBtYWtlIHVzZSBvZiB0aGlzIGRhdGFzZXRzLiAgDQoNCkRhdGEgc291cmNlIGNhbiBiZSBkb3dubG9hZGVkIGhlcmU6IGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vYy9pbnN0YWNhcnQtbWFya2V0LWJhc2tldC1hbmFseXNpcy9kYXRhDQoNCiMjIFIgTGlicmFyaWVzIFVzZWQNCg0KSGVyZSBhcmUgdGhlIFIgbGlicmFyaWVzIHVzZWQgaW4gdGhpcyBhbmFseXNpcy4gDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KbGlicmFyeShrbml0cikgICAgICAjIHdlYiB3aWRnZXQNCmxpYnJhcnkodGlkeXZlcnNlKSAgIyBkYXRhIG1hbmlwdWxhdGlvbg0KbGlicmFyeShkYXRhLnRhYmxlKSAjIGZhc3QgZmlsZSByZWFkaW5nDQpsaWJyYXJ5KGNhcmV0KSAgICAgICMgcm9jciBhbmFseXNpcw0KbGlicmFyeShST0NSKSAgICAgICAjIHJvY3IgYW5hbHlzaXMNCmxpYnJhcnkoa2FibGVFeHRyYSkgIyBuaWNlIHRhYmxlIGh0bWwgZm9ybWF0aW5nIA0KbGlicmFyeShncmlkRXh0cmEpICAjIGFycmFuZ2luZyBnZ3Bsb3QgaW4gZ3JpZA0KYGBgDQoNCiMjIEltcG9ydCBEYXRhc2V0cw0KDQpgYGB7ciwgcmVzdWx0cz0naGlkZScsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpzZXR3ZCgnLi9kYXRhc2V0cycpDQphaXNsZXMgICAgICA9IGZyZWFkKCdhaXNsZXMuY3N2JywgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gVFJVRSkNCmRlcGFydG1lbnRzID0gZnJlYWQoJ2RlcGFydG1lbnRzLmNzdicsIHN0cmluZ3NBc0ZhY3RvcnMgPSBUUlVFKQ0KcHJvZHVjdHMgICAgPSBmcmVhZCgncHJvZHVjdHMuY3N2JywgICAgc3RyaW5nc0FzRmFjdG9ycyA9IFRSVUUpDQpvcmRlcnMgICAgICA9IGZyZWFkKCdvcmRlcnMuY3N2JywgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gVFJVRSkNCm9yZGVyX3Byb2R1Y3RzX3RyYWluID0gZnJlYWQoJ29yZGVyX3Byb2R1Y3RzX190cmFpbi5jc3YnKQ0Kb3JkZXJfcHJvZHVjdHNfcHJpb3IgPSBmcmVhZCgnb3JkZXJfcHJvZHVjdHNfX3ByaW9yLmNzdicpDQpgYGANCg0KYGBge3IgZWNobz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCiMgbG9hZCBwcmVwYXJlZCBjc3YgZm9yIGZhc3RlciBwcm9jZXNzaW5nDQoNCiMgY29uc3RydWN0DQp0cmFpbi5jb25zdHJ1Y3QgPSBmcmVhZCgnLi9kYXRhc2V0cy90cmFpbi5jb25zdHJ1Y3QuY3N2JykNCnRlc3QuY29uc3RydWN0ID0gZnJlYWQoJy4vZGF0YXNldHMvdGVzdC5jb25zdHJ1Y3QuY3N2JykNCg0KIyBmZWF0dXJlcw0KdXNlcnNfb3JkZXJzX3Byb2R1Y3RzXyA9IGZyZWFkKCcuL2RhdGFzZXRzL3VzZXJzX29yZGVyc19wcm9kdWN0c18uY3N2JykNCnVzZXJzXyA9IGZyZWFkKCcuL2RhdGFzZXRzL3VzZXJzXy5jc3YnKQ0KcHJvZHVjdHNfPSBmcmVhZCgnLi9kYXRhc2V0cy9wcm9kdWN0cy5jc3YnKQ0KdXNlcl9wcm9kdWN0c18gPSBmcmVhZCgnLi9kYXRhc2V0cy91c2VyX3Byb2R1Y3RzXy5jc3YnKQ0KbTMudHJhaW4uZGF0YSA9IGZyZWFkKCcuL2RhdGFzZXRzL20zLnRyYWluLmRhdGEuY3N2JykNCm0zLnRlc3QuZGF0YSA9IGZyZWFkKCcuL2RhdGFzZXRzL20zLnRlc3QuZGF0YS5jc3YnKQ0KYGBgDQoNCiMjIERhdGEgRGljdGlvbmFyeQ0KDQpUaGUgZGF0YXNldCBmb3IgdGhpcyBjb21wZXRpdGlvbiBpcyBhIHJlbGF0aW9uYWwgc2V0IG9mIGZpbGVzIGRlc2NyaWJpbmcgY3VzdG9tZXJzJyBvcmRlcnMgb3ZlciB0aW1lLiBUaGV5IGFyZSBhbm9ueW1pemVkIGFuZCBjb250YWlucyBhIHNhbXBsZSBvZiBvdmVyICoqMyBtaWxsaW9uIGdyb2Nlcnkgb3JkZXJzKiogZnJvbSBtb3JlIHRoYW4gKioyMDAsMDAwIEluc3RhY2FydCB1c2VycyoqLiBGb3IgZWFjaCB1c2VyLCBJbnN0YWNhcnQgcHJvdmlkZWQgKipiZXR3ZWVuIDQgYW5kIDEwMCoqIG9mIHRoZWlyIG9yZGVycywgd2l0aCB0aGUgc2VxdWVuY2Ugb2YgcHJvZHVjdHMgcHVyY2hhc2VkIGluIGVhY2ggb3JkZXIsIHRoZSAqKndlZWsgYW5kIGhvdXIgb2YgZGF5KiogdGhlIG9yZGVyIHdhcyBwbGFjZWQsIGFuZCBhICoqcmVsYXRpdmUgbWVhc3VyZSBvZiB0aW1lIGJldHdlZW4gb3JkZXJzKiouDQoNClRvdGFsIHNpeCBkYXRhc2V0cyB3ZXJlIGltcG9ydGVkLiBGb2xsd2luZyBzZWN0aW9uIHdpbGwgZXhwbG9yZSBlYWNoIGRhdGFzZXRzIGluIGZ1cnRoZXIgZGV0YWlsLiBUaGVzZSBkYXRhc2V0cyB3ZXJlIHNvdXJjZWQgZnJvbSBhbiBleGlzdGluZyBLYWdnbGUgY29tcGV0aXRpb24uDQoNCmBvcmRlcnNgICgzLjRtIHJvd3MsIDIwNmsgdXNlcnMpOiAgDQoNCiogYG9yZGVyX2lkYDogb3JkZXIgaWRlbnRpZmllciAgDQoqIGB1c2VyX2lkYDogY3VzdG9tZXIgaWRlbnRpZmllciAgDQoqIGBldmFsX3NldGA6IHdoaWNoIGV2YWx1YXRpb24gc2V0IHRoaXMgb3JkZXIgYmVsb25ncyBpbiAoc2VlIGBTRVRgIGRlc2NyaWJlZCBiZWxvdykgIA0KKiBgb3JkZXJfbnVtYmVyYDogdGhlIG9yZGVyIHNlcXVlbmNlIG51bWJlciBmb3IgdGhpcyB1c2VyICgxID0gZmlyc3QsIG4gPSBudGgpICANCiogYG9yZGVyX2Rvd2A6IHRoZSBkYXkgb2YgdGhlIHdlZWsgdGhlIG9yZGVyIHdhcyBwbGFjZWQgb24gIA0KKiBgb3JkZXJfaG91cl9vZl9kYXlgOiB0aGUgaG91ciBvZiB0aGUgZGF5IHRoZSBvcmRlciB3YXMgcGxhY2VkIG9uICANCiogYGRheXNfc2luY2VfcHJpb3JgOiBkYXlzIHNpbmNlIHRoZSBsYXN0IG9yZGVyLCBjYXBwZWQgYXQgMzAgKHdpdGggTkFzIGZvciBgb3JkZXJfbnVtYmVyYCA9IDEpICANCg0KYHByb2R1Y3RzYCAoNTBrIHJvd3MpOiAgDQoNCiogYHByb2R1Y3RfaWRgOiBwcm9kdWN0IGlkZW50aWZpZXIgIA0KKiBgcHJvZHVjdF9uYW1lYDogbmFtZSBvZiB0aGUgcHJvZHVjdCAgDQoqIGBhaXNsZV9pZGA6IGZvcmVpZ24ga2V5ICANCiogYGRlcGFydG1lbnRfaWRgOiBmb3JlaWduIGtleSAgDQoNCmBhaXNsZXNgICgxMzQgcm93cyk6ICANCg0KKiBgYWlzbGVfaWRgOiBhaXNsZSBpZGVudGlmaWVyICANCiogYGFpc2xlYDogdGhlIG5hbWUgb2YgdGhlIGFpc2xlICANCg0KYGRlcHRhcnRtZW50c2AgKDIxIHJvd3MpOiAgDQoNCiogYGRlcGFydG1lbnRfaWRgOiBkZXBhcnRtZW50IGlkZW50aWZpZXIgIA0KKiBgZGVwYXJ0bWVudGA6IHRoZSBuYW1lIG9mIHRoZSBkZXBhcnRtZW50ICANCg0KYG9yZGVyX3Byb2R1Y3RzX19TRVRgICgzMG0rIHJvd3MpOiAgDQoNCiogYG9yZGVyX2lkYDogZm9yZWlnbiBrZXkgIA0KKiBgcHJvZHVjdF9pZGA6IGZvcmVpZ24ga2V5ICANCiogYGFkZF90b19jYXJ0X29yZGVyYDogb3JkZXIgaW4gd2hpY2ggZWFjaCBwcm9kdWN0IHdhcyBhZGRlZCB0byBjYXJ0ICANCiogYHJlb3JkZXJlZGA6IDEgaWYgdGhpcyBwcm9kdWN0IGhhcyBiZWVuIG9yZGVyZWQgYnkgdGhpcyB1c2VyIGluIHRoZSBwYXN0LCAwIG90aGVyd2lzZSAgDQoNCndoZXJlIGBTRVRgIGlzIG9uZSBvZiB0aGUgZm91ciBmb2xsb3dpbmcgZXZhbHVhdGlvbiBzZXRzIChgZXZhbF9zZXRgIGluIGBvcmRlcnNgKTogIA0KDQoqIGAicHJpb3IiYDogb3JkZXJzIHByaW9yIHRvIHRoYXQgdXNlcnMgbW9zdCByZWNlbnQgb3JkZXIgKH4zLjJtIG9yZGVycykgIA0KKiBgInRyYWluImA6IHRyYWluaW5nIGRhdGEgc3VwcGxpZWQgdG8gcGFydGljaXBhbnRzICh+MTMxayBvcmRlcnMpICANCiogYCJ0ZXN0ImA6IHRlc3QgZGF0YSByZXNlcnZlZCBmb3IgbWFjaGluZSBsZWFybmluZyBjb21wZXRpdGlvbnMgKH43NWsgb3JkZXJzKSAgDQoNCiMjIFVuZGVyc3RhbmRpbmcgRGF0YXNldHMNCg0KIyMjIEFpc2xlcw0KDQpUaGVyZSBhcmUgKioxMzQgYWlsZXMqKiBpbiB0aGlzIGRhdGFzZXQuIEhlcmUgYXJlIGZldyBzYW1wbGUgbmFtZXMgb2YgdGhlIGFpbGVzLiAgDQoNCmBgYHtyLCByZXN1bHRzPSdob2xkJ30NCnBhc3RlKHNvcnQoaGVhZChhaXNsZXMkYWlzbGUpKSwgY29sbGFwc2U9JywgJykNCmBgYA0KDQojIyMgRGVwYXJ0bWVudHMNCg0KVGhlcmUgYXJlICoqMjEgZGVwYXJ0bWVudHMqKiBpbiB0aGlzIGRhdGFzZXQuTmFtZXMgb2YgYWxsIGRlcGFybWVudHMgYXJlIGxpc3RlZCBiZWxvdyBpbiBhcGhhYmV0aWNhbGx5IG9yZGVyZWQuDQoNCmBgYHtyLCByZXN1bHRzPSdob2xkJ30NCnBhc3RlKHNvcnQoZGVwYXJ0bWVudHMkZGVwYXJ0bWVudCksIGNvbGxhcHNlID0gJywgJykNCmBgYA0KDQojIyMgUHJvZHVjdHMNCg0KVGhlcmUgYXJlICoqNDksNjg4IHByb2R1Y3RzKiogaW4gdGhlIGNhdGFsb2d1ZSB3aXRoaW4gKioxMzQgYWlzbGVzIGFuZCAyMSBkZXBhcnRtZW50cyoqLiAgDQoNClNhbXBsZSBwcm9kdWN0cyBhcmUgYXMgYmVsb3cuDQoNCmBgYHtyLCByZXN1bHRzPSdob2xkJ30NCnByb2R1Y3RzICU+JSBoZWFkICU+JSBrYWJsZSAlPiUgDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIiwgImhvdmVyIikpDQpgYGANCg0KIyMjIERlcGFydG1lbnRzIEFuZCBJdHMgUmVsZXZhbnQgUHJvZHVjdHMNCg0KUHJvZHVjdHMgZGF0YWZyYW1lIGlzIHJlbGF0ZWQgdG8gRGVwYXJtZW50cy4gIA0KDQpXZSBzaGFsbCBzZWUgc2FtcGxlIG9mIDMgcHJvZHVjdHMgZm9yIGZldyBkZXBhcm1lbnRzLg0KDQpgYGB7ciwgcmVzdWx0cz0naG9sZCcsIG1lc3NhZ2U9RkFMU0V9DQpsZWZ0X2pvaW4oZGVwYXJ0bWVudHMsIHByb2R1Y3RzKSAlPiUgc2VsZWN0KGRlcGFydG1lbnQsIHByb2R1Y3RfbmFtZSApICU+JQ0KICBncm91cF9ieShkZXBhcnRtZW50KSAlPiUNCiAgc2FtcGxlX24oMykgJT4lDQogIHN1bW1hcmlzZSh0aHJlZV9leGFtcGxlc19wcm9kdWN0PXBhc3RlKHByb2R1Y3RfbmFtZSwgY29sbGFwc2U9JyAvICcpKSAlPiUgDQogIHNhbXBsZV9uKDUpICU+JSBrYWJsZSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCmBgYA0KDQojIyMgQWlzbGVzIEFuZCBJdHMgUmVsZXZhbnQgUHJvZHVjdHMNCg0KUHJvZHVjdHMgZGF0YWZyYW1lIGlzIGFsc28gcmVsYXRlZCB0byBhaXNsZXMuIEVhY2ggYWlzbGUgcmVsYXRlcyB0byBtdWx0aXBsZSBwcm9kdWN0cy4gQnkgam9pbmluZyBib3RoIGFpc2xlcyBhbmQgcHJvZHVjdHMgZGF0YWZyYW1lLCB3ZSBoYXZlIGFuIGlkZWEgd2hhdCB0eXBlIG9mIHByb2RjdXRzIGZvciBlYWNoIGFpbGVzLiAgDQoNCkV4YW1wbGUgYmVsb3cgc2hvd3MgMyBzYW1wbGVzIHByb2R1Y3RzIG9mIGZvciBmZXcgYWlzbGVzLg0KDQpgYGB7ciwgcmVzdWx0cz0naG9sZCcsIG1lc3NhZ2U9RkFMU0V9DQpsZWZ0X2pvaW4oYWlzbGVzLCBwcm9kdWN0cykgJT4lDQogIHNlbGVjdChhaXNsZSwgcHJvZHVjdF9uYW1lICkgJT4lICBncm91cF9ieShhaXNsZSkgJT4lDQogIHNhbXBsZV9uKDMpICU+JSANCiAgc3VtbWFyaXNlKHRocmVlX2V4YW1wbGVzX3Byb2R1Y3Q9cGFzdGUocHJvZHVjdF9uYW1lLCBjb2xsYXBzZT0nIHx8ICcpKSAlPiUgDQogIHNhbXBsZV9uKDUpICU+JSBrYWJsZSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCmBgYA0KDQojIyMgT3JkZXJzDQoNClRoZXJlIGFyZSBvdmVyICoqMyBtaWxsaW9ucyoqIG9ic2VydmF0aW9ucyBpbiBvcmRlcnMgZGF0YXNldC4gRWFjaCByb3cgcmVwcmVzZW50IGFuICoqdW5pcXVlIG9yZGVyKiouIA0KDQojIyMjIFRyYWluIEV2YWxfU2V0DQoNCkxldCdzIGFuYWx5c2UgdGhlIGNvbnN0cnVjdCBvZiBvbmUgdXNlci4gRm9yIGV4YW1wbGUsICoqdXNlcl9pZCAxKiogaGFkIG1hZGUgKioxMCBwcmlvciBvcmRlcnMqKiAob3JkZXIgbnVtYmVyIGZyb20gMSB0byAxMCksIGxhc3Qgb3JkZXIgaXMgYSAqKnRyYWluKiogKGV2YWxfc2V0KS4gTm90ZSB0aGF0IHRoZSBmaXJzdCBvcmRlciAob3JkZXJfbnVtYmVyIDEpIGRvZXMgbm90IGhhdmUgdmFsdWUgZm9yIGRheV9zaW5jZV9wcmlvcl9vcmRlciwgYXMgaXQgaXMgdGhlIGZpcnN0IG9yZGVyIHdpdGhvdXQgcHJpb3IgcmVjb3Jkcy4gIA0KDQpUaGlzIGFsc28gbWVhbnMgKipgPHVzZXJfaWQsIHByb2R1Y3RfaWQ+YCoqIG1hZGUgdXAgdGhlICoqa2V5KiogZm9yIHByZWRpY3Rpb24uICANCg0KYGBge3J9DQpvcmRlcnMgJT4lIGZpbHRlciAodXNlcl9pZD09MSkgJT4lIA0KICBrYWJsZSAlPiVrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKQ0KYGBgDQojIyMjIFRlc3QgRXZhbF9TZXQNCg0KTGV0J3MgYW5hbHlzZSBhbm90aGVyIGNvbnN0cnVjdCBvZiBvcmRlcnMuICoqVXNlcl9pZCAzKiogaGFkIG1hZGUgKioxMiBvcmRlcnMqKiBiZWZvcmUgdGhlIGZpbmFsIG9yZGVyIGxhYmVsZWQgYXMgKip0ZXN0KiogKGV2YWxfc2V0KSBvcmRlci4gRnJvbSB0aGUgZGF0YSB3ZSBrbm93IHRoYXQgb3JkZXJfbnVtYmVyIGlzIGJlaW5nIHJlY3ljbGVkIGZvciBlYWNoIHVzZXIuICANCg0KKipJbnN0YWNhcnQgZGlkIG5vdCBwcm92aWRlIHVzIHRoZSBiYXNrZXQgY29udGVudCBmb3IgdGVzdCBvcmRlcioqLiBUaGlzIGlzIGluIGZhY3QgdGhlICoqdGFyZ2V0IGZvciBwcmVkaWN0aW9uKiouIA0KDQpgYGB7cn0NCm9yZGVycyAlPiUgZmlsdGVyICh1c2VyX2lkPT0zKSAlPiUgDQogIGthYmxlICU+JWthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIiwgImhvdmVyIikpDQpgYGANCg0KIyMjIE9yZGVyX1Byb2R1Y3QNCg0KRWFjaCBvcmRlciBjb250YWluIG11bHRpcGxlIHByb2R1Y3RzIHB1cmNoYXNlZCBieSB1c2VyLiBJbnN0YWNhcnQgaGFkIGNsZWFubHkgY2F0ZWdvcml6ZWQgdGhlIG9yZGVycyBpbnRvICd0cmFpbicgYW5kICdwcmlvcicgaW4gKipTSU5HTEUqKiBvcmRlciBkYXRhc2V0Lg0KDQpIb3dldmVyLCB0aGUgZGV0YWlsIG9mIGVhY2ggb3JkZXJzIGFyZSBzcGxpdHRlZCBpbnRvIHR3byBkYXRzZXRzOiAgDQotICoqYG9yZGVyX3Byb2R1Y3RfdHJhaW5gKio6IGNvbnRhaW4gb25seSBkZXRhaWwgcHJvZHVjdCBpdGVtcyBvZiBsYXN0IG9yZGVyICANCi0gKipgb3JkZXJfcHJvZHVjdF9wcmlvcmAqKjogY29udGFpbiBkZXRhaWwgcHJvZHVjdCBpdGVtcyBvZiBhbGwgcHJpb3Igb3JkZXJzICANCg0KIyMjIE9yZGVyX1Byb2R1Y3RfVHJhaW4NCg0KKipvcmRlcl9wcm9kdWN0X3RyYWluL3ByaW9yKiogZGF0YWZyYW1lIHRlbGxzIHVzIHdoaWNoIHByb2R1Y3RzIHdlcmUgcHVyY2hhc2VkIGF0IGVhY2ggb3JkZXI7IGZvciBib3RoICoqdHJhaW4qKiBhbmQgKipwcmlvcioqIG9yZGVyLiAgDQoNCkZvciBleGFtcGxlLCB3ZSBrbm93ICoqdXNlcl9pZCAxKiogaW4gdGhlICoqTEFTVCBPUkRFUioqIChvcmRlcl9pZCAxMTg3ODk5KSBwdXJjaGFzZWQgKioxMCB1bmlxdWUgcHJvZHVjdHMqKiBieSAqKnF1ZXJpbmcgb3JkZXJfcHJvZHVjdF90cmFpbioqIHdpdGggdGhlIHJlbGV2YW50IG9yZGVyX2lkLiAgDQoNCmBgYHtyfQ0Kb3JkZXJfcHJvZHVjdHNfdHJhaW4gJT4lIGZpbHRlciAob3JkZXJfaWQgPT0gMTE4Nzg5OSkgJT4lIA0KICBrYWJsZSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCmBgYA0KDQojIyMgT3JkZXJfUHJvZHVjdF9UcmFpbl9Qcmlvcg0KDQpTaW1pbGFyeSwgZGV0YWlsIGl0ZW1zIGZvciBhICoqUFJJT1IgT1JERVIqKiAoZXhhbXBsZSBvcmRlcl9pZDogMjU1MDM2MikgY2FuIGJlIHJldGlyZXZlZCBieSBxdWVyaW5nIGRpZmZyZW4gZGF0YXNldCBvcmRlcl9wcm9kdWN0X3ByaW9yLg0KDQpgYGB7cn0NCm9yZGVyX3Byb2R1Y3RzX3ByaW9yICU+JSBmaWx0ZXIgKG9yZGVyX2lkID09IDI1NTAzNjIpICU+JSANCiAga2FibGUgJT4lIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIiwgImhvdmVyIikpDQpgYGANCg0KIyMjIFVzZXJzDQoNClRoZXIgaXMgbm8gZGVkaWNhdGVkIGRhdGFmcmFtZSBmb3IgdXNlcnMuIEhvd2V2ZXIsIHdlIGNhbiBkZXJpdmUgbnVtYmVyIG9mIHVuaXF1ZSB1c2VycyBmcm9tICoqb3JkZXIqKiBkYXRhZnJhbWUuIEJ5IGdyb3VwaW5nIHRoZSB1c2VyX2lkIGFuZCBldmFsX3NldCBjb2x1bW4sIHdlIGZvdW5kIHRoYXQgdGhlcmUgYXJlICoqNzUsMDAwIHRlc3QqKiB1c2VycywgKioxMzEsMjA5IHRyYWluKiogdXNlcnMuICANCg0KYGBge3J9DQpvcmRlcnMgJT4lIGZpbHRlcihldmFsX3NldCAlaW4lIGMoJ3RyYWluJywndGVzdCcpICkgJT4lDQogIGNvdW50KGV2YWxfc2V0KSAlPiUNCiAgbXV0YXRlKHBlcmNlbnRhZ2U9bi9zdW0obikpICU+JQ0KICBrYWJsZSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCmBgYA0KDQoNCiMgRXhwbG9yYXRvcnkgRGF0YSBBbmFseXNpcw0KDQpJbiB0aGlzIHNlY3Rpb24sIHdlIHNoYWxsIHRyeSB0byB1bmRlcnN0YW5kIHRoZSBidXlpbmcgYmVoYXZpb3VyIGJ5IGFza2luZyBzb21lIGludGVyZXN0aW5nIHF1ZXNpdG9ucy4gIA0KDQotIFdoYXQgdXN1YWxseSBkb2VzIHBlb3BsZSBidXksIGFuZCB3aGljaCBvbmUgdGhleSB1c3VhbGx5IHJlb3JkZXINCi0gV2hlbiBkbyB0aGV5IGJ1eSAoZGF5IGFuZCB0aW1lKT8gSXMgdGhlcmUgYSBidXlpbmcgdHJlbmQgYW5kIGRvZXMgaXQgaW5mbHVlbmNlIHdoYXQgdGhleSBidXkgPyAgDQoNClRvIHJlZHVjZSBvdXIgY29kaW5nIHN0ZXBzLCB3ZSBjb25zdHJ1Y3QgYSByZXVzYWJsZSBkYXRhZnJhbWUgY29tYmluaW5nIGFsbCBkZXRhaWxzIGZyb20gb3JkZXJzIGFuZCBpdHMgcHJvZHVjdHMuIFRoaXMgZGF0YWZyYW1lIHdpbGwgY29udGFpbiByb3dzIGZvciAqKnByaW9yIG9yZGVycyBhbmQgcHJvZHVjdHMgb25seSoqIChleGNsdWRpbmcgbGFzdCBvcmRlciB3aGljaCBpcyBsYWJlbGVkIGFzICd0cmFpbicpLiANCg0KYGBge3IgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0NCnVzZXJzX29yZGVyc19wcm9kdWN0c18gPSBvcmRlcnMgJT4lDQogIGlubmVyX2pvaW4ob3JkZXJfcHJvZHVjdHNfcHJpb3IpICU+JSAgIyBpbm5lcl9qb2luIHdpdGggcHJpb3IgdGFibGUgd2lsbCBmaWx0ZXIgb3V0IHRyYWluIG9yZGVycw0KICBsZWZ0X2pvaW4ocHJvZHVjdHMpICU+JQ0KICBsZWZ0X2pvaW4oYWlzbGVzKSAlPiUNCiAgbGVmdF9qb2luKGRlcGFydG1lbnRzKSAlPiUNCiAgYXJyYW5nZSh1c2VyX2lkLCBvcmRlcl9udW1iZXIpICU+JQ0KICBzZWxlY3QodXNlcl9pZCwgb3JkZXJfaWQsIG9yZGVyX251bWJlciwgb3JkZXJfZG93LCBvcmRlcl9ob3VyX29mX2RheSwgZGF5c19zaW5jZV9wcmlvcl9vcmRlciwgcHJvZHVjdF9pZCwgcHJvZHVjdF9uYW1lLCByZW9yZGVyZWQsIGFkZF90b19jYXJ0X29yZGVyLCBkZXBhcnRtZW50X2lkLCBhaXNsZV9pZCwgZGVwYXJ0bWVudCwgYWlzbGUpDQpgYGANCg0KDQojIyBPcmRlcnMNCg0KIyMjIEhvdyBNYW55IE9yZGVycyA/DQoNCk1vc3QgdXNlcnMgbWFkZSBmZXcgb3JkZXJzLiBUaGUgbnVtYmVyIG9mIG9yZGVycyBhIHVzZXJzIG1hZGUgZGVjcmVhc2Ugc2lnbmlmaWNhbGx5IGFsb25nIHRoZSBvcmRlciBudW1iZXJzLiBNYXhpbXVtIG9yZGVycyBhbnkgdXNlcnMgaGFkIG1hZGUgaXMgOTkuDQoNCmBgYHtyIGZpZy53aWR0aD05LjMsIGZpZy5oZWlnaHQ9Mi41fQ0KdG1wID0gdXNlcnNfb3JkZXJzX3Byb2R1Y3RzXyAlPiUgZ3JvdXBfYnkodXNlcl9pZCkgJT4lIHN1bW1hcml6ZShuX29yZGVycyA9IG1heChvcmRlcl9udW1iZXIpKQ0KdG1wICU+JSBnZ3Bsb3QoYWVzKHg9YXMuZmFjdG9yKG5fb3JkZXJzKSkpICsgZ2VvbV9iYXIoKSArDQogICAgeWxhYignQ291bnQgb2YgVXNlcnMnKSArDQogICAgeGxhYignTnVtYmVyIG9mIE9yZGVycyBNYWRlIEJ5IFVzZXJzJykgKw0KICAgIHRoZW1lKA0KICAgICAgYXhpcy50ZXh0LnggID0gZWxlbWVudF90ZXh0IChzaXplID0gNi4wLCBhbmdsZSA9ICg5MCksIGhqdXN0ID0gMSwgdmp1c3QgPSAwLjUpDQogICAgKQ0KYGBgDQojIyMgSG93IFNvb24gVW50aWwgTmV4dCBPcmRlciA/DQoNCkl0IGlzIHZlcnkgb2J2aW91cyB0aGF0IG1vc3QgdXNlcnMgbWFkZSB0aGVpciBvcmRlcnMgKip3ZWVrbHkgKGV2ZXJ5IDcgZGF5cykgYW5kIG1vbnRobHkgKGV2ZXJ5IDMwIGRheXMpKiouIFNlZSB0aGUgcGVhayBvZiBkYXkgNyBhbmQgZGF5IDMwIGluIHRoZSBjaGFydCBiZWxvdy4gDQoNCmBgYHtyIGZpZy53aWR0aD05LjMsIGZpZy5oZWlnaHQ9Mi41fQ0KdG1wID0gdXNlcnNfb3JkZXJzX3Byb2R1Y3RzXyAlPiUgDQogIGZpbHRlcihvcmRlcl9udW1iZXI+MSkgJT4lICMgZGF5c19zaW5jZV9wcmlvciBpcyBOQSBmb3IgZmlyc3Qgb3JkZXIsIG5lZWQgdG8gZmlsdGVyIG91dA0KICBncm91cF9ieShvcmRlcl9pZCkgJT4lIA0KICAgIHN1bW1hcml6ZShuX29yZGVycyA9IG1heChkYXlzX3NpbmNlX3ByaW9yX29yZGVyKSkNCnRtcCAlPiUgZ2dwbG90KGFlcyh4PWFzLmZhY3RvcihuX29yZGVycykpKSArIGdlb21fYmFyKCkgKyB5bGFiKCdDb3VudCBvZiBPcmRlcnMnKSArIHhsYWIoJ0RheXMgU2luY2UgUHJpb3IgRm9yIEVhY2ggT3JkZXInKQ0KYGBgDQoNCiMjIE9yZGVyc19Qcm9kdWN0cw0KDQojIyMgTW9zdCBQb3B1bGFyIFByb2R1Y3RzIFNvbGQNCg0KV2Uga25vdyB0aGF0ICoqYmFuYW5hKiogYXJlIHRoZSBtb3N0IHBvcHVsYXIgcHJvZHVjdHMuIFRoZSBudW1iZXIgb2Ygb3JkZXJzIHZhcmllcyBncmVhdGx5IGZvciBkaWZmZXJlbnQgcHJvZHVjdHMuIElsbHVzdHJhdGlvbiBiZWxvdyB1c2VzIHNob3dzIHNhbXBsZSBvZiBvbmx5ICoqMzAgdG9wIHByb2R1Y3RzKiouICBOb3RpY2UgaG93ZXZlciB0aGUgdmFyaWVuY2UgaXMgbm90IG9idmlvdXMgb3V0c2lkZSB0b3AgMTAgcHJvZHVjdHMuDQoNCmBgYHtyLCBmaWcud2lkdGg9OS41LCBmaWcuaGVpZ2h0PTMuNX0NCnRtcCA9IG9yZGVyX3Byb2R1Y3RzX3RyYWluICU+JQ0KICBsZWZ0X2pvaW4ocHJvZHVjdHMpICU+JQ0KICBncm91cF9ieShwcm9kdWN0X25hbWUpICU+JQ0KICBzdW1tYXJpemUoY291bnQ9bigpKSAlPiUNCiAgdG9wX24obj0zMCwgd3Q9Y291bnQpICU+JSAgbXV0YXRlKHBlcmNlbnRhZ2U9Y291bnQvc3VtKGNvdW50KSkNCg0KcDEgPSBnZ3Bsb3QgKHRtcCwgYWVzKHg9cmVvcmRlcihwcm9kdWN0X25hbWUsY291bnQpLCB5PXBlcmNlbnRhZ2UpKSArICANCiAgZ2VvbV9jb2woKSArIGdndGl0bGUoJ1Byb2R1Y3RzIFRvcCAzMCcpICsgeWxhYignUGVyY2VudGFnZSBvZiBPcmRlcnMnKSArDQogIHRoZW1lICgNCiAgICBheGlzLnRleHQueD1lbGVtZW50X3RleHQoYW5nbGU9OTAsIGhqdXN0PTEsIHZqdXN0PTAuNSksDQogICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpKSANCg0KcDIgPSBnZ3Bsb3QgKGRhdGEgPSB0bXAsIGFlcyggeD0gJycsIHk9cGVyY2VudGFnZSApKSArIA0KICBnZ3RpdGxlKCdQcm9kdWN0cyBUb3AgMzAnKSArIHlsYWIoJ3BlcmNlbnRhZ2Uub2Yub3JkZXJzJykgKyBnZW9tX2JveHBsb3QoKSArIHhsYWIoJ1Byb2R1Y3RzJykNCg0KZ3JpZC5hcnJhbmdlKHAxLCBwMiwgbmNvbCA9IDIpDQpgYGANCmBgYHtyIGVjaG89RkFMU0V9DQpybShsaXN0PWMoJ3RtcCcsJ3AxJywncDInKSkNCmBgYA0KDQojIyMgTW9zdCBQb3B1bGFyIERlcGFydG1lbnQgU29sZA0KDQpDZXJ0YWluIGRlcGFydG1lbnMgYXJlIGNsZWFybHkgbW9yZSBwb3B1bGFyLCBsaWtlICoqcHJvZHVjZSBhbmQgZGFpcnkgZWdncyoqLiBCb3RoIGRlcGFybWVudHMgY29tYmluZWQgY29udHJpYnV0ZWQgdG8gKiptb3JlIHRoYW4gNDAlKiogb2YgdG90YWwgb3JkZXJzLg0KDQpgYGB7ciwgZmlnLndpZHRoPTkuNSwgZmlnLmhlaWdodD0zLjV9DQp0bXAgPSB1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBncm91cF9ieShkZXBhcnRtZW50KSAlPiUNCiAgc3VtbWFyaXplKGNvdW50PW4oKSkgJT4lDQogIG11dGF0ZShwZXJjZW50YWdlPWNvdW50L3N1bShjb3VudCkpDQoNCnAxID0gZ2dwbG90ICh0bXAsIGFlcyh4PXJlb3JkZXIoZGVwYXJ0bWVudCxjb3VudCksIHk9cGVyY2VudGFnZSkpICsgIA0KICBnZW9tX2NvbCgpICsgZ2d0aXRsZSgnRGVwYXJ0bWVudHMnKSArIHlsYWIoJ1BlcmNlbnRhZ2Ugb2YgT3JkZXJzJykgKw0KICB0aGVtZSAoDQogICAgYXhpcy50ZXh0Lng9ZWxlbWVudF90ZXh0KGFuZ2xlPTkwLCBoanVzdD0xLCB2anVzdD0wLjUpLA0KICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSkgDQoNCnAyID0gZ2dwbG90IChkYXRhID0gdG1wLCBhZXMoIHg9ICcnLCB5PXBlcmNlbnRhZ2UgKSkgKyANCiAgZ2d0aXRsZSgnRGVwYXJ0bWVudHMnKSArIHlsYWIoJ3BlcmNlbnRhZ2Uub2Yub3JkZXJzJykgKyBnZW9tX2JveHBsb3QoKSArIHhsYWIoJ0RlcGFydG1lbnRzJykNCg0KZ3JpZC5hcnJhbmdlKHAxLCBwMiwgbmNvbCA9IDIpDQpgYGANCg0KYGBge3IgZWNobz1GQUxTRX0NCnJtKGxpc3Q9YygndG1wJywncDEnLCdwMicpKQ0KYGBgDQoNCiMjIyBNb3N0IFBvcHVsYXIgQWlzbGVzIFNvbGQNCg0KV2UgbG9va2VkIGludG8gdGhlIGJ1eWluZyB0cmVuZCBvZiBwcm9kdWN0IGJ5IGFpbGVzIGFuZCBub3RpY2UgdGhhdCBjZXJ0YWluIGFpc2xlIGxpa2UgKip2ZWdldGFibGVzIGFuZCBmcnVpdHMqKiBjb250cmlidXRlcyB0byAqKmFsbW9zdCAzMCUqKiBvZiB0b3RhbCBvcmRlcnMuIENoYXJ0IGJlbG93IHNob3dzIHRvcCAzMCBhaXNsZXMuDQoNCmBgYHtyLCBmaWcud2lkdGg9OS41LCBmaWcuaGVpZ2h0PTMuNX0NCnRtcCA9IHVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGdyb3VwX2J5KGFpc2xlKSAlPiUNCiAgc3VtbWFyaXplKGNvdW50PW4oKSkgJT4lDQogIHRvcF9uKG49MzAsIHd0PWNvdW50KSAlPiUgIG11dGF0ZShwZXJjZW50YWdlPWNvdW50L3N1bShjb3VudCkpDQoNCnAxID0gZ2dwbG90ICh0bXAsIGFlcyh4PXJlb3JkZXIoYWlzbGUsY291bnQpLCB5PXBlcmNlbnRhZ2UpKSArICANCiAgZ2VvbV9jb2woKSArIGdndGl0bGUoJ0Fpc2xlcyBUb3AgMzAnKSArIHlsYWIoJ1BlcmNlbnRhZ2Ugb2YgT3JkZXJzJykgKw0KICB0aGVtZSAoDQogICAgYXhpcy50ZXh0Lng9ZWxlbWVudF90ZXh0KGFuZ2xlPTkwLCBoanVzdD0xLCB2anVzdD0wLjUpLA0KICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSkgKyAgeWxhYignUGVyY2VudGFnZSBvZiBPcmRlcnMnKSArIHhsYWIoJ0Fpc2xlcycpDQoNCnAyID0gZ2dwbG90ICh0bXAsIGFlcyggeD0gJycsIHk9cGVyY2VudGFnZSApKSArIA0KICBnZ3RpdGxlKCdBaXNsZXMgVG9wIDMwJykgKyB5bGFiKCdwZXJjZW50YWdlLm9mLm9yZGVycycpICsgZ2VvbV9ib3hwbG90KCkgKyB4bGFiKCdBaXNsZXMnKQ0KDQpncmlkLmFycmFuZ2UocDEsIHAyLCBuY29sID0gMikNCmBgYA0KDQpgYGB7ciBlY2hvPUZBTFNFfQ0Kcm0obGlzdD1jKCd0bXAnLCdwMScsJ3AyJykpDQpgYGANCg0KIyMjIFByb2R1Y3RzIE9yZGVyZWQgRGF5IFBhdHRlcm4NCg0KV2UgY2FuIHNlZSB0aGF0IGJvdGggKipEYXkgMCBhbmQgRGF5IDEqKiBzdGFuZHMgb3V0IHRvIGJlIHRoZSBtb3N0IGJ1c3kgc2hvcHBpbmcgZGF5IGZvciBpbnN0YWNhcnQuIFRoaXMgbWVhbnMgdGhhdCBkYXkgb2Ygb3JkZXIgbWFkZSBtYXkgaW5mbHVlbmNlIHRoZSBiYXNrZXQgc2l6ZS4NCg0KYGBge3IsZmlnLndpZHRoPTkuMywgZmlnLmhlaWdodD0zLjB9DQp1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBncm91cF9ieShvcmRlcl9kb3cpICU+JQ0KICAgIHN1bW1hcml6ZShjb3VudCA9IG4oKSkgJT4lDQogIG11dGF0ZShwZXJjZW50YWdlPWNvdW50L3N1bShjb3VudCkpICU+JQ0KICBnZ3Bsb3QgKGFlcyh4PWFzLmZhY3RvcihvcmRlcl9kb3cpLCB5PXBlcmNlbnRhZ2UsIGZpbGw9YXMuZmFjdG9yKG9yZGVyX2RvdykpKSArIA0KICAgIGdlb21fY29sKCkrIHlsYWIoJ1BlcmNlbnRhZ2Ugb2YgT3JkZXJzJykgKyBnZ3RpdGxlKCdEYWlseSBPcmRlcnMnKQ0KYGBgDQoNCldoZW4gd2Ugem9vbSBpbnRvIGRhaWx5IG9yZGVycywgd2Ugbm90aWNlIHRoYXQgdG9wIHRlbiBwcm9kdWN0cyBjb250cmlidXRlcyBiZXR3ZWVuIDclIHRvIDglIG9mIGRhaWx5IG9yZGVycy4gSXQgaXMgaW50ZXJlc3RpbmcgdG8gc2VlIHRoYXQgTGltZXMgYXJlIHBhcnQgb2YgdG9wIHRlbiBmb3IgRGF5IDAgYW5kIERheSA2LCBidXQgbm90IG90aGVyIGRheXMuIFdoZXJlYXMgT3JnYW5pYyBXaG9sZSBNaWxrIGRvZXNuJ3QgbWFrZSBpdCB0byB0b3AgdGVuIGZvciBEYXkgMC4gT3JnYW5pYyBSZXNwYmVycmllcyBkb2VzIG5vdCBtYWtlIGl0IHRvIHRvcCAxMCBvZiBEYXkgNi4gVGhpcyBtZWFucyB0aGF0IHRoZXJlIGlzIGEgY2hhbmNlIG9mIHByZWRpY3RhYmlsaXR5IGJhc2VkIG9uIHRoZSBkYXkgb3JkZXIgaXMgbWFkZS4gDQoNCmBgYHtyLGZpZy53aWR0aD05LjMsIGZpZy5oZWlnaHQ9NC41fQ0KdXNlcnNfb3JkZXJzX3Byb2R1Y3RzXyU+JQ0KICBncm91cF9ieShvcmRlcl9kb3csIHByb2R1Y3RfbmFtZSkgJT4lDQogICAgc3VtbWFyaXplKG49bigpKSAlPiUNCiAgbXV0YXRlKHBlcmNlbnRhZ2U9bi9zdW0obikpICU+JQ0KICB0b3BfbigxMCwgd3Q9bikgJT4lDQogIGdncGxvdCAoYWVzKHg9YXMuZmFjdG9yKG9yZGVyX2RvdyksIHk9cGVyY2VudGFnZSwgZmlsbD1wcm9kdWN0X25hbWUpKSArIA0KICAgIGdlb21fY29sKCkgKyB5bGFiKCdQcm9wcnRpb24gb2YgT3JkZXJzIEluIEEgRGF5JykgKyBnZ3RpdGxlKCdEYWlseSBUb3AgMTAgUHJvZHVjdHMgT3JkZXJlZCcpICsNCiAgICB0aGVtZShsZWdlbmQucG9zaXRpb249ImJvdHRvbSIsbGVnZW5kLmRpcmVjdGlvbj0iaG9yaXpvbnRhbCIpDQpgYGANCg0KIyMjIFByb2R1Y3RzIE9yZGVyZWQgSG91ciBQYXR0ZXJuDQoNCk1vcm5pbmcgdG8gYWZ0ZXJub29uIGFyZSB0aGUgcGVhayBzaG9wcGluZyBob3VycyBmb3IgaW5zdGFjYXJ0IGN1c3RvbWVycy4gVGhlIGhvdXIgb3JkZXIgbWFkZSBpbmZsdWVuY2VzIGJhc2tldCBzaXplLg0KDQpgYGB7cixmaWcud2lkdGg9OS4zLCBmaWcuaGVpZ2h0PTMuMH0NCnVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGdyb3VwX2J5KG9yZGVyX2hvdXJfb2ZfZGF5KSAlPiUNCiAgICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JQ0KICBtdXRhdGUocGVyY2VudGFnZT1jb3VudC9zdW0oY291bnQpKSAlPiUNCiAgZ2dwbG90IChhZXMoeD1hcy5mYWN0b3Iob3JkZXJfaG91cl9vZl9kYXkpLCB5PXBlcmNlbnRhZ2UpKSArIA0KICAgIGdlb21fY29sKCkrIHlsYWIoJ1BlcmNlbnRhZ2Ugb2YgT3JkZXJzJykgKyBnZ3RpdGxlKCdIb3VybHkgT3JkZXJzJykNCmBgYA0KDQpJbiB0aGUgZ3JvY2VyeSwgdGhlcmUgYXJlIGNsb3NlIHRvIDUwLDAwMCBwcm9kdWN0cy4gV2hlbiB3ZSB6b29tIGludG8gaG91cmx5IHB1cmNoYXNlcywgd2Ugbm90aWNlZCB0aGF0IHRvcCAxMCBwcm9kdWN0cyBtYW5hZ2VkIHRvIHNjb3JlIGJldHdlbiA2JSB0byA4JSBvZiBob3VybHkgc2FsZXMuIEV2ZXJ5IGhvdXIgaGFzIHNsaWdodGx5IGRpZmZyZW50IGNvbWJpbmF0aW9uIG9mIHRvcCAxMCBwcm9kdWN0cyAoY29tYmluYXRpb24gb3V0IG9mIDEyIHByb2R1Y3RzKS4gVGhhdCBtZWFucyBjZXJ0YWluIHByb2R1Y3RzIGFyZSBwcmVkaWN0YWJsZSBmb3Igb3JkZXJpbmcgaXJyZWdhcmRsZXNzIG9mIHRoZSBob3VyIG9mIG9yZGVyLiAgDQoNCkl0IGlzIGludGVyZXN0aW5nIHRvIGtub3cgdGhhdCwgc2ltaWxhciB0byBkYWlseSB0b3AgMTAgcHJvZHVjdHMsIHRoZSBPcmdhbmljIFdob2xlbWlsayBhbmQgTGltZXMgaXMgbWlzc2luZyBhcyB0b3AgMTAgZnJvbSBzb21lIGhvdXJzLiAgDQoNCmBgYHtyLGZpZy53aWR0aD05LjMsIGZpZy5oZWlnaHQ9NX0NCnVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGdyb3VwX2J5KG9yZGVyX2hvdXJfb2ZfZGF5LCBwcm9kdWN0X25hbWUpICU+JQ0KICAgIHN1bW1hcml6ZShuPW4oKSkgJT4lDQogIG11dGF0ZShwZXJjZW50YWdlPW4vc3VtKG4pKSAlPiUNCiAgdG9wX24oMTAsIHd0PW4pICU+JQ0KICBnZ3Bsb3QgKGFlcyh4PWFzLmZhY3RvcihvcmRlcl9ob3VyX29mX2RheSksIHk9cGVyY2VudGFnZSwgZmlsbD1wcm9kdWN0X25hbWUpKSArIA0KICAgIGdlb21fY29sKCkgKyB5bGFiKCdQcm9wcnRpb24gb2YgT3JkZXJzIEluIEEgSG91cicpICsgZ2d0aXRsZSgnSG91cmx5IFRvcCAxMCBQcm9kdWN0cyBPcmRlcmVkJykgKw0KICAgIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbj0iYm90dG9tIixsZWdlbmQuZGlyZWN0aW9uPSJob3Jpem9udGFsIikNCmBgYA0KDQoNCiMjIEJhc2tldCBBbmFseXNpcw0KDQojIyMgQmFza2V0IFNpemUgRGlzdHJpYnV0aW9uDQoNCk51bWJlciBvZiBpdGVtcyBpbiBhbGwgb3JkZXJzIHJhbmdlIGZyb20gMSB0byAxNDUuIFRoZSBoaXN0b2dyYW0gYmVsb3cgaXMgaGlnaGx5ICoqc2tld2VkIHRvd2FyZHMgc21hbGwgYmFza2V0IHNpemUqKi4gTWFqb3JpdHkgb2YgdXNlcnMgcHVyY2hhc2VkIDUgaXRlbXMgaW4gdGhlaXIgb3JkZXJzLg0KDQpgYGB7ciBmaWcud2lkdGg9OS4zLCBmaWcuaGVpZ2h0PTN9DQp0bXAgPSB1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBncm91cF9ieShvcmRlcl9pZCkgICU+JQ0KICBzdW1tYXJpemUoIGJhc2tldF9zaXplPW4oKSwgDQogICAgICAgICAgICAgcmVvcmRlcmVkX2l0ZW1zID0gc3VtKHJlb3JkZXJlZCkpICU+JQ0KICBncm91cF9ieShiYXNrZXRfc2l6ZSkgJT4lDQogIHN1bW1hcml6ZShuPW4oKSwgYXZnX3Jlb3JkZXJlZF9pdGVtcyA9bWVhbihyZW9yZGVyZWRfaXRlbXMpKSAlPiUNCiAgYXJyYW5nZShiYXNrZXRfc2l6ZSkNCiAgDQp0bXAgJT4lIGdncGxvdChhZXMoeD1hcy5mYWN0b3IoYmFza2V0X3NpemUpKSkgKw0KICAgIGdlb21fY29sKGFlcyh5PW4pKSArDQogICAgeWxhYignT3JkZXIgQ291bnQnKSArDQogICAgeGxhYignTnVtYmVyIG9mIEl0ZW1zIGluIEJhc2tldCcpICsNCiAgICBnZ3RpdGxlKCdCYXNrZXQgU2l6ZSBEaXN0cmlidXRpb24nKSArDQogICAgdGhlbWUoDQogICAgICBheGlzLnRleHQueCAgPSBlbGVtZW50X3RleHQgKHNpemUgPSA2LjAsIGFuZ2xlID0gKDkwKSwgaGp1c3QgPSAxLCB2anVzdCA9IDAuNSkNCiAgICApDQpgYGANCg0KDQojIyBSZS1PcmRlcmVkIEFuYWx5c2lzDQoNCkFuYWx5emluZyB0aGUgcmUtb3JkZXJlZCBwcm9kdWN0cyBpcyB0aGUgbW9zdCBpbXBvcnRhbnQgcGFydCBvZiB0aGUgRURBLiBUaGlzIGlzIGJlY2FzdWUgaW5zaWdodHMgZnJvbSB0aGlzIGFuYWx5c2lzIGNhbiBoZWxwIHRvIGRldmVsb3AgaW50dWl0aW9uIGZvciBmdXJodGVyIGZlYXR1cmUgZW5naW5lZXJpbmcgdGhhdCB3aWxsIG1ha2UgdGhlIHByZWRpY3Rpb24gbW9yZSBtZWFuaW5nZnVsLg0KDQojIyMgQXZlcmFnZSBSZS1vcmRlcmVkIEl0ZW1zIEluIEJhc2tldCBEaXN0cmlidXRpb24NCg0KYGBge3IgZmlnLndpZHRoPTkuMywgZmlnLmhlaWdodD0zfQ0KdG1wICU+JSBnZ3Bsb3QgKGFlcyh4PWFzLmZhY3RvcihiYXNrZXRfc2l6ZSkpKSArIA0KICAgIGdlb21fcG9pbnQoYWVzKHk9YXZnX3Jlb3JkZXJlZF9pdGVtcyksIGNvbG9yPSdyZWQnKSArDQogICAgeWxhYignQXZnIE51bWJlciBvZiBSZS1PcmRlcmVkIEl0ZW1zJykgKyAgDQogICAgeGxhYignTnVtYmVyIG9mIEl0ZW1zIGluIEJhc2tldCcpICsNCiAgICBnZ3RpdGxlKCdSZW9yZGVyIFJhdGUgYnkgQmFza2V0IFNpemUnKSArDQogICAgdGhlbWUoDQogICAgICBheGlzLnRleHQueCAgPSBlbGVtZW50X3RleHQgKHNpemUgPSA2LjAsIGFuZ2xlID0gKDkwKSwgaGp1c3QgPSAxLCB2anVzdCA9IDAuNSkNCiAgICApICsNCiAgICBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEsIGNvbG9yPSdibHVlJykNCmBgYA0KDQojIyMgUHJvZHVjdCBSZW9yZGVyIFJhdGlvDQoNCk9uZSBvZiB0aGUgdHJpY2tlciB0aGluZ3MgdG8gcHJlZGljdCBpbiB0aGUgSW5zdGFjYXJ0IGRhdGFzZXQgaXMgdGhlIGluY2lkZW5jZSBvZiBvcmRlcnMgd2l0aG91dCByZW9yZGVyZWQgcHJvZHVjdHMuIFBsb3R0aW5nIHRoZSBwcm9wb3J0aW9uIG9mIHRoaXMgaW5jaWRlbmNlIGFjcm9zcyB0aGUgdHJhaW5pbmcgc2FtcGxlIChhIHNuYXBzaG90IG9mIDEzMUsrIHVzZXJzKSBwcm92aWRlcyBzb21lIGluc3BpcmF0aW9uLg0KDQojIyMgSG93IE1hbnkgUHJvZHVjdHMgV2VyZSBSZW9yZGVyZWQNCg0KQW1vbmcgYWxsIHByb2R1Y3QgcHVyY2hhc2VzLCA0MSUgb2YgcHJvZHVjdHMgYXJlIHJlb3JkZXJlZC4gIFRoZSByZW9yZGVyZWQgcmF0ZSBpcyBwYXJ0aWN1bGFybHkgaGlnaCBvbiB0b3AgMTAgcHJvZHVjdHMuIEFzIHNob3duIGluIGNoYXJ0IGJlbG93LCB0b3AgdGVuIHBvcHVsYXIgcHJvZHVjdHMgaGFzIHJlb3JkZXJlZCByYXRlIGlzIGFyb3VuZCA3MCUgdG8gODUlOyBoaWdoZXIgdGhhbiB0aGUgb3ZlcmFsbCByYXRpbyBvZiA0MSUuIA0KDQpgYGB7ciwgZmlnLndpZHRoPTkuMywgZmlnLmhlaWdodD0zLCBtZXNzYWdlPUZBTFNFfQ0KIyMgb3ZlcmFsbCBhbGwgcHJvZHVjdHMgcmVvcmRlcmVkIHJhdGUNCnRtcDEgPSB1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBmaWx0ZXIob3JkZXJfbnVtYmVyID4xKSAlPiUgIyBleGNsdWRlIGZpcnN0IG9yZGVyLCB3aGljaCB3aWxsIG5ldmVyIGhhdmUgcmVvcmRlcmVkDQogIGNvdW50KHJlb3JkZXJlZCkgJT4lDQogIG11dGF0ZShyYXRpbz1uL3N1bShuKSkNCiAgDQpwMSA9IHRtcDEgJT4lIA0KICBnZ3Bsb3QgKGFlcyh4PScnLHk9cmF0aW8sIGZpbGw9YXMuZmFjdG9yKHJlb3JkZXJlZCkpKSArIGdlb21fY29sKHdpZHRoPTEpICsgeWxhYignUHJvZHVjdCBSZW9yZGVyZWQgUmF0aW8nKSArDQogIGNvb3JkX3BvbGFyKHRoZXRhPSd5Jywgc3RhcnQ9MCkgKyANCiAgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArDQogIHRoZW1lKGF4aXMudGl0bGUueSA9IGVsZW1lbnRfYmxhbmsoKSkNCg0KIyMgdG9wMTAgcHJvZHVjdHMgYW5kIGl0cyByZW9yZGVyZWQgcmF0ZQ0KdG1wMiA9IHVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGNvdW50KHByb2R1Y3RfaWQpICU+JSAjIGZpbHRlciBvbmx5IHRvcCAxMCBwcm9kdWN0cyBmb3IgcmVvcmRlciBhbmFseXNpcw0KICB0b3BfbihuPTEwKSAlPiUNCiAgbGVmdF9qb2luKHVzZXJzX29yZGVyc19wcm9kdWN0c18pICU+JSAgIyBub3cgZmluZCBvdXQgdGhlaXIgcmVvcmRlcmVkIHJhdGUNCiAgZ3JvdXBfYnkocHJvZHVjdF9pZCwgcHJvZHVjdF9uYW1lKSAlPiUNCiAgICBzdW1tYXJpemUoDQogICAgICByZW9yZGVyZWRfcmF0ZSA9IHN1bShyZW9yZGVyZWQsbmEucm09VCkvbigpDQogICAgKSAlPiUgDQogIHNlbGVjdChwcm9kdWN0X2lkLCBwcm9kdWN0X25hbWUsIHJlb3JkZXJlZF9yYXRlKSAlPiUNCiAgYXJyYW5nZShkZXNjKHJlb3JkZXJlZF9yYXRlKSkNCg0KcDIgPSB0bXAyICU+JSBnZ3Bsb3QgKGFlcyh4PXJlb3JkZXIocHJvZHVjdF9uYW1lLCByZW9yZGVyZWRfcmF0ZSksIHk9cmVvcmRlcmVkX3JhdGUpKSArIA0KICBnZ3RpdGxlKCdUb3AgMTAgUHJvZHVjdHMgU29sZCBhbmQgVGhlaXIgUmVvcmRlcmluZyBSYXRlJykgKw0KICBnZW9tX2NvbCgpICsgc2NhbGVfeV9jb250aW51b3VzKGxpbWl0cyA9IGMoMCwxKSwgYnJlYWtzID0gc2VxKDAsMSwwLjEpKSArIGNvb3JkX2ZsaXAoKQ0KDQpncmlkLmFycmFuZ2UocDEsIHAyLCBuY29sID0gMikNCmBgYA0KYGBge3IgZWNobz1GQUxTRX0NCnJtKGxpc3Q9YygndG1wMScsJ3RtcDInLCdwMScsJ3AyJykpDQpgYGANCg0KIyMjIFJlb3JkZXJpbmcgdnMgRGF5cyBTaW5jZSBQcmlvciBPcmRlcg0KDQpXZSB1bmRlcnN0YW5kIGZyb20gb3JkZXIgYW5hbHlzaXMgZWFybGllciB0aGF0IG1vc3QgdXNlcnMgcGxhY2UgdGhlaXIgb3JkZXJzIGV2ZXJ5IDcgYW5kIDMwIGRheXMuIEhvd2V2ZXIsIGZyb20gcmVvcmRlciByYXRpb24gcGVyc3BlY3RpdmUsIGRheSA3IGFuZCBkYXkgMzAgaGFzIGhpZ2ggY29udHJhc3Qgd2hlcmVieSBkYXkgNyBvcmRlcnMgaGFzIGhpZ2ggcmVvcmRlciByYXRpbyBhbmQgZGF5IDMwIGhhcyBsb3dlc3QgcmVvcmRlcmluZyByYXRpby4NCg0KYGBge3IgZmlnLndpZHRoPTkuMywgZmlnLmhlaWdodD0yLjV9DQp0bXAgPSB1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBmaWx0ZXIob3JkZXJfbnVtYmVyPjEpICU+JQ0KICBncm91cF9ieShkYXlzX3NpbmNlX3ByaW9yX29yZGVyLCBvcmRlcl9pZCkgJT4lDQogICAgc3VtbWFyaXplKA0KICAgICAgY29udGFpbl9yZW9yZGVyZWQgPSBtYXgocmVvcmRlcmVkKQ0KICAgICkgJT4lDQogICAgc3VtbWFyaXplKCANCiAgICAgIHJlb3JkZXJlZF9vcmRlcnMgPSBzdW0oY29udGFpbl9yZW9yZGVyZWQpLA0KICAgICAgbiA9IG4oKSsxDQogICAgKSAlPiUNCiAgbXV0YXRlKG5vbl9yZW9yZGVyX3JhdGlvPSAxLShyZW9yZGVyZWRfb3JkZXJzL24pKQ0KDQp0bXAgJT4lIGdncGxvdCAoYWVzKHg9ZGF5c19zaW5jZV9wcmlvcl9vcmRlciwgeT1ub25fcmVvcmRlcl9yYXRpbykpICsgZ2VvbV9wb2ludCgpICsgZ2VvbV9saW5lKCkgKw0KICAgIGdndGl0bGUoJ09yZGVycyBOT1QgQ29udGFpbmluZyBSZW9yZGVyZWQgUHJvZHVjdHMgb3ZlciBEYXlzIHNpbmNlIFByaW9yIE9yZGVyJykNCmBgYA0KDQojIyMgUmVvcmRlcmluZyB2cyBIb3VyIE9mIE9yZGVyDQoNCg0KYGBge3IgZmlnLndpZHRoPTkuMywgZmlnLmhlaWdodD0yLjV9DQp0bXAgPSB1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBmaWx0ZXIob3JkZXJfbnVtYmVyPjEpICU+JQ0KICBncm91cF9ieShvcmRlcl9ob3VyX29mX2RheSwgb3JkZXJfaWQpICU+JQ0KICAgIHN1bW1hcml6ZSgNCiAgICAgIGNvbnRhaW5fcmVvcmRlcmVkID0gbWF4KHJlb3JkZXJlZCkNCiAgICApICU+JQ0KICAgIHN1bW1hcml6ZSggDQogICAgICByZW9yZGVyZWRfb3JkZXJzID0gc3VtKGNvbnRhaW5fcmVvcmRlcmVkKSwNCiAgICAgIG4gPSBuKCkNCiAgICApICU+JQ0KICBtdXRhdGUobm9uX3Jlb3JkZXJfcmF0aW89IDEtKHJlb3JkZXJlZF9vcmRlcnMvbikpDQoNCnRtcCAlPiUgZ2dwbG90IChhZXMoeD1vcmRlcl9ob3VyX29mX2RheSwgeT1ub25fcmVvcmRlcl9yYXRpbykpICsgZ2VvbV9wb2ludCgpICsgZ2VvbV9saW5lKCkgKw0KICBnZ3RpdGxlKCdOb24gUmVvcmRlciBSYXRpbyBvdmVyIFRpbWUgb2YgT3JkZXIgUGxhY2VkJykNCmBgYA0KDQojIyMgUmVvcmRlcmluZyB2cyBEYXkgT2YgT3JkZXINCg0KDQpgYGB7ciBmaWcud2lkdGg9OS4zLCBmaWcuaGVpZ2h0PTIuNX0NCnRtcCA9IHVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGZpbHRlcihvcmRlcl9udW1iZXI+MSkgJT4lDQogIGdyb3VwX2J5KG9yZGVyX2Rvdywgb3JkZXJfaWQpICU+JQ0KICAgIHN1bW1hcml6ZSgNCiAgICAgIGNvbnRhaW5fcmVvcmRlcmVkID0gbWF4KHJlb3JkZXJlZCkNCiAgICApICU+JQ0KICAgIHN1bW1hcml6ZSggDQogICAgICByZW9yZGVyZWRfb3JkZXJzID0gc3VtKGNvbnRhaW5fcmVvcmRlcmVkKSwNCiAgICAgIG4gPSBuKCkrMQ0KICAgICkgJT4lDQogIG11dGF0ZShub25fcmVvcmRlcl9yYXRpbz0gMS0ocmVvcmRlcmVkX29yZGVycy9uKSkNCg0KdG1wICU+JSBnZ3Bsb3QgKGFlcyh4PW9yZGVyX2RvdywgeT1ub25fcmVvcmRlcl9yYXRpbykpICsgZ2VvbV9wb2ludCgpICsgZ2VvbV9saW5lKCkgKw0KICBnZ3RpdGxlKCdOb24gUmVvcmRlciBSYXRpbyBvdmVyIERheSBvZiBQdXJjaGFzZScpDQpgYGANCg0KIyMjIFJlb3JkZXJpbmcgdnMgRGF5IE9mIE9yZGVyDQoNCkludHVpdGl2ZWx5LCB3ZSBjYW4gdGhpbmsgb2YgdGhlIG1vcmUgcmVndWxhciBhIGJ1eWVyIGlzLCAgdGhlIHBlcnNvbiB0ZW5kIHRvIHJlcGVhdCBvcmRlcmluZyB0aGUgc2FtZSBwcm9kdWN0cy4gDQoNCmBgYHtyIGZpZy53aWR0aD05LjMsIGZpZy5oZWlnaHQ9Mi41fQ0KdG1wID0gdXNlcnNfb3JkZXJzX3Byb2R1Y3RzXyAlPiUNCiAgZmlsdGVyKG9yZGVyX251bWJlcj4xKSAlPiUNCiAgZ3JvdXBfYnkodXNlcl9pZCwgb3JkZXJfaWQpICU+JQ0KICAgIHN1bW1hcml6ZSgNCiAgICAgIGNvbnRhaW5fcmVvcmRlcmVkID0gbWF4KHJlb3JkZXJlZCkNCiAgICApICU+JQ0KICAgIHN1bW1hcml6ZSggDQogICAgICByZW9yZGVyZWRfb3JkZXJzID0gc3VtKGNvbnRhaW5fcmVvcmRlcmVkKSwNCiAgICAgIHRvdGFsX29yZGVyc19wZXJfdXNlcj1uKCkNCiAgICApICU+JQ0KICAgIGdyb3VwX2J5KHRvdGFsX29yZGVyc19wZXJfdXNlcikgJT4lDQogICAgc3VtbWFyaXplKA0KICAgICAgcmVvcmRlcnMgPSBzdW0ocmVvcmRlcmVkX29yZGVycyksDQogICAgICB0b3RhbCA9IHN1bSh0b3RhbF9vcmRlcnNfcGVyX3VzZXIpKSAlPiUNCiAgICBtdXRhdGUobm9uX3JhdGlvPSAxLShyZW9yZGVycy90b3RhbCkpDQogIA0KdG1wICU+JSBnZ3Bsb3QgKGFlcyh4PXRvdGFsX29yZGVyc19wZXJfdXNlciwgeT1ub25fcmF0aW8pKSArIGdlb21fcG9pbnQoKSArIGdlb21fbGluZSgpICsNCiAgZ2d0aXRsZSgnTm9uIFJlb3JkZXIgUmF0aW8gb3ZlciBEYXkgb2YgUHVyY2hhc2UnKQ0KYGBgDQoNCg0KIyBQcmVkaWN0aXZlIEFuYWx5c2lzDQoNCiMjIFR5cGUgb2YgUHJlZGljdGlvbg0KDQpUaGUgb2JqZWN0aXZlIGlzIHRvIHByZWRpY3Qgd2hhdCBwcm9kdWN0IHdpbGwgdGhlIGN1c3RvbWVyIHB1cmNoYXNlIGluIHRoZSBuZXh0IGJhc2tldC4gSXQgcmVxdWlyZSBwcm9iYWJpbGl0eSBlc3RpbWF0aW9uIG9mIGVhY2ggcHJvZHVjdCB0aGF0IGJhZCBiZWVuIHB1cmNoYXNlZCBiZWZvcmUsIHRoYXQgdG8gYmUgcHVyY2hhc2VkIGJlZm9yZS4qKlRoaXMgaXMgYSBjbGFzc2lmaWNhdGlvbiBwcm9ibGVtKiosIGFzIHdlbGwgYXMgKiphIHJlZ3Jlc3Npb24gb2YgcHJvYmFiaWxpdHkqKiBvZiByZXB1cmNoYXNlcy4gDQoNCkZvciB0aGlzIGFuYWx5c2lzLCB3ZSBzaGFsbCB1c2UgdHdvIE5haXZlIG1vZGVscyAoaGFuZGNyYWZ0ZWQgYmFzZWxpbmUpIGFuZCBvbmUgTWFjaGluZSBMZWFybmluZyBMb2dpc3RpYyByZWdyZXNzaW9uIHdpbGwgYmUgdXNlZCBmb3IgTWFjaGluZSBMZWFybmluZyBhcHByb2FjaCBmb3IgaXRzIHNwZWVkIGFuZCBzaW1wbGljaXR5OyB0byBkZW1vbnN0cmF0ZSB0aGUgZmVhc2liaWxpdHkgdG8gcHJvZHVjaW5nIGEgYmV0dGVyIG91dGNvbWUgdGhlbiBiYXNlbGluZS4NCg0KIyMjIFRyYWluL1Rlc3QgRGF0YXNldCBTcGxpdHRpbmcNCg0KSW5zdGFjYXJ0IGRpZCBub3QgcHJvdmlkZSB1cyAqKnRlc3Qgb3JkZXIgZGV0YWlsKiosIHRoZXJlZm9yZSB3ZSBzaGFsbCB1c2UgdGhlICoqdHJhaW4qKiB1c2VycyBmb3IgYm90aCB0cmFpbm5nIGFuZCB0ZXN0aW5nLiBXZSBhY2hpZXZlIHRoaXMgYnkgc3BsaXR0aW5nIHRoZSAqKnRyYWluKiogdXNlcnMgYW5kIGl0cyByZWxhdGVkIG9yZGVycyBhbmQgcHJvZHVjdHMgaW50byB0cmFpbiBkYXRhc2V0IGFuZCB0cmFpbiBkYXRhc2V0LCBhdCA3MCUvMzAlIHNwbGl0IChieSBudW1iZXIgb2YgdXNlcnMpLiBUaGF0IG1lYW5zIG91ciB0cmFpbi90ZXN0IGRhdGFzZXQgd2lsbCBjb250YWluIGFwcHJveGltYXRlbHkgOTE4NDYgLyAzOSwzNjMgdXNlcnMuDQoNCkZvciB0aGlzIGFuYWx5c2lzLCB3ZSB3aWxsIG5vdCBiZSBzdWJtaXR0aW5nIHRvIEthZ2dsZS4gDQoNCmBgYHtyIHJlc3VsdHM9J2hvbGQnfQ0KIyB1cGRhdGUgdGhpcyB2YXJpYWJsZSBmb3IgY2hhbmdpbmcgc3BsaXQgcmF0aW8NCnRyYWluX3Byb3BvcnRpb24gPSAwLjcNCg0KIyBidWlsZCBsaXN0IG9mIGFsbCB1c2VycyBJRA0KdG1wID0gb3JkZXJzICU+JSBmaWx0ZXIoZXZhbF9zZXQ9PSd0cmFpbicpICU+JSBkaXN0aW5jdCh1c2VyX2lkKQ0KDQojIDcwLzMwIHNwbGl0DQpzZXQuc2VlZCgxMjM0NSkNCnRyYWluLnJvd3MgPSBzYW1wbGUoIDE6bnJvdyh0bXApLCB0cmFpbl9wcm9wb3J0aW9uICogbnJvdyh0bXApICkNCnRyYWluLnVzZXJzID0gdG1wW3RyYWluLnJvd3MsXSAgIyBzZWxlY3QgdHJhaW5pbmcgcm93cywgbGlzdCBvZiB0cmFpbiB1c2Vycw0KdGVzdC51c2VycyAgPSB0bXBbLXRyYWluLnJvd3MsXSAjIHNlbGVjdCB0ZXN0aW5nIHJvd3MsIGxpc3Qgb2YgdGVzdCB1c2Vycw0KDQpjYXQoIlRvdGFsIFJvd3MgaW4gVHJhaW5pbmcgVXNlcnM6ICIsIGxlbmd0aCh0cmFpbi51c2VycyksDQogICAgIlxuVG90YWwgUm93cyBpbiBUZXN0aW5nIFVzZXJzOiAiLCBsZW5ndGgodGVzdC51c2VycyksDQogICAgIlxuVHJhaW4vVGVzdCBTcGxpdCAlIDogIiwgMTAwKmxlbmd0aCh0cmFpbi51c2VycykvKGxlbmd0aCh0ZXN0LnVzZXJzKStsZW5ndGgodHJhaW4udXNlcnMpKSwNCiAgICAiIC8gIiwgMTAwKmxlbmd0aCh0ZXN0LnVzZXJzKS8obGVuZ3RoKHRlc3QudXNlcnMpK2xlbmd0aCh0cmFpbi51c2VycykpKQ0KYGBgDQoNCmBgYHtyIGVjaG89RkFMU0V9DQpybShsaXN0PWMoJ3RtcCcsJ3RyYWluLnJvd3MnKSkgIyBtZW1vcnkgY2xlYW51cA0KYGBgDQoNCiMjIyBUcmFpbmluZyBEYXRhIENvbnN0cnVjdA0KDQpUaGUgZGF0YSBmcmFtZSB1c2VkIGZvciB0cmFpbmluZyBzaG91bGQgY29udGFpbiB0aGUgYmVsb3cgY29sdW1ucyBhbmQgZmVhdHVyZXM6ICANCg0KKipga2V5YCoqICANCg0KLSBUaGlzIGlzIHVuaXF1ZSBwYWlyIG9mIHVzZXJfaWQgYW5kIHByb2R1Y3RfaWQgZnJvbSBvcmRlcnMgIA0KLSBUaGUga2V5cyBzaG91bGQgYmUgY29uc3RydWN0ZWQgZnJvbSBhbGwgYHVzZXJfaWQtcHJvZHVjdF9pZGAgcGFpciB0aGF0IGluY2x1ZGVzIGFsbCBwcmlvciBhbmQgdGVzdC90cmFpbiByb3dzICANCg0KKipgYWN0dWFsKiogIA0KDQotIFRoaXMgaXMgdGhlIHJlc3BvbnNlIHZhcmlhYmxlIHdpdGggdmFsdWUgb2YgMSBvciAwIGZvciBlYWNoIHVuaXF1ZSBrZXkgIA0KLSBUaGUgdmFsdWUgaXMgMSB3aGVuIHRoZSBwcm9kdWN0IGlzIHB1cmNoYXNlZCBpbiB0aGUgbGFzdCBvcmRlciAodHJhaW4gb3IgdGVzdCBzZXQgb2Ygb3JkZXJzKSAgDQotIFRoZSB2YWx1ZSBpcyAwIHdoZW4gdGhlIHByb2R1Y3QgaXMgbm90IHB1cmNoYXNlZCBpbiB0aGUgdHJhaW4gb3IgdGVzdCBzZXQsIGJ1dCB3YXMgYm91Z2h0IGluIHByaW9yIHNldA0KDQoqKmBvdGhlciBmZWF0dXJlc2AqKiAgDQoNCkZyb20gZXhwbG9yYXRvcnkgZGlzY292ZXJ5LCBmZWF0dXJlcyB0aGF0IGNvdWxkIGNvbnRyaWJ1dGUgdG8gdGhlIHByZWRpY3Rpb24gc2hvdWxkIGJlIHBvcHVsYXRlZCBpbnRvIHRoZSBjb25zdHJ1Y3QuIEZlYXR1cmUgZW5naW5lZXJpbmcgd2lsbCBoYXBwZW4gaW4gdGhlIGxhdGVyIHN0YWdlLg0KDQpMZXQncyBwcm9jZWVkIHRvIGNyZWF0ZSB0aGUgYmFzaWMgKip0cmFpbmluZyBjb25zdHJ1Y3QqKi4gVGhpcyB3b24ndCBiZSB1c2VkIGZvciBwcmVkaWN0aW9uIHVudGlsIGZlYXR1cmUgZW5naW5lZXJpbmcgaXMgY29tcGxldGVkIGluIGxhdGVyIHN0YWdlLg0KDQpgYGB7cn0NCiMgbGlzdCBvZiBwcm9kdWN0cyBpbiB0aGUgZmluYWwgb3JkZXIsIHRoaXMgbWFrZSB1cCB0aGUgbGFiZWwNCmNvbnN0cnVjdDEgPSBvcmRlcnMgJT4lICAgIA0KICBmaWx0ZXIodXNlcl9pZCAlaW4lIHRyYWluLnVzZXJzLCBldmFsX3NldD09J3RyYWluJykgJT4lIA0KICBsZWZ0X2pvaW4ob3JkZXJfcHJvZHVjdHNfdHJhaW4pICU+JQ0KICBkaXN0aW5jdCh1c2VyX2lkLCBwcm9kdWN0X2lkKSAlPiUNCiAgbXV0YXRlKGFjdHVhbD0xKSAgI3RyYWluaW5nIGxhYmVsDQoNCiMgbGlzdCBvZiBwcm9kdWN0cyBlYWNoIHVzZXJzIGhhZCBib3VnaHQgYmVmb3JlIGluIHByaW9yIG9yZGVycw0KY29uc3RydWN0MiA9IG9yZGVycyAlPiUgICANCiAgZmlsdGVyKHVzZXJfaWQgJWluJSB0cmFpbi51c2VycywgZXZhbF9zZXQ9PSdwcmlvcicpICU+JSANCiAgbGVmdF9qb2luKG9yZGVyX3Byb2R1Y3RzX3ByaW9yKSAlPiUNCiAgZGlzdGluY3QodXNlcl9pZCxwcm9kdWN0X2lkKQ0KDQojIFRyYWluaW5nIENvbnN0cnVjdA0KdHJhaW4uY29uc3RydWN0ID0gbGVmdF9qb2luKGNvbnN0cnVjdDIsY29uc3RydWN0MSkgJT4lDQogIG11dGF0ZShrZXk9cGFzdGUodXNlcl9pZCxwcm9kdWN0X2lkLHNlcD0iLSIpKSAlPiUgICMga2V5DQogIHNlbGVjdChrZXksIHVzZXJfaWQsIHByb2R1Y3RfaWQsIGFjdHVhbCkgJT4lDQogIGFycmFuZ2UodXNlcl9pZCwgcHJvZHVjdF9pZCkgJT4lDQogIHJlcGxhY2VfbmEobGlzdChhY3R1YWwgPSAwKSkgIyBwcm91ZGN0cyBub3QgaW4gbGFzdCBvcmRlciwgYnV0IGV4aXN0IGluIHByaW9yIG9yZGVyDQojICBkcm9wX25hICMgcmVtb3ZlIHByb3VkY3RzIG5vdCBpbiBoaXN0b3JpY2FsIGJ1dCBhcHBlYXIgaW4gbGFzdCBvcmRlcg0KDQpybShsaXN0PWMoJ2NvbnN0cnVjdDEnLCdjb25zdHJ1Y3QyJykpDQpoZWFkKHRyYWluLmNvbnN0cnVjdCw1MCkNCmBgYA0KDQojIyMgVGVzdGluZyBEYXRhIENvbnN0cnVjdA0KDQpTaW1pbGFyIGFwcHJvYWNoIHRvIHRyYWluaW5nIGRhdGEgY29uc3RydWN0LCBoZXJlIHdlIGZyYW1lIHRoZSB0ZXN0aW5nIGRhdGEgZm9yIGV2YWx1YXRlIG91ciBtb2RlbCBidWlsdCB3aXRoIHRyYWluaW5nIGRhdGEuDQoNCmBgYHtyIGV2YWw9RkFMU0V9DQojIGxpc3Qgb2YgcHJvZHVjdHMgaW4gdGhlIGZpbmFsIG9yZGVyLCB0aGlzIG1ha2UgdXAgdGhlIGxhYmVsDQpjb25zdHJ1Y3QxID0gb3JkZXJzICU+JSAgICANCiAgZmlsdGVyKHVzZXJfaWQgJWluJSB0ZXN0LnVzZXJzLCBldmFsX3NldD09J3RyYWluJykgJT4lIA0KICBsZWZ0X2pvaW4ob3JkZXJfcHJvZHVjdHNfdHJhaW4pICU+JQ0KICBkaXN0aW5jdCh1c2VyX2lkLCBwcm9kdWN0X2lkKSAlPiUNCiAgbXV0YXRlKGFjdHVhbD0xKSAgI3RyYWluaW5nIGxhYmVsDQoNCiMgbGlzdCBvZiBwcm9kdWN0cyBlYWNoIHVzZXJzIGhhZCBib3VnaHQgYmVmb3JlIGluIHByaW9yIG9yZGVycw0KY29uc3RydWN0MiA9IG9yZGVycyAlPiUgICANCiAgZmlsdGVyKHVzZXJfaWQgJWluJSB0ZXN0LnVzZXJzLCBldmFsX3NldD09J3ByaW9yJykgJT4lIA0KICBsZWZ0X2pvaW4ob3JkZXJfcHJvZHVjdHNfcHJpb3IpICU+JQ0KICBkaXN0aW5jdCh1c2VyX2lkLHByb2R1Y3RfaWQpDQoNCiMgVHJhaW5pbmcgQ29uc3RydWN0DQp0ZXN0LmNvbnN0cnVjdCA9IGxlZnRfam9pbihjb25zdHJ1Y3QyLGNvbnN0cnVjdDEpICU+JQ0KICBtdXRhdGUoa2V5PXBhc3RlKHVzZXJfaWQscHJvZHVjdF9pZCxzZXA9Ii0iKSkgJT4lICAjIGtleQ0KICBzZWxlY3Qoa2V5LCB1c2VyX2lkLCBwcm9kdWN0X2lkLCBhY3R1YWwpICU+JQ0KICBhcnJhbmdlKHVzZXJfaWQsIHByb2R1Y3RfaWQpICU+JQ0KICByZXBsYWNlX25hKGxpc3QoYWN0dWFsID0gMCkpICMgcHJvdWRjdHMgbm90IGluIGxhc3Qgb3JkZXIsIGJ1dCBleGlzdCBpbiBwcmlvciBvcmRlcg0KIyAgZHJvcF9uYSAjIHJlbW92ZSBwcm91ZGN0cyBub3QgaW4gaGlzdG9yaWNhbCBidXQgYXBwZWFyIGluIGxhc3Qgb3JkZXINCg0Kcm0obGlzdD1jKCdjb25zdHJ1Y3QxJywnY29uc3RydWN0MicpKQ0KaGVhZCh0ZXN0LmNvbnN0cnVjdCw1MCkNCmBgYA0KDQojIyBNb2RlbCBFdmFsdWF0aW9uICYgT3B0aW1pemF0aW9uDQoNCkluc3RhY2FydCBoYXMgY2xvc2UgdG8gNTBrIHByb2R1Y3RzIGluIHRoZWlyIGNhdGFsb2d1ZS4gQXMgdGhlIG1heGltdW0gbnVtYmVyIG9mIGl0ZW1zIG9yZGVyZWQgYnkgYSB1c2VyIGlzIGp1c3QgYSBmcmFjdGlvbiBvZiB0aGUgNTBrIGF2YWlsYWJsZSBwcm9kdWN0LiBUaGlzIG1lYW5zIGJ5IHNpbXBseSBwcmVkaWN0aW5nIG5vdGhpbmcgaXMgcHVyY2hhc2VkIGluIHRoZSBuZXh0IGJhc2tldCwgd2Ugd291bGQgeWVpbGQgKipjbG9zZSB0byAxMDAlIGFjY3VyYWN5KiouICANCg0KRHVlIHRvIHRoZSAqKmhpZ2hseSBpbWJhbGFuY2UqKiBkYXRhc2V0LCBJbnN0YWNhcnQgcmVxdWlyZSAqKkYxIFNjb3JlKiogYXMgdGhlIGNvbXBldGl0aW9uIHNjb3JpbmcsIGluc3RlYWQgb2YgYWNjdXJhY3kuICANCg0KVG8gZXZhbHVhdGUgdGhlIHBlcmZvcm1hbmNlIG9mIHRoZSBtb2RlbCwgd2UgaGFkIGNyZWF0ZWQgYSBjdXN0b20gZnVuY3Rpb24gdG8gYnVpbGQgYSAqKmNvbmZ1c2lvbiBtYXRyaXggYW5kIGRlcml2ZSBvdGhlciBiaW5hcnkgY2xhc3NpZmljYXRpb24gbWV0cmljcyoqLiANCg0KYGBge3J9DQojIyBDdXN0b20gRnVuY3Rpb24gRm9yIEJpbmFyeSBDbGFzcyBQZXJmb3JtYW5jZSBFdmFsdWF0aW9uDQpiaW5jbGFzc19ldmFsID0gZnVuY3Rpb24gKGFjdHVhbCwgcHJlZGljdCkgew0KICBjbSA9IHRhYmxlKGFzLmludGVnZXIoYWN0dWFsKSwgYXMuaW50ZWdlcihwcmVkaWN0KSwgZG5uPWMoJ0FjdHVhbCcsJ1ByZWRpY3RlZCcpKQ0KICBhYyA9IChjbVsnMScsJzEnXStjbVsnMCcsJzAnXSkvKGNtWycwJywnMSddICsgY21bJzEnLCcwJ10gKyBjbVsnMScsJzEnXSArIGNtWycwJywnMCddKQ0KICBwciA9IGNtWycxJywnMSddLyhjbVsnMCcsJzEnXSArIGNtWycxJywnMSddKQ0KICByYyA9IGNtWycxJywnMSddLyhjbVsnMScsJzAnXSArIGNtWycxJywnMSddKQ0KICBmcyA9IDIqIHByKnJjLyhwcityYykNCiAgbGlzdChjbT1jbSwgcmVjYWxsPXJjLCBwcmVjaXNpb249cHIsIGZzY29yZT1mcywgYWNjdXJhY3k9YWMpDQp9DQpgYGANCg0KSWYgdGhlIHByZWRpY3Rpb24gaXMgYmFzZWQgb24gcHJvYmFiaWxpdHksIHdlIHNoYWxsIGJ1aWxkIGEgZnVuY3Rpb24gdG8gZGlzY292ZXIgKipjdXRvZmYgdGhhdCBvcHRpbWl6ZSB2YXJpb3VzIHBlcmZvcm1hbmNlIG1ldHJpY3MqKi4gDQoNCmBgYHtyfQ0KIyMjIEN1dG9mZiBUaHJlc2hvbGQgT3B0aW1pemF0aW9uDQpvcHRpbWl6ZV9jdXRvZmYgPSBmdW5jdGlvbiAoYWN0dWFsLCBwcm9iYWJpbGl0eSkgew0KICByb2NyLnByZWQgPSBwcmVkaWN0aW9uKHByZWRpY3Rpb25zID0gcHJvYmFiaWxpdHksIGxhYmVscyA9IGFjdHVhbCkNCiAgcm9jci5tZXRyaWNzID0gZGF0YS5mcmFtZSgNCiAgICAgIGN1dG9mZiAgID0gcm9jci5wcmVkQGN1dG9mZnNbWzFdXSwNCiAgICAgIGFjY3VyYWN5ID0gKHJvY3IucHJlZEB0cFtbMV1dICsgcm9jci5wcmVkQHRuW1sxXV0pIC8gDQogICAgICAgICAgICAgICAgICAgKHJvY3IucHJlZEB0cFtbMV1dICsgcm9jci5wcmVkQHRuW1sxXV0gKyByb2NyLnByZWRAZnBbWzFdXSArIHJvY3IucHJlZEBmbltbMV1dKSwNCiAgICAgIHRwciA9IHJvY3IucHJlZEB0cFtbMV1dIC8gKHJvY3IucHJlZEB0cFtbMV1dICsgcm9jci5wcmVkQGZuW1sxXV0pLA0KICAgICAgZnByID0gcm9jci5wcmVkQGZwW1sxXV0gLyAocm9jci5wcmVkQGZwW1sxXV0gKyByb2NyLnByZWRAdG5bWzFdXSksDQogICAgICBwcHYgPSByb2NyLnByZWRAdHBbWzFdXSAvIChyb2NyLnByZWRAdHBbWzFdXSArIHJvY3IucHJlZEBmcFtbMV1dKQ0KICApDQogIHJvY3IubWV0cmljcyRmc2NvcmUgPSAyICogKHJvY3IubWV0cmljcyR0cHIgKiByb2NyLm1ldHJpY3MkcHB2KSAvIChyb2NyLm1ldHJpY3MkdHByICsgcm9jci5tZXRyaWNzJHBwdikNCiAgcm9jci5tZXRyaWNzJHRwcl9mcHIgPSByb2NyLm1ldHJpY3MkdHByIC8gcm9jci5tZXRyaWNzJGZwcg0KICANCiAgIyMgRGlzY292ZXJ5IHRoZSBvcHRpbWFsIHRocmVzaG9sZCBmb3IgdmFyaW91cyBtZXRyaWNzDQogIHJvY3IuYmVzdCA9IHJiaW5kKA0KICAgIGJlc3QuYWNjdXJhY3kgPSBjKG1heCA9IG1heChyb2NyLm1ldHJpY3MkYWNjdXJhY3ksIG5hLnJtID0gVFJVRSksIGN1dG9mZj1yb2NyLm1ldHJpY3MkY3V0b2ZmW3doaWNoLm1heChyb2NyLm1ldHJpY3MkYWNjdXJhY3kpXSksDQogICAgYmVzdC5wcHYgPSBjKG1heCA9IG1heChyb2NyLm1ldHJpY3MkcHB2LCBuYS5ybSA9IFRSVUUpLCBjdXRvZmYgPSByb2NyLm1ldHJpY3MkY3V0b2ZmW3doaWNoLm1heChyb2NyLm1ldHJpY3MkcHB2KV0pLA0KICAgIGJlc3QucmVjYWxsID0gYyhtYXggPSBtYXgocm9jci5tZXRyaWNzJHRwciwgbmEucm0gPSBUUlVFKSwgY3V0b2ZmID0gcm9jci5tZXRyaWNzJGN1dG9mZlt3aGljaC5tYXgocm9jci5tZXRyaWNzJHRwcildKSwNCiAgICBiZXN0LmZzY29yZSA9IGMobWF4ID0gbWF4KHJvY3IubWV0cmljcyRmc2NvcmUsIG5hLnJtID0gVFJVRSksIGN1dG9mZiA9IHJvY3IubWV0cmljcyRjdXRvZmZbd2hpY2gubWF4KHJvY3IubWV0cmljcyRmc2NvcmUpXSksDQogICAgYmVzdC50cHJfZnByID0gYyhtYXggPSBtYXgocm9jci5tZXRyaWNzJHRwcl9mcHIsIG5hLnJtID0gVFJVRSksIGN1dG9mZiA9IHJvY3IubWV0cmljcyRjdXRvZmZbd2hpY2gubWF4KHJvY3IubWV0cmljcyR0cHJfZnByKV0pDQogICkNCiAgDQogIGxpc3QobWV0cmljcyA9IHJvY3IubWV0cmljcywgYmVzdCA9IHJvY3IuYmVzdCkNCn0NCmBgYA0KDQoNCiMjIE1vZGVsIDEgOiBOYWl2ZSBQcmVkaWN0aW9uDQoNCiMjIyBCdWlsZCBUaGUgTW9kZWwNCg0KV2l0aCBpbnRlbnNpb24gdG8gbWFrZSB0aGlzIGEgYmFzZWxpbmUgbW9kZWwsIFdlIHNpbXBseSBwcmVkaWN0IHRoZSBiYXNrZXQgYmFzZWQgb24gdXNlciBsYXN0IG9yZGVyLiANCg0KYGBge3J9DQptMS50cmFpbi5kYXRhID0gdXNlcnNfb3JkZXJzX3Byb2R1Y3RzXyAlPiUNCiAgZmlsdGVyKHVzZXJfaWQgJWluJSB0cmFpbi51c2VycykgJT4lDQogIGdyb3VwX2J5KHVzZXJfaWQpICU+JQ0KICB0b3BfbihuPTEsIHd0PW9yZGVyX251bWJlcikgICU+JSAjbGFzdCBvcmRlciBoYXMgdGhlIGhpZ2hlciBvcmRlcl9udW1iZXINCiAgc2VsZWN0KHVzZXJfaWQsIHByb2R1Y3RfaWQpICU+JSANCiAgbXV0YXRlIChwcmVkaWN0ZWQ9MSkgICU+JSAgICAgICAgI3ByZWRpY3QgYmFzZWQgb24gbGFzdCBvcmRlcmVkLCB0aGVyZWZvcmUgMQ0KICBmdWxsX2pvaW4odHJhaW4uY29uc3RydWN0KSAlPiUgICMgam9pbiB3aXRoIHRyYWluIGNvbnN0cnVjdCBmb3IgaXRlbXMgbm90IHByZWRpY3RlZCBidXQgaW4gZmluYWwgb3JkZXINCiAgc2VsZWN0KHVzZXJfaWQsIHByb2R1Y3RfaWQsIGFjdHVhbCwgcHJlZGljdGVkKSAlPiUNCiAgcmVwbGFjZV9uYShsaXN0KHByZWRpY3RlZCA9IDApKQ0KDQpoZWFkKG0xLnRyYWluLmRhdGEsMjUpDQpgYGANCg0KIyMjIENvbmZ1c2lvbiBNYXRyaXgNCg0KYGBge3IgbW9kZWwxLWV2YWx9DQptMS5ldmFsID0gYmluY2xhc3NfZXZhbChtMS50cmFpbi5kYXRhJGFjdHVhbCwgbTEudHJhaW4uZGF0YSRwcmVkaWN0ZWQpDQptMS5ldmFsJGNtDQpgYGANCg0KIyMjIE1vZGVsIFBlcmZvcm1hbmNlDQoNClRoZSByZXN1bHQgc2hvd3Mgb25seSAqKjAuMzQ2MDgzMyBGMSBTY29yZSoqLg0KDQpgYGB7cn0NCmNhdCgiQWNjdXJhY3k6ICAiLCBtMS5ldmFsJGFjY3VyYWN5LA0KICAgICJcblByZWNpc2lvbjogIiwgbTEuZXZhbCRwcmVjaXNpb24sDQogICAgIlxuUmVjYWxsOiAgICAiLCBtMS5ldmFsJHJlY2FsbCwNCiAgICAiXG5GU2NvcmU6ICAgICIsIG0xLmV2YWwkZnNjb3JlKQ0KYGBgDQoNCmBgYHtyIGVjaG89RkFMU0V9DQpybShsaXN0PWMoJ20xLnRyYWluLmRhdGEnKSkNCmBgYA0KDQojIyBNb2RlbCAyIDogU21hcnRlciBOYWl2ZSBQcmVkaWN0aW9uIChCYXNlbGluZSkNCg0KSW4gdGhpcyBtb2RlbCwgd2UgcHJlZGljdCBwcm9kdWN0cyBpbiB0aGUgYmFza2V0IGJ5IGVzdGltYXRpbmcgdGhlaXIgZnJlcXVlbmN5IG9mIHJlcHVyY2hhc2VkLiBUaGlzIHdheSB3ZSBnZXQgYSByYXRpbyB0byBpbmRpY2F0ZSBwcm9iYWJpbGl0eSBvZiByZS1wdXJjaGFzZXMuIFdlIHVzZSBST0NSIHBhY2thZ2UgdG8gZXN0aW1hdGUgdGhlIGJlc3QgY3V0b2ZmIHBvaW50IChhdCB3aGljaCBhYm92ZSB0aGlzIGN1dG9mZiB3ZSBzaGFsbCBwcmVkaWN0IGZvciByZS1vcmRlcikgdGhhdCBnaXZlIHVzIHRoZSAqKm9wdGltdW0gRjEgc2NvcmUqKi4gDQoNCiMjIyBCdWlsZCBUaGUgTW9kZWwNCg0KYGBge3IgbW9kZWwyLWJ1aWxkaW5nLCBtZXNzYWdlPUZBTFNFfQ0KIyMgQnVpbGQgTW9kZWwNCm0yLnRyYWluLmRhdGEgPSB1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBmaWx0ZXIodXNlcl9pZCAlaW4lIHRyYWluLnVzZXJzKSAlPiUNCiAgZ3JvdXBfYnkodXNlcl9pZCkgJT4lDQogICAgbXV0YXRlKHRvdGFsX29yZGVycyA9IG1heChvcmRlcl9udW1iZXIpKSAlPiUgICMgdG90YWwgbnVtYmVyIG9mIG9yZGVycyBtYWRlIHByZXZpb3VzbHkNCiAgdW5ncm91cCAlPiUgDQogIHNlbGVjdCh1c2VyX2lkLCBvcmRlcl9pZCwgcHJvZHVjdF9pZCwgdG90YWxfb3JkZXJzKSAlPiUNCiAgZ3JvdXBfYnkodXNlcl9pZCwgcHJvZHVjdF9pZCkgJT4lDQogICAgc3VtbWFyaXplKHByZWRpY3RlZD1uKCkvbWF4KHRvdGFsX29yZGVycykpICU+JQ0KICBzZWxlY3QodXNlcl9pZCwgcHJvZHVjdF9pZCwgcHJlZGljdGVkKSAlPiUNCiAgZnVsbF9qb2luKHRyYWluLmNvbnN0cnVjdCkgJT4lICAjIGpvaW4gd2l0aCB0cmFpbiBjb25zdHJ1Y3QgZm9yIGl0ZW1zIG5vdCBwcmVkaWN0ZWQgYnV0IGluIGZpbmFsIG9yZGVyDQogIHNlbGVjdCh1c2VyX2lkLCBwcm9kdWN0X2lkLCBhY3R1YWwsIHByZWRpY3RlZCkgJT4lDQogIHJlcGxhY2VfbmEobGlzdChwcmVkaWN0ZWQgPSAwKSkNCg0KaGVhZChtMi50cmFpbi5kYXRhLDIwKQ0KYGBgDQoNCiMjIyBPcHRpbWl6ZSBDdXRvZmYNCg0KV2Ugc2VlIHRoYXQgaW4gb3JkZXIgdG8gbWF4aW1pemUgKipGMSBTY29yZSoqLCB3ZSBuZWVkIHRvIHNldCB0aGUgY3V0b2ZmIHRocmVzaG9sZCB0byAwLjMzNjgsIHdoaWNoIGlzIHRoZSBuZXh0IHN0ZXAuDQoNCmBgYHtyIG1vZGVsMi1vcHRpbWl6ZX0NCiMjIyBUaHJlc2hvbGQgT3B0aW1pemF0aW9uDQptMi5yb2NyID0gb3B0aW1pemVfY3V0b2ZmKGFjdHVhbCA9IG0yLnRyYWluLmRhdGEkYWN0dWFsLCBwcm9iYWJpbGl0eSA9IG0yLnRyYWluLmRhdGEkcHJlZGljdGVkKQ0Ka2FibGUobTIucm9jciRiZXN0KSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiKSkNCmBgYA0KDQojIyMgQ29uZnVzaW9uIE1hdHJpeA0KDQpMZXQncyBzZXQgdGhlICoqY3V0b2ZmIHRvIDAuMzM2NzM0NyoqIGFzIGRpc2NvdmVyZWQgaW4gcHJldmlvdXMgc3RlcC4NCg0KYGBge3IgbW9kZWwyLWV2YWx9DQptMi5ldmFsID0gYmluY2xhc3NfZXZhbChtMi50cmFpbi5kYXRhJGFjdHVhbCwgbTIudHJhaW4uZGF0YSRwcmVkaWN0ZWQ+MC4zMzY3MzQ3KQ0KbTIuZXZhbCRjbQ0KYGBgDQoNCiMjIyBNb2RlbCBQZXJmb3JtYW5jZQ0KDQpXZSBhcmUgZ2V0dGluZyBzbGlnaHRseSAqKmJldHRlciBGMSBTY29yZSAoMC4zNzUzNTQ0KSoqIGNvbXBhcmUgdG8gcHJldmlvdXMgbmFpdmUgbW9kZWwuIFdlIHNoYWxsIHVzZSB0aGlzIGFzIHRoZSAqKkJBU0VMSU5FKiouDQoNCmBgYHtyfQ0KY2F0KCJBY2N1cmFjeTogICIsIG0yLmV2YWwkYWNjdXJhY3ksDQogICAgIlxuUHJlY2lzaW9uOiAiLCBtMi5ldmFsJHByZWNpc2lvbiwNCiAgICAiXG5SZWNhbGw6ICAgICIsIG0yLmV2YWwkcmVjYWxsLA0KICAgICJcbkZTY29yZTogICAgIiwgbTIuZXZhbCRmc2NvcmUpDQpgYGANCg0KYGBge3IgZWNobz1GQUxTRX0NCnJtKGxpc3Q9YygnbTIudHJhaW4uZGF0YScsJ20yLnJvY3InKSkNCmBgYA0KDQojIyBNYWNoaW5lIExlYXJuaW5nIEZyYW1pbmcNCg0KV2UgY29uc3RydWN0IGFsbCB0aGUgcHJvZHVjdHMgdGhhdCB1c2VycyBoYWQgcHVyY2hhc2VkIGluIHRoZSBsYXN0IDMgb3JkZXJzLCB0aGVuIHVzZSBtYWNoaW5lIGxlYXJuaW5nIGNsYXNzaWZpY2F0aW9uIHRvIHByZWRpY3Qgd2lsbCBlYWNoIG9mIHRoZSBwcm9kdWN0IGJlIHB1cmNoYXNlZCBhZ2Fpbi4gV2Ugc2hhbGwgdXNlICoqZGVjaXNpb24gdHJlZSBhbmQgbG9naXN0aWMgcmVncmVzc2lvbioqIGZvciB0aGlzIHByZWRpY3Rpb24uDQoNCiMjIyBGZWF0dXJlIEVuZ2luZWVyaW5nDQoNCiMjIyMgT3JkZXIgRmVhdHVyZXMNCg0KVGhlc2UgYXJlIG9yaWdpbmFsIGZlYXR1cmVzIHByb3ZpZGVkIGJ5IEluc3RhY2FydC4gQWx0aG91Z2ggdGhlcmUgYXJlIG5vIG90aGVyIGZlYXR1cmVzIGVuZ2luZWVyZWQgc3BlY2lmaWNhbGx5IHRvIGRlc2NyaWJlIE9yZGVyLCB0aHNlIGZlYXR1cmVzIGFyZSBiZWluZyB1c2VkIHRvIGdlbmVyYXRlIG90aGVyIGZlYXR1cmVzIGluIHRoZSBmb2xsb3dpbmcgc2VjdGlvbnMuDQoNCioqYG9yZGVyc2AqKiAgDQotIGBvcmRlcl9kb3dgICANCi0gYG9yZGVyX2hvdXJfb2ZfZGF5YCAgDQotIGBkYXlzX3NpbmNlX3ByaW9yX29yZGVyYCAgDQotIGByZW9yZGVyZWRgICANCg0KIyMjIyBVc2VyIEZlYXR1cmVzDQoNCldlIGNyZWF0ZSBmaXZlIGZlYXR1cmVzIHdoaWNoIGlzIHVuaXF1ZSB0byBlYWNoIGluZGl2aWR1YWwgdXNlci4gVGhlc2UgYXJlIHRoZSBmZWF0dXJlcyB0aGF0IGRlc3JpYmUgdGhlIHVzZXIuDQoNCioqYHVzZXJzYCoqICANCi0gYHVfbl9vcmRlcnNgOiBOdW1iZXIgb2YgT3JkZXJzIFBlciBVc2VyICANCi0gYHVfYXZnX3ByaW9yc2A6IEF2ZXJhZ2Ugd2FpdGluZyBkYXlzIGJldHdlZW4gb3JkZXJzIHBlciBVc2VyICANCi0gYHVfYXZnX2hvZGA6IEF2ZXJhZ2UgT3JkZXIgUGxhY2luZyBIb3VyIFBlciBVc2VyICANCi0gYHVfYXZnX2Rvd2A6IEF2ZXJhZ2UgT3JkZXIgUGxhY2luZyBEYXkgUGVyIFVzZXIgIA0KLSBgdV9hdmdfb3JkZXJfc2l6ZWA6IEF2ZXJhZ2UgU2l6ZSBvZiBCYXNrZXQgKGl0ZW1zIGluIG9yZGVyKSBQZXIgVXNlciAgDQoNCmBgYHtyIGV2YWw9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQojIyMjIHVzZXIgZmVhdHVyZXMNCnVzZXJzXyA9IHVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGdyb3VwX2J5KHVzZXJfaWQsb3JkZXJfaWQpICU+JQ0KICAgIG11dGF0ZSh1X29fc2l6ZSA9IGlmZWxzZShyb3dfbnVtYmVyKCk9PTEsIG1heChhZGRfdG9fY2FydF9vcmRlciksMCkgKSAlPiUNCiAgZ3JvdXBfYnkodXNlcl9pZCkgJT4lDQogICAgc3VtbWFyaXplKA0KICAgICAgdV9uX29yZGVycyA9IG1heChvcmRlcl9udW1iZXIpLA0KICAgICAgdV9hdmdfcHJpb3JzID0gbWVhbihkYXlzX3NpbmNlX3ByaW9yX29yZGVyLG5hLnJtPVRSVUUpLA0KICAgICAgdV9hdmdfaG9kID0gbWVhbihvcmRlcl9ob3VyX29mX2RheSksDQogICAgICB1X2F2Z19kb3cgPSBtZWFuKG9yZGVyX2RvdyksDQogICAgICB1X2F2Z19vcmRlcl9zaXplID0gc3VtKHVfb19zaXplKS9tYXgob3JkZXJfbnVtYmVyKQ0KICAgICkgJT4lIA0KICBhcnJhbmdlKHVzZXJfaWQpDQoNCmhlYWQodXNlcnNfKQ0KYGBgDQoNCiMjIyMgUHJvZHVjdCBGZWF0dXJlcw0KDQpXZSBjcmVhdGUgdHdvIHByb2R1Y3Qgc3BlY2lmaWMgZmVhdHVyZXMuDQoNCioqYHByb2R1Y3RzYCoqICANCg0KLSBgYXZnX3Byb2R1Y3Rfb3JkZXJfZG93YDogQXZlcmFnZSBvZiBwcm9kdWN0IG9yZGVyX2Rvdw0KLSBgYXZnX3Byb2R1Y3Rfb3JkZXJfaG9kYDogQXZlcmFnZSBvZiBwcm9kdWN0IG9yZGVyX2hvdXJfb2ZfZGF5ICANCg0KYGBge3IgZXZhbD1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCnByb2R1Y3RzXyA9IHVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGdyb3VwX2J5KHByb2R1Y3RfaWQpICU+JQ0KICBzdW1tYXJpemUoIA0KICAgIHBfYXZnX2RvdyA9IG1lYW4ob3JkZXJfZG93KSwNCiAgICBwX2F2Z19ob2QgPSBtZWFuKG9yZGVyX2hvdXJfb2ZfZGF5KQ0KICApICU+JSBhcnJhbmdlKHByb2R1Y3RfaWQpDQoNCmhlYWQocHJvZHVjdHNfKQ0KYGBgDQoNCiMjIyMgVXNlci1Qcm9kdWN0IEZlYXR1cmVzDQoNCldlIHNoYWxsIGludHJvZHVjZSBwcm9kdWN0IHJlbGF0ZWQgZmVhdHVyZXMgdGhhdCBhcmUgdXNlci1wcm9kdWN0IHNwZWNpZmMNCg0KLSBgdXBfbl9yZW9yZGVyZWRgIDogaG93IG1hbnkgdGltZXMgYSB1c2VyIHJlb3JkZXJlZHRoaXMgcHJvZHVjdCAgDQotIGB1cF9hdmdfcHJpb3JzYCA6IEF2ZXJhZ2UgbnVtYmVyIG9mIGRheXMgaW4gYmV0d2VlbiBiZWZvcmUgYSB1c2VyIHB1cmNoYXNlIHRoaXMgcHJvZHVjdCAgDQotIGB1cF9hdmdfaG9kYCA6IEF2ZXJhZ2UgaG91ciBhIHVzZXIgcHVyY2hhc2UgdGhpcyBwcm9kdWN0ICANCi0gYHVwX2F2Z19kb3dgIDogQXZlcmFnZSBkYXkgb2Ygd2VlayBhIHVzZXIgcHVyY2hhc2UgdGhpcyBwcm9kdWN0ICANCi0gYHVwX2F2Z19yYW5rYCA6IEF2ZXJhZ2UgYWRkIHRvIGNhcnQgbnVtYmVyIGEgdXNlciBzZWxlY3QgdGhpcyBwcm9kdWN0ICANCg0KYGBge3J9DQojIyMgdXNlcl9wcm9kdWN0cyBmZWF0dXJlcw0KdXNlcl9wcm9kdWN0c18gPSB1c2Vyc19vcmRlcnNfcHJvZHVjdHNfICU+JQ0KICBncm91cF9ieSh1c2VyX2lkLCBwcm9kdWN0X2lkKSAlPiUNCiAgICBzdW1tYXJpemUoDQogICAgICB1cF9uX3Jlb3JkZXJlZCA9IG4oKS0xLCAjIG1pbnVzIG9mZiBmaXJzdCBvcmRlciwgd2hpY2ggaXMgbm90IHJlb3JkZXINCiAgICAgIHVwX2F2Z19wcmlvcnMgPSBtZWFuKGRheXNfc2luY2VfcHJpb3Jfb3JkZXIsbmEucm09VFJVRSksDQogICAgICB1cF9hdmdfaG9kID0gbWVhbihvcmRlcl9ob3VyX29mX2RheSksDQogICAgICB1cF9hdmdfZG93ID0gbWVhbihvcmRlcl9kb3cpLA0KICAgICAgdXBfYXZnX3JhbmsgPSBtZWFuKGFkZF90b19jYXJ0X29yZGVyKQ0KICAgICkgJT4lDQogIHVuZ3JvdXAgJT4lDQogIGxlZnRfam9pbih1c2Vyc18pICU+JSAjIHRvIHJldHJpZXZlIHVfbl9vcmRlcnMNCiAgbXV0YXRlKHVwX3Jlb3JkZXJfcmF0ZSA9IHVwX25fcmVvcmRlcmVkLyh1X25fb3JkZXJzLTEpKSAlPiUNCiAgcmVwbGFjZV9uYShsaXN0KHVwX2F2Z19wcmlvcnMgPSAwKSkgJT4lICMgZml4IHVwIA0KICBhcnJhbmdlKHVzZXJfaWQscHJvZHVjdF9pZCkNCg0KaGVhZCh1c2VyX3Byb2R1Y3RzXywyMCkNCmBgYA0KDQojIyMgQ29uc3RydWN0IFRyYWluaW5nIERhdGENCg0KV2Ugc2hhbGwgY29tYmluZWQgdHJhaW5pbmcgY29uc3RydWN0IHRhYmxlIHdpdGggdGhlIG5ldyBlbmdpbmVlcmVkIGZlYXR1cmVzIHRvIGZvcm0gdGhlIHRyYWluaW5nIGRhdGEuIENhdGVnb3JpY2FsIGRhdGEgd2hpY2ggYXJlIG1lcmVseSBuYW1lcyBvciBpZGVudGlmaWNhdGlvbiB3aWxsIGJlIHJlbW92ZWQgc2luY2UgdGhleSBzaG91bGQgbm90IGNvbnRyaWJ1dGUgdG8gcHJlZGljdGlvbi4NCg0KQWZ0ZXIgdGhpcyBzdGVwLCB0aGUgdHJpYW5pbmcgZGF0YSBpcyByZWFkeSBmb3IgbWFjaGluZSBsZWFybmluZyBhbGdvcml0aG0gb2YgY2hvaWNlLg0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KbTMudHJhaW4uZGF0YSA9IHVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGZpbHRlcih1c2VyX2lkICVpbiUgdHJhaW4udXNlcnMpICU+JQ0KICBsZWZ0X2pvaW4odXNlcl9wcm9kdWN0c18pICU+JSANCiAgbGVmdF9qb2luKHByb2R1Y3RzXykgJT4lDQogICNsZWZ0X2pvaW4odXNlcnNfKSAgI3VzZXJfcHJvZHVjdHNfIGFscmVhZHkgY29udGFpbiB1c2VyIHNwZWNpZmljIGZlYXR1cmVzDQogIGZ1bGxfam9pbih0cmFpbi5jb25zdHJ1Y3QsIGJ5PWMoJ3VzZXJfaWQnLCdwcm9kdWN0X2lkJykpICU+JQ0KICBhcnJhbmdlKHVzZXJfaWQsIHByb2R1Y3RfaWQpICU+JQ0KICBzZWxlY3QoLWMoJ2tleScsJ3VzZXJfaWQnLCdvcmRlcl9pZCcsICdwcm9kdWN0X2lkJywgJ3Byb2R1Y3RfbmFtZScsICdkZXBhcnRtZW50X2lkJywgJ2Fpc2xlX2lkJywgJ2RlcGFydG1lbnQnLCdhaXNsZScsICdkYXlzX3NpbmNlX3ByaW9yX29yZGVyJykpIA0KDQpnbGltcHNlKG0zLnRyYWluLmRhdGEpDQpgYGANCg0KIyMjIENvbnN0cnVjdCBUZXN0aW5nIERhdGENCg0KYGBge3IgZXZhbD1GQUxTRX0NCm0zLnRlc3QuZGF0YSA9IHVzZXJzX29yZGVyc19wcm9kdWN0c18gJT4lDQogIGZpbHRlcih1c2VyX2lkICVpbiUgdGVzdC51c2VycykgJT4lDQogIGxlZnRfam9pbih1c2VyX3Byb2R1Y3RzXykgJT4lIA0KICBsZWZ0X2pvaW4ocHJvZHVjdHNfKSAlPiUNCiAgI2xlZnRfam9pbih1c2Vyc18pICAjdXNlcl9wcm9kdWN0c18gYWxyZWFkeSBjb250YWluIHVzZXIgc3BlY2lmaWMgZmVhdHVyZXMNCiAgZnVsbF9qb2luKHRlc3QuY29uc3RydWN0LCBieT1jKCd1c2VyX2lkJywncHJvZHVjdF9pZCcpKSAlPiUNCiAgYXJyYW5nZSh1c2VyX2lkLCBwcm9kdWN0X2lkKSAlPiUNCiAgc2VsZWN0KC1jKCdrZXknLCd1c2VyX2lkJywnb3JkZXJfaWQnLCAncHJvZHVjdF9pZCcsICdwcm9kdWN0X25hbWUnLCAnZGVwYXJ0bWVudF9pZCcsICdhaXNsZV9pZCcsICdkZXBhcnRtZW50JywnYWlzbGUnLCAnZGF5c19zaW5jZV9wcmlvcl9vcmRlcicpKSANCg0KZ2xpbXBzZShtMy50ZXN0LmRhdGEpDQpgYGANCg0KYGBge3IgZWNobz1GQUxTRX0NCiMgY2xlYW51cCB0byBmcmVlIG1lbW9yeQ0Kcm0obGlzdD1jKCd1c2Vyc18nLCdwcm9kdWN0cycsJ3Byb2R1Y3RzXycsJ29yZGVyX3Byb2R1Y3RzX3RyYWluJywnb3JkZXJfcHJvZHVjdHNfcHJpb3InLCd1c2VyX3Byb2R1Y3RzXycsJ3VzZXJzX29yZGVyc19wcm9kdWN0c18nKSkNCmBgYA0KDQoNCiMjIE1vZGVsIDMgOiBMb2dpc3RpYyBSZWdyZXNzaW9uDQoNCiMjIyBNb2RlbCBUcmFpbm5nDQoNCmBgYHtyIG1vZGVsMy1idWlsZGluZ30NCm0zLmZpdCA9IGdsbShhY3R1YWwgfiAuLCBmYW1pbHkgPSBiaW5vbWlhbCwgZGF0YSA9IG0zLnRyYWluLmRhdGEpDQpgYGANCg0KIyMjIFRyYWluaW5nIERhdGEgUGVyZm9ybWFuY2UNCg0KIyMjIyBQcmVkaWN0aW9uDQoNCmBgYHtyfQ0KbTMucHJlZGljdCA9IHByZWRpY3QobTMuZml0LCB0eXBlID0gJ3Jlc3BvbnNlJywgbmV3ZGF0YSA9IG0zLnRyYWluLmRhdGEpDQpgYGANCg0KIyMjIyBPcHRpbWl6ZSBDdXRvZmYNCg0KYGBge3IgbW9kZWwzLW9wdGltaXplfQ0KIyMjIFRocmVzaG9sZCBPcHRpbWl6YXRpb24NCm0zLnJvY3IgPSBvcHRpbWl6ZV9jdXRvZmYoYWN0dWFsID0gbTMudHJhaW4uZGF0YSRhY3R1YWwsIHByb2JhYmlsaXR5ID0gbTMucHJlZGljdCkNCmthYmxlKG0zLnJvY3IkYmVzdCkgJT4lIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIikpDQpgYGANCg0KIyMjIyBDb25mdXNpb24gTWF0cml4DQoNCmBgYHtyIG1vZGVsMy1ldmFsfQ0KbTMuZXZhbCA9IGJpbmNsYXNzX2V2YWwobTMudHJhaW4uZGF0YSRhY3R1YWwsIG0zLnByZWRpY3Q+MC4yMjMzMTE1KQ0KbTMuZXZhbCRjbQ0KYGBgDQoNCiMjIyMgTW9kZWwgRXZhbHVhdGlvbg0KDQpMb2dpc3RpYyByZWdyZXNzaW9uIHByb2R1Y2UgRjEgU2NvcmUgb2YgMC41Mzg4OTM3IHdpdGggdHJhaW5pbmcgZGF0YSwgYSBtdWNoIGJldHRlciBjb21wYXJlZCB0byBNb2RlbCAxIGFuZCBNb2RlbCAyLiBXZSBzaGFsbCBwcm9jZWVkIHRlc3QgdGhlIG1vZGVsIG9uIHVua25vd24gZGF0YSwgdGhlIHRlc3QgZGF0YS4NCg0KYGBge3J9DQpjYXQoIkFjY3VyYWN5OiAgIiwgICBtMy5ldmFsJGFjY3VyYWN5LA0KICAgICJcblByZWNpc2lvbjogIiwgbTMuZXZhbCRwcmVjaXNpb24sDQogICAgIlxuUmVjYWxsOiAgICAiLCBtMy5ldmFsJHJlY2FsbCwNCiAgICAiXG5GU2NvcmU6ICAgICIsIG0zLmV2YWwkZnNjb3JlKQ0KYGBgDQoNCmBgYHtyLCBldmFsPUZBTFNFfQ0Kcm9jci5wcmVkID0gcHJlZGljdGlvbihtMy5wcmVkaWN0LCBtMy50cmFpbi5kYXRhJGFjdHVhbCkNCnJvY3IucGVyZiA9IHBlcmZvcm1hbmNlKHJvY3IucHJlZCwgbWVhc3VyZSA9ICJ0cHIiLCB4Lm1lYXN1cmUgPSAiZnByIikNCnJvY3IuYXVjID0gYXMubnVtZXJpYyhwZXJmb3JtYW5jZShyb2NyLnByZWQsICJhdWMiKUB5LnZhbHVlcykNCnBsb3Qocm9jci5wZXJmLA0KICAgIGx3ZCA9IDMsIGNvbG9yaXplID0gVFJVRSwNCiAgICBwcmludC5jdXRvZmZzLmF0ID0gc2VxKDAsIDEsIGJ5ID0gMC4xKSwNCiAgICB0ZXh0LmFkaiA9IGMoLTAuMiwgMS43KSwNCiAgICBtYWluID0gJ1JPQyBDdXJ2ZScpDQptdGV4dChwYXN0ZSgnYXVjIDogJywgcm91bmQocm9jci5hdWMsIDUpKSkNCmFibGluZSgwLCAxLCBjb2wgPSAicmVkIiwgbHR5ID0gMikNCmBgYA0KDQpgYGB7ciBtb2RlbDMtY2xlYW51cCwgZWNobz1GQUxTRX0NCnJtKGxpc3Q9YygnbTMucHJlZGljdCcsICdtMy5yb2NyJykpDQpgYGANCg0KIyMjIFRlc3QgRGF0YSBQZXJmb3JtYW5jZQ0KDQojIyMjIFByZWRpY3Rpb24NCg0KYGBge3J9DQptMy5wcmVkaWN0LnRlc3QgPSBwcmVkaWN0KG0zLmZpdCwgdHlwZSA9ICdyZXNwb25zZScsIG5ld2RhdGEgPSBtMy50ZXN0LmRhdGEpDQpgYGANCg0KIyMjIyBPcHRpbWl6ZSBDdXRvZmYNCg0KYGBge3IgbW9kZWwzLW9wdGltaXplMn0NCiMjIyBUaHJlc2hvbGQgT3B0aW1pemF0aW9uDQptMy5yb2NyLnRlc3QgPSBvcHRpbWl6ZV9jdXRvZmYoYWN0dWFsID0gbTMudGVzdC5kYXRhJGFjdHVhbCwgcHJvYmFiaWxpdHkgPSBtMy5wcmVkaWN0LnRlc3QpDQprYWJsZShtMy5yb2NyLnRlc3QkYmVzdCkgJT4lIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIikpDQpgYGANCg0KIyMjIyBDb25mdXNpb24gTWF0cml4DQoNCmBgYHtyIG1vZGVsMy1ldmFsMn0NCm0zLmV2YWwudGVzdCA9IGJpbmNsYXNzX2V2YWwobTMudGVzdC5kYXRhJGFjdHVhbCwgbTMucHJlZGljdC50ZXN0PjAuMjIzMzExNSkNCm0zLmV2YWwudGVzdCRjbQ0KYGBgDQoNCiMjIyMgTW9kZWwgRXZhbHVhdGlvbg0KDQpMb2dpc3RpYyByZWdyZXNzaW9uIHByb2R1Y2UgRjEgU2NvcmUgb2YgMC41Mzg4OTM3IHdpdGggdHJhaW5pbmcgZGF0YSwgYSBtdWNoIGJldHRlciBjb21wYXJlZCB0byBNb2RlbCAxIGFuZCBNb2RlbCAyLiBXZSBzaGFsbCBwcm9jZWVkIHRlc3QgdGhlIG1vZGVsIG9uIHVua25vd24gZGF0YSwgdGhlIHRlc3QgZGF0YS4NCg0KV2UgYWNoZWl2ZWQgRjEgU2NvcmUgb2YgKiowLjU0MDU1ODgqKiwgc2xpZ2h0bHkgaGlnaGVyIHRoYW4gdHJhaW5pbmcgZGF0YS4NCg0KYGBge3J9DQpjYXQoIkFjY3VyYWN5OiAgIiwgICBtMy5ldmFsLnRlc3QkYWNjdXJhY3ksDQogICAgIlxuUHJlY2lzaW9uOiAiLCBtMy5ldmFsLnRlc3QkcHJlY2lzaW9uLA0KICAgICJcblJlY2FsbDogICAgIiwgbTMuZXZhbC50ZXN0JHJlY2FsbCwNCiAgICAiXG5GU2NvcmU6ICAgICIsIG0zLmV2YWwudGVzdCRmc2NvcmUpDQpgYGANCg0KYGBge3J9DQpybShsaXN0PWMoJ20zLmZpdCcsJ20zLnByZWRpY3QnLCAnbTMucm9jcicpKQ0KYGBgDQoNCg0KIyMjIyBST0MNCg0KYGBge3J9DQpyb2NyLnByZWQgPSBwcmVkaWN0aW9uKHByZWRpY3Rpb25zID0gbTMucHJlZGljdC50ZXN0LCBsYWJlbHMgPSBtMy50ZXN0LmRhdGEkYWN0dWFsKQ0Kcm9jci5wZXJmID0gcGVyZm9ybWFuY2Uocm9jci5wcmVkLCBtZWFzdXJlID0gInRwciIsIHgubWVhc3VyZSA9ICJmcHIiKQ0Kcm9jci5hdWMgPSBhcy5udW1lcmljKHBlcmZvcm1hbmNlKHJvY3IucHJlZCwgImF1YyIpQHkudmFsdWVzKQ0Kcm9jci5hdWMNCmBgYA0KDQpgYGB7cn0NCnBsb3Qocm9jci5wZXJmLA0KICAgICBsd2QgPSAzLCBjb2xvcml6ZSA9IFRSVUUsDQogICAgIHByaW50LmN1dG9mZnMuYXQgPSBzZXEoMCwgMSwgYnkgPSAwLjEpLA0KICAgICB0ZXh0LmFkaiA9IGMoLTAuMiwgMS43KSwNCiAgICAgbWFpbiA9ICdST0MgQ3VydmUnKQ0KbXRleHQocGFzdGUoJ2F1YyA6ICcsIHJvdW5kKHJvY3IuYXVjLCA1KSkpDQphYmxpbmUoMCwgMSwgY29sID0gInJlZCIsIGx0eSA9IDIpDQpgYGANCg0KIyBBbmFseXNpcyAmIFJlY29tbWVuZGF0aW9ucw0KDQpUZWNobmljYWwgQ2hhbGxlbmdlcw0KDQotIE1hY2hpbmUgc3BlZWQgYW5kIG1lbW9yeS4gVGhlIEdMTQ0K