If any issues, questions or suggestions feel free to reach me out via
e-mail wieczynskipawel@gmail.com or Linkedin. You
can also visit my Github.
Based on this
tutorial I wrote functions which facilitate data preprocessing,
fitting LSTM neural network for univariate time-series and
interpretation of the results. Although this script is written in R, it
runs in the background Keras and Tensorflow environment. Be cautious
with the version compatibility. In my case it works with Python 3.10.4
and Tensorflow 2.8
if(!require('pacman')) install.packages('pacman')
pacman::p_load(quantmod, keras, tidyverse, timetk, lubridate, Metrics)
Data normalization
LSTM needs data to be normalized, e.g. centered by the mean and
scaled by the standard deviation. Function
get_scaling_factors() stores scaling factors as we will use
them for data preprocessing as well as to “denormalize” data after
making predictions. Function normalize_data() works in both
directions: to normalize (reverse=FALSE) and denormalize data
(reverse=TRUE).
get_scaling_factors <- function(data){
out <- c(mean = mean(data), sd = sd(data))
return(out)
}
normalize_data <- function(data, scaling_factors, reverse = FALSE) {
if (reverse) temp <- (data * scaling_factors[2]) + scaling_factors[1]
else temp <- (data - scaling_factors[1]) / scaling_factors[2]
out <- temp %>% as.matrix()
return(out)
}
Data preprocessing
Keras LSTM as input takes data in specific 3D format [samples,
timesteps, features]. Function which I named
kerasize_data() takes the following parameters:
- data - vector of normalized data
- x - if x=TRUE (default) then regressors are being
“kerasized”. If x=FALSE targets are being “kerasized”. In fact,
for univariate time series targets are just shifted regressors.
- lag - number of previous observations to be used for time-serie
prediction (default=12)
- pred - number of future observiation to be predicted (default=12).
Additionaly kerasize_pred_input() does the same for last
n observations which will be used for prediction moving
forward.
Those functions can be further generalized to be compatible with
multivariate time-series.
kerasize_data <- function(data, x = TRUE, lag = 12, pred = 12) {
if (x) {
temp <- sapply(
1:(length(data) - lag - pred + 1)
,function(x) data[x:(x + lag - 1), 1]
) %>% t()
out <- array(
temp %>% unlist() %>% as.numeric()
,dim = c(nrow(temp), lag, 1)
)
} else {
temp <- sapply(
(1 + lag):(length(data) - pred + 1)
,function(x) data[x:(x + lag - 1), 1]
) %>% t()
out <- array(
temp %>% unlist() %>% as.numeric()
,dim = c(nrow(temp), pred, 1)
)
}
return(out)
}
kerasize_pred_input <- function(data, lag = 12, pred = 12){
temp <- data[(length(data) - pred + 1):length(data)]
temp <- normalize_data(temp, get_scaling_factors(data))
out <- array(temp, c(1, lag, 1))
return(out)
}
Fitting LSTM neural network
Function lstm_build_model() takes the following
parameters:
- x - 3D array of kerasized regressor data, i.e. output of the
kerasize_data() function with parameter x=TRUE
- y - 3D array of kerasized target data, i.e. output of the
kerasize_data() function with parameter x=FALSE
- units - size of the layer (default=50)
- batch - the number of samples to be propagated thru the network
(default=1)
- epochs - how many times algorithm goes thru the dataset (default =
20)
- rate - fraction of the input units to drop (default=0.5)
- seed - sets random seed for R, Numpy and Tensorflow
(default=2137).
If you are familiar with the LSTM architecture you can play with
function’s definition or just play with some parameters,
i.e. units, batch, epochs, rate.
There is also extensive documentation of
the Keras library available. Depending on your computing power or
compatibility of your GPU with Tensorflow it takes more or less time to
build the model.
lstm_build_model <- function(x, y, units = 50, batch = 1, epochs = 20, rate = 0.5, seed = 2137){
lag = dim(x)[2]
lstm_model <- keras_model_sequential()
lstm_model %>%
layer_lstm(units = units
,batch_input_shape = c(batch, lag, 1)
,return_sequences = TRUE
,stateful = TRUE) %>%
layer_dropout(rate = rate) %>%
layer_lstm(units = units
,return_sequences = TRUE
,stateful = TRUE) %>%
layer_dropout(rate = rate) %>%
time_distributed(layer_dense(units = 1))
lstm_model %>%
compile(loss = 'mae'
,optimizer = 'adam'
,metrics = 'accuracy')
tensorflow::set_random_seed(seed)
lstm_model %>% fit(
x = x
,y = y
,batch_size = batch
,epochs = epochs
,verbose = 0
,shuffle = FALSE)
out <- list(
model = lstm_model
,x = x
,batch = batch
,lag = lag
,pred = dim(y)[2]
)
return(out)
}
LSTM prediction
Function lstm_forecast() takes the following
parameters:
- x_test - output of the kerasize_pred_input() function
- model - output of the lstm_build_model() function
- scaling_factors - output of the get_scaling_factors()
function.
lstm_forecast <- function(x_test, model, scaling_factors){
batch <- model$batch
temp <- model$model %>%
predict(x_test, batch_size = batch) %>%
.[, , 1] %>%
normalize_data(scaling_factors = scaling_factors, reverse = TRUE)
out <- list(
forecast = temp
,scaling_factors = scaling_factors
)
return(out)
}
Transform results to the forecast object
Function forecast_transform() takes the following
parameters:
- data - initial data in xts format
- model - output of the lstm_build_model() function
- forecast - output of the lstm_forecast() function
It will facilitate further analysis and plotting of the results.
forecast_transform <- function(data, model, forecast){
lag <- model$lag
freq <- periodicity(data)$scale
frequency <- case_when(
freq == 'weekly' ~ 52
,freq == 'monthly' ~ 12
, freq == 'quarterly' ~ 4
,freq == 'yearly' ~ 1
)
dates <- index(data)
date_start <- c(year(min(dates)), month(min(dates)), day(min(dates)))
date_end <- c(year(max(dates)), month(max(dates)), day(max(dates)))
date_start_shifted <- case_when(
freq == 'weekly' ~ min(dates) %m+% weeks(lag)
,freq == 'monthly' ~ min(dates) %m+% months(lag)
,freq == 'quarterly' ~ min(dates) %m+% months(3*lag)
,freq == 'yearly' ~ min(dates) %m+% years(lag)
)
date_start_shifted <- c(year(date_start_shifted), month(date_start_shifted), day(date_start_shifted))
date_end_shifted <- case_when(
freq == 'weekly' ~ max(dates) %m+% weeks(1)
,freq == 'monthly' ~ max(dates) %m+% months(1)
,freq == 'quarterly' ~ max(dates) %m+% months(3)
,freq == 'yearly' ~ max(dates) %m+% years(1)
)
date_end_shifted <- c(year(date_end_shifted), month(date_end_shifted), day(date_end_shifted))
fitted <- predict(model$model, model$x, batch_size = model$batch) %>% .[, , 1]
if (dim(fitted)[2] > 1) {
fitted <- c(fitted[, 1], fitted[dim(fitted)[1], 2:dim(fitted)[2]])
} else {
fitted <- fitted[, 1]
}
fitted <- normalize_data(fitted, forecast$scaling_factors, reverse = TRUE)
fitted <- ts(fitted, start = date_start_shifted, deltat = 1/frequency)
lstm_forecast <- ts(forecast$forecast, start = date_end_shifted, deltat = 1/frequency)
data_trimmed <- data[(model$lag+1):nrow(data)] %>% ts(start = date_start_shifted
,deltat = 1/frequency)
out <- list(
model = NULL
,method = 'LSTM'
,mean = lstm_forecast
,x = data_trimmed
,fitted = fitted
,residuals = as.numeric(data_trimmed) - as.numeric(fitted)
)
class(out) <- 'forecast'
return(out)
}
Example of use
We download data from FRED
database using quantmod package. Below univariate
time-serie consists of monthly data on Retail Sales: Beer, Wine, and
Liquor Stores from 1992 to 2022 (362 observations).
data <- getSymbols('MRTSSM4453USS', src = 'FRED', auto.assign = FALSE)
dim(data); head(data); tail(data)
[1] 362 1
MRTSSM4453USS
1992-01-01 1713
1992-02-01 1763
1992-03-01 1753
1992-04-01 1784
1992-05-01 1783
1992-06-01 1782
MRTSSM4453USS
2021-09-01 5839
2021-10-01 5846
2021-11-01 5934
2021-12-01 5789
2022-01-01 5929
2022-02-01 6088
Here we run functions defined and explained above. We use the default
lag=12 and pred=12.
scaling_factors <- get_scaling_factors(data$MRTSSM4453USS)
data_normalized <- normalize_data(data$MRTSSM4453USS, scaling_factors)
x_data <- kerasize_data(data_normalized, x = TRUE)
y_data <- kerasize_data(data_normalized, x = FALSE)
x_test <- kerasize_pred_input(data_normalized)
model <- lstm_build_model(x_data, y_data)
prediction <- lstm_forecast(x_test, model, scaling_factors)
final_results <- forecast_transform(data, model, prediction)
What returned function forecast_transform()?
class(final_results)
[1] "forecast"
Prediction 12 months ahead.
final_results$mean
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
2022 6061.903 6077.350 6070.080 6071.238 6070.613 6070.217 6070.649 6070.571 6070.761 6070.717
2023 6070.605 6071.059
Fitted values from the LSTM model. It consists only 350 observations
due to fact that first 12 observations were used to estimate the 13th
one.
length(final_results$fitted); head(final_results$fitted, 20)
[1] 350
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
1993 6065.197 2291.137 2455.853 2458.048 2455.469 2452.407 2452.266 2458.632 2458.522 2459.598 2457.225 2456.776
1994 2456.234 2455.839 2455.624 2455.425 2457.487 2459.290 2462.305 2460.422
Empirical values. First 12 values were omitted here to obtain the
same vector lenth as theoretical values.
length(final_results$x); head(final_results$x, 20)
[1] 350
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
1993 1840 1836 1814 1792 1791 1796 1799 1786 1767 1768 1780 1773
1994 1796 1809 1825 1832 1858 1844 1862 1840
Residuals, i.e. differences beetwen fitted and empirical values.
head(final_results$residuals, 20)
[1] -4225.1973 -455.1367 -641.8534 -666.0480 -664.4688 -656.4067 -653.2656 -672.6322 -691.5219 -691.5978
[11] -677.2250 -683.7761 -660.2342 -646.8386 -630.6242 -623.4253 -599.4868 -615.2900 -600.3050 -620.4218
On the above objects we can calculate different error metrics,
especially using Metrics package.
cat(paste0(
'Mean Absolute Error: ', mae(final_results$x, final_results$fitted) %>% round (2), '\n'
,'Mean Absolute Percent Error: ', mape(final_results$x, final_results$fitted) %>% round (4), '\n'
,'Root Mean Squared Error: ', rmse(final_results$x, final_results$fitted) %>% round (2), '\n'
,'R squared: ', (cor(final_results$x, final_results$fitted)[1])^2 %>% round (4)
))
Mean Absolute Error: 524.59
Mean Absolute Percent Error: 0.171
Root Mean Squared Error: 672.08
R squared: 0.9037
We can use also function forecast::autoplot on the output of
the forecast_transform() function.
forecast::autoplot(final_results)

Example with quarterly granularity
Above set of functions should work well with weekly, quarterly and
anually univariate time-series as well.
As a time-serie with quarterly frequency let’s take Median Sales
Price of Houses Sold for the United States. It contains 236
observations from 1963 to 2021.
data_quarterly <- getSymbols('MSPUS', src = 'FRED', auto.assign = FALSE)
dim(data_quarterly); head(data_quarterly); tail(data_quarterly)
[1] 236 1
MSPUS
1963-01-01 17800
1963-04-01 18000
1963-07-01 17900
1963-10-01 18500
1964-01-01 18500
1964-04-01 18900
MSPUS
2020-07-01 337500
2020-10-01 358700
2021-01-01 369800
2021-04-01 382600
2021-07-01 411200
2021-10-01 408100
Here we use lag=4 and pred=4.
scaling_factors <- get_scaling_factors(data_quarterly$MSPUS)
data_normalized <- normalize_data(data_quarterly$MSPUS, scaling_factors)
x_data <- kerasize_data(data_normalized, x = TRUE, lag = 4, pred = 4)
y_data <- kerasize_data(data_normalized, x = FALSE, lag = 4, pred = 4)
x_test <- kerasize_pred_input(data_normalized, lag = 4, pred = 4)
model <- lstm_build_model(x_data, y_data)
prediction <- lstm_forecast(x_test, model, scaling_factors)
final_results <- forecast_transform(data_quarterly, model, prediction)
final_results$mean
Qtr1 Qtr2 Qtr3 Qtr4
2022 345461.4 346901.4 348643.6 349260.7
head(final_results$fitted, 20)
Qtr1 Qtr2 Qtr3 Qtr4
1964 310151.41 88043.11 61899.75 69332.46
1965 73579.86 73847.98 73653.61 73673.55
1966 73789.27 73805.36 73899.39 74061.21
1967 74173.41 74251.08 74263.75 74384.09
1968 74497.44 74599.57 74633.95 74818.42
head(final_results$x, 20)
Qtr1 Qtr2 Qtr3 Qtr4
1964 18500 18900 18900 19400
1965 20200 19800 20200 20300
1966 21000 22100 21500 21400
1967 22300 23300 22500 22900
1968 23900 24900 24800 25600
cat(paste0(
'Mean Absolute Error: ', mae(final_results$x, final_results$fitted) %>% round (2), '\n'
,'Mean Absolute Percent Error: ', mape(final_results$x, final_results$fitted) %>% round (4), '\n'
,'Root Mean Squared Error: ', rmse(final_results$x, final_results$fitted) %>% round (2), '\n'
,'R squared: ', (cor(final_results$x, final_results$fitted)[1])^2 %>% round (4)
))
Mean Absolute Error: 34914.53
Mean Absolute Percent Error: 0.6166
Root Mean Squared Error: 46684.88
R squared: 0.8996
forecast::autoplot(final_results)

LS0tDQp0aXRsZTogJ0tlcmFzIExTVE0gTmV1dGFsIE5ldHdvcmtzIGZvciBVbml2YXJpYXRlIFRpbWUtU2VyaWVzIGluIFInDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpJZiBhbnkgaXNzdWVzLCBxdWVzdGlvbnMgb3Igc3VnZ2VzdGlvbnMgZmVlbCBmcmVlIHRvIHJlYWNoIG1lIG91dCB2aWEgZS1tYWlsIDx3aWVjenluc2tpcGF3ZWxAZ21haWwuY29tPiBvciBbTGlua2VkaW5dKGh0dHBzOi8vd3d3LmxpbmtlZGluLmNvbS9pbi9wYXdlbC13aWVjenluc2tpLykuIFlvdSBjYW4gYWxzbyB2aXNpdCBteSBbR2l0aHViXShodHRwczovL2dpdGh1Yi5jb20vcGF3ZWwtd2llY3p5bnNraSkuDQoNCkJhc2VkIG9uIFt0aGlzIHR1dG9yaWFsXShodHRwOi8vZGF0YXNpZGVvZmxpZmUuY29tLz9wPTExNzEpIEkgd3JvdGUgZnVuY3Rpb25zIHdoaWNoIGZhY2lsaXRhdGUgZGF0YSBwcmVwcm9jZXNzaW5nLCBmaXR0aW5nIExTVE0gbmV1cmFsIG5ldHdvcmsgZm9yIHVuaXZhcmlhdGUgdGltZS1zZXJpZXMgYW5kIGludGVycHJldGF0aW9uIG9mIHRoZSByZXN1bHRzLiBBbHRob3VnaCB0aGlzIHNjcmlwdCBpcyB3cml0dGVuIGluIFIsIGl0IHJ1bnMgaW4gdGhlIGJhY2tncm91bmQgS2VyYXMgYW5kIFRlbnNvcmZsb3cgZW52aXJvbm1lbnQuIEJlIGNhdXRpb3VzIHdpdGggdGhlIHZlcnNpb24gY29tcGF0aWJpbGl0eS4gSW4gbXkgY2FzZSBpdCB3b3JrcyB3aXRoIFB5dGhvbiAzLjEwLjQgYW5kIFRlbnNvcmZsb3cgMi44DQoNCmBgYHtyIGxpYnJhcmllc30NCmlmKCFyZXF1aXJlKCdwYWNtYW4nKSkgaW5zdGFsbC5wYWNrYWdlcygncGFjbWFuJykNCnBhY21hbjo6cF9sb2FkKHF1YW50bW9kLCBrZXJhcywgdGlkeXZlcnNlLCB0aW1ldGssIGx1YnJpZGF0ZSwgTWV0cmljcykNCmBgYA0KDQojIyMgRGF0YSBub3JtYWxpemF0aW9uDQpMU1RNIG5lZWRzIGRhdGEgdG8gYmUgbm9ybWFsaXplZCwgZS5nLiBjZW50ZXJlZCBieSB0aGUgbWVhbiBhbmQgc2NhbGVkIGJ5IHRoZSBzdGFuZGFyZCBkZXZpYXRpb24uIEZ1bmN0aW9uICpnZXRfc2NhbGluZ19mYWN0b3JzKCkqIHN0b3JlcyBzY2FsaW5nIGZhY3RvcnMgYXMgd2Ugd2lsbCB1c2UgdGhlbSBmb3IgZGF0YSBwcmVwcm9jZXNzaW5nIGFzIHdlbGwgYXMgdG8gImRlbm9ybWFsaXplIiBkYXRhIGFmdGVyIG1ha2luZyBwcmVkaWN0aW9ucy4gRnVuY3Rpb24gKm5vcm1hbGl6ZV9kYXRhKCkqIHdvcmtzIGluIGJvdGggZGlyZWN0aW9uczogdG8gbm9ybWFsaXplICgqcmV2ZXJzZT1GQUxTRSopIGFuZCBkZW5vcm1hbGl6ZSBkYXRhICgqcmV2ZXJzZT1UUlVFKikuDQoNCmBgYHtyIG5vcm1hbGl6YXRpb259DQpnZXRfc2NhbGluZ19mYWN0b3JzIDwtIGZ1bmN0aW9uKGRhdGEpew0KICBvdXQgPC0gYyhtZWFuID0gbWVhbihkYXRhKSwgc2QgPSBzZChkYXRhKSkNCiAgcmV0dXJuKG91dCkNCn0NCg0Kbm9ybWFsaXplX2RhdGEgPC0gZnVuY3Rpb24oZGF0YSwgc2NhbGluZ19mYWN0b3JzLCByZXZlcnNlID0gRkFMU0UpIHsNCiAgDQogIGlmIChyZXZlcnNlKSB0ZW1wIDwtIChkYXRhICogc2NhbGluZ19mYWN0b3JzWzJdKSArIHNjYWxpbmdfZmFjdG9yc1sxXQ0KICBlbHNlIHRlbXAgPC0gKGRhdGEgLSBzY2FsaW5nX2ZhY3RvcnNbMV0pIC8gc2NhbGluZ19mYWN0b3JzWzJdDQogIA0KICBvdXQgPC0gdGVtcCAlPiUgYXMubWF0cml4KCkNCiAgcmV0dXJuKG91dCkNCn0NCmBgYA0KDQojIyMgRGF0YSBwcmVwcm9jZXNzaW5nDQpLZXJhcyBMU1RNIGFzIGlucHV0IHRha2VzIGRhdGEgaW4gc3BlY2lmaWMgM0QgZm9ybWF0IFsqc2FtcGxlcywgdGltZXN0ZXBzLCBmZWF0dXJlcypdLiBGdW5jdGlvbiB3aGljaCBJIG5hbWVkICprZXJhc2l6ZV9kYXRhKCkqIHRha2VzIHRoZSBmb2xsb3dpbmcgcGFyYW1ldGVyczogXA0KIC0gZGF0YSAtIHZlY3RvciBvZiBub3JtYWxpemVkIGRhdGEgXA0KIC0geCAtIGlmICp4PVRSVUUqIChkZWZhdWx0KSB0aGVuIHJlZ3Jlc3NvcnMgYXJlIGJlaW5nICJrZXJhc2l6ZWQiLiBJZiAqeD1GQUxTRSogdGFyZ2V0cyBhcmUgYmVpbmcgImtlcmFzaXplZCIuIEluIGZhY3QsIGZvciB1bml2YXJpYXRlIHRpbWUgc2VyaWVzIHRhcmdldHMgYXJlIGp1c3Qgc2hpZnRlZCByZWdyZXNzb3JzLiBcDQogLSBsYWcgLSBudW1iZXIgb2YgcHJldmlvdXMgb2JzZXJ2YXRpb25zIHRvIGJlIHVzZWQgZm9yIHRpbWUtc2VyaWUgcHJlZGljdGlvbiAoZGVmYXVsdD0xMikgXA0KIC0gcHJlZCAtIG51bWJlciBvZiBmdXR1cmUgb2JzZXJ2aWF0aW9uIHRvIGJlIHByZWRpY3RlZCAoZGVmYXVsdD0xMikuDQogDQpBZGRpdGlvbmFseSAqa2VyYXNpemVfcHJlZF9pbnB1dCgpKiBkb2VzIHRoZSBzYW1lIGZvciBsYXN0ICpuKiBvYnNlcnZhdGlvbnMgd2hpY2ggd2lsbCBiZSB1c2VkIGZvciBwcmVkaWN0aW9uIG1vdmluZyBmb3J3YXJkLg0KIA0KVGhvc2UgZnVuY3Rpb25zIGNhbiBiZSBmdXJ0aGVyIGdlbmVyYWxpemVkIHRvIGJlIGNvbXBhdGlibGUgd2l0aCBtdWx0aXZhcmlhdGUgdGltZS1zZXJpZXMuDQpgYGB7ciBrZXJhc2l6ZX0NCmtlcmFzaXplX2RhdGEgPC0gZnVuY3Rpb24oZGF0YSwgeCA9IFRSVUUsIGxhZyA9IDEyLCBwcmVkID0gMTIpIHsNCiAgDQogIGlmICh4KSB7DQogICAgDQogICAgdGVtcCA8LSBzYXBwbHkoDQogICAgICAxOihsZW5ndGgoZGF0YSkgLSBsYWcgLSBwcmVkICsgMSkNCiAgICAgICxmdW5jdGlvbih4KSBkYXRhW3g6KHggKyBsYWcgLSAxKSwgMV0NCiAgICApICU+JSB0KCkNCiAgICANCiAgICBvdXQgPC0gYXJyYXkoDQogICAgICB0ZW1wICU+JSB1bmxpc3QoKSAlPiUgYXMubnVtZXJpYygpDQogICAgICAsZGltID0gYyhucm93KHRlbXApLCBsYWcsIDEpDQogICAgKQ0KICAgIA0KICB9ICBlbHNlIHsNCiAgICANCiAgICB0ZW1wIDwtIHNhcHBseSgNCiAgICAgICgxICsgbGFnKToobGVuZ3RoKGRhdGEpIC0gcHJlZCArIDEpDQogICAgICAsZnVuY3Rpb24oeCkgZGF0YVt4Oih4ICsgbGFnIC0gMSksIDFdDQogICAgKSAlPiUgdCgpDQogICAgDQogICAgb3V0IDwtIGFycmF5KA0KICAgICAgdGVtcCAlPiUgdW5saXN0KCkgJT4lIGFzLm51bWVyaWMoKQ0KICAgICAgLGRpbSA9IGMobnJvdyh0ZW1wKSwgcHJlZCwgMSkNCiAgICApDQogICAgDQogIH0NCiAgDQogIHJldHVybihvdXQpDQogIA0KfQ0KDQprZXJhc2l6ZV9wcmVkX2lucHV0IDwtIGZ1bmN0aW9uKGRhdGEsIGxhZyA9IDEyLCBwcmVkID0gMTIpew0KICB0ZW1wIDwtIGRhdGFbKGxlbmd0aChkYXRhKSAtIHByZWQgKyAxKTpsZW5ndGgoZGF0YSldDQogIHRlbXAgPC0gbm9ybWFsaXplX2RhdGEodGVtcCwgZ2V0X3NjYWxpbmdfZmFjdG9ycyhkYXRhKSkNCiAgb3V0IDwtIGFycmF5KHRlbXAsIGMoMSwgbGFnLCAxKSkNCiAgcmV0dXJuKG91dCkNCn0NCmBgYA0KDQojIyMgRml0dGluZyBMU1RNIG5ldXJhbCBuZXR3b3JrDQpGdW5jdGlvbiAqbHN0bV9idWlsZF9tb2RlbCgpKiB0YWtlcyB0aGUgZm9sbG93aW5nIHBhcmFtZXRlcnM6IFwNCiAtIHggLSAzRCBhcnJheSBvZiBrZXJhc2l6ZWQgcmVncmVzc29yIGRhdGEsIGkuZS4gb3V0cHV0IG9mIHRoZSAqa2VyYXNpemVfZGF0YSgpKiBmdW5jdGlvbiB3aXRoIHBhcmFtZXRlciAqeD1UUlVFKiBcDQogLSB5IC0gM0QgYXJyYXkgb2Yga2VyYXNpemVkIHRhcmdldCBkYXRhLCBpLmUuIG91dHB1dCBvZiB0aGUgKmtlcmFzaXplX2RhdGEoKSogZnVuY3Rpb24gd2l0aCBwYXJhbWV0ZXIgKng9RkFMU0UqIFwNCiAtIHVuaXRzIC0gc2l6ZSBvZiB0aGUgbGF5ZXIgKGRlZmF1bHQ9NTApIFwNCiAtIGJhdGNoICAtIHRoZSBudW1iZXIgb2Ygc2FtcGxlcyB0byBiZSBwcm9wYWdhdGVkIHRocnUgdGhlIG5ldHdvcmsgKGRlZmF1bHQ9MSkgXA0KIC0gZXBvY2hzIC0gaG93IG1hbnkgdGltZXMgYWxnb3JpdGhtIGdvZXMgdGhydSB0aGUgZGF0YXNldCAoZGVmYXVsdCA9IDIwKSBcDQogLSByYXRlIC0gZnJhY3Rpb24gb2YgdGhlIGlucHV0IHVuaXRzIHRvIGRyb3AgKGRlZmF1bHQ9MC41KSBcDQogLSBzZWVkIC0gc2V0cyByYW5kb20gc2VlZCBmb3IgUiwgTnVtcHkgYW5kIFRlbnNvcmZsb3cgKGRlZmF1bHQ9MjEzNykuDQogDQpJZiB5b3UgYXJlIGZhbWlsaWFyIHdpdGggdGhlIExTVE0gYXJjaGl0ZWN0dXJlIHlvdSBjYW4gcGxheSB3aXRoIGZ1bmN0aW9uJ3MgZGVmaW5pdGlvbiBvciBqdXN0IHBsYXkgd2l0aCBzb21lIHBhcmFtZXRlcnMsIGkuZS4gKnVuaXRzKiwgKmJhdGNoKiwgKmVwb2NocyosICpyYXRlKi4gVGhlcmUgaXMgYWxzbyBbZXh0ZW5zaXZlIGRvY3VtZW50YXRpb24gb2YgdGhlIEtlcmFzIGxpYnJhcnldKGh0dHBzOi8va2VyYXMuaW8vYXBpLykgYXZhaWxhYmxlLiBEZXBlbmRpbmcgb24geW91ciBjb21wdXRpbmcgcG93ZXIgb3IgY29tcGF0aWJpbGl0eSBvZiB5b3VyIEdQVSB3aXRoIFRlbnNvcmZsb3cgaXQgdGFrZXMgbW9yZSBvciBsZXNzIHRpbWUgdG8gYnVpbGQgdGhlIG1vZGVsLg0KYGBge3IgYnVpbGRpbmd9DQpsc3RtX2J1aWxkX21vZGVsIDwtIGZ1bmN0aW9uKHgsIHksIHVuaXRzID0gNTAsIGJhdGNoID0gMSwgZXBvY2hzID0gMjAsIHJhdGUgPSAwLjUsIHNlZWQgPSAyMTM3KXsNCiAgDQogIGxhZyA9IGRpbSh4KVsyXQ0KICANCiAgbHN0bV9tb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkNCg0KICBsc3RtX21vZGVsICU+JQ0KICAgIGxheWVyX2xzdG0odW5pdHMgPSB1bml0cw0KICAgICAgICAgICAgICAgLGJhdGNoX2lucHV0X3NoYXBlID0gYyhiYXRjaCwgbGFnLCAxKQ0KICAgICAgICAgICAgICAgLHJldHVybl9zZXF1ZW5jZXMgPSBUUlVFDQogICAgICAgICAgICAgICAsc3RhdGVmdWwgPSBUUlVFKSAlPiUNCiAgICBsYXllcl9kcm9wb3V0KHJhdGUgPSByYXRlKSAlPiUNCiAgICBsYXllcl9sc3RtKHVuaXRzID0gdW5pdHMNCiAgICAgICAgICAgICAgICxyZXR1cm5fc2VxdWVuY2VzID0gVFJVRQ0KICAgICAgICAgICAgICAgLHN0YXRlZnVsID0gVFJVRSkgJT4lDQogICAgbGF5ZXJfZHJvcG91dChyYXRlID0gcmF0ZSkgJT4lDQogICAgdGltZV9kaXN0cmlidXRlZChsYXllcl9kZW5zZSh1bml0cyA9IDEpKQ0KDQogIGxzdG1fbW9kZWwgJT4lDQogICAgY29tcGlsZShsb3NzID0gJ21hZScNCiAgICAgICAgICAgICxvcHRpbWl6ZXIgPSAnYWRhbScNCiAgICAgICAgICAgICxtZXRyaWNzID0gJ2FjY3VyYWN5JykNCg0KICB0ZW5zb3JmbG93OjpzZXRfcmFuZG9tX3NlZWQoc2VlZCkNCiAgbHN0bV9tb2RlbCAlPiUgZml0KA0KICAgIHggPSB4DQogICAgLHkgPSB5DQogICAgLGJhdGNoX3NpemUgPSBiYXRjaA0KICAgICxlcG9jaHMgPSBlcG9jaHMNCiAgICAsdmVyYm9zZSA9IDANCiAgICAsc2h1ZmZsZSA9IEZBTFNFKQ0KICANCiAgb3V0IDwtIGxpc3QoDQogICAgbW9kZWwgPSBsc3RtX21vZGVsDQogICAgLHggPSB4DQogICAgLGJhdGNoID0gYmF0Y2gNCiAgICAsbGFnID0gbGFnDQogICAgLHByZWQgPSBkaW0oeSlbMl0NCiAgKQ0KICByZXR1cm4ob3V0KQ0KDQp9DQpgYGANCg0KIyMjIExTVE0gcHJlZGljdGlvbg0KRnVuY3Rpb24gKmxzdG1fZm9yZWNhc3QoKSogdGFrZXMgdGhlIGZvbGxvd2luZyBwYXJhbWV0ZXJzOiBcDQogLSB4X3Rlc3QgLSBvdXRwdXQgb2YgdGhlICprZXJhc2l6ZV9wcmVkX2lucHV0KCkqIGZ1bmN0aW9uIFwNCiAtIG1vZGVsIC0gb3V0cHV0IG9mIHRoZSAqbHN0bV9idWlsZF9tb2RlbCgpKiBmdW5jdGlvbiBcDQogLSBzY2FsaW5nX2ZhY3RvcnMgLSBvdXRwdXQgb2YgdGhlICpnZXRfc2NhbGluZ19mYWN0b3JzKCkqIGZ1bmN0aW9uLg0KYGBge3IgcHJlZGljdGlvbn0NCmxzdG1fZm9yZWNhc3QgPC0gZnVuY3Rpb24oeF90ZXN0LCBtb2RlbCwgc2NhbGluZ19mYWN0b3JzKXsNCiAgDQogIGJhdGNoIDwtIG1vZGVsJGJhdGNoDQogIA0KICB0ZW1wIDwtIG1vZGVsJG1vZGVsICU+JQ0KICAgIHByZWRpY3QoeF90ZXN0LCBiYXRjaF9zaXplID0gYmF0Y2gpICU+JSANCiAgICAuWywgLCAxXSAlPiUNCiAgICBub3JtYWxpemVfZGF0YShzY2FsaW5nX2ZhY3RvcnMgPSBzY2FsaW5nX2ZhY3RvcnMsIHJldmVyc2UgPSBUUlVFKQ0KICANCiAgb3V0IDwtIGxpc3QoDQogICAgZm9yZWNhc3QgPSB0ZW1wDQogICAgLHNjYWxpbmdfZmFjdG9ycyA9IHNjYWxpbmdfZmFjdG9ycw0KICApDQogIA0KICByZXR1cm4ob3V0KQ0KICANCn0NCmBgYA0KDQojIyMgVHJhbnNmb3JtIHJlc3VsdHMgdG8gdGhlIGZvcmVjYXN0IG9iamVjdA0KRnVuY3Rpb24gKmZvcmVjYXN0X3RyYW5zZm9ybSgpKiB0YWtlcyB0aGUgZm9sbG93aW5nIHBhcmFtZXRlcnM6IFwNCiAtIGRhdGEgLSBpbml0aWFsIGRhdGEgaW4geHRzIGZvcm1hdCBcDQogLSBtb2RlbCAtIG91dHB1dCBvZiB0aGUgKmxzdG1fYnVpbGRfbW9kZWwoKSogZnVuY3Rpb24gXA0KIC0gZm9yZWNhc3QgLSBvdXRwdXQgb2YgdGhlICpsc3RtX2ZvcmVjYXN0KCkqIGZ1bmN0aW9uIFwNCiANCkl0IHdpbGwgZmFjaWxpdGF0ZSBmdXJ0aGVyIGFuYWx5c2lzIGFuZCBwbG90dGluZyBvZiB0aGUgcmVzdWx0cy4NCmBgYHtyIGZvcmVjYXN0aXplfQ0KZm9yZWNhc3RfdHJhbnNmb3JtIDwtIGZ1bmN0aW9uKGRhdGEsIG1vZGVsLCBmb3JlY2FzdCl7DQogIA0KICBsYWcgPC0gbW9kZWwkbGFnDQogIA0KICBmcmVxIDwtIHBlcmlvZGljaXR5KGRhdGEpJHNjYWxlDQogIGZyZXF1ZW5jeSA8LSBjYXNlX3doZW4oDQogICAgZnJlcSA9PSAnd2Vla2x5JyB+IDUyDQogICAgLGZyZXEgPT0gJ21vbnRobHknIH4gMTINCiAgICwgZnJlcSA9PSAncXVhcnRlcmx5JyB+IDQNCiAgICAsZnJlcSA9PSAneWVhcmx5JyB+IDENCiAgKQ0KICANCiAgZGF0ZXMgPC0gaW5kZXgoZGF0YSkNCiAgZGF0ZV9zdGFydCA8LSBjKHllYXIobWluKGRhdGVzKSksIG1vbnRoKG1pbihkYXRlcykpLCBkYXkobWluKGRhdGVzKSkpDQogIGRhdGVfZW5kIDwtIGMoeWVhcihtYXgoZGF0ZXMpKSwgbW9udGgobWF4KGRhdGVzKSksIGRheShtYXgoZGF0ZXMpKSkNCiAgDQogIGRhdGVfc3RhcnRfc2hpZnRlZCA8LSBjYXNlX3doZW4oDQogICAgZnJlcSA9PSAnd2Vla2x5JyB+IG1pbihkYXRlcykgJW0rJSB3ZWVrcyhsYWcpDQogICAgLGZyZXEgPT0gJ21vbnRobHknIH4gbWluKGRhdGVzKSAlbSslIG1vbnRocyhsYWcpDQogICAgLGZyZXEgPT0gJ3F1YXJ0ZXJseScgfiBtaW4oZGF0ZXMpICVtKyUgbW9udGhzKDMqbGFnKQ0KICAgICxmcmVxID09ICd5ZWFybHknIH4gbWluKGRhdGVzKSAlbSslIHllYXJzKGxhZykNCiAgICApDQogIGRhdGVfc3RhcnRfc2hpZnRlZCA8LSBjKHllYXIoZGF0ZV9zdGFydF9zaGlmdGVkKSwgbW9udGgoZGF0ZV9zdGFydF9zaGlmdGVkKSwgZGF5KGRhdGVfc3RhcnRfc2hpZnRlZCkpDQogIA0KICBkYXRlX2VuZF9zaGlmdGVkIDwtIGNhc2Vfd2hlbigNCiAgICBmcmVxID09ICd3ZWVrbHknIH4gbWF4KGRhdGVzKSAlbSslIHdlZWtzKDEpDQogICAgLGZyZXEgPT0gJ21vbnRobHknIH4gbWF4KGRhdGVzKSAlbSslIG1vbnRocygxKQ0KICAgICxmcmVxID09ICdxdWFydGVybHknIH4gbWF4KGRhdGVzKSAlbSslIG1vbnRocygzKQ0KICAgICxmcmVxID09ICd5ZWFybHknIH4gbWF4KGRhdGVzKSAlbSslIHllYXJzKDEpDQogICAgKQ0KICBkYXRlX2VuZF9zaGlmdGVkIDwtIGMoeWVhcihkYXRlX2VuZF9zaGlmdGVkKSwgbW9udGgoZGF0ZV9lbmRfc2hpZnRlZCksIGRheShkYXRlX2VuZF9zaGlmdGVkKSkNCg0KICBmaXR0ZWQgPC0gcHJlZGljdChtb2RlbCRtb2RlbCwgbW9kZWwkeCwgYmF0Y2hfc2l6ZSA9IG1vZGVsJGJhdGNoKSAlPiUgLlssICwgMV0NCiAgDQogIGlmIChkaW0oZml0dGVkKVsyXSA+IDEpIHsNCiAgICBmaXR0ZWQgPC0gYyhmaXR0ZWRbLCAxXSwgZml0dGVkW2RpbShmaXR0ZWQpWzFdLCAyOmRpbShmaXR0ZWQpWzJdXSkNCiAgfSBlbHNlIHsNCiAgICBmaXR0ZWQgPC0gZml0dGVkWywgMV0NCiAgfQ0KICANCiAgZml0dGVkIDwtIG5vcm1hbGl6ZV9kYXRhKGZpdHRlZCwgZm9yZWNhc3Qkc2NhbGluZ19mYWN0b3JzLCByZXZlcnNlID0gVFJVRSkNCiAgZml0dGVkIDwtIHRzKGZpdHRlZCwgc3RhcnQgPSBkYXRlX3N0YXJ0X3NoaWZ0ZWQsIGRlbHRhdCA9IDEvZnJlcXVlbmN5KQ0KICANCiAgbHN0bV9mb3JlY2FzdCA8LSB0cyhmb3JlY2FzdCRmb3JlY2FzdCwgc3RhcnQgPSBkYXRlX2VuZF9zaGlmdGVkLCBkZWx0YXQgPSAxL2ZyZXF1ZW5jeSkNCiAgDQogIGRhdGFfdHJpbW1lZCA8LSBkYXRhWyhtb2RlbCRsYWcrMSk6bnJvdyhkYXRhKV0gJT4lIHRzKHN0YXJ0ID0gZGF0ZV9zdGFydF9zaGlmdGVkDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICxkZWx0YXQgPSAxL2ZyZXF1ZW5jeSkNCiAgDQogIA0KICBvdXQgPC0gbGlzdCgNCiAgICBtb2RlbCA9IE5VTEwNCiAgICAsbWV0aG9kID0gJ0xTVE0nDQogICAgLG1lYW4gPSBsc3RtX2ZvcmVjYXN0DQogICAgLHggPSBkYXRhX3RyaW1tZWQNCiAgICAsZml0dGVkID0gZml0dGVkDQogICAgLHJlc2lkdWFscyA9IGFzLm51bWVyaWMoZGF0YV90cmltbWVkKSAtIGFzLm51bWVyaWMoZml0dGVkKQ0KICApDQogIA0KICBjbGFzcyhvdXQpIDwtICdmb3JlY2FzdCcNCiAgDQogIHJldHVybihvdXQpDQp9DQpgYGANCg0KIyMjIEV4YW1wbGUgb2YgdXNlDQpXZSBkb3dubG9hZCBkYXRhIGZyb20gW0ZSRUQgZGF0YWJhc2VdKGh0dHBzOi8vZnJlZC5zdGxvdWlzZmVkLm9yZy8pIHVzaW5nICpxdWFudG1vZCogcGFja2FnZS4gQmVsb3cgdW5pdmFyaWF0ZSB0aW1lLXNlcmllIGNvbnNpc3RzIG9mIG1vbnRobHkgZGF0YSBvbiAqUmV0YWlsIFNhbGVzOiBCZWVyLCBXaW5lLCBhbmQgTGlxdW9yIFN0b3JlcyogZnJvbSAxOTkyIHRvIDIwMjIgKDM2MiBvYnNlcnZhdGlvbnMpLg0KYGBge3IgZ2V0X2RhdGF9DQpkYXRhIDwtIGdldFN5bWJvbHMoJ01SVFNTTTQ0NTNVU1MnLCBzcmMgPSAnRlJFRCcsIGF1dG8uYXNzaWduID0gRkFMU0UpDQpkaW0oZGF0YSk7IGhlYWQoZGF0YSk7IHRhaWwoZGF0YSkNCmBgYA0KDQpIZXJlIHdlIHJ1biBmdW5jdGlvbnMgZGVmaW5lZCBhbmQgZXhwbGFpbmVkIGFib3ZlLiBXZSB1c2UgdGhlIGRlZmF1bHQgKmxhZz0xMiogYW5kICpwcmVkPTEyKi4NCmBgYHtyIGV4YW1wbGV9DQpzY2FsaW5nX2ZhY3RvcnMgPC0gZ2V0X3NjYWxpbmdfZmFjdG9ycyhkYXRhJE1SVFNTTTQ0NTNVU1MpDQpkYXRhX25vcm1hbGl6ZWQgPC0gbm9ybWFsaXplX2RhdGEoZGF0YSRNUlRTU000NDUzVVNTLCBzY2FsaW5nX2ZhY3RvcnMpDQoNCnhfZGF0YSA8LSBrZXJhc2l6ZV9kYXRhKGRhdGFfbm9ybWFsaXplZCwgIHggPSBUUlVFKQ0KeV9kYXRhIDwtIGtlcmFzaXplX2RhdGEoZGF0YV9ub3JtYWxpemVkLCB4ID0gRkFMU0UpDQp4X3Rlc3QgPC0ga2VyYXNpemVfcHJlZF9pbnB1dChkYXRhX25vcm1hbGl6ZWQpDQoNCm1vZGVsIDwtIGxzdG1fYnVpbGRfbW9kZWwoeF9kYXRhLCB5X2RhdGEpDQpwcmVkaWN0aW9uIDwtIGxzdG1fZm9yZWNhc3QoeF90ZXN0LCBtb2RlbCwgc2NhbGluZ19mYWN0b3JzKQ0KDQpmaW5hbF9yZXN1bHRzIDwtIGZvcmVjYXN0X3RyYW5zZm9ybShkYXRhLCBtb2RlbCwgcHJlZGljdGlvbikNCmBgYA0KDQpXaGF0IHJldHVybmVkIGZ1bmN0aW9uICpmb3JlY2FzdF90cmFuc2Zvcm0oKSo/IA0KYGBge3IgZnVuY3Rpb25fcmVzdWx0XzF9DQpjbGFzcyhmaW5hbF9yZXN1bHRzKQ0KYGBgDQoNClByZWRpY3Rpb24gMTIgbW9udGhzIGFoZWFkLg0KYGBge3IgZnVuY3Rpb25fcmVzdWx0XzJ9DQpmaW5hbF9yZXN1bHRzJG1lYW4NCmBgYA0KDQpGaXR0ZWQgdmFsdWVzIGZyb20gdGhlIExTVE0gbW9kZWwuIEl0IGNvbnNpc3RzIG9ubHkgMzUwIG9ic2VydmF0aW9ucyBkdWUgdG8gZmFjdCB0aGF0IGZpcnN0IDEyIG9ic2VydmF0aW9ucyB3ZXJlIHVzZWQgdG8gZXN0aW1hdGUgdGhlIDEzdGggb25lLg0KYGBge3IgZnVuY3Rpb25fcmVzdWx0XzR9DQpsZW5ndGgoZmluYWxfcmVzdWx0cyRmaXR0ZWQpOyBoZWFkKGZpbmFsX3Jlc3VsdHMkZml0dGVkLCAyMCkNCmBgYA0KDQpFbXBpcmljYWwgdmFsdWVzLiBGaXJzdCAxMiB2YWx1ZXMgd2VyZSBvbWl0dGVkIGhlcmUgdG8gb2J0YWluIHRoZSBzYW1lIHZlY3RvciBsZW50aCBhcyB0aGVvcmV0aWNhbCB2YWx1ZXMuDQpgYGB7ciBmdW5jdGlvbl9yZXN1bHRfM30NCmxlbmd0aChmaW5hbF9yZXN1bHRzJHgpOyBoZWFkKGZpbmFsX3Jlc3VsdHMkeCwgMjApDQpgYGANCg0KDQpSZXNpZHVhbHMsIGkuZS4gZGlmZmVyZW5jZXMgYmVldHdlbiBmaXR0ZWQgYW5kIGVtcGlyaWNhbCB2YWx1ZXMuDQpgYGB7ciBmdW5jdGlvbl9yZXN1bHRfNX0NCmhlYWQoZmluYWxfcmVzdWx0cyRyZXNpZHVhbHMsIDIwKQ0KYGBgDQoNCk9uIHRoZSBhYm92ZSBvYmplY3RzIHdlIGNhbiBjYWxjdWxhdGUgZGlmZmVyZW50IGVycm9yIG1ldHJpY3MsIGVzcGVjaWFsbHkgdXNpbmcgKk1ldHJpY3MqIHBhY2thZ2UuDQpgYGB7ciBlcnJvcnN9IA0KY2F0KHBhc3RlMCgNCiAgJ01lYW4gQWJzb2x1dGUgRXJyb3I6ICcsIG1hZShmaW5hbF9yZXN1bHRzJHgsIGZpbmFsX3Jlc3VsdHMkZml0dGVkKSAlPiUgcm91bmQgKDIpLCAnXG4nDQogICwnTWVhbiBBYnNvbHV0ZSBQZXJjZW50IEVycm9yOiAnLCBtYXBlKGZpbmFsX3Jlc3VsdHMkeCwgZmluYWxfcmVzdWx0cyRmaXR0ZWQpICU+JSByb3VuZCAoNCksICdcbicNCiAgLCdSb290IE1lYW4gU3F1YXJlZCBFcnJvcjogJywgcm1zZShmaW5hbF9yZXN1bHRzJHgsIGZpbmFsX3Jlc3VsdHMkZml0dGVkKSAlPiUgcm91bmQgKDIpLCAnXG4nDQogICwnUiBzcXVhcmVkOiAnLCAoY29yKGZpbmFsX3Jlc3VsdHMkeCwgZmluYWxfcmVzdWx0cyRmaXR0ZWQpWzFdKV4yICU+JSByb3VuZCAoNCkNCikpIA0KDQpgYGANCg0KV2UgY2FuIHVzZSBhbHNvIGZ1bmN0aW9uICpmb3JlY2FzdDo6YXV0b3Bsb3QqIG9uIHRoZSBvdXRwdXQgb2YgdGhlICpmb3JlY2FzdF90cmFuc2Zvcm0oKSogZnVuY3Rpb24uDQpgYGB7ciBhdXRvcGxvdH0NCmZvcmVjYXN0OjphdXRvcGxvdChmaW5hbF9yZXN1bHRzKQ0KYGBgDQoNCiMjIyBFeGFtcGxlIHdpdGggcXVhcnRlcmx5IGdyYW51bGFyaXR5DQpBYm92ZSBzZXQgb2YgZnVuY3Rpb25zIHNob3VsZCB3b3JrIHdlbGwgd2l0aCB3ZWVrbHksIHF1YXJ0ZXJseSBhbmQgYW51YWxseSB1bml2YXJpYXRlIHRpbWUtc2VyaWVzIGFzIHdlbGwuDQoNCkFzIGEgdGltZS1zZXJpZSB3aXRoIHF1YXJ0ZXJseSBmcmVxdWVuY3kgbGV0J3MgdGFrZSAqTWVkaWFuIFNhbGVzIFByaWNlIG9mIEhvdXNlcyBTb2xkIGZvciB0aGUgVW5pdGVkIFN0YXRlcyouIEl0IGNvbnRhaW5zIDIzNiBvYnNlcnZhdGlvbnMgZnJvbSAxOTYzIHRvIDIwMjEuDQpgYGB7ciBnZXRfZGF0YV9xdWFydGVybHl9DQpkYXRhX3F1YXJ0ZXJseSA8LSBnZXRTeW1ib2xzKCdNU1BVUycsIHNyYyA9ICdGUkVEJywgYXV0by5hc3NpZ24gPSBGQUxTRSkNCmRpbShkYXRhX3F1YXJ0ZXJseSk7IGhlYWQoZGF0YV9xdWFydGVybHkpOyB0YWlsKGRhdGFfcXVhcnRlcmx5KQ0KYGBgDQoNCkhlcmUgd2UgdXNlICpsYWc9NCogYW5kICpwcmVkPTQqLg0KYGBge3IgZXhhbXBsZV9xdWFydGVybHl9DQpzY2FsaW5nX2ZhY3RvcnMgPC0gZ2V0X3NjYWxpbmdfZmFjdG9ycyhkYXRhX3F1YXJ0ZXJseSRNU1BVUykNCmRhdGFfbm9ybWFsaXplZCA8LSBub3JtYWxpemVfZGF0YShkYXRhX3F1YXJ0ZXJseSRNU1BVUywgc2NhbGluZ19mYWN0b3JzKQ0KDQp4X2RhdGEgPC0ga2VyYXNpemVfZGF0YShkYXRhX25vcm1hbGl6ZWQsIHggPSBUUlVFLCBsYWcgPSA0LCBwcmVkID0gNCkNCnlfZGF0YSA8LSBrZXJhc2l6ZV9kYXRhKGRhdGFfbm9ybWFsaXplZCwgeCA9IEZBTFNFLCBsYWcgPSA0LCBwcmVkID0gNCkNCnhfdGVzdCA8LSBrZXJhc2l6ZV9wcmVkX2lucHV0KGRhdGFfbm9ybWFsaXplZCwgbGFnID0gNCwgcHJlZCA9IDQpDQoNCm1vZGVsIDwtIGxzdG1fYnVpbGRfbW9kZWwoeF9kYXRhLCB5X2RhdGEpDQpwcmVkaWN0aW9uIDwtIGxzdG1fZm9yZWNhc3QoeF90ZXN0LCBtb2RlbCwgc2NhbGluZ19mYWN0b3JzKQ0KDQpmaW5hbF9yZXN1bHRzIDwtIGZvcmVjYXN0X3RyYW5zZm9ybShkYXRhX3F1YXJ0ZXJseSwgbW9kZWwsIHByZWRpY3Rpb24pDQoNCmZpbmFsX3Jlc3VsdHMkbWVhbg0KaGVhZChmaW5hbF9yZXN1bHRzJGZpdHRlZCwgMjApDQpoZWFkKGZpbmFsX3Jlc3VsdHMkeCwgMjApDQoNCmNhdChwYXN0ZTAoDQogICdNZWFuIEFic29sdXRlIEVycm9yOiAnLCBtYWUoZmluYWxfcmVzdWx0cyR4LCBmaW5hbF9yZXN1bHRzJGZpdHRlZCkgJT4lIHJvdW5kICgyKSwgJ1xuJw0KICAsJ01lYW4gQWJzb2x1dGUgUGVyY2VudCBFcnJvcjogJywgbWFwZShmaW5hbF9yZXN1bHRzJHgsIGZpbmFsX3Jlc3VsdHMkZml0dGVkKSAlPiUgcm91bmQgKDQpLCAnXG4nDQogICwnUm9vdCBNZWFuIFNxdWFyZWQgRXJyb3I6ICcsIHJtc2UoZmluYWxfcmVzdWx0cyR4LCBmaW5hbF9yZXN1bHRzJGZpdHRlZCkgJT4lIHJvdW5kICgyKSwgJ1xuJw0KICAsJ1Igc3F1YXJlZDogJywgKGNvcihmaW5hbF9yZXN1bHRzJHgsIGZpbmFsX3Jlc3VsdHMkZml0dGVkKVsxXSleMiAlPiUgcm91bmQgKDQpDQopKSANCg0KZm9yZWNhc3Q6OmF1dG9wbG90KGZpbmFsX3Jlc3VsdHMpDQpgYGA=