1 Data

1.1 Notation and time index

Each series is y_1,\dots,y_n\ge0 with n=16{,}992 half-hourly records from 1 January 2018 00:30 to 21 December 2018 00:00, that is, 354 daily blocks running from 00:30 to 24:00. For each time t,

h(t)=\big((t-1)\bmod 48\big)+1,\quad k(t)=\Big\lceil \tfrac{t}{48}\Big\rceil,\quad d(t)=\big((k(t)-1)\bmod 7\big)+1,\quad \ell(t)=\Big\lceil \tfrac{t}{336}\Big\rceil

are the half-hour of the day, the day, the weekday (1 = Monday) and the week. The daily period is s_1=48 and the weekly period is s_2=336. \mathcal O denotes the observed indices and \mathcal M=\{1,\dots,n\}\setminus\mathcal O the missing ones. The code checks that the columns J and Hari of the file equal h(t) and d(t).

if (!file.exists(P$data_path))
  stop("Data file not found: ", P$data_path, ". Put aqms_surabaya_2018.csv in the data/ folder next to this notebook ",
       "or set data_path in the YAML header.", call. = FALSE)
raw <- read.csv(P$data_path, check.names = FALSE)
names(raw) <- gsub("_", " ", names(raw))
N  <- nrow(raw); S1 <- 48L; S2 <- 336L
TS <- seq(as.POSIXct("2018-01-01 00:30", tz = "UTC"), by = "30 min", length.out = N)
POLS     <- c("PM10", "NO2", "SO2", "CO", "O3")
STATIONS <- c("SUF 1", "SUF 6", "SUF 7")
UNIT <- c(PM10 = "\u00b5g/m\u00b3", NO2 = "\u00b5g/m\u00b3", SO2 = "\u00b5g/m\u00b3", CO = "mg/m\u00b3", O3 = "\u00b5g/m\u00b3")
h_of <- function(t) ((t - 1) %% S1) + 1                 # half-hour of the day (1 = 00:30)
d_of <- function(t) (((t - 1) %/% S1) %% 7) + 1         # weekday (1 = Monday)
SERIES <- paste(rep(STATIONS, each = 5), POLS)
if (!all(c("J", "Hari", SERIES) %in% names(raw)))
  stop("The data file does not have the expected columns (J, Hari and 'SUF 1 CO' ... 'SUF 7 SO2').", call. = FALSE)
stopifnot(N == 16992, all(raw$J == h_of(seq_len(N))), all(raw$Hari == d_of(seq_len(N))))
get_series <- function(st, p) as.numeric(raw[[paste(st, p)]])
# check against the reference data, from which all stored results were computed
X <- sapply(SERIES, function(v) as.numeric(raw[[v]]))
data_check <- data.frame(item = c("rows", "missing values", "sum of the observed values"),
                         found = c(N, sum(is.na(X)), sum(X, na.rm = TRUE)), reference = c(16992, 33250, 5022646.3757342435))
data_check$identical <- abs(data_check$found - data_check$reference) <= 1e-6 * pmax(1, abs(data_check$reference))
if (!all(data_check$identical)) cat("NOTE: the data differ from the reference data; set use_cache: false so that",
                                    "every result is recomputed from these data.\n")
knitr::kable(transform(data_check, found = sprintf(c("%.0f", "%.0f", "%.4f"), found),
                       reference = sprintf(c("%.0f", "%.0f", "%.4f"), reference)),
             caption = "Data check against the reference data set")
Data check against the reference data set
item found reference identical
rows 16992 16992 TRUE
missing values 33250 33250 TRUE
sum of the observed values 5022646.3757 5022646.3757 TRUE
TEST_DATE <- as.POSIXct("2018-12-01 00:30", tz = "UTC")
cat("n =", N, "| period:", format(TS[1]), "to", format(TS[N]), "| columns J and Hari verified\n")
n = 16992 | period: 2018-01-01 00:30:00 to 2018-12-21 | columns J and Hari verified

1.2 Missingness and descriptive statistics

gap_lengths <- function(x) { r <- rle(is.na(x)); r$lengths[r$values] }
tab1 <- bind_rows(lapply(POLS, function(p) {
  x <- get_series("SUF 1", p); g <- gap_lengths(x); o <- x[!is.na(x)]
  data.frame(pollutant = p, unit = UNIT[[p]], missing = 100 * mean(is.na(x)), gaps = length(g),
             median_gap = floor(median(g)), max_gap = max(g), in_long = 100 * sum(g[g > 48]) / sum(g),
             mean = mean(o), median = median(o), sd = sd_pop(o), max = max(o),
             suf6 = 100 * mean(is.na(get_series("SUF 6", p))), suf7 = 100 * mean(is.na(get_series("SUF 7", p))))
}))
write.csv(tab1, out_file("table1_data.csv"), row.names = FALSE)
write_tex(c("\\begin{table*}[!t]", "\\centering",
  "\\caption{Missingness and descriptive statistics of the half-hourly pollutant series (1~Jan--20~Dec 2018, $n=16{,}992$ per series). Gap statistics refer to SUF~1; the last two columns give the missing rate at the two development stations.}",
  "\\label{tab:data}", "\\setlength{\\tabcolsep}{4pt}", "\\begin{tabular}{llrrrrrrrrrrr}", "\\toprule",
  " & & \\multicolumn{5}{c}{SUF~1 missingness} & \\multicolumn{4}{c}{SUF~1 observed values} & \\multicolumn{2}{c}{Missing (\\%)}\\\\",
  "\\cmidrule(lr){3-7}\\cmidrule(lr){8-11}\\cmidrule(lr){12-13}",
  "Pollutant & Unit & Missing (\\%) & Gaps & Median gap & Max. gap & In gaps $>$1\\,d (\\%) & Mean & Median & SD & Max. & SUF~6 & SUF~7\\\\",
  "\\midrule",
  with(tab1, sprintf("%s & %s & %.1f & %d & %d & %s & %.1f & %.2f & %.2f & %.2f & %.1f & %.1f & %.1f\\\\",
    POL_TEX[pollutant], UNIT_TEX[pollutant], missing, gaps, as.integer(median_gap),
    formatC(max_gap, format = "d", big.mark = ","), in_long, mean, median, sd, max, suf6, suf7)),
  "\\bottomrule", "\\end{tabular}", "\\end{table*}"), "tab_data")
knitr::kable(tab1, digits = c(0, 0, 1, 0, 0, 0, 1, 2, 2, 2, 1, 1, 1),
  col.names = c("Pollutant", "Unit", "Missing (%)", "Gaps", "Median gap", "Max. gap", "In gaps > 1 d (%)",
                "Mean", "Median", "SD", "Max.", "SUF 6 missing (%)", "SUF 7 missing (%)"),
  caption = cap("Table 2", "Missingness and descriptive statistics (gap statistics: SUF 1)"))
Missingness and descriptive statistics (gap statistics: SUF 1)
Pollutant Unit Missing (%) Gaps Median gap Max. gap In gaps > 1 d (%) Mean Median SD Max. SUF 6 missing (%) SUF 7 missing (%)
PM10 µg/m³ 44.9 225 3 5623 80.1 25.26 12.00 35.41 342.6 9.3 12.9
NO2 µg/m³ 14.3 305 1 1263 83.0 22.57 18.25 16.89 135.6 15.0 5.1
SO2 µg/m³ 7.8 449 2 76 5.8 28.61 23.91 25.25 374.2 6.7 9.6
CO mg/m³ 11.4 420 1 845 54.6 0.63 0.46 0.51 6.1 42.4 3.7
O3 µg/m³ 2.8 340 1 11 0.0 36.55 26.99 30.13 176.5 5.9 3.9

1.3 Missing values on the time axis

Panel (a) shows the five half-hourly series of SUF 1 on their real timestamps. Each run of missing values is shaded red, as in imputeTS::ggplot_na_distribution(), and the label gives the share of missing values; the blue band is the test period. Panel (b) splits the missing values by gap length. The data frame gaps_df, with the start and end of every gap, is reused for the overview diagram at the end.

# fig01_missingness: (a) the half-hourly series of SUF 1 on their real timestamps. Every run of missing values is
# marked by a red tick at the bottom, gaps longer than two hours are also shaded red (the view of
# imputeTS::ggplot_na_distribution), and the test period is shaded blue. (b) Share of the
# missing values by gap length.
na_runs <- function(st, pols = POLS) bind_rows(lapply(pols, function(p) {
  x <- get_series(st, p); r <- rle(is.na(x)); e <- cumsum(r$lengths); s <- e - r$lengths + 1
  data.frame(station = st, pollutant = p, start = TS[s[r$values]], end = TS[e[r$values]] + 1800,
             len = r$lengths[r$values])
}))
POL_LAB <- c(PM10 = "atop(PM[10], '\u00b5g/m\u00b3')", NO2 = "atop(NO[2], '\u00b5g/m\u00b3')",
             SO2 = "atop(SO[2], '\u00b5g/m\u00b3')", CO = "atop(CO, 'mg/m\u00b3')", O3 = "atop(O[3], '\u00b5g/m\u00b3')")
plot_na_series <- function(stations, title) {
  d <- bind_rows(lapply(stations, function(st) bind_rows(lapply(POLS, function(p)
         data.frame(station = st, pollutant = p, t = TS, y = get_series(st, p)))))) |>
       mutate(pollutant = factor(pollutant, POLS))
  g <- bind_rows(lapply(stations, na_runs)) |> mutate(pollutant = factor(pollutant, POLS))
  r <- d |> group_by(station, pollutant) |> summarise(rate = mean(is.na(y)), .groups = "drop")
  ggplot(d, aes(t, y)) +
    annotate("rect", xmin = TEST_DATE, xmax = max(TS), ymin = -Inf, ymax = Inf, fill = "#2471a3", alpha = 0.18) +
    geom_rect(data = g[g$len > 4, ], aes(xmin = start, xmax = end, ymin = -Inf, ymax = Inf), inherit.aes = FALSE,
              fill = "#f2b8b0") +                                  # gaps longer than two hours are shaded
    geom_rug(data = g, aes(x = start), inherit.aes = FALSE, sides = "b", colour = "#c0392b",
             linewidth = 0.15, length = unit(0.12, "npc")) +      # every gap, including single readings
    geom_line(linewidth = 0.12, colour = "grey15", na.rm = TRUE) +
    geom_label(data = r, aes(x = min(TS) + 3 * 86400, y = Inf, label = sprintf("%.1f%% missing", 100 * rate)),
               inherit.aes = FALSE, hjust = 0, vjust = 1.1, size = 2.5, family = FONT_TEXT, colour = "#922b21",
               fill = alpha("white", 0.85), label.size = 0, label.padding = unit(0.1, "lines")) +
    facet_grid(if (length(stations) > 1) pollutant ~ station else pollutant ~ ., scales = "free_y", switch = "y",
               labeller = labeller(pollutant = as_labeller(POL_LAB, label_parsed))) +
    scale_x_datetime(date_breaks = if (length(stations) > 1) "2 months" else "1 month", date_labels = "%b",
                     expand = expansion(mult = 0.004)) +
    scale_y_continuous(n.breaks = 3, expand = expansion(mult = c(0.03, 0.38))) +
    labs(x = NULL, y = NULL, title = title) +
    theme(strip.placement = "outside", strip.text.y.left = element_text(angle = 0, size = 7.8), axis.text = element_text(size = 7.2),
          panel.grid.minor = element_blank(), panel.grid.major.y = element_blank(),
          panel.grid.major.x = element_line(linewidth = 0.2, colour = "grey88"),
          plot.title = element_text(size = 8.6), panel.spacing.y = unit(3, "pt"))
}
cls <- data.frame(lo = c(1, 5, 49, 337), hi = c(4, 48, 336, Inf), lab = c("1-4", "5-48", "49-336", ">336"))
share_df <- bind_rows(lapply(POLS, function(p) {
  g <- gap_lengths(get_series("SUF 1", p))
  data.frame(pollutant = factor(p, levels = POLS), class = factor(cls$lab, levels = cls$lab),
             share = sapply(1:4, function(i) 100 * sum(g[g >= cls$lo[i] & g <= cls$hi[i]]) / sum(g)))
}))
p1a <- plot_na_series("SUF 1", "(a) Half-hourly series at SUF 1: gaps (red ticks; shaded if longer than 2 h) and test period (blue)")
p1b <- ggplot(share_df, aes(share, 1, fill = class)) +
  geom_col(position = position_stack(reverse = TRUE), width = 0.55, orientation = "y") +
  facet_grid(pollutant ~ .) +
  scale_x_continuous(breaks = c(0, 50, 100), labels = function(v) paste0(v, "%"), expand = c(0, 0)) +
  scale_y_continuous(expand = expansion(add = 0.6)) +
  scale_fill_manual(values = c("#f5b7b1", "#ec7063", "#c0392b", "#641e16"), guide = guide_legend(nrow = 1)) +
  labs(x = NULL, y = NULL, fill = "Gap length (half-hour steps):", title = "(b) Share by gap length") +
  theme(strip.text = element_blank(), axis.text.y = element_blank(), panel.grid = element_blank(),
        plot.title = element_text(size = 8.6), panel.spacing.y = unit(3, "pt"), plot.margin = margin(5.5, 10, 5.5, 5.5), axis.text.x = element_text(size = 7.2),
        legend.key.size = unit(0.28, "cm"), legend.title = element_text(size = 7.8), legend.text = element_text(size = 7.8))
p1 <- if (HAS_PATCHWORK) patchwork::wrap_plots(p1a, p1b, widths = c(3.5, 1), guides = "collect") &
  theme(legend.position = "bottom", legend.margin = margin(0, 0, 0, 0)) else p1a
print(p1); save_fig(p1, "fig01_missingness", 7.16, 3.55)

if (!HAS_PATCHWORK) { print(p1b); save_fig(p1b, "fig01b_gap_lengths", 3.5, 2.1) }
gaps_df <- na_runs("SUF 1") |> mutate(y = match(pollutant, rev(POLS)))       # reused by the overview diagram (fig03)

1.4 All 15 series

The same view for the three stations. It shows where each series is missing before any imputation.

# figS1_series_all_stations: the same view for the 15 series of the three stations.
pS1 <- plot_na_series(c("SUF 1", "SUF 6", "SUF 7"),
                      "Half-hourly series of the three stations: gaps (red ticks; shaded if longer than 2 h) and test period (blue)") +
  theme(strip.text.x = element_text(size = 7.5, face = "bold"))
print(pS1); save_fig(pS1, "figS1_series_all_stations", 7.16, 5.6)

1.5 Double seasonality

For day of the week d and half-hour h the mean profile is \bar y_{d,h}=\frac{1}{|\mathcal O_{d,h}|}\sum_{t\in\mathcal O_{d,h}}y_t with \mathcal O_{d,h}=\{t\in\mathcal O:d(t)=d,\ h(t)=h\}, and its average over the seven days, \bar y_{\cdot,h}=\frac17\sum_{d=1}^{7}\bar y_{d,h}, is the mean daily profile. Panel (a) draws \bar y_{d,h} along the 336 half-hours of the week together with \bar y_{\cdot,h} repeated on every day: the repetition is the daily seasonality, and the shaded difference \bar y_{d,h}-\bar y_{\cdot,h}, the weekly deviation, is the weekly seasonality. Panel (b) examines the weekly part where it is largest, in the morning: for every date the mean of 06:00–10:00 is computed, the dates are grouped by day of the week, and each group mean is expressed relative to the working-day mean, with a 95% confidence interval of \pm1.96 standard errors across dates. Intervals wider than \pm45\% (PM_{10}, with many missing mornings) are cut at the edge.

# fig02_seasonality. (a) Mean concentration at each of the 336 half-hours of the week (solid) and the mean
# daily profile repeated on every day (dashed): the repetition is the daily seasonality and the shaded
# difference is the weekly seasonality. (b) Mean of 06:00-10:00 for each day of the week relative to the
# working-day mean, with 95% confidence intervals computed from the daily morning means.
wk <- bind_rows(lapply(POLS, function(p) data.frame(pollutant = p, d = d_of(seq_len(N)), h = h_of(seq_len(N)),
                                                    y = get_series("SUF 1", p)))) |>
  filter(!is.na(y)) |> group_by(pollutant, d, h) |> summarise(m = mean(y), .groups = "drop") |>
  group_by(pollutant, h) |> mutate(dp = mean(m)) |> ungroup() |>                # dp: mean daily profile
  mutate(how = (d - 1) * 48 + h, pollutant = factor(pollutant, POLS))           # how: half-hour of the week
morning <- bind_rows(lapply(POLS, function(p) {
  y <- get_series("SUF 1", p); dd <- (seq_len(N) - 1) %/% 48; mo <- h_of(seq_len(N)) %in% 12:19     # 06:00-10:00
  dm <- tapply(y[mo], dd[mo], function(v) if (sum(!is.na(v)) >= 4) mean(v, na.rm = TRUE) else NA)   # one mean per date
  wd <- tapply(d_of(seq_len(N))[mo], dd[mo], `[`, 1); ok <- !is.na(dm); ref <- mean(dm[ok & wd <= 5])
  bind_rows(lapply(1:7, function(k) { v <- dm[ok & wd == k]
    data.frame(pollutant = p, d = k, est = 100 * (mean(v) / ref - 1), se = 100 * sd(v) / sqrt(length(v)) / ref) }))
})) |> mutate(pollutant = factor(pollutant, POLS), lo = est - 1.96 * se, hi = est + 1.96 * se)
for (p in POLS) { su <- morning[morning$pollutant == p & morning$d == 7, ]; sa <- morning[morning$pollutant == p & morning$d == 6, ]
  key(paste0("sun_morning_", p), su$est); key(paste0("sun_morning_lo_", p), su$lo); key(paste0("sun_morning_hi_", p), su$hi)
  key(paste0("sat_morning_", p), sa$est) }
LINE2 <- c("mean at each half-hour of the week" = "#1f4e79", "mean daily profile, repeated" = "#5d6d7e")
p2a <- ggplot(wk, aes(how)) +
  annotate("rect", xmin = 239.5, xmax = 335.5, ymin = -Inf, ymax = Inf, fill = "grey93") +
  geom_vline(xintercept = 48 * (1:6) - 0.5, colour = "grey70", linewidth = 0.25, linetype = "22") +
  geom_ribbon(aes(ymin = pmin(m, dp), ymax = pmax(m, dp)), fill = "#f5cba7") +
  geom_line(aes(y = dp, colour = names(LINE2)[2], linetype = names(LINE2)[2]), linewidth = 0.35) +
  geom_line(aes(y = m, colour = names(LINE2)[1], linetype = names(LINE2)[1]), linewidth = 0.45) +
  facet_grid(pollutant ~ ., scales = "free_y", switch = "y", labeller = labeller(pollutant = as_labeller(POL_LAB, label_parsed))) +
  scale_colour_manual(NULL, values = LINE2, breaks = names(LINE2)) +
  scale_linetype_manual(NULL, values = setNames(c("solid", "22"), names(LINE2)), breaks = names(LINE2)) +
  scale_x_continuous(breaks = 48 * (0:6) + 23.5, labels = c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"), expand = c(0, 0)) +
  scale_y_continuous(n.breaks = 3, expand = expansion(mult = c(0.04, 0.1))) +
  labs(x = NULL, y = NULL, title = "(a) Mean concentration over the week at SUF 1; shaded: weekly deviation, grey band: weekend") +
  theme(strip.placement = "outside", strip.text.y.left = element_text(angle = 0, size = 7.8), axis.text = element_text(size = 7.2),
        panel.grid.minor = element_blank(), panel.grid.major.x = element_blank(),
        panel.grid.major.y = element_line(linewidth = 0.15, colour = "grey88"), plot.title = element_text(size = 8.6),
        panel.spacing.y = unit(3, "pt"))
lim <- 45                                                   # % (wider intervals are cut at the edge)
p2b <- ggplot(morning, aes(d, est)) +
  annotate("rect", xmin = 5.5, xmax = 7.5, ymin = -Inf, ymax = Inf, fill = "grey93") +
  geom_hline(yintercept = 0, colour = "grey45", linewidth = 0.3) +
  geom_errorbar(aes(ymin = lo, ymax = hi, colour = d >= 6), width = 0, linewidth = 0.55) +
  geom_point(aes(colour = d >= 6), size = 1.4) +
  facet_grid(pollutant ~ .) +
  scale_colour_manual(values = c(`FALSE` = "#1f4e79", `TRUE` = "#d35400"), guide = "none") +
  scale_x_continuous(breaks = 1:7, labels = c("M", "T", "W", "T", "F", "S", "S"), expand = expansion(add = 0.5)) +
  scale_y_continuous(limits = c(-lim, lim), breaks = c(-30, 30), labels = c("\u221230%", "+30%"), oob = scales::oob_squish) +
  labs(x = NULL, y = NULL, title = "(b) Mornings (06\u201310 h)") +
  theme(strip.text = element_blank(), panel.grid.minor = element_blank(), panel.grid.major.x = element_blank(),
        panel.grid.major.y = element_line(linewidth = 0.15, colour = "grey88"), axis.text.x = element_text(size = 7.2),
        axis.text.y = element_text(size = 6.6), plot.title = element_text(size = 8.6), panel.spacing.y = unit(3, "pt"),
        plot.margin = margin(5.5, 8, 5.5, 5.5))
p2 <- if (HAS_PATCHWORK) patchwork::wrap_plots(p2a, p2b, widths = c(3.3, 1.1), guides = "collect") &
  theme(legend.position = "bottom", legend.text = element_text(size = 7.8), legend.key.width = unit(0.8, "cm"),
        legend.margin = margin(0, 0, 0, 0)) else p2a
print(p2); save_fig(p2, "fig02_seasonality", 7.16, 3.6)

knitr::kable(tidyr::pivot_wider(morning |> mutate(v = sprintf("%+.1f [%+.1f, %+.1f]", est, lo, hi)) |> select(pollutant, d, v),
                                names_from = d, values_from = v) |> setNames(c("Pollutant", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")),
             caption = "06:00-10:00 mean by day of the week relative to working days (%), with 95% confidence intervals")
06:00-10:00 mean by day of the week relative to working days (%), with 95% confidence intervals
Pollutant Mon Tue Wed Thu Fri Sat Sun
PM10 +4.3 [-41.4, +50.0] +4.4 [-62.2, +71.0] -5.6 [-51.7, +40.5] +16.9 [-29.0, +62.7] -21.4 [-54.3, +11.4] -0.4 [-46.1, +45.4] -4.3 [-44.1, +35.5]
NO2 -0.4 [-16.5, +15.7] -6.3 [-21.7, +9.1] +1.2 [-16.0, +18.4] +5.4 [-12.2, +23.1] -0.1 [-17.1, +16.9] +6.0 [-13.8, +25.8] -19.8 [-33.3, -6.3]
SO2 -5.2 [-22.9, +12.4] -6.5 [-23.4, +10.4] +8.0 [-9.5, +25.6] +2.2 [-16.6, +21.1] +1.3 [-16.8, +19.3] +23.0 [-1.2, +47.2] -10.6 [-28.0, +6.7]
CO +0.7 [-12.3, +13.8] -3.3 [-18.0, +11.5] -2.4 [-17.7, +12.9] +7.4 [-10.7, +25.4] -2.7 [-17.8, +12.4] -4.0 [-18.8, +10.7] -16.4 [-29.6, -3.2]
O3 -3.0 [-25.7, +19.7] -0.2 [-22.6, +22.1] +1.6 [-21.0, +24.2] +3.0 [-21.4, +27.4] -1.4 [-22.8, +20.0] +2.0 [-21.3, +25.3] +2.0 [-20.7, +24.8]

1.6 Why imputation cannot be avoided

Window-based forecasters (MLP, LSTM, TCN) need complete input windows. For the 334 days before the test period, the code computes the share of days with at least one missing value and the share of complete two-day (96-step) windows.

pre <- seq_len(334 * S1)
mot <- bind_rows(lapply(POLS, function(p) {
  x <- get_series("SUF 1", p)[pre]; miss <- is.na(x)
  M <- matrix(miss, ncol = S1, byrow = TRUE)                       # days x half-hours
  cs <- c(0, cumsum(miss)); L <- 96
  w <- cs[(L + 1):length(cs)] - cs[1:(length(cs) - L)]             # missing values per two-day window
  data.frame(pollutant = p, days = 100 * mean(rowSums(M) > 0), complete = 100 * mean(w == 0))
}))
key("days_with_missing_min_pct", min(mot$days[mot$pollutant != "PM10"]))
key("complete_2day_windows_max_pct", max(mot$complete[mot$pollutant != "PM10"]))
knitr::kable(mot, digits = 1, col.names = c("Pollutant", "Days with >= 1 missing (%)", "Complete two-day windows (%)"),
             caption = "Days with missing values and complete two-day windows (training period, 334 days)")
Days with missing values and complete two-day windows (training period, 334 days)
Pollutant Days with >= 1 missing (%) Complete two-day windows (%)
PM10 67.1 26.6
NO2 93.4 0.0
SO2 99.1 0.3
CO 93.1 0.7
O3 91.6 0.6

2 The proposed method Seas-AR

Seas-AR decomposes the transformed series as

z_t=T_t+D_t+W_t+R_t, \tag{2}

with a smooth trend T_t, a daily profile D_t, a weekday-specific deviation W_t and a remainder R_t. All components are estimated from observed values only; \delta_t=1 if t\in\mathcal O and \delta_t=0 otherwise. The method consists of the seven steps below.

2.1 Step 1 — Variance-stabilising transform (Eq. 1)

z_t=\log\Big(1+\frac{y_t}{m}\Big),\qquad m=\operatorname{median}\{y_s:s\in\mathcal O\}. \tag{1}

The transform is finite at y_t=0, does not depend on the unit, and turns multiplicative seasonal effects into additive ones. If the median is 0, the mean is used as scale, and m=1 if both are 0.

seasar_scale <- function(x) {                       # m in Eq. (1)
  m <- median(x, na.rm = TRUE)
  if (!(m > 0)) { mm <- mean(x, na.rm = TRUE); m <- if (mm > 0) mm else 1 }
  m
}

2.2 Step 2 — Missing-aware trend (Eq. 3)

For a partial residual u_t (initially u_t=z_t), the trend is the centred 2\times336 moving average of the observed values:

T_t=\frac{\sum_{j=-168}^{168}w_j\,\delta_{t+j}\,u_{t+j}}{\sum_{j=-168}^{168}w_j\,\delta_{t+j}},\qquad w_j=\begin{cases}1/336,&|j|<168,\\ 1/672,&|j|=168.\end{cases} \tag{3}

The weights sum to one and span exactly one week, so for complete data any component with period 48 or 336 that sums to zero over its cycle disappears from T_t. Where less than one quarter of the full weight is observed (inside long gaps), T_t is filled by linear interpolation, \hat x_t=x_a+\frac{t-a}{b-a}(x_b-x_a) between the nearest observed points a<t<b, and kept constant beyond the first and last observations. When only one value is observed, it is used everywhere (with stats::approx this case would stop with “need at least two non-NA values”).

Implementation. Eq. (3) is evaluated in O(n) operations with cumulative sums: the inner sum (|j|<168) is a difference of two cumulative sums and the two end points are added with half weight. The observed weight equals (2c_{\rm in}+c_{\rm end})/672, where c_{\rm in} and c_{\rm end} count the observed points; the rule “at least one quarter observed” is therefore checked exactly with integers, 2c_{\rm in}+c_{\rm end}\ge168. Check 2 confirms that this fast version equals the literal convolution of Eq. (3).

interp_linear <- function(x) {                    # linear interpolation, constant at the ends
  obs <- !is.na(x); n_obs <- sum(obs)
  if (n_obs == length(x)) return(x)
  if (n_obs == 0) return(rep(0, length(x)))
  if (n_obs == 1) { x[!obs] <- x[obs]; return(x) }          # a single observation: constant
  t <- seq_along(x)
  x[!obs] <- stats::approx(t[obs], x[obs], xout = t[!obs], rule = 2)$y
  x
}
nan_ma <- function(x, period = S2, min_frac = 0.25) {        # Eq. (3) in O(n) with cumulative sums
  n <- length(x); h <- period %/% 2; obs <- !is.na(x)
  xp <- c(rep(0, h), ifelse(obs, x, 0), rep(0, h))           # zero padding = outside values missing
  op <- c(rep(0, h), as.numeric(obs), rep(0, h))
  cx <- c(0, cumsum(xp)); co <- c(0, cumsum(op))
  i  <- seq_len(n) + h                                       # position of t in the padded series
  in_x <- cx[i + h] - cx[i - h + 1]; in_o <- co[i + h] - co[i - h + 1]      # |j| < h  (weight 1/period)
  en_x <- xp[i - h] + xp[i + h];     en_o <- op[i - h] + op[i + h]          # |j| = h  (weight 1/(2 period))
  cnt2 <- 2 * in_o + en_o                                    # = 2 * period * (observed weight), an integer
  ma <- ifelse(cnt2 >= 2 * period * min_frac, (in_x + 0.5 * en_x) / (in_o + 0.5 * en_o), NA_real_)
  interp_linear(ma)
}
nan_ma_direct <- function(x, period = S2, min_frac = 0.25) {  # literal Eq. (3) by convolution (reference)
  w <- rep(1, period + 1); w[c(1, period + 1)] <- 0.5; w <- w / period; h <- period %/% 2
  conv <- function(v) as.numeric(stats::filter(c(rep(0, h), v, rep(0, h)), w, sides = 2))[(h + 1):(h + length(v))]
  num <- conv(ifelse(is.na(x), 0, x)); den <- conv(as.numeric(!is.na(x)))
  interp_linear(ifelse(den >= min_frac - 1e-12, num / den, NA_real_))
}
tt_demo <- seq_len(10 * S2)
x_demo  <- 3 + sin(2 * pi * tt_demo / S1) + 0.5 * cos(2 * pi * tt_demo / S2)
inner   <- (S2 / 2 + 1):(length(tt_demo) - S2 / 2)
cat("Check 1: max |T - 3| for a pure double-seasonal signal =", signif(max(abs(nan_ma(x_demo)[inner] - 3)), 3), "\n")
Check 1: max |T - 3| for a pure double-seasonal signal = 2.93e-14 
set.seed(2); xr <- rnorm(5000); xr[sample(5000, 1500)] <- NA; xr[1000:1500] <- NA
cat("Check 2: max |fast cumulative-sum version - direct convolution| =", signif(max(abs(nan_ma(xr) - nan_ma_direct(xr))), 3), "\n")
Check 2: max |fast cumulative-sum version - direct convolution| = 1.74e-15 
cat("Check 3: interp_linear(c(NA, 5, NA, NA)) =", interp_linear(c(NA, 5, NA, NA)), "\n")
Check 3: interp_linear(c(NA, 5, NA, NA)) = 5 5 5 5 

2.3 Step 3 — Daily profile by kernel smoothing (Eq. 4)

The partial residual r_t=z_t-T_t-W_t is arranged as a matrix \mathbf V with one row per day k and one column per half-hour h. Each column (a cycle-subseries) is smoothed across days with a Nadaraya–Watson estimator with Gaussian kernel K_h(u)=\exp\{-u^2/(2h^2)\}:

\tilde D_{k,h}=\frac{\sum_{k'}K_{h_D}(k-k')\,\delta_{k',h}\,r_{k',h}}{\sum_{k'}K_{h_D}(k-k')\,\delta_{k',h}}, \qquad D_{k,h}=\tilde D_{k,h}-\frac{1}{48}\sum_{h'=1}^{48}\tilde D_{k,h'}. \tag{4}

In matrix form \tilde{\mathbf D}=(\mathbf K\mathbf V_0)\oslash(\mathbf K\boldsymbol\Delta), where \mathbf K_{kk'}=K_{h_D}(k-k'), \mathbf V_0 is \mathbf V with missing entries set to 0, \boldsymbol\Delta is the indicator matrix of observed entries and \oslash is element-wise division. Centring each row makes D sum to zero within every day. The bandwidth h_D (days) controls how fast the daily profile may change over the year; h_D\to\infty gives a static profile.

gauss_kernel <- function(n, bw) {
  if (!is.finite(bw) || bw > 1e5) return(matrix(1, n, n))  # bw = Inf: equal weights (static profile)
  idx <- 0:(n - 1); exp(-0.5 * (outer(idx, idx, "-") / bw)^2)
}
cycle_smooth <- function(r, period, bw) {         # Eq. (4)/(5) before centring; rows = cycles
  n <- length(r); nc <- ceiling(n / period)
  v <- rep(NA_real_, nc * period); v[seq_len(n)] <- r
  V <- matrix(v, nrow = nc, ncol = period, byrow = TRUE)
  Delta <- (!is.na(V)) * 1; V0 <- V; V0[is.na(V0)] <- 0
  K <- gauss_kernel(nc, bw)
  num <- K %*% V0; den <- K %*% Delta
  S <- num / den; S[!(den > 1e-300)] <- 0; S[is.na(S)] <- 0  # columns without data -> 0
  S
}

2.4 Step 4 — Weekly deviation (Eq. 5)

The partial residual r'_t=z_t-T_t-D_t is arranged by weeks (rows) and the 336 half-hours of the week (columns), smoothed across weeks with bandwidth h_W (weeks), and centred over weekdays:

W_{\ell,(d,h)}=\tilde W_{\ell,(d,h)}-\frac17\sum_{d'=1}^{7}\tilde W_{\ell,(d',h)}. \tag{5}

The centring makes D and W identifiable: W only carries the part of the weekly pattern that is not common to all days. In the code, each row of 336 values is viewed as a 48\times7 array and the mean over the seven weekdays is subtracted at every half-hour, in one vectorised operation.

center_weekly <- function(Sw) {                   # Eq. (5): subtract the mean over weekdays
  nw <- nrow(Sw)
  A  <- array(Sw, dim = c(nw, S1, 7))             # A[week, half-hour, weekday] (columns are day-major)
  M  <- rowMeans(A, dims = 2)                     # mean over the 7 weekdays: weeks x 48
  matrix(A - array(M, dim = c(nw, S1, 7)), nrow = nw, ncol = S2)
}

2.5 Step 5 — Backfitting

Starting from T=\mathrm{MA}(z) and D=W=0, the components are updated K=3 times: (i) D\leftarrow Eq. (4) on z-T-W; (ii) W\leftarrow Eq. (5) on z-T-D; (iii) T\leftarrow Eq. (3) on z-D-W. The remainder is R_t=z_t-T_t-D_t-W_t for t\in\mathcal O and missing on \mathcal M. Because all kernel sums use observed values only, no pre-imputation is needed; inside a long gap the profiles are borrowed from neighbouring days and weeks with Gaussian weights.

dsd_decompose <- function(z, bw_day = 14, bw_week = 16, weekly = TRUE, n_iter = 3) {
  n <- length(z)
  Tr <- nan_ma(z, S2); D <- numeric(n); W <- numeric(n)
  for (it in seq_len(n_iter)) {
    Sd <- cycle_smooth(z - Tr - W, S1, bw_day)    # Eq. (4), days x 48
    Sd <- Sd - rowMeans(Sd)                       # zero mean within each day
    D  <- as.vector(t(Sd))[seq_len(n)]
    if (weekly) W <- as.vector(t(center_weekly(cycle_smooth(z - Tr - D, S2, bw_week))))[seq_len(n)]
    Tr <- nan_ma(z - D - W, S2)                   # Eq. (3)
  }
  list(trend = Tr, daily = D, weekly = W, remainder = z - Tr - D - W)
}

2.6 Step 6 — Remainder imputation by the AR(1) bridge (Eqs. 6–7, Proposition 1)

The remainder is modelled as R_t=\phi R_{t-1}+\varepsilon_t, with \phi estimated from the pairs of consecutive observations \mathcal P=\{t:t\in\mathcal O,\ t-1\in\mathcal O\}:

\tilde\phi=\frac{\sum_{t\in\mathcal P}R_tR_{t-1}}{\sum_{t\in\mathcal P}R_{t-1}^2},\qquad \hat\phi=\min\{0.999,\max\{0,\tilde\phi\}\}. \tag{6}

For a missing t with nearest observed neighbours a<t<b,

\hat R_t=\frac{\phi^{\,t-a}\big(1-\phi^{2(b-t)}\big)R_a+\phi^{\,b-t}\big(1-\phi^{2(t-a)}\big)R_b}{1-\phi^{2(b-a)}}, \tag{7}

while \hat R_t=\phi^{b-t}R_b before the first and \hat R_t=\phi^{t-a}R_a after the last observation (the remainder is centred by its observed mean first).

Proposition 1. For a stationary Gaussian AR(1), (7) equals the Kalman smoother estimate \mathrm E[R_t\mid R_s,s\in\mathcal O], with error variance \gamma_0\big[1-\frac{\phi^{2(t-a)}+\phi^{2(b-t)}-2\phi^{2(b-a)}}{1-\phi^{2(b-a)}}\big], \gamma_0=\mathrm{Var}(R_t). By the Markov property, R_t depends on the other observations only through (R_a,R_b); Gaussian conditioning with \mathrm{Cov}(R_s,R_u)=\gamma_0\phi^{|s-u|} gives the weights \mathbf c^\top\boldsymbol\Sigma^{-1} with \mathbf c=\gamma_0(\phi^{t-a},\phi^{b-t})^\top and \boldsymbol\Sigma=\gamma_0\left[\begin{smallmatrix}1&\phi^{b-a}\\\phi^{b-a}&1\end{smallmatrix}\right]. For short gaps with \phi\approx1, (7) is close to linear interpolation of the remainder; for long gaps \hat R_t\to0 and the imputation reverts to the profile T_t+D_t+W_t. The code verifies the mean and the variance of Proposition 1 against stats::KalmanSmooth.

estimate_ar1 <- function(R) {                     # Eq. (6)
  a <- R[-length(R)]; b <- R[-1]; m <- !is.na(a) & !is.na(b)
  if (sum(m) < 10) return(0)
  min(max(sum(a[m] * b[m]) / sum(a[m]^2), 0), 0.999)
}
ar1_bridge <- function(R, phi = NULL) {           # Eq. (7)
  obs <- !is.na(R)
  if (all(obs)) return(R)
  if (!any(obs)) return(rep(0, length(R)))
  if (is.null(phi)) phi <- estimate_ar1(R)
  mu <- mean(R, na.rm = TRUE); Rc <- R - mu
  io <- which(obs); im <- which(!obs)
  k  <- findInterval(im, io)                      # number of observed indices before t
  hp <- k > 0; hn <- k < length(io)               # has a previous / next observation
  a  <- ifelse(hp, io[pmax(k, 1)], NA_integer_)
  b  <- ifelse(hn, io[pmin(k + 1, length(io))], NA_integer_)
  est <- numeric(length(im)); both <- hp & hn
  if (any(both)) {
    tt <- im[both]; aa <- a[both]; bb <- b[both]
    pa <- phi^(tt - aa); pb <- phi^(bb - tt); den <- 1 - phi^(2 * (bb - aa))
    est[both] <- (pa * (1 - pb^2) * Rc[aa] + pb * (1 - pa^2) * Rc[bb]) / den
  }
  oa <- hp & !hn; if (any(oa)) est[oa] <- phi^(im[oa] - a[oa]) * Rc[a[oa]]
  ob <- !hp & hn; if (any(ob)) est[ob] <- phi^(b[ob] - im[ob]) * Rc[b[ob]]
  R[im] <- est + mu
  R
}
ar1_bridge_var <- function(R, phi, gamma0) {      # variance in Proposition 1 (inner gaps)
  io <- which(!is.na(R)); im <- which(is.na(R)); k <- findInterval(im, io)
  a <- io[k]; b <- io[k + 1]; m <- b - a
  gamma0 * (1 - (phi^(2 * (im - a)) + phi^(2 * (b - im)) - 2 * phi^(2 * m)) / (1 - phi^(2 * m)))
}
set.seed(1); phi0 <- 0.9
r  <- as.numeric(arima.sim(list(ar = phi0), 2000)); r[c(100:180, 500:503)] <- NA
ks <- stats::KalmanSmooth(r - mean(r, na.rm = TRUE), stats::makeARIMA(phi = phi0, theta = numeric(), Delta = numeric()), nit = -1L)
cat("Proposition 1, mean:     max |bridge - Kalman smoother| =",
    signif(max(abs(ar1_bridge(r, phi0) - (ks$smooth[, 1] + mean(r, na.rm = TRUE)))[is.na(r)]), 3), "\n")
Proposition 1, mean:     max |bridge - Kalman smoother| = 8.88e-16 
cat("Proposition 1, variance: max |formula - Kalman smoother| =",
    signif(max(abs(ar1_bridge_var(r, phi0, 1 / (1 - phi0^2)) - ks$var[is.na(r), 1, 1])), 3), "\n")
Proposition 1, variance: max |formula - Kalman smoother| = 2.44e-15 

2.7 Step 7 — Reconstruction (Eq. 8) and the complete function

\hat y_t=\max\Big\{0,\ m\big[\exp\big(T_t+D_t+W_t+\hat R_t\big)-1\big]\Big\},\qquad t\in\mathcal M, \tag{8}

and the observed values are left unchanged. The arguments transform, weekly and remainder define the variants of the ablation study in Experiment 1. The last line measures the computing time for one full series.

impute_seasar <- function(x, bw_day = 14, bw_week = 16, transform = "log", weekly = TRUE,
                        remainder = "ar1", n_iter = 3, return_components = FALSE) {
  obs <- !is.na(x)
  if (all(obs)) return(x)
  m <- seasar_scale(x)
  z <- if (transform == "log") log1p(pmax(x, 0) / m) else x / m          # Eq. (1)
  dc <- dsd_decompose(z, bw_day, bw_week, weekly, n_iter)                # Eqs. (3)-(5)
  Rh <- switch(remainder, ar1 = ar1_bridge(dc$remainder),                # Eqs. (6)-(7)
               linear = interp_linear(dc$remainder),
               zero = ifelse(is.na(dc$remainder), 0, dc$remainder))
  zh <- dc$trend + dc$daily + dc$weekly + Rh
  yh <- pmax(if (transform == "log") m * expm1(zh) else m * zh, 0)        # Eq. (8)
  out <- x; out[!obs] <- yh[!obs]
  if (return_components) return(list(imputed = out, z = z, comp = dc, remainder_imp = Rh))
  out
}
x_no2 <- get_series("SUF 1", "NO2")
t_seasar <- system.time(for (i in 1:5) impute_seasar(x_no2))[["elapsed"]] / 5
cat("Seas-AR imputes the full NO2 series of SUF 1 in", round(t_seasar, 3), "s\n")
Seas-AR imputes the full NO2 series of SUF 1 in 0.03 s

3 Competing methods

All outputs are truncated at zero, \hat y_t\leftarrow\max\{0,\hat y_t\}.

  • MEAN, MEDIAN: \hat y_t=\bar y_{\mathcal O} or \operatorname{median}(y_{\mathcal O}) (imputeTS::na_mean).
  • LI: linear interpolation of Step 2 (na_interpolation).
  • SS-MEAN, SS-MEDIAN, SS-LI (seasonal split, period 336): for every position p of the week, the subseries \{y_t:(t-1)\bmod336=p-1\} is filled with its own mean, median or linear interpolation (na_seasplit). Positions without any observation use the same global statistic.
  • SEADEC: classical additive decomposition with period 48 of the interpolated series, S_t=\bar e_{h(t)}-\frac1{48}\sum_h\bar e_h with \bar e_h the mean of y-\mathrm{MA}_{2\times48}(y) at half-hour h; then \hat y_t=\mathrm{LI}(y-S)_t+S_t. This is the scheme of na_seadec, which itself uses a robust STL (evaluated separately in Section 6.8).
  • MSTL-LI: as SEADEC, with S_t=S^{(48)}_t+S^{(336)}_t from forecast::mstl.
  • KALMAN: an ARIMA(2,0,1) model for y_t-\bar y fitted by maximum likelihood (the Kalman filter handles the missing values exactly), and \hat y_t=\mathbf Z^\top\hat{\boldsymbol\alpha}_{t\mid n}+\bar y from the Kalman smoother; an AR(1) is used if the optimisation fails.
impute_mean   <- function(x) { x[is.na(x)] <- mean(x, na.rm = TRUE); x }
impute_median <- function(x) { x[is.na(x)] <- median(x, na.rm = TRUE); x }
impute_seasplit <- function(x, algorithm = "median", period = S2) {
  x0 <- x
  for (k in seq_len(period)) {
    idx <- seq(k, length(x), by = period); sub <- x[idx]
    if (anyNA(sub)) x[idx] <- switch(algorithm, mean = impute_mean(sub),
                                     median = impute_median(sub), interpolation = interp_linear(sub))
  }
  if (anyNA(x)) {                                 # positions without any observation: global fallback
    fb <- switch(algorithm, mean = impute_mean, median = impute_median, interpolation = interp_linear)
    x[is.na(x)] <- fb(x0)[is.na(x)]
  }
  x
}
impute_seadec <- function(x, period = S1) {       # classical additive decomposition
  seas <- as.numeric(stats::decompose(ts(interp_linear(x), frequency = period))$seasonal)
  xd <- interp_linear(x - seas); x[is.na(x)] <- (xd + seas)[is.na(x)]; x
}
impute_seadec_stl <- function(x, period = S1) {   # replica of imputeTS::na_seadec (robust STL)
  seas <- as.numeric(stats::stl(ts(interp_linear(x), frequency = period), robust = TRUE,
                                s.window = 11)$time.series[, "seasonal"])
  xd <- interp_linear(x - seas); x[is.na(x)] <- (xd + seas)[is.na(x)]; x
}
impute_mstl_li <- function(x) {
  fit  <- forecast::mstl(forecast::msts(interp_linear(x), seasonal.periods = c(S1, S2)))
  seas <- rowSums(fit[, grep("Seasonal", colnames(fit)), drop = FALSE])
  xd <- interp_linear(x - seas); x[is.na(x)] <- (xd + seas)[is.na(x)]; x
}
impute_kalman <- function(x, order = c(2, 0, 1)) {
  mu <- mean(x, na.rm = TRUE); x0 <- x - mu
  fit <- tryCatch(stats::arima(x0, order = order, include.mean = FALSE, method = "ML"),
                  error = function(e) stats::arima(x0, order = c(1, 0, 0), include.mean = FALSE, method = "ML"))
  sm <- stats::KalmanSmooth(x0, fit$model, nit = -1L)$smooth
  yhat <- as.numeric(sm %*% fit$model$Z) + mu
  x[is.na(x)] <- yhat[is.na(x)]; x
}
nonneg <- function(f) function(x) pmax(f(x), 0)
METHOD_ORDER <- c("MEAN", "MEDIAN", "LI", "SS-MEAN", "SS-MEDIAN", "SS-LI", "SEADEC", "MSTL-LI", "KALMAN", "Seas-AR")
METHODS <- lapply(list(MEAN = impute_mean, MEDIAN = impute_median, LI = interp_linear,
  `SS-MEAN` = function(x) impute_seasplit(x, "mean"), `SS-MEDIAN` = function(x) impute_seasplit(x, "median"),
  `SS-LI` = function(x) impute_seasplit(x, "interpolation"), SEADEC = impute_seadec,
  `MSTL-LI` = impute_mstl_li, KALMAN = impute_kalman, `Seas-AR` = impute_seasar), nonneg)

Validation against imputeTS. In imputeTS the seasonal-split median is requested with algorithm = "mean", option = "median". The last row is non-zero by design (classical decomposition versus robust STL).

if (HAS_IMPUTETS) {
  xs1 <- ts(x_no2, frequency = S1); xs2 <- ts(x_no2, frequency = S2)
  chk <- function(a, b) signif(max(abs(a - as.numeric(b))), 3)
  knitr::kable(data.frame(
    comparison = c("LI vs na_interpolation", "SS-MEAN vs na_seasplit(algorithm = 'mean')",
                   "SS-MEDIAN vs na_seasplit(algorithm = 'mean', option = 'median')",
                   "SS-LI vs na_seasplit(algorithm = 'interpolation')",
                   "robust-STL replica vs na_seadec", "SEADEC (classical decomposition) vs na_seadec"),
    max_abs_diff = c(
      chk(interp_linear(x_no2), imputeTS::na_interpolation(x_no2)),
      chk(impute_seasplit(x_no2, "mean"), imputeTS::na_seasplit(xs2, algorithm = "mean")),
      chk(impute_seasplit(x_no2, "median"), imputeTS::na_seasplit(xs2, algorithm = "mean", option = "median")),
      chk(impute_seasplit(x_no2, "interpolation"), imputeTS::na_seasplit(xs2, algorithm = "interpolation")),
      chk(impute_seadec_stl(x_no2), imputeTS::na_seadec(xs1, algorithm = "interpolation")),
      chk(impute_seadec(x_no2), imputeTS::na_seadec(xs1, algorithm = "interpolation")))),
    caption = "Agreement with imputeTS (0 = identical; the last row differs by design)")
} else cat("imputeTS is not installed; check skipped.\n")
Agreement with imputeTS (0 = identical; the last row differs by design)
comparison max_abs_diff
LI vs na_interpolation 0
SS-MEAN vs na_seasplit(algorithm = ‘mean’) 0
SS-MEDIAN vs na_seasplit(algorithm = ‘mean’, option = ‘median’) 0
SS-LI vs na_seasplit(algorithm = ‘interpolation’) 0
robust-STL replica vs na_seadec 0
SEADEC (classical decomposition) vs na_seadec 21

4 Experimental design

4.1 Artificial gaps

The true values behind real gaps are unknown, so accuracy is measured on observations removed on purpose. For a gap regime with lengths in [L_{\min},L_{\max}] and a rate \rho:

  1. draw a length L\sim\mathrm U\{L_{\min},\dots,L_{\max}\} and a start s at random;
  2. accept the block \{s,\dots,s+L-1\} only if the block and one guard point on each side (s-1 and s+L) are observed and unused, so that the realised gap has length exactly L;
  3. repeat until a fraction \rho of the observed values is removed.

The regimes are short \mathrm U\{1..4\} (up to 2 h), medium \mathrm U\{5..48\} (up to one day) and long \mathrm U\{49..336\} (up to one week), with \rho\in\{10\%,30\%\} and three replicates: 18 scenarios × 15 series = 270 scenarios. With mask_source = "file" the blocks are read from masks/, generated once with this algorithm and fixed seeds, so every run uses the same scenarios.

4.2 Accuracy measures (Eq. 9)

\mathrm{RMSE}=\Big(\tfrac{1}{|\mathcal A|}\sum_{t\in\mathcal A}(\hat y_t-y_t)^2\Big)^{1/2},\qquad \mathrm{MAE}=\tfrac{1}{|\mathcal A|}\sum_{t\in\mathcal A}|\hat y_t-y_t|,\qquad \mathrm{NRMSE}=\frac{\mathrm{RMSE}}{\sigma_y}, \tag{9}

where \mathcal A is the set of removed points. NRMSE makes errors comparable across pollutants.

REGIMES <- list(short = c(1, 4), medium = c(5, 48), long = c(49, 336)); REG_NAMES <- names(REGIMES)
make_block_mask <- function(x, rate, regime, seed) {      # used when mask_source = "R"
  set.seed(seed)
  n <- length(x); obs <- !is.na(x); target <- round(rate * sum(obs))
  lr <- REGIMES[[regime]]; avail <- obs; mask <- logical(n); cnt <- 0; tries <- 0
  while (cnt < target && tries < 200000) {
    tries <- tries + 1
    L <- sample(lr[1]:lr[2], 1); s <- sample.int(n - L - 2, 1) + 1
    if (all(avail[(s - 1):(s + L)])) {                     # block and both guard points available
      mask[s:(s + L - 1)] <- TRUE; avail[(s - 1):(s + L)] <- FALSE; cnt <- cnt + L
    }
  }
  mask
}
load_masks <- function(exp) {
  f_sc <- file.path(P$mask_dir, sprintf("masks_%s_scenarios.csv", exp))
  f_bl <- file.path(P$mask_dir, sprintf("masks_%s_blocks.csv", exp))
  if (!file.exists(f_sc) || !file.exists(f_bl)) return(NULL)
  bl <- read.csv(f_bl)
  list(sc = read.csv(f_sc), blocks = split(bl[, c("start", "len")], bl$sid))
}
MK <- list(exp0 = NULL, exp1 = NULL)
mask_files <- file.path(P$mask_dir, sprintf("masks_%s_%s.csv", rep(c("exp0", "exp1"), each = 2), c("scenarios", "blocks")))
if (P$mask_source == "file" && !all(file.exists(mask_files))) {
  cat("NOTE: the stored masks were not found in", P$mask_dir, "- new masks are generated in R and every result is",
      "recomputed, so the numbers differ slightly from the stored results.\n")
  P$mask_source <- "R"; P$use_cache <- FALSE
}
NOTE: the stored masks were not found in masks - new masks are generated in R and every result is recomputed, so the numbers differ slightly from the stored results.
if (P$mask_source == "file") { MK$exp0 <- load_masks("exp0"); MK$exp1 <- load_masks("exp1") }
TAG0 <- if (is.null(MK$exp0)) "R-masks" else "file-masks"
TAG1 <- if (is.null(MK$exp1)) "R-masks" else "file-masks"
get_mask <- function(exp, x, st, p, reg, rate, rep, seed) {
  mk <- MK[[exp]]
  if (is.null(mk)) return(make_block_mask(x, rate, reg, seed))
  sid <- mk$sc$sid[mk$sc$station == st & mk$sc$pollutant == p & mk$sc$regime == reg &
                   abs(mk$sc$rate - rate) < 1e-9 & mk$sc$rep == rep]
  b <- mk$blocks[[as.character(sid)]]
  m <- logical(length(x)); m[unlist(mapply(function(s, l) s:(s + l - 1), b$start, b$len))] <- TRUE
  m
}
cat("Masks: Experiment 0 =", TAG0, "| Experiment 1 =", TAG1, "\n")
Masks: Experiment 0 = R-masks | Experiment 1 = R-masks 

5 Experiment 0: bandwidth selection

For a target station s^* the bandwidths are chosen on the other two stations:

(\hat h_D,\hat h_W)_{s^*}=\arg\min_{h_D\in\{3.5,7,14,28\},\ h_W\in\{2,4,8,16\}}\ \frac{1}{|\mathcal S_{-s^*}|}\sum_{(s,p,r,i)\in\mathcal S_{-s^*}}\mathrm{NRMSE}_{s,p,r,i}(h_D,h_W),

where \mathcal S_{-s^*} contains the scenarios of the two other stations (5 pollutants × 3 regimes × 2 replicates, \rho=20\%). SUF 1 never influences its own bandwidths.

BW_DAY  <- if (QUICK) c(7, 14) else c(3.5, 7, 14, 28)
BW_WEEK <- if (QUICK) c(4, 16) else c(2, 4, 8, 16)
dir0 <- out_file(paste0("exp0_", TAG0, if (QUICK) "_quick" else "")); dir.create(dir0, showWarnings = FALSE)
for (si in seq_along(STATIONS)) for (pk in seq_along(POLS)) {
  st <- STATIONS[si]; p <- POLS[pk]; f <- file.path(dir0, sprintf("%s_%s.csv", gsub(" ", "", st), p))
  if (isTRUE(P$use_cache) && file.exists(f)) next
  x <- get_series(st, p); sdx <- sd_pop(x); rows <- list()
  for (ri in seq_along(REG_NAMES)) for (rep in seq_len(if (QUICK) 1 else 2)) {
    m <- get_mask("exp0", x, st, p, REG_NAMES[ri], 0.20, rep, seed = 10000 + 100 * si + 10 * pk + 3 * ri + rep)
    xm <- x; xm[m] <- NA
    for (bd in BW_DAY) for (bw in BW_WEEK) {
      y <- impute_seasar(xm, bw_day = bd, bw_week = bw)
      rows[[length(rows) + 1]] <- data.frame(station = st, pollutant = p, regime = REG_NAMES[ri], rep = rep,
                                             bw_day = bd, bw_week = bw, nrmse = rmse(y[m], x[m]) / sdx)
    }
  }
  write.csv(bind_rows(rows), f, row.names = FALSE)
}
grid <- bind_rows(lapply(list.files(dir0, pattern = "\\.csv$", full.names = TRUE), read.csv))
BW <- lapply(setNames(STATIONS, STATIONS), function(st) {
  g <- grid |> filter(station != st) |> group_by(bw_day, bw_week) |>
    summarise(nrmse = mean(nrmse), .groups = "drop") |> arrange(bw_day, bw_week) |>
    slice_min(nrmse, n = 1, with_ties = FALSE)
  list(bw_day = g$bw_day, bw_week = g$bw_week, nrmse = g$nrmse)
})
cat("Experiment 0:", nrow(grid), "grid evaluations\n")
Experiment 0: 1440 grid evaluations
for (st in STATIONS) { key(paste0("bw_day_", gsub(" ", "", st)), BW[[st]]$bw_day); key(paste0("bw_week_", gsub(" ", "", st)), BW[[st]]$bw_week) }
g4 <- grid |> filter(station != "SUF 1") |> group_by(bw_day, bw_week) |>
  summarise(nrmse = mean(nrmse), .groups = "drop") |> mutate(best = nrmse == min(nrmse))
key("grid_best_nrmse", min(g4$nrmse)); key("grid_worst_nrmse", max(g4$nrmse))
key("grid_worst_vs_best_pct", 100 * (max(g4$nrmse) / min(g4$nrmse) - 1))
key("grid_worst_pair", paste(g4$bw_day[which.max(g4$nrmse)], g4$bw_week[which.max(g4$nrmse)]))
p4 <- ggplot(g4, aes(factor(bw_week), factor(bw_day), fill = nrmse)) + geom_tile(colour = "white") +
  geom_text(aes(label = sprintf("%.4f", nrmse), fontface = ifelse(best, "bold", "plain"),
                colour = nrmse < mean(range(nrmse))), size = 2.65, family = FONT_TEXT, show.legend = FALSE) +
  scale_colour_manual(values = c(`TRUE` = "white", `FALSE` = "black")) +
  scale_fill_distiller(palette = "Blues", direction = -1) + scale_y_discrete(limits = rev) +
  labs(x = expression(h[W] ~ "(weeks)"), y = expression(h[D] ~ "(days)"), fill = "Mean\nNRMSE") +
  theme(panel.grid = element_blank(), legend.key.height = unit(0.5, "cm"), axis.text = element_text(size = 7.5),
        axis.title = element_text(size = 9), legend.text = element_text(size = 7), legend.title = element_text(size = 8))
print(p4); save_fig(p4, "fig04_bandwidth", 3.5, 2.45)

knitr::kable(data.frame(station = STATIONS, h_D = sapply(BW, `[[`, "bw_day"), h_W = sapply(BW, `[[`, "bw_week"),
                        nrmse = sapply(BW, `[[`, "nrmse")), digits = 4,
             col.names = c("Target station", "h_D (days)", "h_W (weeks)", "Mean NRMSE on the other stations"),
             caption = "Selected bandwidths (leave-one-station-out)")
Selected bandwidths (leave-one-station-out)
Target station h_D (days) h_W (weeks) Mean NRMSE on the other stations
SUF 1 SUF 1 14 16 0.5128
SUF 6 SUF 6 14 16 0.5371
SUF 7 SUF 7 14 16 0.5268

6 Experiment 1: imputation accuracy

6.1 Running the experiment

For every scenario the ten methods, the five ablation variants of Seas-AR and, when imputeTS is installed, the original na_seadec are applied to the same masked series, and the computing time is recorded. Seas-AR uses the bandwidths of Experiment 0 for the station concerned. Results are stored per series, so an interrupted run resumes where it stopped.

RATES  <- if (QUICK) 0.30 else c(0.10, 0.30)
N_REPS <- if (QUICK) 1 else 3
st_list <- if (QUICK) "SUF 1" else STATIONS
dir1 <- out_file(paste0("exp1_", TAG1, if (QUICK) "_quick" else "")); dir.create(dir1, showWarnings = FALSE)
for (st in st_list) for (p in POLS) {
  f <- file.path(dir1, sprintf("%s_%s.csv", gsub(" ", "", st), p))
  if (isTRUE(P$use_cache) && file.exists(f)) next
  si <- match(st, STATIONS); pk <- match(p, POLS)
  x <- get_series(st, p); sdx <- sd_pop(x); bw <- BW[[st]]
  meths <- METHODS
  meths$`Seas-AR` <- function(v) impute_seasar(v, bw_day = bw$bw_day, bw_week = bw$bw_week)
  abl <- list(
    `w/o log transform` = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, transform = "none"),
    `w/o weekly comp.`  = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, weekly = FALSE),
    `static profiles`   = function(v) impute_seasar(v, Inf, Inf),
    `remainder = 0`     = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, remainder = "zero"),
    `remainder LI`      = function(v) impute_seasar(v, bw$bw_day, bw$bw_week, remainder = "linear"))
  extra <- if (HAS_IMPUTETS) list(`NA_SEADEC (imputeTS)` = function(v)
    as.numeric(imputeTS::na_seadec(ts(v, frequency = S1), algorithm = "interpolation"))) else list()
  allf  <- c(meths, abl, extra)
  kinds <- c(rep("main", length(meths)), rep("ablation", length(abl)), rep("extra", length(extra)))
  rows <- list()
  for (ri in seq_along(REG_NAMES)) for (rate in RATES) for (rep in seq_len(N_REPS)) {
    seed <- 20000 + 1000 * si + 100 * pk + 10 * ri + 3 * match(rate, c(0.10, 0.30)) + rep
    m <- get_mask("exp1", x, st, p, REG_NAMES[ri], rate, rep, seed); xm <- x; xm[m] <- NA
    for (j in seq_along(allf)) {
      t1 <- proc.time()[["elapsed"]]; y <- pmax(allf[[j]](xm), 0); dt <- proc.time()[["elapsed"]] - t1
      rows[[length(rows) + 1]] <- data.frame(station = st, pollutant = p, regime = REG_NAMES[ri], rate = rate,
        rep = rep, method = names(allf)[j], kind = kinds[j], n_masked = sum(m),
        rmse = rmse(y[m], x[m]), mae = mae(y[m], x[m]), nrmse = rmse(y[m], x[m]) / sdx, sec = dt)
    }
  }
  write.csv(bind_rows(rows), f, row.names = FALSE)
}
res1 <- bind_rows(lapply(list.files(dir1, pattern = "\\.csv$", full.names = TRUE), read.csv))
main <- res1 |> filter(kind == "main") |>
  mutate(scenario = paste(station, pollutant, regime, rate, rep, sep = "|"), method = factor(method, levels = METHOD_ORDER))
cat("Experiment 1:", n_distinct(main$scenario), "scenarios,", nrow(res1), "result rows\n")
Experiment 1: 270 scenarios, 4320 result rows

6.2 Accuracy by method

Left: RMSE at SUF 1 in original units (mean of 18 scenarios per pollutant). Middle: NRMSE averaged over the 15 series for each gap regime. Right: average Friedman rank and mean computing time per series.

Pw <- main |> select(scenario, method, nrmse) |> pivot_wider(names_from = method, values_from = nrmse)
Pm <- as.matrix(Pw[, METHOD_ORDER])
avg_rank <- colMeans(t(apply(Pm, 1, rank)))                 # rank 1 = most accurate
t4 <- data.frame(method = factor(METHOD_ORDER, levels = METHOD_ORDER)) |>
  left_join(main |> filter(station == "SUF 1") |> group_by(method, pollutant) |> summarise(v = mean(rmse), .groups = "drop") |>
              pivot_wider(names_from = pollutant, values_from = v), by = "method") |>
  left_join(main |> group_by(method, regime) |> summarise(v = mean(nrmse), .groups = "drop") |>
              pivot_wider(names_from = regime, values_from = v), by = "method") |>
  left_join(main |> group_by(method) |> summarise(all = mean(nrmse), sec = mean(sec), .groups = "drop"), by = "method")
t4$rank <- avg_rank[as.character(t4$method)]
write.csv(t4, out_file("exp1_table4.csv"), row.names = FALSE)
pc <- intersect(POLS, names(t4)); rc <- intersect(REG_NAMES, names(t4))
md4 <- data.frame(Method = as.character(t4$method), check.names = FALSE)
for (p in pc) md4[[p]] <- fmt_md(t4[[p]], if (p == "CO") 3 else 2)
for (r in rc) md4[[paste("NRMSE", r)]] <- fmt_md(t4[[r]], 3)
md4[["Avg. rank"]] <- fmt_md(t4$rank, 2); md4[["Time (s)"]] <- fmt_num(t4$sec, 3)
tx <- lapply(pc, function(p) fmt_tex(t4[[p]], if (p == "CO") 3 else 2)); tr <- lapply(rc, function(r) fmt_tex(t4[[r]], 3))
rows4 <- sapply(seq_len(nrow(t4)), function(i) paste0(paste(c(
  if (t4$method[i] == "Seas-AR") "\\textbf{Seas-AR (proposed)}" else as.character(t4$method[i]),
  sapply(tx, `[`, i), sapply(tr, `[`, i), fmt_tex(t4$rank, 2)[i], fmt_num(t4$sec[i], 3)), collapse = " & "), " \\\\"))
write_tex(c("\\begin{table*}[!t]", "\\centering",
  sprintf("\\caption{Imputation accuracy under artificial block missingness. Left: RMSE at SUF~1 in original units, averaged over the three gap regimes, two missing rates and three replicates. Middle: NRMSE (RMSE divided by the standard deviation of the series) averaged over all 15 series (three stations $\\times$ five pollutants) for each gap regime. Right: average Friedman rank over the %d scenarios and mean computing time per imputation of a full series (R, one CPU core). Best values in bold.}", nrow(Pm)),
  "\\label{tab:imp}", "\\setlength{\\tabcolsep}{4.2pt}", "\\begin{tabular}{lrrrrrrrrrr}", "\\toprule",
  " & \\multicolumn{5}{c}{RMSE at SUF~1} & \\multicolumn{3}{c}{NRMSE, all stations} & & \\\\",
  "\\cmidrule(lr){2-6}\\cmidrule(lr){7-9}",
  "Method & PM$_{10}$ & NO$_2$ & SO$_2$ & CO & O$_3$ & Short & Medium & Long & Avg. rank & Time (s)\\\\", "\\midrule",
  rows4, "\\bottomrule", "\\end{tabular}", "\\end{table*}"), "tab_imputation")
knitr::kable(md4, align = "r", caption = cap("Table 5", "RMSE at SUF 1, NRMSE of all stations by gap regime, average rank and time"))
RMSE at SUF 1, NRMSE of all stations by gap regime, average rank and time
Method PM10 NO2 SO2 CO O3 NRMSE short NRMSE medium NRMSE long Avg. rank Time (s)
MEAN 35.24 17.02 26.75 0.501 30.43 0.990 0.966 1.021 8.08 0.000
MEDIAN 37.16 17.89 26.89 0.531 31.28 1.027 0.993 1.059 9.16 0.000
LI 21.00 12.16 27.07 0.346 13.36 0.300 0.737 1.034 5.10 0.000
SS-MEAN 36.36 16.27 26.12 0.473 28.52 0.899 0.877 0.935 6.38 0.002
SS-MEDIAN 37.17 16.95 26.21 0.502 28.51 0.923 0.898 0.960 7.33 0.004
SS-LI 33.36 15.20 26.28 0.425 17.41 0.837 0.827 0.982 6.63 0.005
SEADEC 21.02 11.06 27.16 0.306 10.42 0.292 0.626 0.895 2.95 0.004
MSTL-LI 21.36 10.68 27.17 0.310 9.36 0.305 0.622 0.879 3.44 0.021
KALMAN 22.06 11.52 21.17 0.337 13.03 0.293 0.673 0.924 3.63 0.080
Seas-AR 20.68 9.17 20.36 0.265 8.41 0.312 0.529 0.698 2.30 0.023

6.3 NRMSE by gap regime and missing rate

Each row is a method (ordered by overall NRMSE, best at the top), each panel a gap regime; open and filled circles are the 10% and 30% missing rates, and Seas-AR is drawn in red.

ord5 <- main |> group_by(method) |> summarise(v = mean(nrmse), .groups = "drop") |> arrange(desc(v)) |> pull(method)
d5 <- main |> group_by(method, regime, rate) |> summarise(nrmse = mean(nrmse), .groups = "drop") |>
  mutate(regime = factor(regime, levels = REG_NAMES, labels = c("Short gaps (1-4 steps)", "Medium gaps (5-48 steps)", "Long gaps (49-336 steps)")),
         rate = factor(sprintf("%d%%", round(100 * rate))), method = factor(method, levels = as.character(ord5)),
         seasar = method == "Seas-AR")
p5 <- ggplot(d5, aes(nrmse, method)) +
  geom_line(aes(group = method), colour = "grey75", linewidth = 0.5) +
  geom_point(aes(shape = rate, colour = seasar), size = 1.7, stroke = 0.6) +
  scale_colour_manual(values = c(`FALSE` = "grey20", `TRUE` = "#c0392b"), guide = "none") +
  scale_shape_manual(values = c(1, 16), name = "Additional missing rate") +
  facet_wrap(~regime, nrow = 1) + labs(x = "NRMSE (mean over series)", y = NULL) +
  theme(legend.position = "bottom", panel.grid.major.y = element_line(colour = "grey92"))
print(p5); save_fig(p5, "fig05_accuracy_by_regime", 7.16, 2.6)

6.4 Ranks, Friedman test and Nemenyi critical difference

Within each scenario j the methods receive ranks r_{ij} (1 = smallest NRMSE) and \bar r_i=\frac1N\sum_j r_{ij}. The Friedman test checks H_0: all methods are equivalent. Two average ranks differ significantly (Nemenyi) when their difference exceeds

\mathrm{CD}=q_{\alpha}\sqrt{\frac{k(k+1)}{6N}},\qquad k=10,\ N=270,\ q_{0.05}=3.164 .

fr <- friedman.test(Pm)
k_ <- ncol(Pm); N_ <- nrow(Pm); CD <- 3.164 * sqrt(k_ * (k_ + 1) / (6 * N_))     # q_0.05 = 3.164 for k = 10
for (mm in names(avg_rank)) key(paste0("rank_", mm), avg_rank[[mm]])
key("friedman_chi2", unname(fr$statistic)); key("friedman_p", fr$p.value); key("nemenyi_cd", CD); key("n_scenarios", N_)
cat(sprintf("Friedman chi^2(%d) = %.1f, p = %.2g | Nemenyi CD = %.3f (k = %d, N = %d)\n", k_ - 1, fr$statistic, fr$p.value, CD, k_, N_))
Friedman chi^2(9) = 1474.0, p = 8e-312 | Nemenyi CD = 0.824 (k = 10, N = 270)
d6 <- data.frame(method = names(avg_rank), rank = avg_rank) |> arrange(rank) |> mutate(y = rev(seq_along(rank)))
p6 <- ggplot(d6, aes(rank, y)) +
  annotate("rect", xmin = min(d6$rank), xmax = min(d6$rank) + CD, ymin = -Inf, ymax = Inf, fill = "#c0392b", alpha = .08) +
  annotate("segment", x = min(d6$rank), xend = min(d6$rank) + CD, y = nrow(d6) + .8, yend = nrow(d6) + .8, linewidth = 0.8) +
  annotate("text", x = min(d6$rank), y = nrow(d6) + 1.35, label = sprintf("CD = %.2f", CD), hjust = 0, size = 2.4, family = FONT_TEXT) +
  geom_point(aes(colour = method), size = 1.8, show.legend = FALSE) +
  geom_text(aes(label = sprintf("%s (%.2f)", method, rank)), hjust = -0.15, size = 2.4, family = FONT_TEXT) +
  scale_colour_manual(values = COL) + scale_x_continuous(limits = c(1, 12), breaks = seq(2, 10, 2)) +
  scale_y_continuous(limits = c(0.5, nrow(d6) + 1.6)) +
  labs(x = "Average rank over the imputation scenarios (1 = best)", y = NULL) +
  theme(axis.text.y = element_blank(), panel.grid.major.y = element_blank())
print(p6); save_fig(p6, "fig06_ranks", 3.5, 2.4)

6.5 Paired Wilcoxon tests with Holm correction

For each competitor c the differences \Delta_j=\mathrm{NRMSE}_j(\mathrm{Seas-AR})-\mathrm{NRMSE}_j(c) are tested with the two-sided Wilcoxon signed-rank test (normal approximation without continuity correction), and the nine p-values are adjusted with Holm’s method. Negative \Delta favours Seas-AR.

t5 <- bind_rows(lapply(setdiff(METHOD_ORDER, "Seas-AR"), function(cc) {
  d <- Pm[, "Seas-AR"] - Pm[, cc]
  data.frame(competitor = cc, median_diff = median(d), wins = 100 * mean(d < 0),
             p = wilcox.test(Pm[, "Seas-AR"], Pm[, cc], paired = TRUE, exact = FALSE, correct = FALSE)$p.value)
}))
t5$p_holm <- p.adjust(t5$p, method = "holm")
write.csv(t5, out_file("exp1_tests.csv"), row.names = FALSE)
ptxt <- function(p) ifelse(p < 1e-4, "$<10^{-4}$", formatC(p, format = "f", digits = 4))
write_tex(c("\\begin{table}[!t]", "\\centering",
  sprintf("\\caption{Paired comparison of Seas-AR with each competing imputation method over the %d masking scenarios: median NRMSE difference (negative favours Seas-AR), percentage of scenarios in which Seas-AR is more accurate, and Holm-adjusted Wilcoxon signed-rank $p$-values. Friedman test: $\\chi^2_{%d}=%.1f$, $p<10^{-4}$.}", N_, k_ - 1, fr$statistic),
  "\\label{tab:tests}", "\\setlength{\\tabcolsep}{5pt}", "\\begin{tabular}{lrrr}", "\\toprule",
  "Competitor & Median $\\Delta$NRMSE & Seas-AR wins (\\%) & $p_{\\mathrm{Holm}}$\\\\", "\\midrule",
  sprintf("%s & %+.4f & %.1f & %s \\\\", t5$competitor, t5$median_diff, t5$wins, ptxt(t5$p_holm)),
  "\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_tests")
for (i in seq_len(nrow(t5))) { key(paste0("wil_meddiff_", t5$competitor[i]), t5$median_diff[i]); key(paste0("wil_wins_", t5$competitor[i]), t5$wins[i]) }
knitr::kable(t5 |> transmute(Competitor = competitor, `Median ΔNRMSE` = median_diff, `Seas-AR wins (%)` = wins,
                             `p (Holm)` = ifelse(p_holm < 1e-4, "< 1e-4", formatC(p_holm, format = "g", digits = 3))),
             digits = 4, caption = cap("Table 6", "Seas-AR versus each competitor (Wilcoxon signed-rank test, Holm correction)"))
Seas-AR versus each competitor (Wilcoxon signed-rank test, Holm correction)
Competitor Median ΔNRMSE Seas-AR wins (%) p (Holm)
MEAN -0.4537 97.4074 < 1e-4
MEDIAN -0.4797 98.1481 < 1e-4
LI -0.1262 73.7037 < 1e-4
SS-MEAN -0.3794 97.4074 < 1e-4
SS-MEDIAN -0.4021 97.4074 < 1e-4
SS-LI -0.3466 99.2593 < 1e-4
SEADEC -0.0487 66.6667 < 1e-4
MSTL-LI -0.0503 71.4815 < 1e-4
KALMAN -0.0733 68.1481 < 1e-4

6.6 Gap length, missing rate and station

reg_tab <- main |> group_by(regime, method) |> summarise(nrmse = mean(nrmse), .groups = "drop")
rel <- bind_rows(lapply(REG_NAMES, function(r) {
  g <- reg_tab |> filter(regime == r); comp <- g |> filter(method != "Seas-AR") |> slice_min(nrmse, n = 1)
  seasar <- g$nrmse[g$method == "Seas-AR"]; sel <- grepl(paste0("\\|", r, "\\|"), Pw$scenario)
  data.frame(regime = r, best = as.character(comp$method), nrmse_best = comp$nrmse, nrmse_seasar = seasar,
             rel_pct = 100 * (seasar / comp$nrmse - 1),
             beats_kalman = 100 * mean(Pm[sel, "Seas-AR"] < Pm[sel, "KALMAN"]), beats_li = 100 * mean(Pm[sel, "Seas-AR"] < Pm[sel, "LI"]),
             beats_seadec = 100 * mean(Pm[sel, "Seas-AR"] < Pm[sel, "SEADEC"]))
}))
write.csv(rel, out_file("exp1_regime.csv"), row.names = FALSE)
for (i in seq_len(nrow(rel))) for (cc in names(rel)[-1]) key(paste0("regime_", rel$regime[i], "_", cc), rel[[cc]][i])
by_rate <- main |> filter(method == "Seas-AR") |> group_by(rate) |> summarise(nrmse = mean(nrmse))
key("seasar_nrmse_rate10", by_rate$nrmse[by_rate$rate == 0.1]); key("seasar_nrmse_rate30", by_rate$nrmse[by_rate$rate == 0.3])
st_rank <- sapply(STATIONS, function(st) { sel <- startsWith(Pw$scenario, paste0(st, "|"))
  if (!any(sel)) return(rep(NA, length(METHOD_ORDER))); colMeans(t(apply(Pm[sel, , drop = FALSE], 1, rank))) })
rownames(st_rank) <- METHOD_ORDER
for (st in STATIONS) key(paste0("seasar_rank_", gsub(" ", "", st)), st_rank["Seas-AR", st])
suf <- t4 |> filter(method != "Seas-AR")
for (p in pc) {
  b <- suf$method[which.min(suf[[p]])]; key(paste0("suf1_best_", p), as.character(b)); key(paste0("suf1_bestval_", p), min(suf[[p]]))
  key(paste0("suf1_seasar_", p), t4[[p]][t4$method == "Seas-AR"]); key(paste0("suf1_rel_", p), 100 * (1 - t4[[p]][t4$method == "Seas-AR"] / min(suf[[p]])))
  key(paste0("suf1_vs_ssmedian_", p), 100 * (1 - t4[[p]][t4$method == "Seas-AR"] / t4[[p]][t4$method == "SS-MEDIAN"]))
}
knitr::kable(rel, digits = c(0, 0, 4, 4, 1, 1, 1, 1),
  col.names = c("Gap regime", "Best competitor", "NRMSE best competitor", "NRMSE Seas-AR", "Seas-AR vs best (%)",
                "Seas-AR beats KALMAN (%)", "Seas-AR beats LI (%)", "Seas-AR beats SEADEC (%)"),
  caption = "Seas-AR versus the best competitor in each gap regime")
Seas-AR versus the best competitor in each gap regime
Gap regime Best competitor NRMSE best competitor NRMSE Seas-AR Seas-AR vs best (%) Seas-AR beats KALMAN (%) Seas-AR beats LI (%) Seas-AR beats SEADEC (%)
short SEADEC 0.2918 0.3121 7.0 25.6 40.0 18.9
medium MSTL-LI 0.6221 0.5293 -14.9 93.3 93.3 92.2
long MSTL-LI 0.8787 0.6983 -20.5 85.6 87.8 88.9
knitr::kable(data.frame(method = METHOD_ORDER, st_rank, check.names = FALSE), digits = 2, caption = "Average rank per station")
Average rank per station
method SUF 1 SUF 6 SUF 7
MEAN MEAN 8.06 7.96 8.23
MEDIAN MEDIAN 9.30 9.18 8.99
LI LI 4.48 5.34 5.47
SS-MEAN SS-MEAN 7.10 6.03 6.00
SS-MEDIAN SS-MEDIAN 8.10 7.02 6.87
SS-LI SS-LI 6.07 7.13 6.70
SEADEC SEADEC 3.07 2.84 2.94
MSTL-LI MSTL-LI 3.39 3.42 3.50
KALMAN KALMAN 3.33 3.60 3.97
Seas-AR Seas-AR 2.11 2.47 2.33

6.7 Ablation study

Each variant removes one component of Seas-AR: no log transform (z=y/m), daily component only (W_t\equiv0), static profiles (h_D=h_W=\infty), no remainder model (\hat R_t=0), and linear interpolation of the remainder instead of the AR(1) bridge.

ABL_ORDER <- c("Seas-AR", "w/o log transform", "w/o weekly comp.", "static profiles", "remainder = 0", "remainder LI")
ABL_TEX <- c(`Seas-AR` = "Full Seas-AR", `w/o log transform` = "no log transform ($z=y/m$)",
             `w/o weekly comp.` = "daily component only ($W_t\\equiv0$)", `static profiles` = "static profiles ($h_D=h_W=\\infty$)",
             `remainder = 0` = "no remainder model ($\\hat R_t=0$)", `remainder LI` = "remainder by linear interpolation")
ab <- res1 |> filter(kind == "ablation" | method == "Seas-AR")
t6 <- ab |> group_by(method, regime) |> summarise(v = mean(nrmse), .groups = "drop") |>
  pivot_wider(names_from = regime, values_from = v) |>
  left_join(ab |> group_by(method) |> summarise(All = mean(nrmse)), by = "method") |>
  mutate(method = factor(method, levels = ABL_ORDER)) |> arrange(method)
write.csv(t6, out_file("exp1_ablation.csv"), row.names = FALSE)
cc6 <- c(intersect(REG_NAMES, names(t6)), "All")
for (i in seq_len(nrow(t6))) for (cc in cc6) key(paste0("abl_", gsub("[^A-Za-z0-9]", "", as.character(t6$method[i])), "_", cc), t6[[cc]][i])
tx6 <- lapply(cc6, function(cc) fmt_tex(t6[[cc]], 4))
write_tex(c("\\begin{table}[!t]", "\\centering",
  "\\caption{Ablation study: mean NRMSE over the 15 series (three stations $\\times$ five pollutants) by gap regime. Best values in bold.}",
  "\\label{tab:ablation}", "\\setlength{\\tabcolsep}{3.5pt}", "\\begin{tabular}{lrrrr}", "\\toprule",
  "Variant & Short & Medium & Long & All\\\\", "\\midrule",
  sapply(seq_len(nrow(t6)), function(i) paste0(paste(c(ABL_TEX[[as.character(t6$method[i])]], sapply(tx6, `[`, i)), collapse = " & "), " \\\\")),
  "\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_ablation")
md6 <- data.frame(Variant = as.character(t6$method), check.names = FALSE)
for (cc in cc6) md6[[cc]] <- fmt_md(t6[[cc]], 4)
knitr::kable(md6, align = "r", caption = cap("Table 7", "Ablation study (mean NRMSE)"))
Ablation study (mean NRMSE)
Variant short medium long All
Seas-AR 0.3121 0.5293 0.6983 0.5132
w/o log transform 0.3162 0.5364 0.7017 0.5181
w/o weekly comp. 0.2955 0.5213 0.6908 0.5025
static profiles 0.2974 0.5564 0.7401 0.5313
remainder = 0 0.6337 0.6261 0.7215 0.6604
remainder LI 0.3088 0.5569 0.8070 0.5575

6.8 Additional check: the original imputeTS::na_seadec

if (any(res1$kind == "extra")) {
  ex <- res1 |> filter(kind == "extra") |> mutate(scenario = paste(station, pollutant, regime, rate, rep, sep = "|"))
  jj <- ex |> select(scenario, ex = nrmse) |>
    inner_join(main |> filter(method == "Seas-AR") |> select(scenario, seasar = nrmse), by = "scenario") |>
    inner_join(main |> filter(method == "SEADEC") |> select(scenario, seadec = nrmse), by = "scenario")
  wx <- wilcox.test(jj$seasar, jj$ex, paired = TRUE, exact = FALSE, correct = FALSE)
  key("naseadec_nrmse", mean(jj$ex)); key("seadec_nrmse", mean(jj$seadec)); key("seasar_nrmse", mean(jj$seasar))
  key("naseadec_seasar_wins", 100 * mean(jj$seasar < jj$ex)); key("naseadec_p", wx$p.value)
  cat(sprintf("Mean NRMSE: na_seadec (robust STL) = %.4f | SEADEC (classical) = %.4f | Seas-AR = %.4f\n", mean(jj$ex), mean(jj$seadec), mean(jj$seasar)))
  cat(sprintf("Seas-AR is more accurate than na_seadec in %.1f%% of the scenarios (Wilcoxon p = %.2g)\n", 100 * mean(jj$seasar < jj$ex), wx$p.value))
} else cat("imputeTS is not available; check skipped.\n")
Mean NRMSE: na_seadec (robust STL) = 0.6126 | SEADEC (classical) = 0.6041 | Seas-AR = 0.5132
Seas-AR is more accurate than na_seadec in 83.7% of the scenarios (Wilcoxon p = 2.1e-28)

6.9 Illustration of a removed block

A four-day NO_2 block is chosen as the block starting at 00:30, between 7 May and the end of October, with the fewest missing values. It is removed and imputed by five methods. Each panel shows the true series (grey) and one imputation (colour) inside the shaded gap, with the RMSE over the block.

x7 <- get_series("SUF 1", "NO2"); L7 <- 48 * 4; ctx <- 48 * 2
i0 <- which(TS >= as.POSIXct("2018-05-07 00:30", tz = "UTC"))[1] - 1             # 0-based start
cand <- seq(i0, 304 * 48 - L7 - ctx - 1, by = 48)
i0 <- cand[which.min(sapply(cand, function(k) sum(is.na(x7[(k + 1):(k + L7)]))))]
blk <- (i0 + 1):(i0 + L7); xm7 <- x7; xm7[blk] <- NA; okb <- !is.na(x7[blk]); sl <- (i0 + 1 - ctx):(i0 + L7 + ctx)
M7 <- c("LI", "KALMAN", "SEADEC", "MSTL-LI", "Seas-AR")
f7 <- c(METHODS[c("LI", "KALMAN", "SEADEC", "MSTL-LI")],
        list(`Seas-AR` = function(v) impute_seasar(v, BW[["SUF 1"]]$bw_day, BW[["SUF 1"]]$bw_week)))
imp7 <- lapply(f7, function(f) f(xm7)); rm7 <- sapply(imp7, function(y) rmse(y[blk][okb], x7[blk][okb]))
for (mm in M7) key(paste0("example_rmse_", mm), rm7[[mm]])
lab7 <- setNames(sprintf("%s (RMSE %.1f)", M7, rm7[M7]), M7)
d7 <- bind_rows(lapply(M7, function(mm) data.frame(method = mm, t = TS[sl], y = ifelse(sl %in% blk, imp7[[mm]][sl], NA)))) |>
  mutate(panel = factor(lab7[method], levels = lab7))
truth7 <- tidyr::crossing(panel = factor(lab7, levels = lab7), data.frame(t = TS[sl], y = x7[sl]))
p7 <- ggplot() + annotate("rect", xmin = TS[min(blk)], xmax = TS[max(blk)], ymin = -Inf, ymax = Inf, fill = "grey93") +
  geom_line(data = truth7, aes(t, y), colour = "grey30", linewidth = 0.3, na.rm = TRUE) +
  geom_line(data = d7, aes(t, y, colour = method), linewidth = 0.6, na.rm = TRUE) +
  facet_wrap(~panel, ncol = 1) + scale_colour_manual(values = COL, guide = "none") +
  scale_x_datetime(date_breaks = "1 day", date_labels = "%d %b") +
  labs(x = NULL, y = expression(NO[2] ~ "[\u00b5g/m"^3 * "]"))
print(p7); save_fig(p7, "fig07_example_imputation", 7.16, 4.2)

key("example_block", paste(format(TS[min(blk)], "%d %b"), "-", format(TS[max(blk)], "%d %b")))

6.10 Seas-AR decomposition

Four weeks of log-scaled NO_2 at SUF 1 from 6 August 2018, split into its four components. Gaps longer than two hours are shaded red; inside them the imputed z_t and the AR(1) bridge for R_t are drawn in red with a pointwise 95% band from the closed-form conditional variance of the bridge. Weekends are shaded grey, which shows where the weekly deviation W_t departs from zero.

# Seas-AR decomposition of the log-scaled NO2 series of SUF 1 over four weeks. Gaps longer than two hours
# are shaded red; the imputed z_t and the AR(1) bridge for R_t are drawn in red with a pointwise 95% band from the
# closed-form conditional variance of the bridge; weekends are shaded grey.
dec <- impute_seasar(get_series("SUF 1", "NO2"), BW[["SUF 1"]]$bw_day, BW[["SUF 1"]]$bw_week, return_components = TRUE)
win <- which(TS >= as.POSIXct("2018-08-06 00:30", tz = "UTC"))[1] + 0:(48 * 28 - 1)
miss <- is.na(dec$z[win]); zfill <- (dec$comp$trend + dec$comp$daily + dec$comp$weekly + dec$remainder_imp)[win]
# conditional standard deviation of the bridge: gamma0 (1 - phi^2k)(1 - phi^2(n-k)) / (1 - phi^2n)
r <- dec$comp$remainder; n <- length(r); pr <- !is.na(r[-1]) & !is.na(r[-n])
phi <- min(0.999, max(0, sum(r[-1][pr] * r[-n][pr]) / sum(r[-n][pr]^2)))
g0 <- mean((r[-1][pr] - phi * r[-n][pr])^2) / (1 - phi^2)
sd_b <- rep(NA_real_, n); rr <- rle(is.na(r)); e <- cumsum(rr$lengths); s <- e - rr$lengths + 1
for (k in which(rr$values)) {
  a <- s[k] - 1; b <- e[k] + 1; i <- s[k]:e[k]
  sd_b[i] <- if (a >= 1 && b <= n) sqrt(g0 * (1 - phi^(2 * (i - a))) * (1 - phi^(2 * (b - i))) / (1 - phi^(2 * (b - a)))) else
             if (a >= 1) sqrt(g0 * (1 - phi^(2 * (i - a)))) else sqrt(g0 * (1 - phi^(2 * (b - i))))
}
LAB8 <- c(z = "atop(z[t], 'log scale')", T = "atop(T[t], 'trend')", D = "atop(D[t], 'daily')",
          W = "atop(W[t], 'weekly')", R = "atop(R[t], 'remainder')")
COL8 <- setNames(c("#2c3e50", "#1f618d", "#148f77", "#ca6f1e", "#7d3c98"), LAB8)
pf <- function(k) factor(unname(LAB8[k]), levels = LAB8)
d_obs <- bind_rows(data.frame(panel = pf("z"), t = TS[win], v = dec$z[win]),
                   data.frame(panel = pf("T"), t = TS[win], v = dec$comp$trend[win]),
                   data.frame(panel = pf("D"), t = TS[win], v = dec$comp$daily[win]),
                   data.frame(panel = pf("W"), t = TS[win], v = dec$comp$weekly[win]),
                   data.frame(panel = pf("R"), t = TS[win], v = dec$comp$remainder[win]))
d_imp <- bind_rows(data.frame(panel = pf("z"), t = TS[win], v = ifelse(miss, zfill, NA), sd = sd_b[win]),
                   data.frame(panel = pf("R"), t = TS[win], v = ifelse(miss, dec$remainder_imp[win], NA), sd = sd_b[win])) |>
  mutate(lo = v - 1.96 * sd, hi = v + 1.96 * sd)
rw <- rle(miss); ew <- cumsum(rw$lengths); sw <- ew - rw$lengths + 1; kw <- which(rw$values)
gaps8 <- data.frame(xmin = TS[win][sw[kw]] - 900, xmax = TS[win][ew[kw]] + 900, len = rw$lengths[kw])
gl <- gaps8[which.max(gaps8$len), ]
gl_lab <- data.frame(panel = pf("z"), t = gl$xmin + (gl$xmax - gl$xmin) / 2,
                     lab = if (gl$len >= 48) sprintf("%.1f-day gap", gl$len / 48) else sprintf("%g-h gap", gl$len / 2))
dd <- seq(as.Date(min(TS[win])), as.Date(max(TS[win])), by = "day"); sat <- dd[format(dd, "%u") == "6"]
wkend <- data.frame(xmin = pmax(as.POSIXct(paste(sat, "00:00"), tz = "UTC"), min(TS[win])),
                    xmax = pmin(as.POSIXct(paste(sat + 2, "00:00"), tz = "UTC"), max(TS[win])))
p8 <- ggplot() +
  geom_rect(data = wkend, aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf), fill = "grey93") +
  geom_rect(data = gaps8[gaps8$len > 4, ], aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf), fill = "#fadbd8") +
  geom_hline(data = data.frame(panel = pf(c("D", "W", "R")), y = 0), aes(yintercept = y), colour = "grey50",
             linewidth = 0.2, linetype = "22") +
  geom_ribbon(data = filter(d_obs, panel %in% pf(c("D", "W"))), aes(t, ymin = pmin(v, 0), ymax = pmax(v, 0), fill = panel),
              alpha = 0.25, na.rm = TRUE) +
  geom_ribbon(data = d_imp, aes(t, ymin = lo, ymax = hi), fill = "#e74c3c", alpha = 0.25) +   # NA rows split the band
  geom_line(data = d_obs, aes(t, v, colour = panel, linewidth = panel == LAB8[["T"]]), na.rm = TRUE) +
  geom_line(data = d_imp, aes(t, v), colour = "#c0392b", linewidth = 0.5, na.rm = TRUE) +
  geom_text(data = gl_lab, aes(t, Inf, label = lab), vjust = 1.5, size = 2.3, family = FONT_TEXT, colour = "#922b21") +
  facet_grid(panel ~ ., scales = "free_y", switch = "y", labeller = label_parsed) +
  scale_colour_manual(values = COL8, guide = "none") + scale_fill_manual(values = COL8, guide = "none") +
  scale_linewidth_manual(values = c(`FALSE` = 0.3, `TRUE` = 0.7), guide = "none") +
  scale_x_datetime(breaks = as.POSIXct(c("2018-08-13", "2018-08-20", "2018-08-27"), tz = "UTC"), date_labels = "%d %b",
                   expand = c(0, 0)) +
  scale_y_continuous(n.breaks = 3, expand = expansion(mult = c(0.06, 0.14))) +
  labs(x = NULL, y = NULL, title = expression(z[t] == T[t] + D[t] + W[t] + R[t]),
       subtitle = "red: gaps imputed by Seas-AR, with 95% band; grey: weekends") +
  theme(strip.placement = "outside", strip.text.y.left = element_text(angle = 0, size = 7.5),
        panel.grid.minor = element_blank(), panel.grid.major.y = element_line(linewidth = 0.15, colour = "grey88"),
        panel.grid.major.x = element_line(linewidth = 0.2, colour = "grey82"), panel.spacing.y = unit(5, "pt"),
        plot.title = element_text(size = 9, hjust = 0.5), plot.subtitle = element_text(size = 7, hjust = 0.5, colour = "grey30"),
        axis.text = element_text(size = 6.8), plot.margin = margin(4, 8, 4, 2))
print(p8); save_fig(p8, "fig08_seasar_components", 3.5, 4.6)

6.11 Computing time

for (mm in METHOD_ORDER) key(paste0("sec_", mm), t4$sec[t4$method == mm])
knitr::kable(t4 |> transmute(method, seconds = sec), digits = 3, caption = "Mean computing time per imputation of a full series (R, one CPU core)")
Mean computing time per imputation of a full series (R, one CPU core)
method seconds
MEAN 0.000
MEDIAN 0.000
LI 0.000
SS-MEAN 0.002
SS-MEDIAN 0.004
SS-LI 0.005
SEADEC 0.004
MSTL-LI 0.021
KALMAN 0.080
Seas-AR 0.023

7 Forecasting models and protocol

7.1 Data split, origins and causal imputation

An origin \tau is the number of observations available when the forecast is issued (at midnight); the targets are y_{\tau+1},\dots,y_{\tau+48} (day-ahead, horizons h=1,\dots,48).

  • Test: 1–20 December 2018, \tau_j=16{,}032+48(j-1), j=1,\dots,20 (960 target values).
  • Validation: origins in November (14{,}592\le\tau\le15{,}984, every 2 steps) for early stopping; the loss uses observed targets only.
  • Training: origins from 15 January to 31 October (672\le\tau\le14{,}544, every 6 steps, 2313 origins).

Three rules prevent leakage: (1) the history used for training is imputed with the data before 1 December only, \tilde y=\mathrm{Imp}(y_{1:16032}); (2) at every test origin the imputation is re-run causally, \tilde y^{(\tau)}=\mathrm{Imp}(y_{1:\tau}); (3) validation and test errors are computed on genuinely observed values only, while the training targets are the imputed series.

H <- 48L; L_SEQ <- 48L; TEST_START <- 334L * 48L; VAL_START <- 304L * 48L
MIN_ORIGIN <- 2L * S2; N_TEST_DAYS <- 20L
test_origins <- TEST_START + 48L * (0:(N_TEST_DAYS - 1))
tr_orig <- seq(MIN_ORIGIN, VAL_START - H, by = 6)
va_orig <- seq(VAL_START, TEST_START - H, by = 2)
hod <- function(j) j %% 48L                      # j = 0-based index; 0 = 00:30
dow <- function(j) (j %/% 48L) %% 7L             # 0 = Monday
cat("training origins:", length(tr_orig), "| validation origins:", length(va_orig), "| test origins:", length(test_origins),
    "(", format(TS[TEST_START + 1]), "to", format(TS[TEST_START + 960]), ")\n")
training origins: 2313 | validation origins: 697 | test origins: 20 ( 2018-12-01 00:30:00 to 2018-12-21 )

7.2 Seasonal naive and TSR (Eq. 10)

Seasonal naive: \hat y_{\tau+h}=\tilde y^{(\tau)}_{\tau+h-336}. TSR (additive double-seasonal time series regression):

y_t=\beta_0+\beta_1t+\sum_{i=2}^{48}\alpha_iH_{i,t}+\sum_{j=2}^{7}\gamma_jE_{j,t}+e_t, \tag{10}

with half-hour dummies H_{i,t} and weekday dummies E_{j,t} (base categories 00:30 and Monday), fitted by least squares on the whole imputed history. Time is scaled as t/10^4 for numerical stability; the scaling does not change the forecasts.

tsr_design <- function(j) {
  n <- length(j); hh <- hod(j); dd <- dow(j)
  Xh <- matrix(0, n, 47); Xh[cbind(which(hh > 0), hh[hh > 0])] <- 1
  Xd <- matrix(0, n, 6);  Xd[cbind(which(dd > 0), dd[dd > 0])] <- 1
  cbind(1, j / 1e4, Xh, Xd)
}
tsr_fit  <- function(y, j) qr.coef(qr(tsr_design(j)), y)
tsr_pred <- function(beta, j) { b <- beta; b[is.na(b)] <- 0; as.numeric(tsr_design(j) %*% b) }

7.3 LightGBM (direct multi-horizon model)

Each (origin \tau, horizon h) pair is one row with 14 features: h, half-hour and weekday of the target, the lags 1, 2, 3, 6 and 12 at the origin, the values 48, 96, 336 and 672 steps before the target, and the means of the last day and week. Settings: learning rate 0.03, 31 leaves, at least 50 samples per leaf, bagging 0.8, feature fraction 0.9, L_2 penalty 1, early stopping after 100 rounds (at most 2000 trees) on the observed validation targets.

gbm_rows <- function(y, tau) {
  h <- 0:(H - 1); tgt <- tau + h                                   # 0-based target indices
  cs <- c(0, cumsum(y)); m48 <- (cs[tau + 1] - cs[tau + 1 - 48]) / 48; m336 <- (cs[tau + 1] - cs[tau + 1 - 336]) / 336
  lag <- function(k) rep(y[tau - k + 1], H); sl <- function(k) y[tgt - k + 1]
  cbind(h = h, hod = hod(tgt), dow = dow(tgt), lag1 = lag(1), lag2 = lag(2), lag3 = lag(3), lag6 = lag(6),
        lag12 = lag(12), slag48 = sl(48), slag96 = sl(96), slag336 = sl(336), slag672 = sl(672),
        mean48 = m48, mean336 = m336)
}
build_gbm <- function(y, ytarget, origins) list(
  X = do.call(rbind, lapply(origins, function(t) gbm_rows(y, t))),
  Y = unlist(lapply(origins, function(t) ytarget[(t + 1):(t + H)])))
fit_predict_gbm <- function(y_hist, x_hist_raw, hist_at) {
  tr <- build_gbm(y_hist, y_hist, tr_orig); va <- build_gbm(y_hist, x_hist_raw, va_orig); ok <- !is.na(va$Y)
  dtr <- lightgbm::lgb.Dataset(tr$X, label = tr$Y)
  dva <- lightgbm::lgb.Dataset.create.valid(dtr, va$X[ok, , drop = FALSE], label = va$Y[ok])
  prm <- list(objective = "regression", learning_rate = 0.03, num_leaves = 31L, min_data_in_leaf = 50L,
              bagging_fraction = 0.8, bagging_freq = 1L, feature_fraction = 0.9, lambda_l2 = 1,
              num_threads = 1L, verbose = -1L, seed = 1L)
  gb <- lightgbm::lgb.train(params = prm, data = dtr, nrounds = 2000L, valids = list(valid = dva),
                            early_stopping_rounds = 100L, verbose = -1L)
  Xte <- do.call(rbind, lapply(seq_along(test_origins), function(k) gbm_rows(hist_at[[k]], test_origins[k])))
  matrix(predict(gb, Xte), nrow = N_TEST_DAYS, byrow = TRUE)
}

7.4 Neural networks: MLP, LSTM, TCN and the TSR–LSTM hybrid

All networks use standardised data and the same inputs: the sequence \mathbf s_\tau=(\tilde y_{\tau-47},\dots,\tilde y_\tau) and the side vector \mathbf v_\tau=(\tilde y_{\tau-335},\dots,\tilde y_{\tau-288},\sin\tfrac{2\pi h_\tau}{48},\cos\tfrac{2\pi h_\tau}{48},\sin\tfrac{2\pi d_\tau}{7},\cos\tfrac{2\pi d_\tau}{7}) (the same period one week earlier and calendar codes of the origin).

  • MLP: \hat{\mathbf y}=\mathbf W_3\,\mathrm{ReLU}(\mathbf W_2\,\mathrm{ReLU}(\mathbf W_1[\mathbf s_\tau;\mathbf v_\tau]+\mathbf b_1)+\mathbf b_2)+\mathbf b_3 (128 and 64 units).
  • LSTM: 64 units, \mathbf c_t=\mathbf f_t\odot\mathbf c_{t-1}+\mathbf i_t\odot\tilde{\mathbf c}_t and \mathbf h_t=\mathbf o_t\odot\tanh(\mathbf c_t) with sigmoid gates \mathbf i_t,\mathbf f_t,\mathbf o_t; \mathbf h_{48} is concatenated with \mathbf v_\tau, followed by Dense 64 (ReLU) and Dense 48.
  • TCN: five causal dilated convolutions (F*_dx)(t)=\sum_{i=0}^{2}f(i)\,x_{t-d\,i}, d\in\{1,2,4,8,16\}, 32 filters (receptive field 63), then as above.
  • TSR–LSTM: the LSTM forecasts the TSR residuals, \hat y_{\tau+h}=\hat L_{\tau+h}+\hat N_{\tau+h}.

Loss: masked mean squared error over observed targets, \mathcal L=\sum_{i,h}m_{ih}(y_{ih}-\hat y_{ih})^2/\sum_{i,h}m_{ih}. Adam (learning rate 2\times10^{-3}), batch 256, at most 20 epochs, early stopping with patience 3 and best weights restored; two seeds, forecasts averaged. Without keras3 only an MLP from nnet is available.

origin_cal <- function(tau) { t <- tau - 1; a <- 2 * pi * hod(t) / 48; b <- 2 * pi * dow(t) / 7
  c(sin(a), cos(a), sin(b), cos(b)) }
nn_inputs <- function(y, tau) list(seq = y[(tau - L_SEQ + 1):tau],
                                   side = c(y[(tau - S2 + 1):(tau - S2 + H)], origin_cal(tau)))
build_nn_samples <- function(y, ytarget, origins) {
  n <- length(origins); SEQ <- array(0, c(n, L_SEQ, 1)); SIDE <- matrix(0, n, H + 4); Y <- matrix(0, n, H)
  for (i in seq_len(n)) { tau <- origins[i]; inp <- nn_inputs(y, tau)
    SEQ[i, , 1] <- inp$seq; SIDE[i, ] <- inp$side; Y[i, ] <- ytarget[(tau + 1):(tau + H)] }
  list(seq = SEQ, side = SIDE, y = Y)
}
stack_test <- function(lst) list(seq = array(t(sapply(lst, `[[`, "seq")), c(length(lst), L_SEQ, 1)),
                                 side = t(sapply(lst, `[[`, "side")))
if (HAS_KERAS) {
  library(keras3)
  masked_mse <- function(y_true, y_pred) {        # mean squared error over observed targets only
    mask <- op_logical_not(op_isnan(y_true))
    diff <- op_where(mask, y_true - y_pred, op_zeros_like(y_pred))
    op_sum(op_square(diff)) / op_maximum(op_sum(op_cast(mask, "float32")), 1)
  }
  build_nn <- function(kind, units = 64, lr = 2e-3) {
    a <- keras_input(shape = c(L_SEQ, 1L)); b <- keras_input(shape = c(H + 4L))
    if (kind == "MLP") {
      z <- layer_concatenate(list(layer_flatten(a), b)) |>
        layer_dense(128, activation = "relu") |> layer_dense(64, activation = "relu")
    } else {
      if (kind %in% c("LSTM", "TSR-LSTM")) h <- a |> layer_lstm(units)
      if (kind == "TCN") {
        h <- a
        for (d in c(1, 2, 4, 8, 16)) h <- h |> layer_conv_1d(32, 3, dilation_rate = d, padding = "causal", activation = "relu")
        h <- h |> layer_cropping_1d(cropping = c(L_SEQ - 1L, 0L)) |> layer_flatten()   # last time step
      }
      z <- layer_concatenate(list(h, b)) |> layer_dense(64, activation = "relu")
    }
    m <- keras_model(list(a, b), z |> layer_dense(H))
    m |> compile(optimizer = optimizer_adam(lr), loss = masked_mse)
    m
  }
  fit_predict_nn <- function(kind, tr, va, te, seed, epochs = 20, patience = 3) {
    clear_session(); set_random_seed(seed)
    m <- build_nn(kind)
    m |> fit(list(tr$seq, tr$side), tr$y, validation_data = list(list(va$seq, va$side), va$y),
             epochs = epochs, batch_size = 256, verbose = 0,
             callbacks = list(callback_early_stopping(patience = patience, restore_best_weights = TRUE)))
    m |> predict(list(te$seq, te$side), verbose = 0)
  }
  NN_MODELS <- c("MLP", "LSTM", "TCN", "TSR-LSTM")
} else {
  fit_predict_nn <- function(kind, tr, va, te, seed, epochs = NA, patience = NA) {   # fallback without keras3
    set.seed(seed)
    Xtr <- cbind(matrix(tr$seq, nrow(tr$y)), tr$side); Xte <- cbind(matrix(te$seq, nrow(te$side)), te$side)
    fit <- nnet::nnet(Xtr, tr$y, size = 12, linout = TRUE, decay = 1e-3, maxit = 300, MaxNWts = 10000, trace = FALSE)
    predict(fit, Xte)
  }
  NN_MODELS <- "MLP"
}

7.5 One complete run: from imputation to forecasts

run_forecast() applies the protocol for one pollutant and one imputation method and returns the forecasts of all models (truncated at zero) and the RMSE of every seed.

run_forecast <- function(x_raw, impute_fn, seeds = SEEDS) {
  y_hist  <- impute_fn(x_raw[1:TEST_START])                                    # rule 1
  hist_at <- lapply(test_origins, function(tau) impute_fn(x_raw[1:tau]))        # rule 2 (causal)
  y_true  <- t(sapply(test_origins, function(tau) x_raw[(tau + 1):(tau + H)]))  # NA = not observed (rule 3)
  x_hist_raw <- x_raw[1:TEST_START]
  out <- list(); seed_rows <- list()
  out$SNAIVE <- t(sapply(seq_along(test_origins), function(k) {
    tau <- test_origins[k]; hist_at[[k]][(tau - S2 + 1):(tau - S2 + H)] }))
  j_hist <- 0:(TEST_START - 1); beta <- tsr_fit(y_hist, j_hist)
  out$TSR <- t(sapply(test_origins, function(tau) tsr_pred(beta, tau + 0:(H - 1))))
  if (HAS_LGB) out$LightGBM <- fit_predict_gbm(y_hist, x_hist_raw, hist_at)
  mu <- mean(y_hist[1:VAL_START]); sdv <- sd(y_hist[1:VAL_START])               # standardisation
  trs <- build_nn_samples((y_hist - mu) / sdv, (y_hist - mu) / sdv, tr_orig)
  vas <- build_nn_samples((y_hist - mu) / sdv, (x_hist_raw - mu) / sdv, va_orig)
  tes <- stack_test(lapply(seq_along(test_origins), function(k) nn_inputs((hist_at[[k]] - mu) / sdv, test_origins[k])))
  r_hist <- y_hist - tsr_pred(beta, j_hist); r_sd <- sd(r_hist[1:VAL_START])    # TSR residuals
  for (kind in NN_MODELS) {
    preds <- lapply(seeds, function(s) {
      if (kind == "TSR-LSTM") {
        trr <- build_nn_samples(r_hist / r_sd, r_hist / r_sd, tr_orig)
        var <- build_nn_samples(r_hist / r_sd, (x_hist_raw - tsr_pred(beta, j_hist)) / r_sd, va_orig)
        ter <- stack_test(lapply(seq_along(test_origins), function(k) {
          tau <- test_origins[k]; nn_inputs((hist_at[[k]] - tsr_pred(beta, 0:(tau - 1))) / r_sd, tau) }))
        fit_predict_nn(kind, trr, var, ter, s) * r_sd + out$TSR
      } else fit_predict_nn(kind, trs, vas, tes, s) * sdv + mu
    })
    for (i in seq_along(seeds)) seed_rows[[length(seed_rows) + 1]] <- data.frame(
      model = kind, seed = seeds[i], rmse = sqrt(mean((pmax(preds[[i]], 0) - y_true)^2, na.rm = TRUE)))
    out[[kind]] <- Reduce(`+`, preds) / length(preds)
  }
  fc <- bind_rows(lapply(names(out), function(mn) data.frame(model = mn,
    origin = rep(test_origins, each = H), h = rep(1:H, N_TEST_DAYS),
    y = as.vector(t(y_true)), yhat = pmax(as.vector(t(out[[mn]])), 0))))
  list(fc = fc, seeds = bind_rows(seed_rows))
}

7.6 Diebold–Mariano test (Eq. 11)

With d_t=e_{1t}^2-e_{2t}^2 (Seas-AR history minus alternative history),

\mathrm{DM}=\sqrt{\frac{N+1-2h+h(h-1)/N}{N}}\;\frac{\bar d}{\sqrt{\widehat{\mathrm{Var}}(\bar d)}},\qquad \widehat{\mathrm{Var}}(\bar d)=\frac1N\Big(\hat\gamma_0+2\sum_{k=1}^{h-1}\hat\gamma_k\Big), \tag{11}

with sample autocovariances \hat\gamma_k of d_t, h=48, the Harvey–Leybourne–Newbold factor and a t_{N-1} reference distribution; negative values favour Seas-AR. If the variance estimate is not positive, \hat\gamma_0/N is used. This equals forecast::dm.test(varestimator = "acf") (checked in Section 8.4).

dm_test <- function(e1, e2, h = 48, power = 2) {   # Eq. (11)
  ok <- !(is.na(e1) | is.na(e2)); d <- abs(e1[ok])^power - abs(e2[ok])^power
  n <- length(d); dc <- d - mean(d)
  gam <- sapply(0:(h - 1), function(k) sum(dc[(k + 1):n] * dc[1:(n - k)]) / n)
  v <- (gam[1] + 2 * sum(gam[-1])) / n
  if (v <= 0) v <- gam[1] / n
  stat <- mean(d) / sqrt(v) * sqrt((n + 1 - 2 * h + h * (h - 1) / n) / n)
  c(stat = stat, p = 2 * pt(-abs(stat), df = n - 1))
}

8 Experiment 2: impact of imputation on forecasting

8.1 Running the experiment

Five imputations are compared for seven models at SUF 1: MEDIAN (a common practical default), LI (the generic default), SEADEC (the strongest competitor in Experiment 1), KALMAN (state-space smoothing) and Seas-AR (bandwidths of SUF 1 from Experiment 0). Forecasts are stored per pollutant and imputation, so an interrupted run resumes where it stopped.

IMP_FC <- c("MEDIAN", "LI", "SEADEC", "KALMAN", "Seas-AR")
MODELS <- c("SNAIVE", "TSR", "LightGBM", "MLP", "LSTM", "TCN", "TSR-LSTM")
MODEL_TEX <- c(SNAIVE = "Seasonal na\\\"ive", TSR = "TSR", LightGBM = "LightGBM", MLP = "MLP", LSTM = "LSTM",
               TCN = "TCN", `TSR-LSTM` = "TSR--LSTM")
imputer_for <- function(imp) switch(imp, MEDIAN = METHODS$MEDIAN, LI = METHODS$LI, SEADEC = METHODS$SEADEC,
  KALMAN = METHODS$KALMAN, `Seas-AR` = function(v) impute_seasar(v, BW[["SUF 1"]]$bw_day, BW[["SUF 1"]]$bw_week))
dir2 <- out_file(if (QUICK) "exp2_quick" else "exp2"); dir.create(dir2, showWarnings = FALSE)
pols_fc <- if (QUICK) "NO2" else POLS; imps_fc <- if (QUICK) c("MEDIAN", "Seas-AR") else IMP_FC
if (!(HAS_KERAS && HAS_LGB)) cat("Note: the full model set needs lightgbm and keras3; missing models are skipped.\n")
Note: the full model set needs lightgbm and keras3; missing models are skipped.
for (p in pols_fc) for (imp in imps_fc) {
  ff <- file.path(dir2, sprintf("fc_%s_%s.csv", p, imp))
  if (isTRUE(P$use_cache) && file.exists(ff)) next
  t0 <- Sys.time()
  res <- run_forecast(get_series("SUF 1", p), imputer_for(imp), seeds = if (QUICK) SEEDS[1] else SEEDS)
  write.csv(res$fc |> mutate(pollutant = p, imputation = imp), ff, row.names = FALSE)
  write.csv(res$seeds |> mutate(pollutant = p, imputation = imp), sub("fc_", "seeds_", ff), row.names = FALSE)
  cat(p, imp, "finished in", round(as.numeric(Sys.time() - t0, units = "secs")), "s\n")
}
PM10 MEDIAN finished in 15 s
PM10 LI finished in 16 s
PM10 SEADEC finished in 16 s
PM10 KALMAN finished in 17 s
PM10 Seas-AR finished in 16 s
NO2 MEDIAN finished in 16 s
NO2 LI finished in 17 s
NO2 SEADEC finished in 17 s
NO2 KALMAN finished in 20 s
NO2 Seas-AR finished in 16 s
SO2 MEDIAN finished in 16 s
SO2 LI finished in 16 s
SO2 SEADEC finished in 16 s
SO2 KALMAN finished in 20 s
SO2 Seas-AR finished in 16 s
CO MEDIAN finished in 16 s
CO LI finished in 16 s
CO SEADEC finished in 17 s
CO KALMAN finished in 18 s
CO Seas-AR finished in 17 s
O3 MEDIAN finished in 16 s
O3 LI finished in 16 s
O3 SEADEC finished in 16 s
O3 KALMAN finished in 17 s
O3 Seas-AR finished in 16 s
fc_all <- bind_rows(lapply(list.files(dir2, pattern = "^fc_.*\\.csv$", full.names = TRUE), read.csv)) |>
  select(pollutant, imputation, model, origin, h, y, yhat)
seed_files <- list.files(dir2, pattern = "^seeds_.*\\.csv$", full.names = TRUE)
seeds_all <- if (length(seed_files)) bind_rows(lapply(seed_files, read.csv)) else NULL
cat("Experiment 2:", nrow(fc_all), "forecast rows | models:", paste(intersect(MODELS, unique(fc_all$model)), collapse = ", "), "\n")
Experiment 2: 96000 forecast rows | models: SNAIVE, TSR, LightGBM, MLP 

8.2 RMSE and MAE on the test days

\mathrm{RMSE}=\big(\frac{1}{|\mathcal T|}\sum_{(\tau,h)\in\mathcal T}(\hat y_{\tau+h}-y_{\tau+h})^2\big)^{1/2} over the pairs \mathcal T whose target is observed. Bold marks the best imputation for each model.

sc <- fc_all |> filter(!is.na(y)) |> group_by(pollutant, model, imputation) |>
  summarise(RMSE = sqrt(mean((yhat - y)^2)), MAE = mean(abs(yhat - y)), n = n(), .groups = "drop")
write.csv(sc, out_file("exp2_scores.csv"), row.names = FALSE)
wide7 <- function(metric) sc |> select(pollutant, model, imputation, v = all_of(metric)) |>
  pivot_wider(names_from = imputation, values_from = v) |>
  mutate(pollutant = factor(pollutant, levels = POLS), model = factor(model, levels = MODELS)) |> arrange(pollutant, model)
md7 <- function(metric) {
  w <- wide7(metric); ic <- intersect(IMP_FC, names(w)); M <- as.matrix(w[, ic])
  Mf <- do.call(rbind, lapply(seq_len(nrow(M)), function(i) fmt_md(M[i, ], if (w$pollutant[i] == "CO") 3 else 2)))
  colnames(Mf) <- ic; data.frame(Pollutant = as.character(w$pollutant), Model = as.character(w$model), Mf, check.names = FALSE)
}
w7 <- wide7("RMSE"); ic7 <- intersect(IMP_FC, names(w7)); rows7 <- c()
for (p in intersect(POLS, as.character(w7$pollutant))) {
  w <- w7 |> filter(pollutant == p); M <- as.matrix(w[, ic7]); best_all <- min(M, na.rm = TRUE); dg <- if (p == "CO") 3 else 2
  for (i in seq_len(nrow(w))) {
    cells <- sapply(seq_along(ic7), function(j) { v <- M[i, j]; s <- fmt_num(v, dg)
      if (abs(v - min(M[i, ], na.rm = TRUE)) < 1e-12) s <- paste0("\\textbf{", s, "}")
      if (abs(v - best_all) < 1e-12) s <- paste0("\\underline{", s, "}"); s })
    lead <- if (i == 1) sprintf("\\multirow{%d}{*}{%s}", nrow(w), POL_TEX[[p]]) else ""
    rows7 <- c(rows7, paste0(paste(c(lead, MODEL_TEX[[as.character(w$model[i])]], cells), collapse = " & "), " \\\\"))
  }
  rows7 <- c(rows7, "\\midrule")
}
write_tex(c("\\begin{table}[!t]", "\\centering",
  sprintf("\\caption{Day-ahead forecast RMSE on the 20 test days (1--20 Dec 2018, observed values only) for models trained on histories imputed by %d methods. Bold: best imputation for each model; underlined: best model--imputation pair for each pollutant.}", length(ic7)),
  "\\label{tab:forecast}", "\\setlength{\\tabcolsep}{2.6pt}", sprintf("\\begin{tabular}{ll%s}", strrep("r", length(ic7))), "\\toprule",
  paste0(" & Model & ", paste(ic7, collapse = " & "), "\\\\"), "\\midrule", head(rows7, -1),
  "\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_forecast")
knitr::kable(md7("RMSE"), align = "r", caption = cap("Table 8", "Day-ahead RMSE on the 20 test days (observed values)"))
Day-ahead RMSE on the 20 test days (observed values)
Pollutant Model MEDIAN LI SEADEC KALMAN Seas-AR
PM10 SNAIVE 47.44 47.44 47.44 47.44 47.44
PM10 TSR 50.92 52.57 52.54 49.39 51.68
PM10 LightGBM 35.15 35.49 36.90 37.77 37.92
PM10 MLP 34.65 42.87 36.91 38.10 34.30
NO2 SNAIVE 10.70 8.24 11.71 12.41 6.35
NO2 TSR 12.33 9.72 10.95 13.30 9.61
NO2 LightGBM 5.99 5.24 6.02 6.44 4.95
NO2 MLP 7.46 7.49 6.55 7.27 6.08
SO2 SNAIVE 23.94 23.82 23.84 23.75 23.59
SO2 TSR 33.84 35.91 36.03 35.44 35.31
SO2 LightGBM 20.04 20.21 20.15 20.20 20.07
SO2 MLP 25.79 24.67 24.02 24.80 25.32
CO SNAIVE 0.899 0.891 0.892 0.892 0.889
CO TSR 0.938 0.931 0.922 0.919 0.932
CO LightGBM 0.742 0.732 0.671 0.721 0.684
CO MLP 0.890 0.891 0.923 0.902 0.892
O3 SNAIVE 25.52 25.42 25.42 25.42 25.40
O3 TSR 29.36 29.54 29.53 29.53 29.52
O3 LightGBM 25.30 25.20 25.53 25.21 25.23
O3 MLP 32.43 26.61 27.16 27.52 28.10
knitr::kable(md7("MAE"), align = "r", caption = "Day-ahead MAE on the 20 test days (observed values)")
Day-ahead MAE on the 20 test days (observed values)
Pollutant Model MEDIAN LI SEADEC KALMAN Seas-AR
PM10 SNAIVE 26.93 26.93 26.93 26.93 26.93
PM10 TSR 30.39 33.16 33.09 28.43 31.46
PM10 LightGBM 19.75 20.00 20.31 20.76 20.24
PM10 MLP 20.26 23.86 20.64 20.24 18.68
NO2 SNAIVE 8.13 6.39 8.50 9.08 4.70
NO2 TSR 10.89 8.23 9.30 11.86 8.06
NO2 LightGBM 5.06 4.46 5.09 5.52 4.12
NO2 MLP 6.47 6.80 5.50 5.88 5.32
SO2 SNAIVE 8.57 8.18 8.19 8.12 7.97
SO2 TSR 30.62 33.00 33.10 32.46 32.24
SO2 LightGBM 9.03 9.82 9.37 8.78 9.12
SO2 MLP 20.47 18.99 17.91 19.29 20.02
CO SNAIVE 0.654 0.647 0.647 0.647 0.645
CO TSR 0.700 0.692 0.684 0.678 0.696
CO LightGBM 0.507 0.492 0.453 0.483 0.468
CO MLP 0.639 0.642 0.669 0.647 0.647
O3 SNAIVE 17.86 17.75 17.74 17.74 17.71
O3 TSR 26.08 26.28 26.27 26.27 26.26
O3 LightGBM 18.80 18.57 18.83 18.62 18.65
O3 MLP 26.68 20.76 21.18 22.10 21.15
bestp <- sc |> group_by(pollutant) |> slice_min(RMSE, n = 1, with_ties = FALSE) |> ungroup() |>
  mutate(pollutant = factor(pollutant, levels = POLS)) |> arrange(pollutant)
for (i in seq_len(nrow(bestp))) { p <- as.character(bestp$pollutant[i])
  key(paste0("bestpair_", p), paste(bestp$model[i], bestp$imputation[i])); key(paste0("bestpair_rmse_", p), bestp$RMSE[i]) }
knitr::kable(bestp, digits = 3, caption = "Best model-imputation pair for each pollutant")
Best model-imputation pair for each pollutant
pollutant model imputation RMSE MAE n
PM10 MLP Seas-AR 34.299 18.681 959
NO2 LightGBM Seas-AR 4.951 4.117 937
SO2 LightGBM MEDIAN 20.038 9.028 925
CO LightGBM SEADEC 0.671 0.453 942
O3 LightGBM LI 25.197 18.568 935

8.3 Change relative to median imputation

\Delta_{p,m,i}=100\big(\mathrm{RMSE}_{p,m,i}/\mathrm{RMSE}_{p,m,\mathrm{MEDIAN}}-1\big) for pollutant p, model m and imputation i; blue cells mean a lower error than with a median-imputed history.

ABBR <- c(MEDIAN = "MED", LI = "LI", SEADEC = "SEA", KALMAN = "KAL", `Seas-AR` = "S-AR")
d9 <- sc |> group_by(pollutant, model) |> mutate(change = 100 * (RMSE / RMSE[imputation == "MEDIAN"] - 1)) |> ungroup() |>
  mutate(pollutant = factor(pollutant, levels = POLS, labels = c("PM[10]", "NO[2]", "SO[2]", "CO", "O[3]")),
         model = factor(model, levels = rev(MODELS), labels = rev(c("Seasonal naive", MODELS[-1]))),
         imputation = factor(ABBR[imputation], levels = ABBR))
p9 <- ggplot(d9, aes(imputation, model, fill = pmax(pmin(change, 30), -30))) + geom_tile(colour = "white") +
  geom_text(aes(label = sprintf("%+.1f", change)), size = 1.9, family = FONT_TEXT) +
  scale_fill_gradient2(low = "#2166ac", high = "#b2182b", limits = c(-30, 30), name = "RMSE change\nvs MEDIAN (%)") +
  facet_wrap(~pollutant, nrow = 1, labeller = label_parsed) + labs(x = NULL, y = NULL) +
  theme(panel.grid = element_blank(), axis.text.x = element_text(size = 6), legend.key.height = unit(0.5, "cm"))
print(p9); save_fig(p9, "fig09_forecast_heatmap", 7.16, 2.3)

8.4 Diebold–Mariano tests

Each cell counts the seven models as W/T/L: Seas-AR significantly better / no significant difference / significantly worse at the 5% level. The first line checks dm_test() against forecast::dm.test().

ex_ref <- fc_all |> filter(pollutant == "NO2", model == "MLP", imputation == "Seas-AR") |> arrange(origin, h)
ex_oth <- fc_all |> filter(pollutant == "NO2", model == "MLP", imputation == "MEDIAN") |> arrange(origin, h)
if (nrow(ex_ref) && nrow(ex_oth)) {
  ok <- !is.na(ex_ref$y); e1 <- (ex_ref$yhat - ex_ref$y)[ok]; e2 <- (ex_oth$yhat - ex_oth$y)[ok]
  mine <- dm_test(e1, e2, h = 48); fdm <- suppressWarnings(forecast::dm.test(e1, e2, h = 48, power = 2, varestimator = "acf"))
  cat(sprintf("Check (NO2, MLP, Seas-AR vs MEDIAN): dm_test DM = %.4f (p = %.3g) | forecast::dm.test DM = %.4f (p = %.3g)\n",
              mine[["stat"]], mine[["p"]], fdm$statistic, fdm$p.value))
}
Check (NO2, MLP, Seas-AR vs MEDIAN): dm_test DM = -2.9493 (p = 0.00326) | forecast::dm.test DM = -2.9493 (p = 0.00326)
dm <- bind_rows(lapply(split(fc_all, list(fc_all$pollutant, fc_all$model), drop = TRUE), function(g) {
  ref <- g |> filter(imputation == "Seas-AR") |> arrange(origin, h)
  if (!nrow(ref)) return(NULL)
  bind_rows(lapply(setdiff(unique(g$imputation), "Seas-AR"), function(imp) {
    oth <- g |> filter(imputation == imp) |> arrange(origin, h)
    r <- dm_test(ref$yhat - ref$y, oth$yhat - oth$y, h = 48)
    data.frame(pollutant = ref$pollutant[1], model = ref$model[1], competitor = imp, DM = r[["stat"]], p = r[["p"]])
  }))
}))
write.csv(dm, out_file("exp2_dm.csv"), row.names = FALSE)
wtl_f <- function(d) d |> summarise(W = sum(p < .05 & DM < 0), L = sum(p < .05 & DM > 0), n = n(), .groups = "drop") |>
  mutate(T = n - W - L, cell = sprintf("%d/%d/%d", W, T, L))
wtl <- bind_rows(dm |> group_by(pollutant, competitor) |> wtl_f(), dm |> group_by(competitor) |> wtl_f() |> mutate(pollutant = "Total"))
write.csv(wtl, out_file("exp2_wtl.csv"), row.names = FALSE)
for (i in which(wtl$pollutant == "Total")) key(paste0("dm_total_", wtl$competitor[i]), wtl$cell[i])
comp_order <- intersect(setdiff(IMP_FC, "Seas-AR"), unique(dm$competitor))
t8 <- wtl |> select(pollutant, competitor, cell) |> pivot_wider(names_from = competitor, values_from = cell) |>
  mutate(pollutant = factor(pollutant, levels = c(POLS, "Total"))) |> arrange(pollutant)
rows8 <- sapply(seq_len(nrow(t8)), function(i) { p <- as.character(t8$pollutant[i])
  paste0(paste(c(if (p == "Total") "\\textit{Total}" else POL_TEX[[p]], unlist(t8[i, comp_order])), collapse = " & "), " \\\\") })
write_tex(c("\\begin{table}[!t]", "\\centering",
  "\\caption{Diebold--Mariano tests (squared loss, $h=48$, Harvey--Leybourne--Newbold correction) comparing forecasts obtained with Seas-AR-imputed histories against each alternative imputation. Each cell counts the seven forecasting models as W/T/L: Seas-AR significantly better / no significant difference / significantly worse at the 5\\% level.}",
  "\\label{tab:dm}", "\\setlength{\\tabcolsep}{5pt}", sprintf("\\begin{tabular}{l%s}", strrep("r", length(comp_order))), "\\toprule",
  paste0("Pollutant & ", paste(paste("vs", comp_order), collapse = " & "), "\\\\"), "\\midrule",
  head(rows8, -1), "\\midrule", tail(rows8, 1), "\\bottomrule", "\\end{tabular}", "\\end{table}"), "tab_dm")
knitr::kable(t8[, c("pollutant", comp_order)], col.names = c("Pollutant", paste("vs", comp_order)),
             caption = cap("Table 9", "Diebold-Mariano tests: Seas-AR vs each alternative (W/T/L over the seven models)"))
Diebold-Mariano tests: Seas-AR vs each alternative (W/T/L over the seven models)
Pollutant vs MEDIAN vs LI vs SEADEC vs KALMAN
PM10 0/3/1 1/3/0 1/3/0 1/2/1
NO2 4/0/0 3/1/0 2/2/0 2/2/0
SO2 1/2/1 1/3/0 1/2/1 1/3/0
CO 3/1/0 1/2/1 0/3/1 1/2/1
O3 0/3/1 1/3/0 2/2/0 1/3/0
Total 8/9/3 7/12/1 6/12/2 6/12/2

8.5 Summary over pollutants and models

M7 <- as.matrix(w7[, ic7]); best_imp <- ic7[apply(M7, 1, which.min)]
rk7 <- colMeans(t(apply(M7, 1, rank)))
chg <- colMeans(100 * (M7 / M7[, "MEDIAN"] - 1))
summ <- data.frame(imputation = ic7, best_for = as.integer(table(factor(best_imp, levels = ic7))), avg_rank = rk7, mean_change_vs_median = chg)
write.csv(summ, out_file("exp2_summary.csv"), row.names = FALSE)
for (i in seq_len(nrow(summ))) { key(paste0("fc_best_", summ$imputation[i]), summ$best_for[i]); key(paste0("fc_rank_", summ$imputation[i]), summ$avg_rank[i])
  key(paste0("fc_chg_", summ$imputation[i]), summ$mean_change_vs_median[i]) }
rel7 <- w7; for (cc in ic7) rel7[[cc]] <- 100 * (w7[[cc]] / w7$MEDIAN - 1)
chg_p <- rel7 |> group_by(pollutant) |> summarise(across(all_of(ic7), mean), .groups = "drop")
bestc <- w7 |> mutate(best = best_imp) |> count(pollutant, best) |> pivot_wider(names_from = best, values_from = n, values_fill = 0)
write.csv(chg_p, out_file("exp2_change_by_pollutant.csv"), row.names = FALSE); write.csv(bestc, out_file("exp2_bestcount_by_pollutant.csv"), row.names = FALSE)
for (i in seq_len(nrow(chg_p))) for (cc in ic7) key(paste0("chg_", chg_p$pollutant[i], "_", cc), chg_p[[cc]][i])
knitr::kable(summ, digits = 2, col.names = c("Imputation", "Best for (pairs)", "Average rank", "Mean RMSE change vs MEDIAN (%)"),
             caption = sprintf("Summary over the %d pollutant-model pairs", nrow(M7)))
Summary over the 20 pollutant-model pairs
Imputation Best for (pairs) Average rank Mean RMSE change vs MEDIAN (%)
MEDIAN MEDIAN 6 3.15 0.00
LI LI 2 3.15 -2.35
SEADEC SEADEC 2 3.15 -1.12
KALMAN KALMAN 2 3.20 1.23
Seas-AR Seas-AR 8 2.35 -5.60
knitr::kable(chg_p, digits = 2, caption = "Mean RMSE change vs MEDIAN (%) by pollutant")
Mean RMSE change vs MEDIAN (%) by pollutant
pollutant MEDIAN LI SEADEC KALMAN Seas-AR
PM10 0 6.98 3.67 3.60 2.08
NO2 0 -14.07 -3.35 7.20 -24.66
SO2 0 0.54 -0.07 0.22 0.31
CO 0 -0.69 -2.07 -1.04 -2.33
O3 0 -4.53 -3.79 -3.83 -3.39
knitr::kable(bestc, caption = "Number of models for which each imputation is best, by pollutant")
Number of models for which each imputation is best, by pollutant
pollutant KALMAN MEDIAN Seas-AR SEADEC LI
PM10 1 2 1 0 0
NO2 0 0 4 0 0
SO2 0 2 1 1 0
CO 1 1 1 1 0
O3 0 1 1 0 2

8.6 Example forecasts for NO_2

The best model with a Seas-AR history is shown for the first four test days, one panel per imputation: observed values in grey, forecasts in colour, and the test RMSE of that model–imputation pair in the panel title.

best_m <- sc |> filter(pollutant == "NO2", imputation == "Seas-AR") |> slice_min(RMSE, n = 1, with_ties = FALSE) |> pull(model)
rm10 <- sc |> filter(pollutant == "NO2", model == best_m) |> mutate(imputation = factor(imputation, levels = IMP_FC)) |> arrange(imputation)
lab10 <- setNames(sprintf("%s trained on a %s-imputed history (test RMSE %.2f)", best_m, rm10$imputation, rm10$RMSE), as.character(rm10$imputation))
key("fig10_model", best_m)
d10 <- fc_all |> filter(pollutant == "NO2", model == best_m, origin < min(origin) + 4 * 48) |>
  mutate(time = TS[origin + h], panel = factor(lab10[imputation], levels = lab10))
p10 <- ggplot(d10, aes(time)) + geom_line(aes(y = y), colour = "grey30", linewidth = 0.35, na.rm = TRUE) +
  geom_line(aes(y = yhat, colour = imputation), linewidth = 0.6) + facet_wrap(~panel, ncol = 1) +
  scale_colour_manual(values = COL[IMP_FC], guide = "none") +
  scale_x_datetime(date_breaks = "1 day", date_labels = "%d %b") +
  labs(x = NULL, y = expression(NO[2] ~ "[" * mu * "g/m"^3 * "]"))
print(p10); save_fig(p10, "fig10_forecast_example", 7.16, 4.2)

LS0tCnRpdGxlOiAiRG91YmxlLVNlYXNvbmFsIERlY29tcG9zaXRpb24gSW1wdXRhdGlvbiBXaXRoIGFuIEF1dG9yZWdyZXNzaXZlIEJyaWRnZSBmb3IgSGFsZi1Ib3VybHkgQWlyIFBvbGx1dGFudCBTZXJpZXMsIGFuZCBJdHMgSW1wYWN0IG9uIE1hY2hpbmUgTGVhcm5pbmcgYW5kIERlZXAgTGVhcm5pbmcgRm9yZWNhc3RpbmciCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiB0cnVlCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUKICAgIG1hdGhfbWV0aG9kOiBrYXRleApwYXJhbXM6CiAgZGF0YV9wYXRoOgogICAgbGFiZWw6ICJEYXRhIGZpbGUgKENTVikiCiAgICB2YWx1ZTogImFxbXNfc3VyYWJheWFfMjAxOC5jc3YiCiAgICBpbnB1dDogZmlsZQogIG1hc2tfZGlyOiAibWFza3MiCiAgb3V0X2RpcjogInJlc3VsdHNfUiIKICBmaWdfZGlyOiAiZmlndXJlc19SIgogIHRhYl9kaXI6ICJ0YWJsZXNfUiIKICBtYXNrX3NvdXJjZTogImZpbGUiCiAgdXNlX2NhY2hlOiB0cnVlCiAgcXVpY2s6IGZhbHNlCiAgc2VlZHM6IFsxLCAyXQogIHBhcGVyX3JlZnM6IGZhbHNlCi0tLQoKYGBge2NzcywgZWNobyA9IEZBTFNFfQp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpIHsKICB3aWR0aDogYXV0byAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IG1hcmdpbjogMC42ZW0gMCAxLjZlbSAwICFpbXBvcnRhbnQ7CiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTsgYm9yZGVyLXRvcDogMnB4IHNvbGlkICMyYzNlNTA7IGJvcmRlci1ib3R0b206IDJweCBzb2xpZCAjMmMzZTUwOwogIGZvbnQtc2l6ZTogMC45ZW07IGZvbnQtdmFyaWFudC1udW1lcmljOiB0YWJ1bGFyLW51bXM7Cn0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSBjYXB0aW9uIHsKICBjYXB0aW9uLXNpZGU6IHRvcDsgdGV4dC1hbGlnbjogbGVmdDsgY29sb3I6ICMyYzNlNTA7IGZvbnQtd2VpZ2h0OiA2MDA7IHBhZGRpbmc6IDAgMCA2cHggMDsKfQp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpID4gdGhlYWQgPiB0ciA+IHRoIHsKICBiYWNrZ3JvdW5kOiAjZjJmNWY4OyBib3JkZXItdG9wOiBub25lICFpbXBvcnRhbnQ7IGJvcmRlci1ib3R0b206IDFweCBzb2xpZCAjMmMzZTUwICFpbXBvcnRhbnQ7CiAgZm9udC13ZWlnaHQ6IDYwMDsgdmVydGljYWwtYWxpZ246IGJvdHRvbSAhaW1wb3J0YW50Owp9CnRhYmxlOm5vdCgudWktZGF0ZXBpY2tlci1jYWxlbmRhcikgPiB0aGVhZCA+IHRyID4gdGgsCnRhYmxlOm5vdCgudWktZGF0ZXBpY2tlci1jYWxlbmRhcikgPiB0Ym9keSA+IHRyID4gdGQgeyBwYWRkaW5nOiA0cHggMTBweCAhaW1wb3J0YW50OyBib3JkZXItdG9wOiBub25lICFpbXBvcnRhbnQ7IH0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSA+IHRoZWFkID4gdHIgPiB0aFthbGlnbj0icmlnaHQiXSB7IHRleHQtYWxpZ246IHJpZ2h0ICFpbXBvcnRhbnQ7IH0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSA+IHRoZWFkID4gdHIgPiB0aFthbGlnbj0iY2VudGVyIl0geyB0ZXh0LWFsaWduOiBjZW50ZXIgIWltcG9ydGFudDsgfQp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpID4gdGhlYWQgPiB0ciA+IHRoOmZpcnN0LWNoaWxkLAp0YWJsZTpub3QoLnVpLWRhdGVwaWNrZXItY2FsZW5kYXIpID4gdGJvZHkgPiB0ciA+IHRkOmZpcnN0LWNoaWxkIHsgdGV4dC1hbGlnbjogbGVmdCAhaW1wb3J0YW50OyB3aGl0ZS1zcGFjZTogbm93cmFwOyB9CnRhYmxlOm5vdCgudWktZGF0ZXBpY2tlci1jYWxlbmRhcikgPiB0Ym9keSA+IHRyOm50aC1jaGlsZChldmVuKSB7IGJhY2tncm91bmQ6ICNmOGZhZmM7IH0KdGFibGU6bm90KC51aS1kYXRlcGlja2VyLWNhbGVuZGFyKSA+IHRib2R5ID4gdHI6aG92ZXIgeyBiYWNrZ3JvdW5kOiAjZTlmMWZhOyB9CmBgYAoKIyBEYXRhCgojIyBOb3RhdGlvbiBhbmQgdGltZSBpbmRleAoKRWFjaCBzZXJpZXMgaXMgJHlfMSxcZG90cyx5X25cZ2UwJCB3aXRoICRuPTE2eyx9OTkyJCBoYWxmLWhvdXJseSByZWNvcmRzIGZyb20gMSBKYW51YXJ5IDIwMTggMDA6MzAKdG8gMjEgRGVjZW1iZXIgMjAxOCAwMDowMCwgdGhhdCBpcywgMzU0IGRhaWx5IGJsb2NrcyBydW5uaW5nIGZyb20gMDA6MzAgdG8gMjQ6MDAuIEZvciBlYWNoIHRpbWUgJHQkLAoKJCQKaCh0KT1cYmlnKCh0LTEpXGJtb2QgNDhcYmlnKSsxLFxxdWFkIGsodCk9XEJpZ1xsY2VpbCBcdGZyYWN7dH17NDh9XEJpZ1xyY2VpbCxccXVhZApkKHQpPVxiaWcoKGsodCktMSlcYm1vZCA3XGJpZykrMSxccXVhZCBcZWxsKHQpPVxCaWdcbGNlaWwgXHRmcmFje3R9ezMzNn1cQmlnXHJjZWlsCiQkCgphcmUgdGhlIGhhbGYtaG91ciBvZiB0aGUgZGF5LCB0aGUgZGF5LCB0aGUgd2Vla2RheSAoMSA9IE1vbmRheSkgYW5kIHRoZSB3ZWVrLiBUaGUgZGFpbHkgcGVyaW9kIGlzCiRzXzE9NDgkIGFuZCB0aGUgd2Vla2x5IHBlcmlvZCBpcyAkc18yPTMzNiQuICRcbWF0aGNhbCBPJCBkZW5vdGVzIHRoZSBvYnNlcnZlZCBpbmRpY2VzIGFuZAokXG1hdGhjYWwgTT1cezEsXGRvdHMsblx9XHNldG1pbnVzXG1hdGhjYWwgTyQgdGhlIG1pc3Npbmcgb25lcy4gVGhlIGNvZGUgY2hlY2tzIHRoYXQgdGhlIGNvbHVtbnMgYEpgCmFuZCBgSGFyaWAgb2YgdGhlIGZpbGUgZXF1YWwgJGgodCkkIGFuZCAkZCh0KSQuCgpgYGB7ciBkYXRhfQppZiAoIWZpbGUuZXhpc3RzKFAkZGF0YV9wYXRoKSkKICBzdG9wKCJEYXRhIGZpbGUgbm90IGZvdW5kOiAiLCBQJGRhdGFfcGF0aCwgIi4gUHV0IGFxbXNfc3VyYWJheWFfMjAxOC5jc3YgaW4gdGhlIGRhdGEvIGZvbGRlciBuZXh0IHRvIHRoaXMgbm90ZWJvb2sgIiwKICAgICAgICJvciBzZXQgZGF0YV9wYXRoIGluIHRoZSBZQU1MIGhlYWRlci4iLCBjYWxsLiA9IEZBTFNFKQpyYXcgPC0gcmVhZC5jc3YoUCRkYXRhX3BhdGgsIGNoZWNrLm5hbWVzID0gRkFMU0UpCm5hbWVzKHJhdykgPC0gZ3N1YigiXyIsICIgIiwgbmFtZXMocmF3KSkKTiAgPC0gbnJvdyhyYXcpOyBTMSA8LSA0OEw7IFMyIDwtIDMzNkwKVFMgPC0gc2VxKGFzLlBPU0lYY3QoIjIwMTgtMDEtMDEgMDA6MzAiLCB0eiA9ICJVVEMiKSwgYnkgPSAiMzAgbWluIiwgbGVuZ3RoLm91dCA9IE4pClBPTFMgICAgIDwtIGMoIlBNMTAiLCAiTk8yIiwgIlNPMiIsICJDTyIsICJPMyIpClNUQVRJT05TIDwtIGMoIlNVRiAxIiwgIlNVRiA2IiwgIlNVRiA3IikKVU5JVCA8LSBjKFBNMTAgPSAiXHUwMGI1Zy9tXHUwMGIzIiwgTk8yID0gIlx1MDBiNWcvbVx1MDBiMyIsIFNPMiA9ICJcdTAwYjVnL21cdTAwYjMiLCBDTyA9ICJtZy9tXHUwMGIzIiwgTzMgPSAiXHUwMGI1Zy9tXHUwMGIzIikKaF9vZiA8LSBmdW5jdGlvbih0KSAoKHQgLSAxKSAlJSBTMSkgKyAxICAgICAgICAgICAgICAgICAjIGhhbGYtaG91ciBvZiB0aGUgZGF5ICgxID0gMDA6MzApCmRfb2YgPC0gZnVuY3Rpb24odCkgKCgodCAtIDEpICUvJSBTMSkgJSUgNykgKyAxICAgICAgICAgIyB3ZWVrZGF5ICgxID0gTW9uZGF5KQpTRVJJRVMgPC0gcGFzdGUocmVwKFNUQVRJT05TLCBlYWNoID0gNSksIFBPTFMpCmlmICghYWxsKGMoIkoiLCAiSGFyaSIsIFNFUklFUykgJWluJSBuYW1lcyhyYXcpKSkKICBzdG9wKCJUaGUgZGF0YSBmaWxlIGRvZXMgbm90IGhhdmUgdGhlIGV4cGVjdGVkIGNvbHVtbnMgKEosIEhhcmkgYW5kICdTVUYgMSBDTycgLi4uICdTVUYgNyBTTzInKS4iLCBjYWxsLiA9IEZBTFNFKQpzdG9waWZub3QoTiA9PSAxNjk5MiwgYWxsKHJhdyRKID09IGhfb2Yoc2VxX2xlbihOKSkpLCBhbGwocmF3JEhhcmkgPT0gZF9vZihzZXFfbGVuKE4pKSkpCmdldF9zZXJpZXMgPC0gZnVuY3Rpb24oc3QsIHApIGFzLm51bWVyaWMocmF3W1twYXN0ZShzdCwgcCldXSkKIyBjaGVjayBhZ2FpbnN0IHRoZSByZWZlcmVuY2UgZGF0YSwgZnJvbSB3aGljaCBhbGwgc3RvcmVkIHJlc3VsdHMgd2VyZSBjb21wdXRlZApYIDwtIHNhcHBseShTRVJJRVMsIGZ1bmN0aW9uKHYpIGFzLm51bWVyaWMocmF3W1t2XV0pKQpkYXRhX2NoZWNrIDwtIGRhdGEuZnJhbWUoaXRlbSA9IGMoInJvd3MiLCAibWlzc2luZyB2YWx1ZXMiLCAic3VtIG9mIHRoZSBvYnNlcnZlZCB2YWx1ZXMiKSwKICAgICAgICAgICAgICAgICAgICAgICAgIGZvdW5kID0gYyhOLCBzdW0oaXMubmEoWCkpLCBzdW0oWCwgbmEucm0gPSBUUlVFKSksIHJlZmVyZW5jZSA9IGMoMTY5OTIsIDMzMjUwLCA1MDIyNjQ2LjM3NTczNDI0MzUpKQpkYXRhX2NoZWNrJGlkZW50aWNhbCA8LSBhYnMoZGF0YV9jaGVjayRmb3VuZCAtIGRhdGFfY2hlY2skcmVmZXJlbmNlKSA8PSAxZS02ICogcG1heCgxLCBhYnMoZGF0YV9jaGVjayRyZWZlcmVuY2UpKQppZiAoIWFsbChkYXRhX2NoZWNrJGlkZW50aWNhbCkpIGNhdCgiTk9URTogdGhlIGRhdGEgZGlmZmVyIGZyb20gdGhlIHJlZmVyZW5jZSBkYXRhOyBzZXQgdXNlX2NhY2hlOiBmYWxzZSBzbyB0aGF0IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImV2ZXJ5IHJlc3VsdCBpcyByZWNvbXB1dGVkIGZyb20gdGhlc2UgZGF0YS5cbiIpCmtuaXRyOjprYWJsZSh0cmFuc2Zvcm0oZGF0YV9jaGVjaywgZm91bmQgPSBzcHJpbnRmKGMoIiUuMGYiLCAiJS4wZiIsICIlLjRmIiksIGZvdW5kKSwKICAgICAgICAgICAgICAgICAgICAgICByZWZlcmVuY2UgPSBzcHJpbnRmKGMoIiUuMGYiLCAiJS4wZiIsICIlLjRmIiksIHJlZmVyZW5jZSkpLAogICAgICAgICAgICAgY2FwdGlvbiA9ICJEYXRhIGNoZWNrIGFnYWluc3QgdGhlIHJlZmVyZW5jZSBkYXRhIHNldCIpClRFU1RfREFURSA8LSBhcy5QT1NJWGN0KCIyMDE4LTEyLTAxIDAwOjMwIiwgdHogPSAiVVRDIikKY2F0KCJuID0iLCBOLCAifCBwZXJpb2Q6IiwgZm9ybWF0KFRTWzFdKSwgInRvIiwgZm9ybWF0KFRTW05dKSwgInwgY29sdW1ucyBKIGFuZCBIYXJpIHZlcmlmaWVkXG4iKQpgYGAKCiMjIE1pc3NpbmduZXNzIGFuZCBkZXNjcmlwdGl2ZSBzdGF0aXN0aWNzCgoKYGBge3IgdGFibGUxfQpnYXBfbGVuZ3RocyA8LSBmdW5jdGlvbih4KSB7IHIgPC0gcmxlKGlzLm5hKHgpKTsgciRsZW5ndGhzW3IkdmFsdWVzXSB9CnRhYjEgPC0gYmluZF9yb3dzKGxhcHBseShQT0xTLCBmdW5jdGlvbihwKSB7CiAgeCA8LSBnZXRfc2VyaWVzKCJTVUYgMSIsIHApOyBnIDwtIGdhcF9sZW5ndGhzKHgpOyBvIDwtIHhbIWlzLm5hKHgpXQogIGRhdGEuZnJhbWUocG9sbHV0YW50ID0gcCwgdW5pdCA9IFVOSVRbW3BdXSwgbWlzc2luZyA9IDEwMCAqIG1lYW4oaXMubmEoeCkpLCBnYXBzID0gbGVuZ3RoKGcpLAogICAgICAgICAgICAgbWVkaWFuX2dhcCA9IGZsb29yKG1lZGlhbihnKSksIG1heF9nYXAgPSBtYXgoZyksIGluX2xvbmcgPSAxMDAgKiBzdW0oZ1tnID4gNDhdKSAvIHN1bShnKSwKICAgICAgICAgICAgIG1lYW4gPSBtZWFuKG8pLCBtZWRpYW4gPSBtZWRpYW4obyksIHNkID0gc2RfcG9wKG8pLCBtYXggPSBtYXgobyksCiAgICAgICAgICAgICBzdWY2ID0gMTAwICogbWVhbihpcy5uYShnZXRfc2VyaWVzKCJTVUYgNiIsIHApKSksIHN1ZjcgPSAxMDAgKiBtZWFuKGlzLm5hKGdldF9zZXJpZXMoIlNVRiA3IiwgcCkpKSkKfSkpCndyaXRlLmNzdih0YWIxLCBvdXRfZmlsZSgidGFibGUxX2RhdGEuY3N2IiksIHJvdy5uYW1lcyA9IEZBTFNFKQp3cml0ZV90ZXgoYygiXFxiZWdpbnt0YWJsZSp9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgIlxcY2FwdGlvbntNaXNzaW5nbmVzcyBhbmQgZGVzY3JpcHRpdmUgc3RhdGlzdGljcyBvZiB0aGUgaGFsZi1ob3VybHkgcG9sbHV0YW50IHNlcmllcyAoMX5KYW4tLTIwfkRlYyAyMDE4LCAkbj0xNnssfTk5MiQgcGVyIHNlcmllcykuIEdhcCBzdGF0aXN0aWNzIHJlZmVyIHRvIFNVRn4xOyB0aGUgbGFzdCB0d28gY29sdW1ucyBnaXZlIHRoZSBtaXNzaW5nIHJhdGUgYXQgdGhlIHR3byBkZXZlbG9wbWVudCBzdGF0aW9ucy59IiwKICAiXFxsYWJlbHt0YWI6ZGF0YX0iLCAiXFxzZXRsZW5ndGh7XFx0YWJjb2xzZXB9ezRwdH0iLCAiXFxiZWdpbnt0YWJ1bGFyfXtsbHJycnJycnJycnJyfSIsICJcXHRvcHJ1bGUiLAogICIgJiAmIFxcbXVsdGljb2x1bW57NX17Y317U1VGfjEgbWlzc2luZ25lc3N9ICYgXFxtdWx0aWNvbHVtbns0fXtjfXtTVUZ+MSBvYnNlcnZlZCB2YWx1ZXN9ICYgXFxtdWx0aWNvbHVtbnsyfXtjfXtNaXNzaW5nIChcXCUpfVxcXFwiLAogICJcXGNtaWRydWxlKGxyKXszLTd9XFxjbWlkcnVsZShscil7OC0xMX1cXGNtaWRydWxlKGxyKXsxMi0xM30iLAogICJQb2xsdXRhbnQgJiBVbml0ICYgTWlzc2luZyAoXFwlKSAmIEdhcHMgJiBNZWRpYW4gZ2FwICYgTWF4LiBnYXAgJiBJbiBnYXBzICQ+JDFcXCxkIChcXCUpICYgTWVhbiAmIE1lZGlhbiAmIFNEICYgTWF4LiAmIFNVRn42ICYgU1VGfjdcXFxcIiwKICAiXFxtaWRydWxlIiwKICB3aXRoKHRhYjEsIHNwcmludGYoIiVzICYgJXMgJiAlLjFmICYgJWQgJiAlZCAmICVzICYgJS4xZiAmICUuMmYgJiAlLjJmICYgJS4yZiAmICUuMWYgJiAlLjFmICYgJS4xZlxcXFwiLAogICAgUE9MX1RFWFtwb2xsdXRhbnRdLCBVTklUX1RFWFtwb2xsdXRhbnRdLCBtaXNzaW5nLCBnYXBzLCBhcy5pbnRlZ2VyKG1lZGlhbl9nYXApLAogICAgZm9ybWF0QyhtYXhfZ2FwLCBmb3JtYXQgPSAiZCIsIGJpZy5tYXJrID0gIiwiKSwgaW5fbG9uZywgbWVhbiwgbWVkaWFuLCBzZCwgbWF4LCBzdWY2LCBzdWY3KSksCiAgIlxcYm90dG9tcnVsZSIsICJcXGVuZHt0YWJ1bGFyfSIsICJcXGVuZHt0YWJsZSp9IiksICJ0YWJfZGF0YSIpCmtuaXRyOjprYWJsZSh0YWIxLCBkaWdpdHMgPSBjKDAsIDAsIDEsIDAsIDAsIDAsIDEsIDIsIDIsIDIsIDEsIDEsIDEpLAogIGNvbC5uYW1lcyA9IGMoIlBvbGx1dGFudCIsICJVbml0IiwgIk1pc3NpbmcgKCUpIiwgIkdhcHMiLCAiTWVkaWFuIGdhcCIsICJNYXguIGdhcCIsICJJbiBnYXBzID4gMSBkICglKSIsCiAgICAgICAgICAgICAgICAiTWVhbiIsICJNZWRpYW4iLCAiU0QiLCAiTWF4LiIsICJTVUYgNiBtaXNzaW5nICglKSIsICJTVUYgNyBtaXNzaW5nICglKSIpLAogIGNhcHRpb24gPSBjYXAoIlRhYmxlIDIiLCAiTWlzc2luZ25lc3MgYW5kIGRlc2NyaXB0aXZlIHN0YXRpc3RpY3MgKGdhcCBzdGF0aXN0aWNzOiBTVUYgMSkiKSkKYGBgCgojIyBNaXNzaW5nIHZhbHVlcyBvbiB0aGUgdGltZSBheGlzCgpQYW5lbCAoYSkgc2hvd3MgdGhlIGZpdmUgaGFsZi1ob3VybHkgc2VyaWVzIG9mIFNVRiAxIG9uIHRoZWlyIHJlYWwgdGltZXN0YW1wcy4gRWFjaCBydW4gb2YgbWlzc2luZyB2YWx1ZXMKaXMgc2hhZGVkIHJlZCwgYXMgaW4gYGltcHV0ZVRTOjpnZ3Bsb3RfbmFfZGlzdHJpYnV0aW9uKClgLCBhbmQgdGhlIGxhYmVsIGdpdmVzIHRoZSBzaGFyZSBvZiBtaXNzaW5nIHZhbHVlczsKdGhlIGJsdWUgYmFuZCBpcyB0aGUgdGVzdCBwZXJpb2QuIFBhbmVsIChiKSBzcGxpdHMgdGhlIG1pc3NpbmcgdmFsdWVzIGJ5IGdhcCBsZW5ndGguIFRoZSBkYXRhIGZyYW1lCmBnYXBzX2RmYCwgd2l0aCB0aGUgc3RhcnQgYW5kIGVuZCBvZiBldmVyeSBnYXAsIGlzIHJldXNlZCBmb3IgdGhlIG92ZXJ2aWV3IGRpYWdyYW0gYXQgdGhlIGVuZC4KCmBgYHtyIGZpZzEsIGZpZy53aWR0aD03LjE2LCBmaWcuaGVpZ2h0PTMuNTV9CiMgZmlnMDFfbWlzc2luZ25lc3M6IChhKSB0aGUgaGFsZi1ob3VybHkgc2VyaWVzIG9mIFNVRiAxIG9uIHRoZWlyIHJlYWwgdGltZXN0YW1wcy4gRXZlcnkgcnVuIG9mIG1pc3NpbmcgdmFsdWVzIGlzCiMgbWFya2VkIGJ5IGEgcmVkIHRpY2sgYXQgdGhlIGJvdHRvbSwgZ2FwcyBsb25nZXIgdGhhbiB0d28gaG91cnMgYXJlIGFsc28gc2hhZGVkIHJlZCAodGhlIHZpZXcgb2YKIyBpbXB1dGVUUzo6Z2dwbG90X25hX2Rpc3RyaWJ1dGlvbiksIGFuZCB0aGUgdGVzdCBwZXJpb2QgaXMgc2hhZGVkIGJsdWUuIChiKSBTaGFyZSBvZiB0aGUKIyBtaXNzaW5nIHZhbHVlcyBieSBnYXAgbGVuZ3RoLgpuYV9ydW5zIDwtIGZ1bmN0aW9uKHN0LCBwb2xzID0gUE9MUykgYmluZF9yb3dzKGxhcHBseShwb2xzLCBmdW5jdGlvbihwKSB7CiAgeCA8LSBnZXRfc2VyaWVzKHN0LCBwKTsgciA8LSBybGUoaXMubmEoeCkpOyBlIDwtIGN1bXN1bShyJGxlbmd0aHMpOyBzIDwtIGUgLSByJGxlbmd0aHMgKyAxCiAgZGF0YS5mcmFtZShzdGF0aW9uID0gc3QsIHBvbGx1dGFudCA9IHAsIHN0YXJ0ID0gVFNbc1tyJHZhbHVlc11dLCBlbmQgPSBUU1tlW3IkdmFsdWVzXV0gKyAxODAwLAogICAgICAgICAgICAgbGVuID0gciRsZW5ndGhzW3IkdmFsdWVzXSkKfSkpClBPTF9MQUIgPC0gYyhQTTEwID0gImF0b3AoUE1bMTBdLCAnXHUwMGI1Zy9tXHUwMGIzJykiLCBOTzIgPSAiYXRvcChOT1syXSwgJ1x1MDBiNWcvbVx1MDBiMycpIiwKICAgICAgICAgICAgIFNPMiA9ICJhdG9wKFNPWzJdLCAnXHUwMGI1Zy9tXHUwMGIzJykiLCBDTyA9ICJhdG9wKENPLCAnbWcvbVx1MDBiMycpIiwgTzMgPSAiYXRvcChPWzNdLCAnXHUwMGI1Zy9tXHUwMGIzJykiKQpwbG90X25hX3NlcmllcyA8LSBmdW5jdGlvbihzdGF0aW9ucywgdGl0bGUpIHsKICBkIDwtIGJpbmRfcm93cyhsYXBwbHkoc3RhdGlvbnMsIGZ1bmN0aW9uKHN0KSBiaW5kX3Jvd3MobGFwcGx5KFBPTFMsIGZ1bmN0aW9uKHApCiAgICAgICAgIGRhdGEuZnJhbWUoc3RhdGlvbiA9IHN0LCBwb2xsdXRhbnQgPSBwLCB0ID0gVFMsIHkgPSBnZXRfc2VyaWVzKHN0LCBwKSkpKSkpIHw+CiAgICAgICBtdXRhdGUocG9sbHV0YW50ID0gZmFjdG9yKHBvbGx1dGFudCwgUE9MUykpCiAgZyA8LSBiaW5kX3Jvd3MobGFwcGx5KHN0YXRpb25zLCBuYV9ydW5zKSkgfD4gbXV0YXRlKHBvbGx1dGFudCA9IGZhY3Rvcihwb2xsdXRhbnQsIFBPTFMpKQogIHIgPC0gZCB8PiBncm91cF9ieShzdGF0aW9uLCBwb2xsdXRhbnQpIHw+IHN1bW1hcmlzZShyYXRlID0gbWVhbihpcy5uYSh5KSksIC5ncm91cHMgPSAiZHJvcCIpCiAgZ2dwbG90KGQsIGFlcyh0LCB5KSkgKwogICAgYW5ub3RhdGUoInJlY3QiLCB4bWluID0gVEVTVF9EQVRFLCB4bWF4ID0gbWF4KFRTKSwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYsIGZpbGwgPSAiIzI0NzFhMyIsIGFscGhhID0gMC4xOCkgKwogICAgZ2VvbV9yZWN0KGRhdGEgPSBnW2ckbGVuID4gNCwgXSwgYWVzKHhtaW4gPSBzdGFydCwgeG1heCA9IGVuZCwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYpLCBpbmhlcml0LmFlcyA9IEZBTFNFLAogICAgICAgICAgICAgIGZpbGwgPSAiI2YyYjhiMCIpICsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBnYXBzIGxvbmdlciB0aGFuIHR3byBob3VycyBhcmUgc2hhZGVkCiAgICBnZW9tX3J1ZyhkYXRhID0gZywgYWVzKHggPSBzdGFydCksIGluaGVyaXQuYWVzID0gRkFMU0UsIHNpZGVzID0gImIiLCBjb2xvdXIgPSAiI2MwMzkyYiIsCiAgICAgICAgICAgICBsaW5ld2lkdGggPSAwLjE1LCBsZW5ndGggPSB1bml0KDAuMTIsICJucGMiKSkgKyAgICAgICMgZXZlcnkgZ2FwLCBpbmNsdWRpbmcgc2luZ2xlIHJlYWRpbmdzCiAgICBnZW9tX2xpbmUobGluZXdpZHRoID0gMC4xMiwgY29sb3VyID0gImdyZXkxNSIsIG5hLnJtID0gVFJVRSkgKwogICAgZ2VvbV9sYWJlbChkYXRhID0gciwgYWVzKHggPSBtaW4oVFMpICsgMyAqIDg2NDAwLCB5ID0gSW5mLCBsYWJlbCA9IHNwcmludGYoIiUuMWYlJSBtaXNzaW5nIiwgMTAwICogcmF0ZSkpLAogICAgICAgICAgICAgICBpbmhlcml0LmFlcyA9IEZBTFNFLCBoanVzdCA9IDAsIHZqdXN0ID0gMS4xLCBzaXplID0gMi41LCBmYW1pbHkgPSBGT05UX1RFWFQsIGNvbG91ciA9ICIjOTIyYjIxIiwKICAgICAgICAgICAgICAgZmlsbCA9IGFscGhhKCJ3aGl0ZSIsIDAuODUpLCBsYWJlbC5zaXplID0gMCwgbGFiZWwucGFkZGluZyA9IHVuaXQoMC4xLCAibGluZXMiKSkgKwogICAgZmFjZXRfZ3JpZChpZiAobGVuZ3RoKHN0YXRpb25zKSA+IDEpIHBvbGx1dGFudCB+IHN0YXRpb24gZWxzZSBwb2xsdXRhbnQgfiAuLCBzY2FsZXMgPSAiZnJlZV95Iiwgc3dpdGNoID0gInkiLAogICAgICAgICAgICAgICBsYWJlbGxlciA9IGxhYmVsbGVyKHBvbGx1dGFudCA9IGFzX2xhYmVsbGVyKFBPTF9MQUIsIGxhYmVsX3BhcnNlZCkpKSArCiAgICBzY2FsZV94X2RhdGV0aW1lKGRhdGVfYnJlYWtzID0gaWYgKGxlbmd0aChzdGF0aW9ucykgPiAxKSAiMiBtb250aHMiIGVsc2UgIjEgbW9udGgiLCBkYXRlX2xhYmVscyA9ICIlYiIsCiAgICAgICAgICAgICAgICAgICAgIGV4cGFuZCA9IGV4cGFuc2lvbihtdWx0ID0gMC4wMDQpKSArCiAgICBzY2FsZV95X2NvbnRpbnVvdXMobi5icmVha3MgPSAzLCBleHBhbmQgPSBleHBhbnNpb24obXVsdCA9IGMoMC4wMywgMC4zOCkpKSArCiAgICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgdGl0bGUgPSB0aXRsZSkgKwogICAgdGhlbWUoc3RyaXAucGxhY2VtZW50ID0gIm91dHNpZGUiLCBzdHJpcC50ZXh0LnkubGVmdCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDAsIHNpemUgPSA3LjgpLCBheGlzLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuMiksCiAgICAgICAgICBwYW5lbC5ncmlkLm1pbm9yID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkLm1ham9yLnkgPSBlbGVtZW50X2JsYW5rKCksCiAgICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2xpbmUobGluZXdpZHRoID0gMC4yLCBjb2xvdXIgPSAiZ3JleTg4IiksCiAgICAgICAgICBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA4LjYpLCBwYW5lbC5zcGFjaW5nLnkgPSB1bml0KDMsICJwdCIpKQp9CmNscyA8LSBkYXRhLmZyYW1lKGxvID0gYygxLCA1LCA0OSwgMzM3KSwgaGkgPSBjKDQsIDQ4LCAzMzYsIEluZiksIGxhYiA9IGMoIjEtNCIsICI1LTQ4IiwgIjQ5LTMzNiIsICI+MzM2IikpCnNoYXJlX2RmIDwtIGJpbmRfcm93cyhsYXBwbHkoUE9MUywgZnVuY3Rpb24ocCkgewogIGcgPC0gZ2FwX2xlbmd0aHMoZ2V0X3NlcmllcygiU1VGIDEiLCBwKSkKICBkYXRhLmZyYW1lKHBvbGx1dGFudCA9IGZhY3RvcihwLCBsZXZlbHMgPSBQT0xTKSwgY2xhc3MgPSBmYWN0b3IoY2xzJGxhYiwgbGV2ZWxzID0gY2xzJGxhYiksCiAgICAgICAgICAgICBzaGFyZSA9IHNhcHBseSgxOjQsIGZ1bmN0aW9uKGkpIDEwMCAqIHN1bShnW2cgPj0gY2xzJGxvW2ldICYgZyA8PSBjbHMkaGlbaV1dKSAvIHN1bShnKSkpCn0pKQpwMWEgPC0gcGxvdF9uYV9zZXJpZXMoIlNVRiAxIiwgIihhKSBIYWxmLWhvdXJseSBzZXJpZXMgYXQgU1VGIDE6IGdhcHMgKHJlZCB0aWNrczsgc2hhZGVkIGlmIGxvbmdlciB0aGFuIDIgaCkgYW5kIHRlc3QgcGVyaW9kIChibHVlKSIpCnAxYiA8LSBnZ3Bsb3Qoc2hhcmVfZGYsIGFlcyhzaGFyZSwgMSwgZmlsbCA9IGNsYXNzKSkgKwogIGdlb21fY29sKHBvc2l0aW9uID0gcG9zaXRpb25fc3RhY2socmV2ZXJzZSA9IFRSVUUpLCB3aWR0aCA9IDAuNTUsIG9yaWVudGF0aW9uID0gInkiKSArCiAgZmFjZXRfZ3JpZChwb2xsdXRhbnQgfiAuKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IGMoMCwgNTAsIDEwMCksIGxhYmVscyA9IGZ1bmN0aW9uKHYpIHBhc3RlMCh2LCAiJSIpLCBleHBhbmQgPSBjKDAsIDApKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGV4cGFuZCA9IGV4cGFuc2lvbihhZGQgPSAwLjYpKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygiI2Y1YjdiMSIsICIjZWM3MDYzIiwgIiNjMDM5MmIiLCAiIzY0MWUxNiIpLCBndWlkZSA9IGd1aWRlX2xlZ2VuZChucm93ID0gMSkpICsKICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgZmlsbCA9ICJHYXAgbGVuZ3RoIChoYWxmLWhvdXIgc3RlcHMpOiIsIHRpdGxlID0gIihiKSBTaGFyZSBieSBnYXAgbGVuZ3RoIikgKwogIHRoZW1lKHN0cmlwLnRleHQgPSBlbGVtZW50X2JsYW5rKCksIGF4aXMudGV4dC55ID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkID0gZWxlbWVudF9ibGFuaygpLAogICAgICAgIHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDguNiksIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoMywgInB0IiksIHBsb3QubWFyZ2luID0gbWFyZ2luKDUuNSwgMTAsIDUuNSwgNS41KSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuMiksCiAgICAgICAgbGVnZW5kLmtleS5zaXplID0gdW5pdCgwLjI4LCAiY20iKSwgbGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LjgpLCBsZWdlbmQudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gNy44KSkKcDEgPC0gaWYgKEhBU19QQVRDSFdPUkspIHBhdGNod29yazo6d3JhcF9wbG90cyhwMWEsIHAxYiwgd2lkdGhzID0gYygzLjUsIDEpLCBndWlkZXMgPSAiY29sbGVjdCIpICYKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwgbGVnZW5kLm1hcmdpbiA9IG1hcmdpbigwLCAwLCAwLCAwKSkgZWxzZSBwMWEKcHJpbnQocDEpOyBzYXZlX2ZpZyhwMSwgImZpZzAxX21pc3NpbmduZXNzIiwgNy4xNiwgMy41NSkKaWYgKCFIQVNfUEFUQ0hXT1JLKSB7IHByaW50KHAxYik7IHNhdmVfZmlnKHAxYiwgImZpZzAxYl9nYXBfbGVuZ3RocyIsIDMuNSwgMi4xKSB9CmdhcHNfZGYgPC0gbmFfcnVucygiU1VGIDEiKSB8PiBtdXRhdGUoeSA9IG1hdGNoKHBvbGx1dGFudCwgcmV2KFBPTFMpKSkgICAgICAgIyByZXVzZWQgYnkgdGhlIG92ZXJ2aWV3IGRpYWdyYW0gKGZpZzAzKQpgYGAKCiMjIEFsbCAxNSBzZXJpZXMKClRoZSBzYW1lIHZpZXcgZm9yIHRoZSB0aHJlZSBzdGF0aW9ucy4gSXQgc2hvd3Mgd2hlcmUgZWFjaCBzZXJpZXMgaXMKbWlzc2luZyBiZWZvcmUgYW55IGltcHV0YXRpb24uCgpgYGB7ciBmaWdTMSwgZmlnLndpZHRoPTcuMTYsIGZpZy5oZWlnaHQ9NS42fQojIGZpZ1MxX3Nlcmllc19hbGxfc3RhdGlvbnM6IHRoZSBzYW1lIHZpZXcgZm9yIHRoZSAxNSBzZXJpZXMgb2YgdGhlIHRocmVlIHN0YXRpb25zLgpwUzEgPC0gcGxvdF9uYV9zZXJpZXMoYygiU1VGIDEiLCAiU1VGIDYiLCAiU1VGIDciKSwKICAgICAgICAgICAgICAgICAgICAgICJIYWxmLWhvdXJseSBzZXJpZXMgb2YgdGhlIHRocmVlIHN0YXRpb25zOiBnYXBzIChyZWQgdGlja3M7IHNoYWRlZCBpZiBsb25nZXIgdGhhbiAyIGgpIGFuZCB0ZXN0IHBlcmlvZCAoYmx1ZSkiKSArCiAgdGhlbWUoc3RyaXAudGV4dC54ID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LjUsIGZhY2UgPSAiYm9sZCIpKQpwcmludChwUzEpOyBzYXZlX2ZpZyhwUzEsICJmaWdTMV9zZXJpZXNfYWxsX3N0YXRpb25zIiwgNy4xNiwgNS42KQpgYGAKCiMjIERvdWJsZSBzZWFzb25hbGl0eQoKRm9yIGRheSBvZiB0aGUgd2VlayAkZCQgYW5kIGhhbGYtaG91ciAkaCQgdGhlIG1lYW4gcHJvZmlsZSBpcwokXGJhciB5X3tkLGh9PVxmcmFjezF9e3xcbWF0aGNhbCBPX3tkLGh9fH1cc3VtX3t0XGluXG1hdGhjYWwgT197ZCxofX15X3QkIHdpdGgKJFxtYXRoY2FsIE9fe2QsaH09XHt0XGluXG1hdGhjYWwgTzpkKHQpPWQsXCBoKHQpPWhcfSQsIGFuZCBpdHMgYXZlcmFnZSBvdmVyIHRoZSBzZXZlbiBkYXlzLAokXGJhciB5X3tcY2RvdCxofT1cZnJhYzE3XHN1bV97ZD0xfV57N31cYmFyIHlfe2QsaH0kLCBpcyB0aGUgbWVhbiBkYWlseSBwcm9maWxlLiBQYW5lbCAoYSkgZHJhd3MKJFxiYXIgeV97ZCxofSQgYWxvbmcgdGhlIDMzNiBoYWxmLWhvdXJzIG9mIHRoZSB3ZWVrIHRvZ2V0aGVyIHdpdGggJFxiYXIgeV97XGNkb3QsaH0kIHJlcGVhdGVkIG9uIGV2ZXJ5IGRheToKdGhlIHJlcGV0aXRpb24gaXMgdGhlIGRhaWx5IHNlYXNvbmFsaXR5LCBhbmQgdGhlIHNoYWRlZCBkaWZmZXJlbmNlICRcYmFyIHlfe2QsaH0tXGJhciB5X3tcY2RvdCxofSQsIHRoZSB3ZWVrbHkKZGV2aWF0aW9uLCBpcyB0aGUgd2Vla2x5IHNlYXNvbmFsaXR5LiBQYW5lbCAoYikgZXhhbWluZXMgdGhlIHdlZWtseSBwYXJ0IHdoZXJlIGl0IGlzIGxhcmdlc3QsIGluIHRoZSBtb3JuaW5nOgpmb3IgZXZlcnkgZGF0ZSB0aGUgbWVhbiBvZiAwNjowMOKAkzEwOjAwIGlzIGNvbXB1dGVkLCB0aGUgZGF0ZXMgYXJlIGdyb3VwZWQgYnkgZGF5IG9mIHRoZSB3ZWVrLCBhbmQgZWFjaCBncm91cAptZWFuIGlzIGV4cHJlc3NlZCByZWxhdGl2ZSB0byB0aGUgd29ya2luZy1kYXkgbWVhbiwgd2l0aCBhIDk1JSBjb25maWRlbmNlIGludGVydmFsIG9mICRccG0xLjk2JCBzdGFuZGFyZCBlcnJvcnMKYWNyb3NzIGRhdGVzLiBJbnRlcnZhbHMgd2lkZXIgdGhhbiAkXHBtNDVcJSQgKFBNJF97MTB9JCwgd2l0aCBtYW55IG1pc3NpbmcgbW9ybmluZ3MpIGFyZSBjdXQgYXQgdGhlIGVkZ2UuCgpgYGB7ciBmaWcyLCBmaWcud2lkdGg9Ny4xNiwgZmlnLmhlaWdodD0zLjZ9CiMgZmlnMDJfc2Vhc29uYWxpdHkuIChhKSBNZWFuIGNvbmNlbnRyYXRpb24gYXQgZWFjaCBvZiB0aGUgMzM2IGhhbGYtaG91cnMgb2YgdGhlIHdlZWsgKHNvbGlkKSBhbmQgdGhlIG1lYW4KIyBkYWlseSBwcm9maWxlIHJlcGVhdGVkIG9uIGV2ZXJ5IGRheSAoZGFzaGVkKTogdGhlIHJlcGV0aXRpb24gaXMgdGhlIGRhaWx5IHNlYXNvbmFsaXR5IGFuZCB0aGUgc2hhZGVkCiMgZGlmZmVyZW5jZSBpcyB0aGUgd2Vla2x5IHNlYXNvbmFsaXR5LiAoYikgTWVhbiBvZiAwNjowMC0xMDowMCBmb3IgZWFjaCBkYXkgb2YgdGhlIHdlZWsgcmVsYXRpdmUgdG8gdGhlCiMgd29ya2luZy1kYXkgbWVhbiwgd2l0aCA5NSUgY29uZmlkZW5jZSBpbnRlcnZhbHMgY29tcHV0ZWQgZnJvbSB0aGUgZGFpbHkgbW9ybmluZyBtZWFucy4Kd2sgPC0gYmluZF9yb3dzKGxhcHBseShQT0xTLCBmdW5jdGlvbihwKSBkYXRhLmZyYW1lKHBvbGx1dGFudCA9IHAsIGQgPSBkX29mKHNlcV9sZW4oTikpLCBoID0gaF9vZihzZXFfbGVuKE4pKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHkgPSBnZXRfc2VyaWVzKCJTVUYgMSIsIHApKSkpIHw+CiAgZmlsdGVyKCFpcy5uYSh5KSkgfD4gZ3JvdXBfYnkocG9sbHV0YW50LCBkLCBoKSB8PiBzdW1tYXJpc2UobSA9IG1lYW4oeSksIC5ncm91cHMgPSAiZHJvcCIpIHw+CiAgZ3JvdXBfYnkocG9sbHV0YW50LCBoKSB8PiBtdXRhdGUoZHAgPSBtZWFuKG0pKSB8PiB1bmdyb3VwKCkgfD4gICAgICAgICAgICAgICAgIyBkcDogbWVhbiBkYWlseSBwcm9maWxlCiAgbXV0YXRlKGhvdyA9IChkIC0gMSkgKiA0OCArIGgsIHBvbGx1dGFudCA9IGZhY3Rvcihwb2xsdXRhbnQsIFBPTFMpKSAgICAgICAgICAgIyBob3c6IGhhbGYtaG91ciBvZiB0aGUgd2Vlawptb3JuaW5nIDwtIGJpbmRfcm93cyhsYXBwbHkoUE9MUywgZnVuY3Rpb24ocCkgewogIHkgPC0gZ2V0X3NlcmllcygiU1VGIDEiLCBwKTsgZGQgPC0gKHNlcV9sZW4oTikgLSAxKSAlLyUgNDg7IG1vIDwtIGhfb2Yoc2VxX2xlbihOKSkgJWluJSAxMjoxOSAgICAgIyAwNjowMC0xMDowMAogIGRtIDwtIHRhcHBseSh5W21vXSwgZGRbbW9dLCBmdW5jdGlvbih2KSBpZiAoc3VtKCFpcy5uYSh2KSkgPj0gNCkgbWVhbih2LCBuYS5ybSA9IFRSVUUpIGVsc2UgTkEpICAgIyBvbmUgbWVhbiBwZXIgZGF0ZQogIHdkIDwtIHRhcHBseShkX29mKHNlcV9sZW4oTikpW21vXSwgZGRbbW9dLCBgW2AsIDEpOyBvayA8LSAhaXMubmEoZG0pOyByZWYgPC0gbWVhbihkbVtvayAmIHdkIDw9IDVdKQogIGJpbmRfcm93cyhsYXBwbHkoMTo3LCBmdW5jdGlvbihrKSB7IHYgPC0gZG1bb2sgJiB3ZCA9PSBrXQogICAgZGF0YS5mcmFtZShwb2xsdXRhbnQgPSBwLCBkID0gaywgZXN0ID0gMTAwICogKG1lYW4odikgLyByZWYgLSAxKSwgc2UgPSAxMDAgKiBzZCh2KSAvIHNxcnQobGVuZ3RoKHYpKSAvIHJlZikgfSkpCn0pKSB8PiBtdXRhdGUocG9sbHV0YW50ID0gZmFjdG9yKHBvbGx1dGFudCwgUE9MUyksIGxvID0gZXN0IC0gMS45NiAqIHNlLCBoaSA9IGVzdCArIDEuOTYgKiBzZSkKZm9yIChwIGluIFBPTFMpIHsgc3UgPC0gbW9ybmluZ1ttb3JuaW5nJHBvbGx1dGFudCA9PSBwICYgbW9ybmluZyRkID09IDcsIF07IHNhIDwtIG1vcm5pbmdbbW9ybmluZyRwb2xsdXRhbnQgPT0gcCAmIG1vcm5pbmckZCA9PSA2LCBdCiAga2V5KHBhc3RlMCgic3VuX21vcm5pbmdfIiwgcCksIHN1JGVzdCk7IGtleShwYXN0ZTAoInN1bl9tb3JuaW5nX2xvXyIsIHApLCBzdSRsbyk7IGtleShwYXN0ZTAoInN1bl9tb3JuaW5nX2hpXyIsIHApLCBzdSRoaSkKICBrZXkocGFzdGUwKCJzYXRfbW9ybmluZ18iLCBwKSwgc2EkZXN0KSB9CkxJTkUyIDwtIGMoIm1lYW4gYXQgZWFjaCBoYWxmLWhvdXIgb2YgdGhlIHdlZWsiID0gIiMxZjRlNzkiLCAibWVhbiBkYWlseSBwcm9maWxlLCByZXBlYXRlZCIgPSAiIzVkNmQ3ZSIpCnAyYSA8LSBnZ3Bsb3Qod2ssIGFlcyhob3cpKSArCiAgYW5ub3RhdGUoInJlY3QiLCB4bWluID0gMjM5LjUsIHhtYXggPSAzMzUuNSwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYsIGZpbGwgPSAiZ3JleTkzIikgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDQ4ICogKDE6NikgLSAwLjUsIGNvbG91ciA9ICJncmV5NzAiLCBsaW5ld2lkdGggPSAwLjI1LCBsaW5ldHlwZSA9ICIyMiIpICsKICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHBtaW4obSwgZHApLCB5bWF4ID0gcG1heChtLCBkcCkpLCBmaWxsID0gIiNmNWNiYTciKSArCiAgZ2VvbV9saW5lKGFlcyh5ID0gZHAsIGNvbG91ciA9IG5hbWVzKExJTkUyKVsyXSwgbGluZXR5cGUgPSBuYW1lcyhMSU5FMilbMl0pLCBsaW5ld2lkdGggPSAwLjM1KSArCiAgZ2VvbV9saW5lKGFlcyh5ID0gbSwgY29sb3VyID0gbmFtZXMoTElORTIpWzFdLCBsaW5ldHlwZSA9IG5hbWVzKExJTkUyKVsxXSksIGxpbmV3aWR0aCA9IDAuNDUpICsKICBmYWNldF9ncmlkKHBvbGx1dGFudCB+IC4sIHNjYWxlcyA9ICJmcmVlX3kiLCBzd2l0Y2ggPSAieSIsIGxhYmVsbGVyID0gbGFiZWxsZXIocG9sbHV0YW50ID0gYXNfbGFiZWxsZXIoUE9MX0xBQiwgbGFiZWxfcGFyc2VkKSkpICsKICBzY2FsZV9jb2xvdXJfbWFudWFsKE5VTEwsIHZhbHVlcyA9IExJTkUyLCBicmVha3MgPSBuYW1lcyhMSU5FMikpICsKICBzY2FsZV9saW5ldHlwZV9tYW51YWwoTlVMTCwgdmFsdWVzID0gc2V0TmFtZXMoYygic29saWQiLCAiMjIiKSwgbmFtZXMoTElORTIpKSwgYnJlYWtzID0gbmFtZXMoTElORTIpKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IDQ4ICogKDA6NikgKyAyMy41LCBsYWJlbHMgPSBjKCJNb24iLCAiVHVlIiwgIldlZCIsICJUaHUiLCAiRnJpIiwgIlNhdCIsICJTdW4iKSwgZXhwYW5kID0gYygwLCAwKSkgKwogIHNjYWxlX3lfY29udGludW91cyhuLmJyZWFrcyA9IDMsIGV4cGFuZCA9IGV4cGFuc2lvbihtdWx0ID0gYygwLjA0LCAwLjEpKSkgKwogIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMLCB0aXRsZSA9ICIoYSkgTWVhbiBjb25jZW50cmF0aW9uIG92ZXIgdGhlIHdlZWsgYXQgU1VGIDE7IHNoYWRlZDogd2Vla2x5IGRldmlhdGlvbiwgZ3JleSBiYW5kOiB3ZWVrZW5kIikgKwogIHRoZW1lKHN0cmlwLnBsYWNlbWVudCA9ICJvdXRzaWRlIiwgc3RyaXAudGV4dC55LmxlZnQgPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBzaXplID0gNy44KSwgYXhpcy50ZXh0ID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LjIpLAogICAgICAgIHBhbmVsLmdyaWQubWlub3IgPSBlbGVtZW50X2JsYW5rKCksIHBhbmVsLmdyaWQubWFqb3IueCA9IGVsZW1lbnRfYmxhbmsoKSwKICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnkgPSBlbGVtZW50X2xpbmUobGluZXdpZHRoID0gMC4xNSwgY29sb3VyID0gImdyZXk4OCIpLCBwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA4LjYpLAogICAgICAgIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoMywgInB0IikpCmxpbSA8LSA0NSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgJSAod2lkZXIgaW50ZXJ2YWxzIGFyZSBjdXQgYXQgdGhlIGVkZ2UpCnAyYiA8LSBnZ3Bsb3QobW9ybmluZywgYWVzKGQsIGVzdCkpICsKICBhbm5vdGF0ZSgicmVjdCIsIHhtaW4gPSA1LjUsIHhtYXggPSA3LjUsIHltaW4gPSAtSW5mLCB5bWF4ID0gSW5mLCBmaWxsID0gImdyZXk5MyIpICsKICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLCBjb2xvdXIgPSAiZ3JleTQ1IiwgbGluZXdpZHRoID0gMC4zKSArCiAgZ2VvbV9lcnJvcmJhcihhZXMoeW1pbiA9IGxvLCB5bWF4ID0gaGksIGNvbG91ciA9IGQgPj0gNiksIHdpZHRoID0gMCwgbGluZXdpZHRoID0gMC41NSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91ciA9IGQgPj0gNiksIHNpemUgPSAxLjQpICsKICBmYWNldF9ncmlkKHBvbGx1dGFudCB+IC4pICsKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGMoYEZBTFNFYCA9ICIjMWY0ZTc5IiwgYFRSVUVgID0gIiNkMzU0MDAiKSwgZ3VpZGUgPSAibm9uZSIpICsKICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzID0gMTo3LCBsYWJlbHMgPSBjKCJNIiwgIlQiLCAiVyIsICJUIiwgIkYiLCAiUyIsICJTIiksIGV4cGFuZCA9IGV4cGFuc2lvbihhZGQgPSAwLjUpKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGxpbWl0cyA9IGMoLWxpbSwgbGltKSwgYnJlYWtzID0gYygtMzAsIDMwKSwgbGFiZWxzID0gYygiXHUyMjEyMzAlIiwgIiszMCUiKSwgb29iID0gc2NhbGVzOjpvb2Jfc3F1aXNoKSArCiAgbGFicyh4ID0gTlVMTCwgeSA9IE5VTEwsIHRpdGxlID0gIihiKSBNb3JuaW5ncyAoMDZcdTIwMTMxMCBoKSIpICsKICB0aGVtZShzdHJpcC50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkLm1pbm9yID0gZWxlbWVudF9ibGFuaygpLCBwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2JsYW5rKCksCiAgICAgICAgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKGxpbmV3aWR0aCA9IDAuMTUsIGNvbG91ciA9ICJncmV5ODgiKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuMiksCiAgICAgICAgYXhpcy50ZXh0LnkgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDYuNiksIHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDguNiksIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoMywgInB0IiksCiAgICAgICAgcGxvdC5tYXJnaW4gPSBtYXJnaW4oNS41LCA4LCA1LjUsIDUuNSkpCnAyIDwtIGlmIChIQVNfUEFUQ0hXT1JLKSBwYXRjaHdvcms6OndyYXBfcGxvdHMocDJhLCBwMmIsIHdpZHRocyA9IGMoMy4zLCAxLjEpLCBndWlkZXMgPSAiY29sbGVjdCIpICYKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwgbGVnZW5kLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcuOCksIGxlZ2VuZC5rZXkud2lkdGggPSB1bml0KDAuOCwgImNtIiksCiAgICAgICAgbGVnZW5kLm1hcmdpbiA9IG1hcmdpbigwLCAwLCAwLCAwKSkgZWxzZSBwMmEKcHJpbnQocDIpOyBzYXZlX2ZpZyhwMiwgImZpZzAyX3NlYXNvbmFsaXR5IiwgNy4xNiwgMy42KQprbml0cjo6a2FibGUodGlkeXI6OnBpdm90X3dpZGVyKG1vcm5pbmcgfD4gbXV0YXRlKHYgPSBzcHJpbnRmKCIlKy4xZiBbJSsuMWYsICUrLjFmXSIsIGVzdCwgbG8sIGhpKSkgfD4gc2VsZWN0KHBvbGx1dGFudCwgZCwgdiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmFtZXNfZnJvbSA9IGQsIHZhbHVlc19mcm9tID0gdikgfD4gc2V0TmFtZXMoYygiUG9sbHV0YW50IiwgIk1vbiIsICJUdWUiLCAiV2VkIiwgIlRodSIsICJGcmkiLCAiU2F0IiwgIlN1biIpKSwKICAgICAgICAgICAgIGNhcHRpb24gPSAiMDY6MDAtMTA6MDAgbWVhbiBieSBkYXkgb2YgdGhlIHdlZWsgcmVsYXRpdmUgdG8gd29ya2luZyBkYXlzICglKSwgd2l0aCA5NSUgY29uZmlkZW5jZSBpbnRlcnZhbHMiKQpgYGAKCiMjIFdoeSBpbXB1dGF0aW9uIGNhbm5vdCBiZSBhdm9pZGVkCgpXaW5kb3ctYmFzZWQgZm9yZWNhc3RlcnMgKE1MUCwgTFNUTSwgVENOKSBuZWVkIGNvbXBsZXRlIGlucHV0IHdpbmRvd3MuIEZvciB0aGUgMzM0IGRheXMgYmVmb3JlIHRoZQp0ZXN0IHBlcmlvZCwgdGhlIGNvZGUgY29tcHV0ZXMgdGhlIHNoYXJlIG9mIGRheXMgd2l0aCBhdCBsZWFzdCBvbmUgbWlzc2luZyB2YWx1ZSBhbmQgdGhlIHNoYXJlIG9mCmNvbXBsZXRlIHR3by1kYXkgKDk2LXN0ZXApIHdpbmRvd3MuCgpgYGB7ciBtb3RpdmF0aW9ufQpwcmUgPC0gc2VxX2xlbigzMzQgKiBTMSkKbW90IDwtIGJpbmRfcm93cyhsYXBwbHkoUE9MUywgZnVuY3Rpb24ocCkgewogIHggPC0gZ2V0X3NlcmllcygiU1VGIDEiLCBwKVtwcmVdOyBtaXNzIDwtIGlzLm5hKHgpCiAgTSA8LSBtYXRyaXgobWlzcywgbmNvbCA9IFMxLCBieXJvdyA9IFRSVUUpICAgICAgICAgICAgICAgICAgICAgICAjIGRheXMgeCBoYWxmLWhvdXJzCiAgY3MgPC0gYygwLCBjdW1zdW0obWlzcykpOyBMIDwtIDk2CiAgdyA8LSBjc1soTCArIDEpOmxlbmd0aChjcyldIC0gY3NbMToobGVuZ3RoKGNzKSAtIEwpXSAgICAgICAgICAgICAjIG1pc3NpbmcgdmFsdWVzIHBlciB0d28tZGF5IHdpbmRvdwogIGRhdGEuZnJhbWUocG9sbHV0YW50ID0gcCwgZGF5cyA9IDEwMCAqIG1lYW4ocm93U3VtcyhNKSA+IDApLCBjb21wbGV0ZSA9IDEwMCAqIG1lYW4odyA9PSAwKSkKfSkpCmtleSgiZGF5c193aXRoX21pc3NpbmdfbWluX3BjdCIsIG1pbihtb3QkZGF5c1ttb3QkcG9sbHV0YW50ICE9ICJQTTEwIl0pKQprZXkoImNvbXBsZXRlXzJkYXlfd2luZG93c19tYXhfcGN0IiwgbWF4KG1vdCRjb21wbGV0ZVttb3QkcG9sbHV0YW50ICE9ICJQTTEwIl0pKQprbml0cjo6a2FibGUobW90LCBkaWdpdHMgPSAxLCBjb2wubmFtZXMgPSBjKCJQb2xsdXRhbnQiLCAiRGF5cyB3aXRoID49IDEgbWlzc2luZyAoJSkiLCAiQ29tcGxldGUgdHdvLWRheSB3aW5kb3dzICglKSIpLAogICAgICAgICAgICAgY2FwdGlvbiA9ICJEYXlzIHdpdGggbWlzc2luZyB2YWx1ZXMgYW5kIGNvbXBsZXRlIHR3by1kYXkgd2luZG93cyAodHJhaW5pbmcgcGVyaW9kLCAzMzQgZGF5cykiKQpgYGAKCiMgVGhlIHByb3Bvc2VkIG1ldGhvZCBTZWFzLUFSCgpTZWFzLUFSIGRlY29tcG9zZXMgdGhlIHRyYW5zZm9ybWVkIHNlcmllcyBhcwoKJCQKel90PVRfdCtEX3QrV190K1JfdCwgXHRhZ3syfQokJAoKd2l0aCBhIHNtb290aCB0cmVuZCAkVF90JCwgYSBkYWlseSBwcm9maWxlICREX3QkLCBhIHdlZWtkYXktc3BlY2lmaWMgZGV2aWF0aW9uICRXX3QkIGFuZCBhIHJlbWFpbmRlcgokUl90JC4gQWxsIGNvbXBvbmVudHMgYXJlIGVzdGltYXRlZCAqKmZyb20gb2JzZXJ2ZWQgdmFsdWVzIG9ubHkqKjsgJFxkZWx0YV90PTEkIGlmICR0XGluXG1hdGhjYWwgTyQKYW5kICRcZGVsdGFfdD0wJCBvdGhlcndpc2UuIFRoZSBtZXRob2QgY29uc2lzdHMgb2YgdGhlIHNldmVuIHN0ZXBzIGJlbG93LgoKIyMgU3RlcCAxIOKAlCBWYXJpYW5jZS1zdGFiaWxpc2luZyB0cmFuc2Zvcm0gKEVxLiAxKQoKJCQKel90PVxsb2dcQmlnKDErXGZyYWN7eV90fXttfVxCaWcpLFxxcXVhZCBtPVxvcGVyYXRvcm5hbWV7bWVkaWFufVx7eV9zOnNcaW5cbWF0aGNhbCBPXH0uIFx0YWd7MX0KJCQKClRoZSB0cmFuc2Zvcm0gaXMgZmluaXRlIGF0ICR5X3Q9MCQsIGRvZXMgbm90IGRlcGVuZCBvbiB0aGUgdW5pdCwgYW5kIHR1cm5zIG11bHRpcGxpY2F0aXZlIHNlYXNvbmFsCmVmZmVjdHMgaW50byBhZGRpdGl2ZSBvbmVzLiBJZiB0aGUgbWVkaWFuIGlzIDAsIHRoZSBtZWFuIGlzIHVzZWQgYXMgc2NhbGUsIGFuZCAkbT0xJCBpZiBib3RoIGFyZSAwLgoKYGBge3Igc3RlcDF9CnNlYXNhcl9zY2FsZSA8LSBmdW5jdGlvbih4KSB7ICAgICAgICAgICAgICAgICAgICAgICAjIG0gaW4gRXEuICgxKQogIG0gPC0gbWVkaWFuKHgsIG5hLnJtID0gVFJVRSkKICBpZiAoIShtID4gMCkpIHsgbW0gPC0gbWVhbih4LCBuYS5ybSA9IFRSVUUpOyBtIDwtIGlmIChtbSA+IDApIG1tIGVsc2UgMSB9CiAgbQp9CmBgYAoKIyMgU3RlcCAyIOKAlCBNaXNzaW5nLWF3YXJlIHRyZW5kIChFcS4gMykKCkZvciBhIHBhcnRpYWwgcmVzaWR1YWwgJHVfdCQgKGluaXRpYWxseSAkdV90PXpfdCQpLCB0aGUgdHJlbmQgaXMgdGhlIGNlbnRyZWQgJDJcdGltZXMzMzYkIG1vdmluZwphdmVyYWdlIG9mIHRoZSBvYnNlcnZlZCB2YWx1ZXM6CgokJApUX3Q9XGZyYWN7XHN1bV97aj0tMTY4fV57MTY4fXdfalwsXGRlbHRhX3t0K2p9XCx1X3t0K2p9fXtcc3VtX3tqPS0xNjh9XnsxNjh9d19qXCxcZGVsdGFfe3Qran19LFxxcXVhZAp3X2o9XGJlZ2lue2Nhc2VzfTEvMzM2LCZ8anw8MTY4LFxcIDEvNjcyLCZ8anw9MTY4LlxlbmR7Y2FzZXN9IFx0YWd7M30KJCQKClRoZSB3ZWlnaHRzIHN1bSB0byBvbmUgYW5kIHNwYW4gZXhhY3RseSBvbmUgd2Vlaywgc28gZm9yIGNvbXBsZXRlIGRhdGEgYW55IGNvbXBvbmVudCB3aXRoIHBlcmlvZCA0OApvciAzMzYgdGhhdCBzdW1zIHRvIHplcm8gb3ZlciBpdHMgY3ljbGUgZGlzYXBwZWFycyBmcm9tICRUX3QkLiBXaGVyZSBsZXNzIHRoYW4gb25lIHF1YXJ0ZXIgb2YgdGhlIGZ1bGwKd2VpZ2h0IGlzIG9ic2VydmVkIChpbnNpZGUgbG9uZyBnYXBzKSwgJFRfdCQgaXMgZmlsbGVkIGJ5IGxpbmVhciBpbnRlcnBvbGF0aW9uLAokXGhhdCB4X3Q9eF9hK1xmcmFje3QtYX17Yi1hfSh4X2IteF9hKSQgYmV0d2VlbiB0aGUgbmVhcmVzdCBvYnNlcnZlZCBwb2ludHMgJGE8dDxiJCwgYW5kIGtlcHQgY29uc3RhbnQKYmV5b25kIHRoZSBmaXJzdCBhbmQgbGFzdCBvYnNlcnZhdGlvbnMuIFdoZW4gb25seSBvbmUgdmFsdWUgaXMgb2JzZXJ2ZWQsIGl0IGlzIHVzZWQgZXZlcnl3aGVyZSAod2l0aApgc3RhdHM6OmFwcHJveGAgdGhpcyBjYXNlIHdvdWxkIHN0b3Agd2l0aCAqIm5lZWQgYXQgbGVhc3QgdHdvIG5vbi1OQSB2YWx1ZXMiKikuCgoqKkltcGxlbWVudGF0aW9uLioqIEVxLiAoMykgaXMgZXZhbHVhdGVkIGluICRPKG4pJCBvcGVyYXRpb25zIHdpdGggY3VtdWxhdGl2ZSBzdW1zOiB0aGUgaW5uZXIgc3VtCigkfGp8PDE2OCQpIGlzIGEgZGlmZmVyZW5jZSBvZiB0d28gY3VtdWxhdGl2ZSBzdW1zIGFuZCB0aGUgdHdvIGVuZCBwb2ludHMgYXJlIGFkZGVkIHdpdGggaGFsZiB3ZWlnaHQuClRoZSBvYnNlcnZlZCB3ZWlnaHQgZXF1YWxzICQoMmNfe1xybSBpbn0rY197XHJtIGVuZH0pLzY3MiQsIHdoZXJlICRjX3tccm0gaW59JCBhbmQgJGNfe1xybSBlbmR9JCBjb3VudAp0aGUgb2JzZXJ2ZWQgcG9pbnRzOyB0aGUgcnVsZSAiYXQgbGVhc3Qgb25lIHF1YXJ0ZXIgb2JzZXJ2ZWQiIGlzIHRoZXJlZm9yZSBjaGVja2VkICoqZXhhY3RseSoqIHdpdGgKaW50ZWdlcnMsICQyY197XHJtIGlufStjX3tccm0gZW5kfVxnZTE2OCQuIENoZWNrIDIgY29uZmlybXMgdGhhdCB0aGlzIGZhc3QgdmVyc2lvbiBlcXVhbHMgdGhlIGxpdGVyYWwKY29udm9sdXRpb24gb2YgRXEuICgzKS4KCmBgYHtyIHN0ZXAyfQppbnRlcnBfbGluZWFyIDwtIGZ1bmN0aW9uKHgpIHsgICAgICAgICAgICAgICAgICAgICMgbGluZWFyIGludGVycG9sYXRpb24sIGNvbnN0YW50IGF0IHRoZSBlbmRzCiAgb2JzIDwtICFpcy5uYSh4KTsgbl9vYnMgPC0gc3VtKG9icykKICBpZiAobl9vYnMgPT0gbGVuZ3RoKHgpKSByZXR1cm4oeCkKICBpZiAobl9vYnMgPT0gMCkgcmV0dXJuKHJlcCgwLCBsZW5ndGgoeCkpKQogIGlmIChuX29icyA9PSAxKSB7IHhbIW9ic10gPC0geFtvYnNdOyByZXR1cm4oeCkgfSAgICAgICAgICAjIGEgc2luZ2xlIG9ic2VydmF0aW9uOiBjb25zdGFudAogIHQgPC0gc2VxX2Fsb25nKHgpCiAgeFshb2JzXSA8LSBzdGF0czo6YXBwcm94KHRbb2JzXSwgeFtvYnNdLCB4b3V0ID0gdFshb2JzXSwgcnVsZSA9IDIpJHkKICB4Cn0KbmFuX21hIDwtIGZ1bmN0aW9uKHgsIHBlcmlvZCA9IFMyLCBtaW5fZnJhYyA9IDAuMjUpIHsgICAgICAgICMgRXEuICgzKSBpbiBPKG4pIHdpdGggY3VtdWxhdGl2ZSBzdW1zCiAgbiA8LSBsZW5ndGgoeCk7IGggPC0gcGVyaW9kICUvJSAyOyBvYnMgPC0gIWlzLm5hKHgpCiAgeHAgPC0gYyhyZXAoMCwgaCksIGlmZWxzZShvYnMsIHgsIDApLCByZXAoMCwgaCkpICAgICAgICAgICAjIHplcm8gcGFkZGluZyA9IG91dHNpZGUgdmFsdWVzIG1pc3NpbmcKICBvcCA8LSBjKHJlcCgwLCBoKSwgYXMubnVtZXJpYyhvYnMpLCByZXAoMCwgaCkpCiAgY3ggPC0gYygwLCBjdW1zdW0oeHApKTsgY28gPC0gYygwLCBjdW1zdW0ob3ApKQogIGkgIDwtIHNlcV9sZW4obikgKyBoICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBwb3NpdGlvbiBvZiB0IGluIHRoZSBwYWRkZWQgc2VyaWVzCiAgaW5feCA8LSBjeFtpICsgaF0gLSBjeFtpIC0gaCArIDFdOyBpbl9vIDwtIGNvW2kgKyBoXSAtIGNvW2kgLSBoICsgMV0gICAgICAjIHxqfCA8IGggICh3ZWlnaHQgMS9wZXJpb2QpCiAgZW5feCA8LSB4cFtpIC0gaF0gKyB4cFtpICsgaF07ICAgICBlbl9vIDwtIG9wW2kgLSBoXSArIG9wW2kgKyBoXSAgICAgICAgICAjIHxqfCA9IGggICh3ZWlnaHQgMS8oMiBwZXJpb2QpKQogIGNudDIgPC0gMiAqIGluX28gKyBlbl9vICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyA9IDIgKiBwZXJpb2QgKiAob2JzZXJ2ZWQgd2VpZ2h0KSwgYW4gaW50ZWdlcgogIG1hIDwtIGlmZWxzZShjbnQyID49IDIgKiBwZXJpb2QgKiBtaW5fZnJhYywgKGluX3ggKyAwLjUgKiBlbl94KSAvIChpbl9vICsgMC41ICogZW5fbyksIE5BX3JlYWxfKQogIGludGVycF9saW5lYXIobWEpCn0KbmFuX21hX2RpcmVjdCA8LSBmdW5jdGlvbih4LCBwZXJpb2QgPSBTMiwgbWluX2ZyYWMgPSAwLjI1KSB7ICAjIGxpdGVyYWwgRXEuICgzKSBieSBjb252b2x1dGlvbiAocmVmZXJlbmNlKQogIHcgPC0gcmVwKDEsIHBlcmlvZCArIDEpOyB3W2MoMSwgcGVyaW9kICsgMSldIDwtIDAuNTsgdyA8LSB3IC8gcGVyaW9kOyBoIDwtIHBlcmlvZCAlLyUgMgogIGNvbnYgPC0gZnVuY3Rpb24odikgYXMubnVtZXJpYyhzdGF0czo6ZmlsdGVyKGMocmVwKDAsIGgpLCB2LCByZXAoMCwgaCkpLCB3LCBzaWRlcyA9IDIpKVsoaCArIDEpOihoICsgbGVuZ3RoKHYpKV0KICBudW0gPC0gY29udihpZmVsc2UoaXMubmEoeCksIDAsIHgpKTsgZGVuIDwtIGNvbnYoYXMubnVtZXJpYyghaXMubmEoeCkpKQogIGludGVycF9saW5lYXIoaWZlbHNlKGRlbiA+PSBtaW5fZnJhYyAtIDFlLTEyLCBudW0gLyBkZW4sIE5BX3JlYWxfKSkKfQp0dF9kZW1vIDwtIHNlcV9sZW4oMTAgKiBTMikKeF9kZW1vICA8LSAzICsgc2luKDIgKiBwaSAqIHR0X2RlbW8gLyBTMSkgKyAwLjUgKiBjb3MoMiAqIHBpICogdHRfZGVtbyAvIFMyKQppbm5lciAgIDwtIChTMiAvIDIgKyAxKToobGVuZ3RoKHR0X2RlbW8pIC0gUzIgLyAyKQpjYXQoIkNoZWNrIDE6IG1heCB8VCAtIDN8IGZvciBhIHB1cmUgZG91YmxlLXNlYXNvbmFsIHNpZ25hbCA9Iiwgc2lnbmlmKG1heChhYnMobmFuX21hKHhfZGVtbylbaW5uZXJdIC0gMykpLCAzKSwgIlxuIikKc2V0LnNlZWQoMik7IHhyIDwtIHJub3JtKDUwMDApOyB4cltzYW1wbGUoNTAwMCwgMTUwMCldIDwtIE5BOyB4clsxMDAwOjE1MDBdIDwtIE5BCmNhdCgiQ2hlY2sgMjogbWF4IHxmYXN0IGN1bXVsYXRpdmUtc3VtIHZlcnNpb24gLSBkaXJlY3QgY29udm9sdXRpb258ID0iLCBzaWduaWYobWF4KGFicyhuYW5fbWEoeHIpIC0gbmFuX21hX2RpcmVjdCh4cikpKSwgMyksICJcbiIpCmNhdCgiQ2hlY2sgMzogaW50ZXJwX2xpbmVhcihjKE5BLCA1LCBOQSwgTkEpKSA9IiwgaW50ZXJwX2xpbmVhcihjKE5BLCA1LCBOQSwgTkEpKSwgIlxuIikKYGBgCgojIyBTdGVwIDMg4oCUIERhaWx5IHByb2ZpbGUgYnkga2VybmVsIHNtb290aGluZyAoRXEuIDQpCgpUaGUgcGFydGlhbCByZXNpZHVhbCAkcl90PXpfdC1UX3QtV190JCBpcyBhcnJhbmdlZCBhcyBhIG1hdHJpeCAkXG1hdGhiZiBWJCB3aXRoIG9uZSByb3cgcGVyIGRheSAkayQKYW5kIG9uZSBjb2x1bW4gcGVyIGhhbGYtaG91ciAkaCQuIEVhY2ggY29sdW1uIChhICpjeWNsZS1zdWJzZXJpZXMqKSBpcyBzbW9vdGhlZCBhY3Jvc3MgZGF5cyB3aXRoIGEKTmFkYXJheWHigJNXYXRzb24gZXN0aW1hdG9yIHdpdGggR2F1c3NpYW4ga2VybmVsICRLX2godSk9XGV4cFx7LXVeMi8oMmheMilcfSQ6CgokJApcdGlsZGUgRF97ayxofT1cZnJhY3tcc3VtX3trJ31LX3toX0R9KGstaycpXCxcZGVsdGFfe2snLGh9XCxyX3trJyxofX17XHN1bV97ayd9S197aF9EfShrLWsnKVwsXGRlbHRhX3trJyxofX0sClxxcXVhZCBEX3trLGh9PVx0aWxkZSBEX3trLGh9LVxmcmFjezF9ezQ4fVxzdW1fe2gnPTF9Xns0OH1cdGlsZGUgRF97ayxoJ30uIFx0YWd7NH0KJCQKCkluIG1hdHJpeCBmb3JtICRcdGlsZGV7XG1hdGhiZiBEfT0oXG1hdGhiZiBLXG1hdGhiZiBWXzApXG9zbGFzaChcbWF0aGJmIEtcYm9sZHN5bWJvbFxEZWx0YSkkLCB3aGVyZQokXG1hdGhiZiBLX3trayd9PUtfe2hfRH0oay1rJykkLCAkXG1hdGhiZiBWXzAkIGlzICRcbWF0aGJmIFYkIHdpdGggbWlzc2luZyBlbnRyaWVzIHNldCB0byAwLAokXGJvbGRzeW1ib2xcRGVsdGEkIGlzIHRoZSBpbmRpY2F0b3IgbWF0cml4IG9mIG9ic2VydmVkIGVudHJpZXMgYW5kICRcb3NsYXNoJCBpcyBlbGVtZW50LXdpc2UKZGl2aXNpb24uIENlbnRyaW5nIGVhY2ggcm93IG1ha2VzICREJCBzdW0gdG8gemVybyB3aXRoaW4gZXZlcnkgZGF5LiBUaGUgYmFuZHdpZHRoICRoX0QkIChkYXlzKQpjb250cm9scyBob3cgZmFzdCB0aGUgZGFpbHkgcHJvZmlsZSBtYXkgY2hhbmdlIG92ZXIgdGhlIHllYXI7ICRoX0RcdG9caW5mdHkkIGdpdmVzIGEgc3RhdGljIHByb2ZpbGUuCgpgYGB7ciBzdGVwM30KZ2F1c3Nfa2VybmVsIDwtIGZ1bmN0aW9uKG4sIGJ3KSB7CiAgaWYgKCFpcy5maW5pdGUoYncpIHx8IGJ3ID4gMWU1KSByZXR1cm4obWF0cml4KDEsIG4sIG4pKSAgIyBidyA9IEluZjogZXF1YWwgd2VpZ2h0cyAoc3RhdGljIHByb2ZpbGUpCiAgaWR4IDwtIDA6KG4gLSAxKTsgZXhwKC0wLjUgKiAob3V0ZXIoaWR4LCBpZHgsICItIikgLyBidyleMikKfQpjeWNsZV9zbW9vdGggPC0gZnVuY3Rpb24ociwgcGVyaW9kLCBidykgeyAgICAgICAgICMgRXEuICg0KS8oNSkgYmVmb3JlIGNlbnRyaW5nOyByb3dzID0gY3ljbGVzCiAgbiA8LSBsZW5ndGgocik7IG5jIDwtIGNlaWxpbmcobiAvIHBlcmlvZCkKICB2IDwtIHJlcChOQV9yZWFsXywgbmMgKiBwZXJpb2QpOyB2W3NlcV9sZW4obildIDwtIHIKICBWIDwtIG1hdHJpeCh2LCBucm93ID0gbmMsIG5jb2wgPSBwZXJpb2QsIGJ5cm93ID0gVFJVRSkKICBEZWx0YSA8LSAoIWlzLm5hKFYpKSAqIDE7IFYwIDwtIFY7IFYwW2lzLm5hKFYwKV0gPC0gMAogIEsgPC0gZ2F1c3Nfa2VybmVsKG5jLCBidykKICBudW0gPC0gSyAlKiUgVjA7IGRlbiA8LSBLICUqJSBEZWx0YQogIFMgPC0gbnVtIC8gZGVuOyBTWyEoZGVuID4gMWUtMzAwKV0gPC0gMDsgU1tpcy5uYShTKV0gPC0gMCAgIyBjb2x1bW5zIHdpdGhvdXQgZGF0YSAtPiAwCiAgUwp9CmBgYAoKIyMgU3RlcCA0IOKAlCBXZWVrbHkgZGV2aWF0aW9uIChFcS4gNSkKClRoZSBwYXJ0aWFsIHJlc2lkdWFsICRyJ190PXpfdC1UX3QtRF90JCBpcyBhcnJhbmdlZCBieSB3ZWVrcyAocm93cykgYW5kIHRoZSAzMzYgaGFsZi1ob3VycyBvZiB0aGUKd2VlayAoY29sdW1ucyksIHNtb290aGVkIGFjcm9zcyB3ZWVrcyB3aXRoIGJhbmR3aWR0aCAkaF9XJCAod2Vla3MpLCBhbmQgY2VudHJlZCBvdmVyIHdlZWtkYXlzOgoKJCQKV197XGVsbCwoZCxoKX09XHRpbGRlIFdfe1xlbGwsKGQsaCl9LVxmcmFjMTdcc3VtX3tkJz0xfV57N31cdGlsZGUgV197XGVsbCwoZCcsaCl9LiBcdGFnezV9CiQkCgpUaGUgY2VudHJpbmcgbWFrZXMgJEQkIGFuZCAkVyQgaWRlbnRpZmlhYmxlOiAkVyQgb25seSBjYXJyaWVzIHRoZSBwYXJ0IG9mIHRoZSB3ZWVrbHkgcGF0dGVybiB0aGF0IGlzCm5vdCBjb21tb24gdG8gYWxsIGRheXMuIEluIHRoZSBjb2RlLCBlYWNoIHJvdyBvZiAzMzYgdmFsdWVzIGlzIHZpZXdlZCBhcyBhICQ0OFx0aW1lczckIGFycmF5IGFuZCB0aGUKbWVhbiBvdmVyIHRoZSBzZXZlbiB3ZWVrZGF5cyBpcyBzdWJ0cmFjdGVkIGF0IGV2ZXJ5IGhhbGYtaG91ciwgaW4gb25lIHZlY3RvcmlzZWQgb3BlcmF0aW9uLgoKYGBge3Igc3RlcDR9CmNlbnRlcl93ZWVrbHkgPC0gZnVuY3Rpb24oU3cpIHsgICAgICAgICAgICAgICAgICAgIyBFcS4gKDUpOiBzdWJ0cmFjdCB0aGUgbWVhbiBvdmVyIHdlZWtkYXlzCiAgbncgPC0gbnJvdyhTdykKICBBICA8LSBhcnJheShTdywgZGltID0gYyhudywgUzEsIDcpKSAgICAgICAgICAgICAjIEFbd2VlaywgaGFsZi1ob3VyLCB3ZWVrZGF5XSAoY29sdW1ucyBhcmUgZGF5LW1ham9yKQogIE0gIDwtIHJvd01lYW5zKEEsIGRpbXMgPSAyKSAgICAgICAgICAgICAgICAgICAgICMgbWVhbiBvdmVyIHRoZSA3IHdlZWtkYXlzOiB3ZWVrcyB4IDQ4CiAgbWF0cml4KEEgLSBhcnJheShNLCBkaW0gPSBjKG53LCBTMSwgNykpLCBucm93ID0gbncsIG5jb2wgPSBTMikKfQpgYGAKCiMjIFN0ZXAgNSDigJQgQmFja2ZpdHRpbmcKClN0YXJ0aW5nIGZyb20gJFQ9XG1hdGhybXtNQX0oeikkIGFuZCAkRD1XPTAkLCB0aGUgY29tcG9uZW50cyBhcmUgdXBkYXRlZCAkSz0zJCB0aW1lczoKKGkpICREXGxlZnRhcnJvdyQgRXEuICg0KSBvbiAkei1ULVckOyAoaWkpICRXXGxlZnRhcnJvdyQgRXEuICg1KSBvbiAkei1ULUQkOyAoaWlpKSAkVFxsZWZ0YXJyb3ckIEVxLiAoMykKb24gJHotRC1XJC4gVGhlIHJlbWFpbmRlciBpcyAkUl90PXpfdC1UX3QtRF90LVdfdCQgZm9yICR0XGluXG1hdGhjYWwgTyQgYW5kIG1pc3Npbmcgb24gJFxtYXRoY2FsIE0kLgpCZWNhdXNlIGFsbCBrZXJuZWwgc3VtcyB1c2Ugb2JzZXJ2ZWQgdmFsdWVzIG9ubHksIG5vIHByZS1pbXB1dGF0aW9uIGlzIG5lZWRlZDsgaW5zaWRlIGEgbG9uZyBnYXAgdGhlCnByb2ZpbGVzIGFyZSBib3Jyb3dlZCBmcm9tIG5laWdoYm91cmluZyBkYXlzIGFuZCB3ZWVrcyB3aXRoIEdhdXNzaWFuIHdlaWdodHMuCgpgYGB7ciBzdGVwNX0KZHNkX2RlY29tcG9zZSA8LSBmdW5jdGlvbih6LCBid19kYXkgPSAxNCwgYndfd2VlayA9IDE2LCB3ZWVrbHkgPSBUUlVFLCBuX2l0ZXIgPSAzKSB7CiAgbiA8LSBsZW5ndGgoeikKICBUciA8LSBuYW5fbWEoeiwgUzIpOyBEIDwtIG51bWVyaWMobik7IFcgPC0gbnVtZXJpYyhuKQogIGZvciAoaXQgaW4gc2VxX2xlbihuX2l0ZXIpKSB7CiAgICBTZCA8LSBjeWNsZV9zbW9vdGgoeiAtIFRyIC0gVywgUzEsIGJ3X2RheSkgICAgIyBFcS4gKDQpLCBkYXlzIHggNDgKICAgIFNkIDwtIFNkIC0gcm93TWVhbnMoU2QpICAgICAgICAgICAgICAgICAgICAgICAjIHplcm8gbWVhbiB3aXRoaW4gZWFjaCBkYXkKICAgIEQgIDwtIGFzLnZlY3Rvcih0KFNkKSlbc2VxX2xlbihuKV0KICAgIGlmICh3ZWVrbHkpIFcgPC0gYXMudmVjdG9yKHQoY2VudGVyX3dlZWtseShjeWNsZV9zbW9vdGgoeiAtIFRyIC0gRCwgUzIsIGJ3X3dlZWspKSkpW3NlcV9sZW4obildCiAgICBUciA8LSBuYW5fbWEoeiAtIEQgLSBXLCBTMikgICAgICAgICAgICAgICAgICAgIyBFcS4gKDMpCiAgfQogIGxpc3QodHJlbmQgPSBUciwgZGFpbHkgPSBELCB3ZWVrbHkgPSBXLCByZW1haW5kZXIgPSB6IC0gVHIgLSBEIC0gVykKfQpgYGAKCiMjIFN0ZXAgNiDigJQgUmVtYWluZGVyIGltcHV0YXRpb24gYnkgdGhlIEFSKDEpIGJyaWRnZSAoRXFzLiA24oCTNywgUHJvcG9zaXRpb24gMSkKClRoZSByZW1haW5kZXIgaXMgbW9kZWxsZWQgYXMgJFJfdD1ccGhpIFJfe3QtMX0rXHZhcmVwc2lsb25fdCQsIHdpdGggJFxwaGkkIGVzdGltYXRlZCBmcm9tIHRoZSBwYWlycyBvZgpjb25zZWN1dGl2ZSBvYnNlcnZhdGlvbnMgJFxtYXRoY2FsIFA9XHt0OnRcaW5cbWF0aGNhbCBPLFwgdC0xXGluXG1hdGhjYWwgT1x9JDoKCiQkClx0aWxkZVxwaGk9XGZyYWN7XHN1bV97dFxpblxtYXRoY2FsIFB9Ul90Ul97dC0xfX17XHN1bV97dFxpblxtYXRoY2FsIFB9Ul97dC0xfV4yfSxccXF1YWQKXGhhdFxwaGk9XG1pblx7MC45OTksXG1heFx7MCxcdGlsZGVccGhpXH1cfS4gXHRhZ3s2fQokJAoKRm9yIGEgbWlzc2luZyAkdCQgd2l0aCBuZWFyZXN0IG9ic2VydmVkIG5laWdoYm91cnMgJGE8dDxiJCwKCiQkClxoYXQgUl90PVxmcmFje1xwaGlee1wsdC1hfVxiaWcoMS1ccGhpXnsyKGItdCl9XGJpZylSX2ErXHBoaV57XCxiLXR9XGJpZygxLVxwaGleezIodC1hKX1cYmlnKVJfYn17MS1ccGhpXnsyKGItYSl9fSwgXHRhZ3s3fQokJAoKd2hpbGUgJFxoYXQgUl90PVxwaGlee2ItdH1SX2IkIGJlZm9yZSB0aGUgZmlyc3QgYW5kICRcaGF0IFJfdD1ccGhpXnt0LWF9Ul9hJCBhZnRlciB0aGUgbGFzdCBvYnNlcnZhdGlvbgoodGhlIHJlbWFpbmRlciBpcyBjZW50cmVkIGJ5IGl0cyBvYnNlcnZlZCBtZWFuIGZpcnN0KS4KCioqUHJvcG9zaXRpb24gMS4qKiBGb3IgYSBzdGF0aW9uYXJ5IEdhdXNzaWFuIEFSKDEpLCAoNykgZXF1YWxzIHRoZSBLYWxtYW4gc21vb3RoZXIgZXN0aW1hdGUKJFxtYXRocm0gRVtSX3RcbWlkIFJfcyxzXGluXG1hdGhjYWwgT10kLCB3aXRoIGVycm9yIHZhcmlhbmNlCiRcZ2FtbWFfMFxiaWdbMS1cZnJhY3tccGhpXnsyKHQtYSl9K1xwaGleezIoYi10KX0tMlxwaGleezIoYi1hKX19ezEtXHBoaV57MihiLWEpfX1cYmlnXSQsICRcZ2FtbWFfMD1cbWF0aHJte1Zhcn0oUl90KSQuCkJ5IHRoZSBNYXJrb3YgcHJvcGVydHksICRSX3QkIGRlcGVuZHMgb24gdGhlIG90aGVyIG9ic2VydmF0aW9ucyBvbmx5IHRocm91Z2ggJChSX2EsUl9iKSQ7IEdhdXNzaWFuCmNvbmRpdGlvbmluZyB3aXRoICRcbWF0aHJte0Nvdn0oUl9zLFJfdSk9XGdhbW1hXzBccGhpXnt8cy11fH0kIGdpdmVzIHRoZSB3ZWlnaHRzCiRcbWF0aGJmIGNeXHRvcFxib2xkc3ltYm9sXFNpZ21hXnstMX0kIHdpdGggJFxtYXRoYmYgYz1cZ2FtbWFfMChccGhpXnt0LWF9LFxwaGlee2ItdH0pXlx0b3AkIGFuZAokXGJvbGRzeW1ib2xcU2lnbWE9XGdhbW1hXzBcbGVmdFtcYmVnaW57c21hbGxtYXRyaXh9MSZccGhpXntiLWF9XFxccGhpXntiLWF9JjFcZW5ke3NtYWxsbWF0cml4fVxyaWdodF0kLgpGb3Igc2hvcnQgZ2FwcyB3aXRoICRccGhpXGFwcHJveDEkLCAoNykgaXMgY2xvc2UgdG8gbGluZWFyIGludGVycG9sYXRpb24gb2YgdGhlIHJlbWFpbmRlcjsgZm9yIGxvbmcgZ2FwcwokXGhhdCBSX3RcdG8wJCBhbmQgdGhlIGltcHV0YXRpb24gcmV2ZXJ0cyB0byB0aGUgcHJvZmlsZSAkVF90K0RfdCtXX3QkLiBUaGUgY29kZSB2ZXJpZmllcyB0aGUgbWVhbgoqKmFuZCoqIHRoZSB2YXJpYW5jZSBvZiBQcm9wb3NpdGlvbiAxIGFnYWluc3QgYHN0YXRzOjpLYWxtYW5TbW9vdGhgLgoKYGBge3Igc3RlcDZ9CmVzdGltYXRlX2FyMSA8LSBmdW5jdGlvbihSKSB7ICAgICAgICAgICAgICAgICAgICAgIyBFcS4gKDYpCiAgYSA8LSBSWy1sZW5ndGgoUildOyBiIDwtIFJbLTFdOyBtIDwtICFpcy5uYShhKSAmICFpcy5uYShiKQogIGlmIChzdW0obSkgPCAxMCkgcmV0dXJuKDApCiAgbWluKG1heChzdW0oYVttXSAqIGJbbV0pIC8gc3VtKGFbbV1eMiksIDApLCAwLjk5OSkKfQphcjFfYnJpZGdlIDwtIGZ1bmN0aW9uKFIsIHBoaSA9IE5VTEwpIHsgICAgICAgICAgICMgRXEuICg3KQogIG9icyA8LSAhaXMubmEoUikKICBpZiAoYWxsKG9icykpIHJldHVybihSKQogIGlmICghYW55KG9icykpIHJldHVybihyZXAoMCwgbGVuZ3RoKFIpKSkKICBpZiAoaXMubnVsbChwaGkpKSBwaGkgPC0gZXN0aW1hdGVfYXIxKFIpCiAgbXUgPC0gbWVhbihSLCBuYS5ybSA9IFRSVUUpOyBSYyA8LSBSIC0gbXUKICBpbyA8LSB3aGljaChvYnMpOyBpbSA8LSB3aGljaCghb2JzKQogIGsgIDwtIGZpbmRJbnRlcnZhbChpbSwgaW8pICAgICAgICAgICAgICAgICAgICAgICMgbnVtYmVyIG9mIG9ic2VydmVkIGluZGljZXMgYmVmb3JlIHQKICBocCA8LSBrID4gMDsgaG4gPC0gayA8IGxlbmd0aChpbykgICAgICAgICAgICAgICAjIGhhcyBhIHByZXZpb3VzIC8gbmV4dCBvYnNlcnZhdGlvbgogIGEgIDwtIGlmZWxzZShocCwgaW9bcG1heChrLCAxKV0sIE5BX2ludGVnZXJfKQogIGIgIDwtIGlmZWxzZShobiwgaW9bcG1pbihrICsgMSwgbGVuZ3RoKGlvKSldLCBOQV9pbnRlZ2VyXykKICBlc3QgPC0gbnVtZXJpYyhsZW5ndGgoaW0pKTsgYm90aCA8LSBocCAmIGhuCiAgaWYgKGFueShib3RoKSkgewogICAgdHQgPC0gaW1bYm90aF07IGFhIDwtIGFbYm90aF07IGJiIDwtIGJbYm90aF0KICAgIHBhIDwtIHBoaV4odHQgLSBhYSk7IHBiIDwtIHBoaV4oYmIgLSB0dCk7IGRlbiA8LSAxIC0gcGhpXigyICogKGJiIC0gYWEpKQogICAgZXN0W2JvdGhdIDwtIChwYSAqICgxIC0gcGJeMikgKiBSY1thYV0gKyBwYiAqICgxIC0gcGFeMikgKiBSY1tiYl0pIC8gZGVuCiAgfQogIG9hIDwtIGhwICYgIWhuOyBpZiAoYW55KG9hKSkgZXN0W29hXSA8LSBwaGleKGltW29hXSAtIGFbb2FdKSAqIFJjW2Fbb2FdXQogIG9iIDwtICFocCAmIGhuOyBpZiAoYW55KG9iKSkgZXN0W29iXSA8LSBwaGleKGJbb2JdIC0gaW1bb2JdKSAqIFJjW2Jbb2JdXQogIFJbaW1dIDwtIGVzdCArIG11CiAgUgp9CmFyMV9icmlkZ2VfdmFyIDwtIGZ1bmN0aW9uKFIsIHBoaSwgZ2FtbWEwKSB7ICAgICAgIyB2YXJpYW5jZSBpbiBQcm9wb3NpdGlvbiAxIChpbm5lciBnYXBzKQogIGlvIDwtIHdoaWNoKCFpcy5uYShSKSk7IGltIDwtIHdoaWNoKGlzLm5hKFIpKTsgayA8LSBmaW5kSW50ZXJ2YWwoaW0sIGlvKQogIGEgPC0gaW9ba107IGIgPC0gaW9bayArIDFdOyBtIDwtIGIgLSBhCiAgZ2FtbWEwICogKDEgLSAocGhpXigyICogKGltIC0gYSkpICsgcGhpXigyICogKGIgLSBpbSkpIC0gMiAqIHBoaV4oMiAqIG0pKSAvICgxIC0gcGhpXigyICogbSkpKQp9CnNldC5zZWVkKDEpOyBwaGkwIDwtIDAuOQpyICA8LSBhcy5udW1lcmljKGFyaW1hLnNpbShsaXN0KGFyID0gcGhpMCksIDIwMDApKTsgcltjKDEwMDoxODAsIDUwMDo1MDMpXSA8LSBOQQprcyA8LSBzdGF0czo6S2FsbWFuU21vb3RoKHIgLSBtZWFuKHIsIG5hLnJtID0gVFJVRSksIHN0YXRzOjptYWtlQVJJTUEocGhpID0gcGhpMCwgdGhldGEgPSBudW1lcmljKCksIERlbHRhID0gbnVtZXJpYygpKSwgbml0ID0gLTFMKQpjYXQoIlByb3Bvc2l0aW9uIDEsIG1lYW46ICAgICBtYXggfGJyaWRnZSAtIEthbG1hbiBzbW9vdGhlcnwgPSIsCiAgICBzaWduaWYobWF4KGFicyhhcjFfYnJpZGdlKHIsIHBoaTApIC0gKGtzJHNtb290aFssIDFdICsgbWVhbihyLCBuYS5ybSA9IFRSVUUpKSlbaXMubmEocildKSwgMyksICJcbiIpCmNhdCgiUHJvcG9zaXRpb24gMSwgdmFyaWFuY2U6IG1heCB8Zm9ybXVsYSAtIEthbG1hbiBzbW9vdGhlcnwgPSIsCiAgICBzaWduaWYobWF4KGFicyhhcjFfYnJpZGdlX3ZhcihyLCBwaGkwLCAxIC8gKDEgLSBwaGkwXjIpKSAtIGtzJHZhcltpcy5uYShyKSwgMSwgMV0pKSwgMyksICJcbiIpCmBgYAoKIyMgU3RlcCA3IOKAlCBSZWNvbnN0cnVjdGlvbiAoRXEuIDgpIGFuZCB0aGUgY29tcGxldGUgZnVuY3Rpb24KCiQkClxoYXQgeV90PVxtYXhcQmlnXHswLFwgbVxiaWdbXGV4cFxiaWcoVF90K0RfdCtXX3QrXGhhdCBSX3RcYmlnKS0xXGJpZ11cQmlnXH0sXHFxdWFkIHRcaW5cbWF0aGNhbCBNLCBcdGFnezh9CiQkCgphbmQgdGhlIG9ic2VydmVkIHZhbHVlcyBhcmUgbGVmdCB1bmNoYW5nZWQuIFRoZSBhcmd1bWVudHMgYHRyYW5zZm9ybWAsIGB3ZWVrbHlgIGFuZCBgcmVtYWluZGVyYCBkZWZpbmUKdGhlIHZhcmlhbnRzIG9mIHRoZSBhYmxhdGlvbiBzdHVkeSBpbiBFeHBlcmltZW50IDEuIFRoZSBsYXN0IGxpbmUgbWVhc3VyZXMgdGhlIGNvbXB1dGluZyB0aW1lIGZvciBvbmUgZnVsbCBzZXJpZXMuCgpgYGB7ciBzdGVwN30KaW1wdXRlX3NlYXNhciA8LSBmdW5jdGlvbih4LCBid19kYXkgPSAxNCwgYndfd2VlayA9IDE2LCB0cmFuc2Zvcm0gPSAibG9nIiwgd2Vla2x5ID0gVFJVRSwKICAgICAgICAgICAgICAgICAgICAgICAgcmVtYWluZGVyID0gImFyMSIsIG5faXRlciA9IDMsIHJldHVybl9jb21wb25lbnRzID0gRkFMU0UpIHsKICBvYnMgPC0gIWlzLm5hKHgpCiAgaWYgKGFsbChvYnMpKSByZXR1cm4oeCkKICBtIDwtIHNlYXNhcl9zY2FsZSh4KQogIHogPC0gaWYgKHRyYW5zZm9ybSA9PSAibG9nIikgbG9nMXAocG1heCh4LCAwKSAvIG0pIGVsc2UgeCAvIG0gICAgICAgICAgIyBFcS4gKDEpCiAgZGMgPC0gZHNkX2RlY29tcG9zZSh6LCBid19kYXksIGJ3X3dlZWssIHdlZWtseSwgbl9pdGVyKSAgICAgICAgICAgICAgICAjIEVxcy4gKDMpLSg1KQogIFJoIDwtIHN3aXRjaChyZW1haW5kZXIsIGFyMSA9IGFyMV9icmlkZ2UoZGMkcmVtYWluZGVyKSwgICAgICAgICAgICAgICAgIyBFcXMuICg2KS0oNykKICAgICAgICAgICAgICAgbGluZWFyID0gaW50ZXJwX2xpbmVhcihkYyRyZW1haW5kZXIpLAogICAgICAgICAgICAgICB6ZXJvID0gaWZlbHNlKGlzLm5hKGRjJHJlbWFpbmRlciksIDAsIGRjJHJlbWFpbmRlcikpCiAgemggPC0gZGMkdHJlbmQgKyBkYyRkYWlseSArIGRjJHdlZWtseSArIFJoCiAgeWggPC0gcG1heChpZiAodHJhbnNmb3JtID09ICJsb2ciKSBtICogZXhwbTEoemgpIGVsc2UgbSAqIHpoLCAwKSAgICAgICAgIyBFcS4gKDgpCiAgb3V0IDwtIHg7IG91dFshb2JzXSA8LSB5aFshb2JzXQogIGlmIChyZXR1cm5fY29tcG9uZW50cykgcmV0dXJuKGxpc3QoaW1wdXRlZCA9IG91dCwgeiA9IHosIGNvbXAgPSBkYywgcmVtYWluZGVyX2ltcCA9IFJoKSkKICBvdXQKfQp4X25vMiA8LSBnZXRfc2VyaWVzKCJTVUYgMSIsICJOTzIiKQp0X3NlYXNhciA8LSBzeXN0ZW0udGltZShmb3IgKGkgaW4gMTo1KSBpbXB1dGVfc2Vhc2FyKHhfbm8yKSlbWyJlbGFwc2VkIl1dIC8gNQpjYXQoIlNlYXMtQVIgaW1wdXRlcyB0aGUgZnVsbCBOTzIgc2VyaWVzIG9mIFNVRiAxIGluIiwgcm91bmQodF9zZWFzYXIsIDMpLCAic1xuIikKYGBgCgojIENvbXBldGluZyBtZXRob2RzCgpBbGwgb3V0cHV0cyBhcmUgdHJ1bmNhdGVkIGF0IHplcm8sICRcaGF0IHlfdFxsZWZ0YXJyb3dcbWF4XHswLFxoYXQgeV90XH0kLgoKKiAqKk1FQU4sIE1FRElBTjoqKiAkXGhhdCB5X3Q9XGJhciB5X3tcbWF0aGNhbCBPfSQgb3IgJFxvcGVyYXRvcm5hbWV7bWVkaWFufSh5X3tcbWF0aGNhbCBPfSkkIChgaW1wdXRlVFM6Om5hX21lYW5gKS4KKiAqKkxJOioqIGxpbmVhciBpbnRlcnBvbGF0aW9uIG9mIFN0ZXAgMiAoYG5hX2ludGVycG9sYXRpb25gKS4KKiAqKlNTLU1FQU4sIFNTLU1FRElBTiwgU1MtTEkqKiAoc2Vhc29uYWwgc3BsaXQsIHBlcmlvZCAzMzYpOiBmb3IgZXZlcnkgcG9zaXRpb24gJHAkIG9mIHRoZSB3ZWVrLCB0aGUKICBzdWJzZXJpZXMgJFx7eV90Oih0LTEpXGJtb2QzMzY9cC0xXH0kIGlzIGZpbGxlZCB3aXRoIGl0cyBvd24gbWVhbiwgbWVkaWFuIG9yIGxpbmVhciBpbnRlcnBvbGF0aW9uCiAgKGBuYV9zZWFzcGxpdGApLiBQb3NpdGlvbnMgd2l0aG91dCBhbnkgb2JzZXJ2YXRpb24gdXNlIHRoZSBzYW1lIGdsb2JhbCBzdGF0aXN0aWMuCiogKipTRUFERUM6KiogY2xhc3NpY2FsIGFkZGl0aXZlIGRlY29tcG9zaXRpb24gd2l0aCBwZXJpb2QgNDggb2YgdGhlIGludGVycG9sYXRlZCBzZXJpZXMsCiAgJFNfdD1cYmFyIGVfe2godCl9LVxmcmFjMXs0OH1cc3VtX2hcYmFyIGVfaCQgd2l0aCAkXGJhciBlX2gkIHRoZSBtZWFuIG9mICR5LVxtYXRocm17TUF9X3syXHRpbWVzNDh9KHkpJAogIGF0IGhhbGYtaG91ciAkaCQ7IHRoZW4gJFxoYXQgeV90PVxtYXRocm17TEl9KHktUylfdCtTX3QkLiBUaGlzIGlzIHRoZSBzY2hlbWUgb2YgYG5hX3NlYWRlY2AsIHdoaWNoCiAgaXRzZWxmIHVzZXMgYSByb2J1c3QgU1RMIChldmFsdWF0ZWQgc2VwYXJhdGVseSBpbiBTZWN0aW9uIDYuOCkuCiogKipNU1RMLUxJOioqIGFzIFNFQURFQywgd2l0aCAkU190PVNeeyg0OCl9X3QrU157KDMzNil9X3QkIGZyb20gYGZvcmVjYXN0Ojptc3RsYC4KKiAqKktBTE1BTjoqKiBhbiBBUklNQSgyLDAsMSkgbW9kZWwgZm9yICR5X3QtXGJhciB5JCBmaXR0ZWQgYnkgbWF4aW11bSBsaWtlbGlob29kICh0aGUgS2FsbWFuIGZpbHRlcgogIGhhbmRsZXMgdGhlIG1pc3NpbmcgdmFsdWVzIGV4YWN0bHkpLCBhbmQgJFxoYXQgeV90PVxtYXRoYmYgWl5cdG9wXGhhdHtcYm9sZHN5bWJvbFxhbHBoYX1fe3RcbWlkIG59K1xiYXIgeSQKICBmcm9tIHRoZSBLYWxtYW4gc21vb3RoZXI7IGFuIEFSKDEpIGlzIHVzZWQgaWYgdGhlIG9wdGltaXNhdGlvbiBmYWlscy4KCmBgYHtyIGJhc2VsaW5lc30KaW1wdXRlX21lYW4gICA8LSBmdW5jdGlvbih4KSB7IHhbaXMubmEoeCldIDwtIG1lYW4oeCwgbmEucm0gPSBUUlVFKTsgeCB9CmltcHV0ZV9tZWRpYW4gPC0gZnVuY3Rpb24oeCkgeyB4W2lzLm5hKHgpXSA8LSBtZWRpYW4oeCwgbmEucm0gPSBUUlVFKTsgeCB9CmltcHV0ZV9zZWFzcGxpdCA8LSBmdW5jdGlvbih4LCBhbGdvcml0aG0gPSAibWVkaWFuIiwgcGVyaW9kID0gUzIpIHsKICB4MCA8LSB4CiAgZm9yIChrIGluIHNlcV9sZW4ocGVyaW9kKSkgewogICAgaWR4IDwtIHNlcShrLCBsZW5ndGgoeCksIGJ5ID0gcGVyaW9kKTsgc3ViIDwtIHhbaWR4XQogICAgaWYgKGFueU5BKHN1YikpIHhbaWR4XSA8LSBzd2l0Y2goYWxnb3JpdGhtLCBtZWFuID0gaW1wdXRlX21lYW4oc3ViKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1lZGlhbiA9IGltcHV0ZV9tZWRpYW4oc3ViKSwgaW50ZXJwb2xhdGlvbiA9IGludGVycF9saW5lYXIoc3ViKSkKICB9CiAgaWYgKGFueU5BKHgpKSB7ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBwb3NpdGlvbnMgd2l0aG91dCBhbnkgb2JzZXJ2YXRpb246IGdsb2JhbCBmYWxsYmFjawogICAgZmIgPC0gc3dpdGNoKGFsZ29yaXRobSwgbWVhbiA9IGltcHV0ZV9tZWFuLCBtZWRpYW4gPSBpbXB1dGVfbWVkaWFuLCBpbnRlcnBvbGF0aW9uID0gaW50ZXJwX2xpbmVhcikKICAgIHhbaXMubmEoeCldIDwtIGZiKHgwKVtpcy5uYSh4KV0KICB9CiAgeAp9CmltcHV0ZV9zZWFkZWMgPC0gZnVuY3Rpb24oeCwgcGVyaW9kID0gUzEpIHsgICAgICAgIyBjbGFzc2ljYWwgYWRkaXRpdmUgZGVjb21wb3NpdGlvbgogIHNlYXMgPC0gYXMubnVtZXJpYyhzdGF0czo6ZGVjb21wb3NlKHRzKGludGVycF9saW5lYXIoeCksIGZyZXF1ZW5jeSA9IHBlcmlvZCkpJHNlYXNvbmFsKQogIHhkIDwtIGludGVycF9saW5lYXIoeCAtIHNlYXMpOyB4W2lzLm5hKHgpXSA8LSAoeGQgKyBzZWFzKVtpcy5uYSh4KV07IHgKfQppbXB1dGVfc2VhZGVjX3N0bCA8LSBmdW5jdGlvbih4LCBwZXJpb2QgPSBTMSkgeyAgICMgcmVwbGljYSBvZiBpbXB1dGVUUzo6bmFfc2VhZGVjIChyb2J1c3QgU1RMKQogIHNlYXMgPC0gYXMubnVtZXJpYyhzdGF0czo6c3RsKHRzKGludGVycF9saW5lYXIoeCksIGZyZXF1ZW5jeSA9IHBlcmlvZCksIHJvYnVzdCA9IFRSVUUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcy53aW5kb3cgPSAxMSkkdGltZS5zZXJpZXNbLCAic2Vhc29uYWwiXSkKICB4ZCA8LSBpbnRlcnBfbGluZWFyKHggLSBzZWFzKTsgeFtpcy5uYSh4KV0gPC0gKHhkICsgc2VhcylbaXMubmEoeCldOyB4Cn0KaW1wdXRlX21zdGxfbGkgPC0gZnVuY3Rpb24oeCkgewogIGZpdCAgPC0gZm9yZWNhc3Q6Om1zdGwoZm9yZWNhc3Q6Om1zdHMoaW50ZXJwX2xpbmVhcih4KSwgc2Vhc29uYWwucGVyaW9kcyA9IGMoUzEsIFMyKSkpCiAgc2VhcyA8LSByb3dTdW1zKGZpdFssIGdyZXAoIlNlYXNvbmFsIiwgY29sbmFtZXMoZml0KSksIGRyb3AgPSBGQUxTRV0pCiAgeGQgPC0gaW50ZXJwX2xpbmVhcih4IC0gc2Vhcyk7IHhbaXMubmEoeCldIDwtICh4ZCArIHNlYXMpW2lzLm5hKHgpXTsgeAp9CmltcHV0ZV9rYWxtYW4gPC0gZnVuY3Rpb24oeCwgb3JkZXIgPSBjKDIsIDAsIDEpKSB7CiAgbXUgPC0gbWVhbih4LCBuYS5ybSA9IFRSVUUpOyB4MCA8LSB4IC0gbXUKICBmaXQgPC0gdHJ5Q2F0Y2goc3RhdHM6OmFyaW1hKHgwLCBvcmRlciA9IG9yZGVyLCBpbmNsdWRlLm1lYW4gPSBGQUxTRSwgbWV0aG9kID0gIk1MIiksCiAgICAgICAgICAgICAgICAgIGVycm9yID0gZnVuY3Rpb24oZSkgc3RhdHM6OmFyaW1hKHgwLCBvcmRlciA9IGMoMSwgMCwgMCksIGluY2x1ZGUubWVhbiA9IEZBTFNFLCBtZXRob2QgPSAiTUwiKSkKICBzbSA8LSBzdGF0czo6S2FsbWFuU21vb3RoKHgwLCBmaXQkbW9kZWwsIG5pdCA9IC0xTCkkc21vb3RoCiAgeWhhdCA8LSBhcy5udW1lcmljKHNtICUqJSBmaXQkbW9kZWwkWikgKyBtdQogIHhbaXMubmEoeCldIDwtIHloYXRbaXMubmEoeCldOyB4Cn0Kbm9ubmVnIDwtIGZ1bmN0aW9uKGYpIGZ1bmN0aW9uKHgpIHBtYXgoZih4KSwgMCkKTUVUSE9EX09SREVSIDwtIGMoIk1FQU4iLCAiTUVESUFOIiwgIkxJIiwgIlNTLU1FQU4iLCAiU1MtTUVESUFOIiwgIlNTLUxJIiwgIlNFQURFQyIsICJNU1RMLUxJIiwgIktBTE1BTiIsICJTZWFzLUFSIikKTUVUSE9EUyA8LSBsYXBwbHkobGlzdChNRUFOID0gaW1wdXRlX21lYW4sIE1FRElBTiA9IGltcHV0ZV9tZWRpYW4sIExJID0gaW50ZXJwX2xpbmVhciwKICBgU1MtTUVBTmAgPSBmdW5jdGlvbih4KSBpbXB1dGVfc2Vhc3BsaXQoeCwgIm1lYW4iKSwgYFNTLU1FRElBTmAgPSBmdW5jdGlvbih4KSBpbXB1dGVfc2Vhc3BsaXQoeCwgIm1lZGlhbiIpLAogIGBTUy1MSWAgPSBmdW5jdGlvbih4KSBpbXB1dGVfc2Vhc3BsaXQoeCwgImludGVycG9sYXRpb24iKSwgU0VBREVDID0gaW1wdXRlX3NlYWRlYywKICBgTVNUTC1MSWAgPSBpbXB1dGVfbXN0bF9saSwgS0FMTUFOID0gaW1wdXRlX2thbG1hbiwgYFNlYXMtQVJgID0gaW1wdXRlX3NlYXNhciksIG5vbm5lZykKYGBgCgoqKlZhbGlkYXRpb24gYWdhaW5zdCBgaW1wdXRlVFNgLioqIEluIGBpbXB1dGVUU2AgdGhlIHNlYXNvbmFsLXNwbGl0IG1lZGlhbiBpcyByZXF1ZXN0ZWQgd2l0aApgYWxnb3JpdGhtID0gIm1lYW4iLCBvcHRpb24gPSAibWVkaWFuImAuIFRoZSBsYXN0IHJvdyBpcyBub24temVybyBieSBkZXNpZ24gKGNsYXNzaWNhbCBkZWNvbXBvc2l0aW9uCnZlcnN1cyByb2J1c3QgU1RMKS4KCmBgYHtyIGNyb3NzY2hlY2staW1wdXRldHN9CmlmIChIQVNfSU1QVVRFVFMpIHsKICB4czEgPC0gdHMoeF9ubzIsIGZyZXF1ZW5jeSA9IFMxKTsgeHMyIDwtIHRzKHhfbm8yLCBmcmVxdWVuY3kgPSBTMikKICBjaGsgPC0gZnVuY3Rpb24oYSwgYikgc2lnbmlmKG1heChhYnMoYSAtIGFzLm51bWVyaWMoYikpKSwgMykKICBrbml0cjo6a2FibGUoZGF0YS5mcmFtZSgKICAgIGNvbXBhcmlzb24gPSBjKCJMSSB2cyBuYV9pbnRlcnBvbGF0aW9uIiwgIlNTLU1FQU4gdnMgbmFfc2Vhc3BsaXQoYWxnb3JpdGhtID0gJ21lYW4nKSIsCiAgICAgICAgICAgICAgICAgICAiU1MtTUVESUFOIHZzIG5hX3NlYXNwbGl0KGFsZ29yaXRobSA9ICdtZWFuJywgb3B0aW9uID0gJ21lZGlhbicpIiwKICAgICAgICAgICAgICAgICAgICJTUy1MSSB2cyBuYV9zZWFzcGxpdChhbGdvcml0aG0gPSAnaW50ZXJwb2xhdGlvbicpIiwKICAgICAgICAgICAgICAgICAgICJyb2J1c3QtU1RMIHJlcGxpY2EgdnMgbmFfc2VhZGVjIiwgIlNFQURFQyAoY2xhc3NpY2FsIGRlY29tcG9zaXRpb24pIHZzIG5hX3NlYWRlYyIpLAogICAgbWF4X2Fic19kaWZmID0gYygKICAgICAgY2hrKGludGVycF9saW5lYXIoeF9ubzIpLCBpbXB1dGVUUzo6bmFfaW50ZXJwb2xhdGlvbih4X25vMikpLAogICAgICBjaGsoaW1wdXRlX3NlYXNwbGl0KHhfbm8yLCAibWVhbiIpLCBpbXB1dGVUUzo6bmFfc2Vhc3BsaXQoeHMyLCBhbGdvcml0aG0gPSAibWVhbiIpKSwKICAgICAgY2hrKGltcHV0ZV9zZWFzcGxpdCh4X25vMiwgIm1lZGlhbiIpLCBpbXB1dGVUUzo6bmFfc2Vhc3BsaXQoeHMyLCBhbGdvcml0aG0gPSAibWVhbiIsIG9wdGlvbiA9ICJtZWRpYW4iKSksCiAgICAgIGNoayhpbXB1dGVfc2Vhc3BsaXQoeF9ubzIsICJpbnRlcnBvbGF0aW9uIiksIGltcHV0ZVRTOjpuYV9zZWFzcGxpdCh4czIsIGFsZ29yaXRobSA9ICJpbnRlcnBvbGF0aW9uIikpLAogICAgICBjaGsoaW1wdXRlX3NlYWRlY19zdGwoeF9ubzIpLCBpbXB1dGVUUzo6bmFfc2VhZGVjKHhzMSwgYWxnb3JpdGhtID0gImludGVycG9sYXRpb24iKSksCiAgICAgIGNoayhpbXB1dGVfc2VhZGVjKHhfbm8yKSwgaW1wdXRlVFM6Om5hX3NlYWRlYyh4czEsIGFsZ29yaXRobSA9ICJpbnRlcnBvbGF0aW9uIikpKSksCiAgICBjYXB0aW9uID0gIkFncmVlbWVudCB3aXRoIGltcHV0ZVRTICgwID0gaWRlbnRpY2FsOyB0aGUgbGFzdCByb3cgZGlmZmVycyBieSBkZXNpZ24pIikKfSBlbHNlIGNhdCgiaW1wdXRlVFMgaXMgbm90IGluc3RhbGxlZDsgY2hlY2sgc2tpcHBlZC5cbiIpCmBgYAoKIyBFeHBlcmltZW50YWwgZGVzaWduCgojIyBBcnRpZmljaWFsIGdhcHMKClRoZSB0cnVlIHZhbHVlcyBiZWhpbmQgcmVhbCBnYXBzIGFyZSB1bmtub3duLCBzbyBhY2N1cmFjeSBpcyBtZWFzdXJlZCBvbiBvYnNlcnZhdGlvbnMgcmVtb3ZlZCBvbgpwdXJwb3NlLiBGb3IgYSBnYXAgcmVnaW1lIHdpdGggbGVuZ3RocyBpbiAkW0xfe1xtaW59LExfe1xtYXh9XSQgYW5kIGEgcmF0ZSAkXHJobyQ6CgoxLiBkcmF3IGEgbGVuZ3RoICRMXHNpbVxtYXRocm0gVVx7TF97XG1pbn0sXGRvdHMsTF97XG1heH1cfSQgYW5kIGEgc3RhcnQgJHMkIGF0IHJhbmRvbTsKMi4gYWNjZXB0IHRoZSBibG9jayAkXHtzLFxkb3RzLHMrTC0xXH0kIG9ubHkgaWYgdGhlIGJsb2NrICoqYW5kKiogb25lIGd1YXJkIHBvaW50IG9uIGVhY2ggc2lkZQogICAoJHMtMSQgYW5kICRzK0wkKSBhcmUgb2JzZXJ2ZWQgYW5kIHVudXNlZCwgc28gdGhhdCB0aGUgcmVhbGlzZWQgZ2FwIGhhcyBsZW5ndGggZXhhY3RseSAkTCQ7CjMuIHJlcGVhdCB1bnRpbCBhIGZyYWN0aW9uICRccmhvJCBvZiB0aGUgb2JzZXJ2ZWQgdmFsdWVzIGlzIHJlbW92ZWQuCgpUaGUgcmVnaW1lcyBhcmUgKnNob3J0KiAkXG1hdGhybSBVXHsxLi40XH0kICh1cCB0byAyIGgpLCAqbWVkaXVtKiAkXG1hdGhybSBVXHs1Li40OFx9JCAodXAgdG8gb25lIGRheSkKYW5kICpsb25nKiAkXG1hdGhybSBVXHs0OS4uMzM2XH0kICh1cCB0byBvbmUgd2VlayksIHdpdGggJFxyaG9caW5cezEwXCUsMzBcJVx9JCBhbmQgdGhyZWUgcmVwbGljYXRlczoKMTggc2NlbmFyaW9zIMOXIDE1IHNlcmllcyA9ICoqMjcwIHNjZW5hcmlvcyoqLiBXaXRoIGBtYXNrX3NvdXJjZSA9ICJmaWxlImAgdGhlIGJsb2NrcyBhcmUgcmVhZCBmcm9tCmBtYXNrcy9gLCBnZW5lcmF0ZWQgb25jZSB3aXRoIHRoaXMgYWxnb3JpdGhtIGFuZCBmaXhlZCBzZWVkcywgc28gZXZlcnkgcnVuIHVzZXMgdGhlIHNhbWUgc2NlbmFyaW9zLgoKIyMgQWNjdXJhY3kgbWVhc3VyZXMgKEVxLiA5KQoKJCQKXG1hdGhybXtSTVNFfT1cQmlnKFx0ZnJhY3sxfXt8XG1hdGhjYWwgQXx9XHN1bV97dFxpblxtYXRoY2FsIEF9KFxoYXQgeV90LXlfdCleMlxCaWcpXnsxLzJ9LFxxcXVhZApcbWF0aHJte01BRX09XHRmcmFjezF9e3xcbWF0aGNhbCBBfH1cc3VtX3t0XGluXG1hdGhjYWwgQX18XGhhdCB5X3QteV90fCxccXF1YWQKXG1hdGhybXtOUk1TRX09XGZyYWN7XG1hdGhybXtSTVNFfX17XHNpZ21hX3l9LCBcdGFnezl9CiQkCgp3aGVyZSAkXG1hdGhjYWwgQSQgaXMgdGhlIHNldCBvZiByZW1vdmVkIHBvaW50cy4gTlJNU0UgbWFrZXMgZXJyb3JzIGNvbXBhcmFibGUgYWNyb3NzIHBvbGx1dGFudHMuCgpgYGB7ciBtYXNraW5nfQpSRUdJTUVTIDwtIGxpc3Qoc2hvcnQgPSBjKDEsIDQpLCBtZWRpdW0gPSBjKDUsIDQ4KSwgbG9uZyA9IGMoNDksIDMzNikpOyBSRUdfTkFNRVMgPC0gbmFtZXMoUkVHSU1FUykKbWFrZV9ibG9ja19tYXNrIDwtIGZ1bmN0aW9uKHgsIHJhdGUsIHJlZ2ltZSwgc2VlZCkgeyAgICAgICMgdXNlZCB3aGVuIG1hc2tfc291cmNlID0gIlIiCiAgc2V0LnNlZWQoc2VlZCkKICBuIDwtIGxlbmd0aCh4KTsgb2JzIDwtICFpcy5uYSh4KTsgdGFyZ2V0IDwtIHJvdW5kKHJhdGUgKiBzdW0ob2JzKSkKICBsciA8LSBSRUdJTUVTW1tyZWdpbWVdXTsgYXZhaWwgPC0gb2JzOyBtYXNrIDwtIGxvZ2ljYWwobik7IGNudCA8LSAwOyB0cmllcyA8LSAwCiAgd2hpbGUgKGNudCA8IHRhcmdldCAmJiB0cmllcyA8IDIwMDAwMCkgewogICAgdHJpZXMgPC0gdHJpZXMgKyAxCiAgICBMIDwtIHNhbXBsZShsclsxXTpsclsyXSwgMSk7IHMgPC0gc2FtcGxlLmludChuIC0gTCAtIDIsIDEpICsgMQogICAgaWYgKGFsbChhdmFpbFsocyAtIDEpOihzICsgTCldKSkgeyAgICAgICAgICAgICAgICAgICAgICMgYmxvY2sgYW5kIGJvdGggZ3VhcmQgcG9pbnRzIGF2YWlsYWJsZQogICAgICBtYXNrW3M6KHMgKyBMIC0gMSldIDwtIFRSVUU7IGF2YWlsWyhzIC0gMSk6KHMgKyBMKV0gPC0gRkFMU0U7IGNudCA8LSBjbnQgKyBMCiAgICB9CiAgfQogIG1hc2sKfQpsb2FkX21hc2tzIDwtIGZ1bmN0aW9uKGV4cCkgewogIGZfc2MgPC0gZmlsZS5wYXRoKFAkbWFza19kaXIsIHNwcmludGYoIm1hc2tzXyVzX3NjZW5hcmlvcy5jc3YiLCBleHApKQogIGZfYmwgPC0gZmlsZS5wYXRoKFAkbWFza19kaXIsIHNwcmludGYoIm1hc2tzXyVzX2Jsb2Nrcy5jc3YiLCBleHApKQogIGlmICghZmlsZS5leGlzdHMoZl9zYykgfHwgIWZpbGUuZXhpc3RzKGZfYmwpKSByZXR1cm4oTlVMTCkKICBibCA8LSByZWFkLmNzdihmX2JsKQogIGxpc3Qoc2MgPSByZWFkLmNzdihmX3NjKSwgYmxvY2tzID0gc3BsaXQoYmxbLCBjKCJzdGFydCIsICJsZW4iKV0sIGJsJHNpZCkpCn0KTUsgPC0gbGlzdChleHAwID0gTlVMTCwgZXhwMSA9IE5VTEwpCm1hc2tfZmlsZXMgPC0gZmlsZS5wYXRoKFAkbWFza19kaXIsIHNwcmludGYoIm1hc2tzXyVzXyVzLmNzdiIsIHJlcChjKCJleHAwIiwgImV4cDEiKSwgZWFjaCA9IDIpLCBjKCJzY2VuYXJpb3MiLCAiYmxvY2tzIikpKQppZiAoUCRtYXNrX3NvdXJjZSA9PSAiZmlsZSIgJiYgIWFsbChmaWxlLmV4aXN0cyhtYXNrX2ZpbGVzKSkpIHsKICBjYXQoIk5PVEU6IHRoZSBzdG9yZWQgbWFza3Mgd2VyZSBub3QgZm91bmQgaW4iLCBQJG1hc2tfZGlyLCAiLSBuZXcgbWFza3MgYXJlIGdlbmVyYXRlZCBpbiBSIGFuZCBldmVyeSByZXN1bHQgaXMiLAogICAgICAicmVjb21wdXRlZCwgc28gdGhlIG51bWJlcnMgZGlmZmVyIHNsaWdodGx5IGZyb20gdGhlIHN0b3JlZCByZXN1bHRzLlxuIikKICBQJG1hc2tfc291cmNlIDwtICJSIjsgUCR1c2VfY2FjaGUgPC0gRkFMU0UKfQppZiAoUCRtYXNrX3NvdXJjZSA9PSAiZmlsZSIpIHsgTUskZXhwMCA8LSBsb2FkX21hc2tzKCJleHAwIik7IE1LJGV4cDEgPC0gbG9hZF9tYXNrcygiZXhwMSIpIH0KVEFHMCA8LSBpZiAoaXMubnVsbChNSyRleHAwKSkgIlItbWFza3MiIGVsc2UgImZpbGUtbWFza3MiClRBRzEgPC0gaWYgKGlzLm51bGwoTUskZXhwMSkpICJSLW1hc2tzIiBlbHNlICJmaWxlLW1hc2tzIgpnZXRfbWFzayA8LSBmdW5jdGlvbihleHAsIHgsIHN0LCBwLCByZWcsIHJhdGUsIHJlcCwgc2VlZCkgewogIG1rIDwtIE1LW1tleHBdXQogIGlmIChpcy5udWxsKG1rKSkgcmV0dXJuKG1ha2VfYmxvY2tfbWFzayh4LCByYXRlLCByZWcsIHNlZWQpKQogIHNpZCA8LSBtayRzYyRzaWRbbWskc2Mkc3RhdGlvbiA9PSBzdCAmIG1rJHNjJHBvbGx1dGFudCA9PSBwICYgbWskc2MkcmVnaW1lID09IHJlZyAmCiAgICAgICAgICAgICAgICAgICBhYnMobWskc2MkcmF0ZSAtIHJhdGUpIDwgMWUtOSAmIG1rJHNjJHJlcCA9PSByZXBdCiAgYiA8LSBtayRibG9ja3NbW2FzLmNoYXJhY3RlcihzaWQpXV0KICBtIDwtIGxvZ2ljYWwobGVuZ3RoKHgpKTsgbVt1bmxpc3QobWFwcGx5KGZ1bmN0aW9uKHMsIGwpIHM6KHMgKyBsIC0gMSksIGIkc3RhcnQsIGIkbGVuKSldIDwtIFRSVUUKICBtCn0KY2F0KCJNYXNrczogRXhwZXJpbWVudCAwID0iLCBUQUcwLCAifCBFeHBlcmltZW50IDEgPSIsIFRBRzEsICJcbiIpCmBgYAoKIyBFeHBlcmltZW50IDA6IGJhbmR3aWR0aCBzZWxlY3Rpb24KCkZvciBhIHRhcmdldCBzdGF0aW9uICRzXiokIHRoZSBiYW5kd2lkdGhzIGFyZSBjaG9zZW4gb24gdGhlICoqb3RoZXIgdHdvIHN0YXRpb25zKio6CgokJAooXGhhdCBoX0QsXGhhdCBoX1cpX3tzXip9PVxhcmdcbWluX3toX0RcaW5cezMuNSw3LDE0LDI4XH0sXCBoX1dcaW5cezIsNCw4LDE2XH19XApcZnJhY3sxfXt8XG1hdGhjYWwgU197LXNeKn18fVxzdW1feyhzLHAscixpKVxpblxtYXRoY2FsIFNfey1zXip9fVxtYXRocm17TlJNU0V9X3tzLHAscixpfShoX0QsaF9XKSwKJCQKCndoZXJlICRcbWF0aGNhbCBTX3stc14qfSQgY29udGFpbnMgdGhlIHNjZW5hcmlvcyBvZiB0aGUgdHdvIG90aGVyIHN0YXRpb25zICg1IHBvbGx1dGFudHMgw5cgMyByZWdpbWVzIMOXCjIgcmVwbGljYXRlcywgJFxyaG89MjBcJSQpLiBTVUYgMSBuZXZlciBpbmZsdWVuY2VzIGl0cyBvd24gYmFuZHdpZHRocy4KCmBgYHtyIGV4cDAtcnVufQpCV19EQVkgIDwtIGlmIChRVUlDSykgYyg3LCAxNCkgZWxzZSBjKDMuNSwgNywgMTQsIDI4KQpCV19XRUVLIDwtIGlmIChRVUlDSykgYyg0LCAxNikgZWxzZSBjKDIsIDQsIDgsIDE2KQpkaXIwIDwtIG91dF9maWxlKHBhc3RlMCgiZXhwMF8iLCBUQUcwLCBpZiAoUVVJQ0spICJfcXVpY2siIGVsc2UgIiIpKTsgZGlyLmNyZWF0ZShkaXIwLCBzaG93V2FybmluZ3MgPSBGQUxTRSkKZm9yIChzaSBpbiBzZXFfYWxvbmcoU1RBVElPTlMpKSBmb3IgKHBrIGluIHNlcV9hbG9uZyhQT0xTKSkgewogIHN0IDwtIFNUQVRJT05TW3NpXTsgcCA8LSBQT0xTW3BrXTsgZiA8LSBmaWxlLnBhdGgoZGlyMCwgc3ByaW50ZigiJXNfJXMuY3N2IiwgZ3N1YigiICIsICIiLCBzdCksIHApKQogIGlmIChpc1RSVUUoUCR1c2VfY2FjaGUpICYmIGZpbGUuZXhpc3RzKGYpKSBuZXh0CiAgeCA8LSBnZXRfc2VyaWVzKHN0LCBwKTsgc2R4IDwtIHNkX3BvcCh4KTsgcm93cyA8LSBsaXN0KCkKICBmb3IgKHJpIGluIHNlcV9hbG9uZyhSRUdfTkFNRVMpKSBmb3IgKHJlcCBpbiBzZXFfbGVuKGlmIChRVUlDSykgMSBlbHNlIDIpKSB7CiAgICBtIDwtIGdldF9tYXNrKCJleHAwIiwgeCwgc3QsIHAsIFJFR19OQU1FU1tyaV0sIDAuMjAsIHJlcCwgc2VlZCA9IDEwMDAwICsgMTAwICogc2kgKyAxMCAqIHBrICsgMyAqIHJpICsgcmVwKQogICAgeG0gPC0geDsgeG1bbV0gPC0gTkEKICAgIGZvciAoYmQgaW4gQldfREFZKSBmb3IgKGJ3IGluIEJXX1dFRUspIHsKICAgICAgeSA8LSBpbXB1dGVfc2Vhc2FyKHhtLCBid19kYXkgPSBiZCwgYndfd2VlayA9IGJ3KQogICAgICByb3dzW1tsZW5ndGgocm93cykgKyAxXV0gPC0gZGF0YS5mcmFtZShzdGF0aW9uID0gc3QsIHBvbGx1dGFudCA9IHAsIHJlZ2ltZSA9IFJFR19OQU1FU1tyaV0sIHJlcCA9IHJlcCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYndfZGF5ID0gYmQsIGJ3X3dlZWsgPSBidywgbnJtc2UgPSBybXNlKHlbbV0sIHhbbV0pIC8gc2R4KQogICAgfQogIH0KICB3cml0ZS5jc3YoYmluZF9yb3dzKHJvd3MpLCBmLCByb3cubmFtZXMgPSBGQUxTRSkKfQpncmlkIDwtIGJpbmRfcm93cyhsYXBwbHkobGlzdC5maWxlcyhkaXIwLCBwYXR0ZXJuID0gIlxcLmNzdiQiLCBmdWxsLm5hbWVzID0gVFJVRSksIHJlYWQuY3N2KSkKQlcgPC0gbGFwcGx5KHNldE5hbWVzKFNUQVRJT05TLCBTVEFUSU9OUyksIGZ1bmN0aW9uKHN0KSB7CiAgZyA8LSBncmlkIHw+IGZpbHRlcihzdGF0aW9uICE9IHN0KSB8PiBncm91cF9ieShid19kYXksIGJ3X3dlZWspIHw+CiAgICBzdW1tYXJpc2UobnJtc2UgPSBtZWFuKG5ybXNlKSwgLmdyb3VwcyA9ICJkcm9wIikgfD4gYXJyYW5nZShid19kYXksIGJ3X3dlZWspIHw+CiAgICBzbGljZV9taW4obnJtc2UsIG4gPSAxLCB3aXRoX3RpZXMgPSBGQUxTRSkKICBsaXN0KGJ3X2RheSA9IGckYndfZGF5LCBid193ZWVrID0gZyRid193ZWVrLCBucm1zZSA9IGckbnJtc2UpCn0pCmNhdCgiRXhwZXJpbWVudCAwOiIsIG5yb3coZ3JpZCksICJncmlkIGV2YWx1YXRpb25zXG4iKQpgYGAKCmBgYHtyIGV4cDAtcmVzdWx0cywgZmlnLndpZHRoPTMuNSwgZmlnLmhlaWdodD0yLjQ1LCBvdXQud2lkdGg9JzU1JSd9CmZvciAoc3QgaW4gU1RBVElPTlMpIHsga2V5KHBhc3RlMCgiYndfZGF5XyIsIGdzdWIoIiAiLCAiIiwgc3QpKSwgQldbW3N0XV0kYndfZGF5KTsga2V5KHBhc3RlMCgiYndfd2Vla18iLCBnc3ViKCIgIiwgIiIsIHN0KSksIEJXW1tzdF1dJGJ3X3dlZWspIH0KZzQgPC0gZ3JpZCB8PiBmaWx0ZXIoc3RhdGlvbiAhPSAiU1VGIDEiKSB8PiBncm91cF9ieShid19kYXksIGJ3X3dlZWspIHw+CiAgc3VtbWFyaXNlKG5ybXNlID0gbWVhbihucm1zZSksIC5ncm91cHMgPSAiZHJvcCIpIHw+IG11dGF0ZShiZXN0ID0gbnJtc2UgPT0gbWluKG5ybXNlKSkKa2V5KCJncmlkX2Jlc3RfbnJtc2UiLCBtaW4oZzQkbnJtc2UpKTsga2V5KCJncmlkX3dvcnN0X25ybXNlIiwgbWF4KGc0JG5ybXNlKSkKa2V5KCJncmlkX3dvcnN0X3ZzX2Jlc3RfcGN0IiwgMTAwICogKG1heChnNCRucm1zZSkgLyBtaW4oZzQkbnJtc2UpIC0gMSkpCmtleSgiZ3JpZF93b3JzdF9wYWlyIiwgcGFzdGUoZzQkYndfZGF5W3doaWNoLm1heChnNCRucm1zZSldLCBnNCRid193ZWVrW3doaWNoLm1heChnNCRucm1zZSldKSkKcDQgPC0gZ2dwbG90KGc0LCBhZXMoZmFjdG9yKGJ3X3dlZWspLCBmYWN0b3IoYndfZGF5KSwgZmlsbCA9IG5ybXNlKSkgKyBnZW9tX3RpbGUoY29sb3VyID0gIndoaXRlIikgKwogIGdlb21fdGV4dChhZXMobGFiZWwgPSBzcHJpbnRmKCIlLjRmIiwgbnJtc2UpLCBmb250ZmFjZSA9IGlmZWxzZShiZXN0LCAiYm9sZCIsICJwbGFpbiIpLAogICAgICAgICAgICAgICAgY29sb3VyID0gbnJtc2UgPCBtZWFuKHJhbmdlKG5ybXNlKSkpLCBzaXplID0gMi42NSwgZmFtaWx5ID0gRk9OVF9URVhULCBzaG93LmxlZ2VuZCA9IEZBTFNFKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKGBUUlVFYCA9ICJ3aGl0ZSIsIGBGQUxTRWAgPSAiYmxhY2siKSkgKwogIHNjYWxlX2ZpbGxfZGlzdGlsbGVyKHBhbGV0dGUgPSAiQmx1ZXMiLCBkaXJlY3Rpb24gPSAtMSkgKyBzY2FsZV95X2Rpc2NyZXRlKGxpbWl0cyA9IHJldikgKwogIGxhYnMoeCA9IGV4cHJlc3Npb24oaFtXXSB+ICIod2Vla3MpIiksIHkgPSBleHByZXNzaW9uKGhbRF0gfiAiKGRheXMpIiksIGZpbGwgPSAiTWVhblxuTlJNU0UiKSArCiAgdGhlbWUocGFuZWwuZ3JpZCA9IGVsZW1lbnRfYmxhbmsoKSwgbGVnZW5kLmtleS5oZWlnaHQgPSB1bml0KDAuNSwgImNtIiksIGF4aXMudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gNy41KSwKICAgICAgICBheGlzLnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA5KSwgbGVnZW5kLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcpLCBsZWdlbmQudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDgpKQpwcmludChwNCk7IHNhdmVfZmlnKHA0LCAiZmlnMDRfYmFuZHdpZHRoIiwgMy41LCAyLjQ1KQprbml0cjo6a2FibGUoZGF0YS5mcmFtZShzdGF0aW9uID0gU1RBVElPTlMsIGhfRCA9IHNhcHBseShCVywgYFtbYCwgImJ3X2RheSIpLCBoX1cgPSBzYXBwbHkoQlcsIGBbW2AsICJid193ZWVrIiksCiAgICAgICAgICAgICAgICAgICAgICAgIG5ybXNlID0gc2FwcGx5KEJXLCBgW1tgLCAibnJtc2UiKSksIGRpZ2l0cyA9IDQsCiAgICAgICAgICAgICBjb2wubmFtZXMgPSBjKCJUYXJnZXQgc3RhdGlvbiIsICJoX0QgKGRheXMpIiwgImhfVyAod2Vla3MpIiwgIk1lYW4gTlJNU0Ugb24gdGhlIG90aGVyIHN0YXRpb25zIiksCiAgICAgICAgICAgICBjYXB0aW9uID0gIlNlbGVjdGVkIGJhbmR3aWR0aHMgKGxlYXZlLW9uZS1zdGF0aW9uLW91dCkiKQpgYGAKCiMgRXhwZXJpbWVudCAxOiBpbXB1dGF0aW9uIGFjY3VyYWN5CgojIyBSdW5uaW5nIHRoZSBleHBlcmltZW50CgpGb3IgZXZlcnkgc2NlbmFyaW8gdGhlIHRlbiBtZXRob2RzLCB0aGUgZml2ZSBhYmxhdGlvbiB2YXJpYW50cyBvZiBTZWFzLUFSIGFuZCwgd2hlbiBgaW1wdXRlVFNgIGlzCmluc3RhbGxlZCwgdGhlIG9yaWdpbmFsIGBuYV9zZWFkZWNgIGFyZSBhcHBsaWVkIHRvIHRoZSBzYW1lIG1hc2tlZCBzZXJpZXMsIGFuZCB0aGUgY29tcHV0aW5nIHRpbWUgaXMKcmVjb3JkZWQuIFNlYXMtQVIgdXNlcyB0aGUgYmFuZHdpZHRocyBvZiBFeHBlcmltZW50IDAgZm9yIHRoZSBzdGF0aW9uIGNvbmNlcm5lZC4gUmVzdWx0cyBhcmUgc3RvcmVkIHBlcgpzZXJpZXMsIHNvIGFuIGludGVycnVwdGVkIHJ1biByZXN1bWVzIHdoZXJlIGl0IHN0b3BwZWQuCgpgYGB7ciBleHAxLXJ1bn0KUkFURVMgIDwtIGlmIChRVUlDSykgMC4zMCBlbHNlIGMoMC4xMCwgMC4zMCkKTl9SRVBTIDwtIGlmIChRVUlDSykgMSBlbHNlIDMKc3RfbGlzdCA8LSBpZiAoUVVJQ0spICJTVUYgMSIgZWxzZSBTVEFUSU9OUwpkaXIxIDwtIG91dF9maWxlKHBhc3RlMCgiZXhwMV8iLCBUQUcxLCBpZiAoUVVJQ0spICJfcXVpY2siIGVsc2UgIiIpKTsgZGlyLmNyZWF0ZShkaXIxLCBzaG93V2FybmluZ3MgPSBGQUxTRSkKZm9yIChzdCBpbiBzdF9saXN0KSBmb3IgKHAgaW4gUE9MUykgewogIGYgPC0gZmlsZS5wYXRoKGRpcjEsIHNwcmludGYoIiVzXyVzLmNzdiIsIGdzdWIoIiAiLCAiIiwgc3QpLCBwKSkKICBpZiAoaXNUUlVFKFAkdXNlX2NhY2hlKSAmJiBmaWxlLmV4aXN0cyhmKSkgbmV4dAogIHNpIDwtIG1hdGNoKHN0LCBTVEFUSU9OUyk7IHBrIDwtIG1hdGNoKHAsIFBPTFMpCiAgeCA8LSBnZXRfc2VyaWVzKHN0LCBwKTsgc2R4IDwtIHNkX3BvcCh4KTsgYncgPC0gQldbW3N0XV0KICBtZXRocyA8LSBNRVRIT0RTCiAgbWV0aHMkYFNlYXMtQVJgIDwtIGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgYndfZGF5ID0gYnckYndfZGF5LCBid193ZWVrID0gYnckYndfd2VlaykKICBhYmwgPC0gbGlzdCgKICAgIGB3L28gbG9nIHRyYW5zZm9ybWAgPSBmdW5jdGlvbih2KSBpbXB1dGVfc2Vhc2FyKHYsIGJ3JGJ3X2RheSwgYnckYndfd2VlaywgdHJhbnNmb3JtID0gIm5vbmUiKSwKICAgIGB3L28gd2Vla2x5IGNvbXAuYCAgPSBmdW5jdGlvbih2KSBpbXB1dGVfc2Vhc2FyKHYsIGJ3JGJ3X2RheSwgYnckYndfd2Vlaywgd2Vla2x5ID0gRkFMU0UpLAogICAgYHN0YXRpYyBwcm9maWxlc2AgICA9IGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgSW5mLCBJbmYpLAogICAgYHJlbWFpbmRlciA9IDBgICAgICA9IGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgYnckYndfZGF5LCBidyRid193ZWVrLCByZW1haW5kZXIgPSAiemVybyIpLAogICAgYHJlbWFpbmRlciBMSWAgICAgICA9IGZ1bmN0aW9uKHYpIGltcHV0ZV9zZWFzYXIodiwgYnckYndfZGF5LCBidyRid193ZWVrLCByZW1haW5kZXIgPSAibGluZWFyIikpCiAgZXh0cmEgPC0gaWYgKEhBU19JTVBVVEVUUykgbGlzdChgTkFfU0VBREVDIChpbXB1dGVUUylgID0gZnVuY3Rpb24odikKICAgIGFzLm51bWVyaWMoaW1wdXRlVFM6Om5hX3NlYWRlYyh0cyh2LCBmcmVxdWVuY3kgPSBTMSksIGFsZ29yaXRobSA9ICJpbnRlcnBvbGF0aW9uIikpKSBlbHNlIGxpc3QoKQogIGFsbGYgIDwtIGMobWV0aHMsIGFibCwgZXh0cmEpCiAga2luZHMgPC0gYyhyZXAoIm1haW4iLCBsZW5ndGgobWV0aHMpKSwgcmVwKCJhYmxhdGlvbiIsIGxlbmd0aChhYmwpKSwgcmVwKCJleHRyYSIsIGxlbmd0aChleHRyYSkpKQogIHJvd3MgPC0gbGlzdCgpCiAgZm9yIChyaSBpbiBzZXFfYWxvbmcoUkVHX05BTUVTKSkgZm9yIChyYXRlIGluIFJBVEVTKSBmb3IgKHJlcCBpbiBzZXFfbGVuKE5fUkVQUykpIHsKICAgIHNlZWQgPC0gMjAwMDAgKyAxMDAwICogc2kgKyAxMDAgKiBwayArIDEwICogcmkgKyAzICogbWF0Y2gocmF0ZSwgYygwLjEwLCAwLjMwKSkgKyByZXAKICAgIG0gPC0gZ2V0X21hc2soImV4cDEiLCB4LCBzdCwgcCwgUkVHX05BTUVTW3JpXSwgcmF0ZSwgcmVwLCBzZWVkKTsgeG0gPC0geDsgeG1bbV0gPC0gTkEKICAgIGZvciAoaiBpbiBzZXFfYWxvbmcoYWxsZikpIHsKICAgICAgdDEgPC0gcHJvYy50aW1lKClbWyJlbGFwc2VkIl1dOyB5IDwtIHBtYXgoYWxsZltbal1dKHhtKSwgMCk7IGR0IDwtIHByb2MudGltZSgpW1siZWxhcHNlZCJdXSAtIHQxCiAgICAgIHJvd3NbW2xlbmd0aChyb3dzKSArIDFdXSA8LSBkYXRhLmZyYW1lKHN0YXRpb24gPSBzdCwgcG9sbHV0YW50ID0gcCwgcmVnaW1lID0gUkVHX05BTUVTW3JpXSwgcmF0ZSA9IHJhdGUsCiAgICAgICAgcmVwID0gcmVwLCBtZXRob2QgPSBuYW1lcyhhbGxmKVtqXSwga2luZCA9IGtpbmRzW2pdLCBuX21hc2tlZCA9IHN1bShtKSwKICAgICAgICBybXNlID0gcm1zZSh5W21dLCB4W21dKSwgbWFlID0gbWFlKHlbbV0sIHhbbV0pLCBucm1zZSA9IHJtc2UoeVttXSwgeFttXSkgLyBzZHgsIHNlYyA9IGR0KQogICAgfQogIH0KICB3cml0ZS5jc3YoYmluZF9yb3dzKHJvd3MpLCBmLCByb3cubmFtZXMgPSBGQUxTRSkKfQpyZXMxIDwtIGJpbmRfcm93cyhsYXBwbHkobGlzdC5maWxlcyhkaXIxLCBwYXR0ZXJuID0gIlxcLmNzdiQiLCBmdWxsLm5hbWVzID0gVFJVRSksIHJlYWQuY3N2KSkKbWFpbiA8LSByZXMxIHw+IGZpbHRlcihraW5kID09ICJtYWluIikgfD4KICBtdXRhdGUoc2NlbmFyaW8gPSBwYXN0ZShzdGF0aW9uLCBwb2xsdXRhbnQsIHJlZ2ltZSwgcmF0ZSwgcmVwLCBzZXAgPSAifCIpLCBtZXRob2QgPSBmYWN0b3IobWV0aG9kLCBsZXZlbHMgPSBNRVRIT0RfT1JERVIpKQpjYXQoIkV4cGVyaW1lbnQgMToiLCBuX2Rpc3RpbmN0KG1haW4kc2NlbmFyaW8pLCAic2NlbmFyaW9zLCIsIG5yb3cocmVzMSksICJyZXN1bHQgcm93c1xuIikKYGBgCgojIyBBY2N1cmFjeSBieSBtZXRob2QKCkxlZnQ6IFJNU0UgYXQgU1VGIDEgaW4gb3JpZ2luYWwgdW5pdHMgKG1lYW4gb2YgMTggc2NlbmFyaW9zIHBlciBwb2xsdXRhbnQpLiBNaWRkbGU6IE5STVNFIGF2ZXJhZ2VkIG92ZXIKdGhlIDE1IHNlcmllcyBmb3IgZWFjaCBnYXAgcmVnaW1lLiBSaWdodDogYXZlcmFnZSBGcmllZG1hbiByYW5rIGFuZCBtZWFuIGNvbXB1dGluZyB0aW1lIHBlciBzZXJpZXMuCgpgYGB7ciB0YWJsZTR9ClB3IDwtIG1haW4gfD4gc2VsZWN0KHNjZW5hcmlvLCBtZXRob2QsIG5ybXNlKSB8PiBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gbWV0aG9kLCB2YWx1ZXNfZnJvbSA9IG5ybXNlKQpQbSA8LSBhcy5tYXRyaXgoUHdbLCBNRVRIT0RfT1JERVJdKQphdmdfcmFuayA8LSBjb2xNZWFucyh0KGFwcGx5KFBtLCAxLCByYW5rKSkpICAgICAgICAgICAgICAgICAjIHJhbmsgMSA9IG1vc3QgYWNjdXJhdGUKdDQgPC0gZGF0YS5mcmFtZShtZXRob2QgPSBmYWN0b3IoTUVUSE9EX09SREVSLCBsZXZlbHMgPSBNRVRIT0RfT1JERVIpKSB8PgogIGxlZnRfam9pbihtYWluIHw+IGZpbHRlcihzdGF0aW9uID09ICJTVUYgMSIpIHw+IGdyb3VwX2J5KG1ldGhvZCwgcG9sbHV0YW50KSB8PiBzdW1tYXJpc2UodiA9IG1lYW4ocm1zZSksIC5ncm91cHMgPSAiZHJvcCIpIHw+CiAgICAgICAgICAgICAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHBvbGx1dGFudCwgdmFsdWVzX2Zyb20gPSB2KSwgYnkgPSAibWV0aG9kIikgfD4KICBsZWZ0X2pvaW4obWFpbiB8PiBncm91cF9ieShtZXRob2QsIHJlZ2ltZSkgfD4gc3VtbWFyaXNlKHYgPSBtZWFuKG5ybXNlKSwgLmdyb3VwcyA9ICJkcm9wIikgfD4KICAgICAgICAgICAgICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gcmVnaW1lLCB2YWx1ZXNfZnJvbSA9IHYpLCBieSA9ICJtZXRob2QiKSB8PgogIGxlZnRfam9pbihtYWluIHw+IGdyb3VwX2J5KG1ldGhvZCkgfD4gc3VtbWFyaXNlKGFsbCA9IG1lYW4obnJtc2UpLCBzZWMgPSBtZWFuKHNlYyksIC5ncm91cHMgPSAiZHJvcCIpLCBieSA9ICJtZXRob2QiKQp0NCRyYW5rIDwtIGF2Z19yYW5rW2FzLmNoYXJhY3Rlcih0NCRtZXRob2QpXQp3cml0ZS5jc3YodDQsIG91dF9maWxlKCJleHAxX3RhYmxlNC5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCnBjIDwtIGludGVyc2VjdChQT0xTLCBuYW1lcyh0NCkpOyByYyA8LSBpbnRlcnNlY3QoUkVHX05BTUVTLCBuYW1lcyh0NCkpCm1kNCA8LSBkYXRhLmZyYW1lKE1ldGhvZCA9IGFzLmNoYXJhY3Rlcih0NCRtZXRob2QpLCBjaGVjay5uYW1lcyA9IEZBTFNFKQpmb3IgKHAgaW4gcGMpIG1kNFtbcF1dIDwtIGZtdF9tZCh0NFtbcF1dLCBpZiAocCA9PSAiQ08iKSAzIGVsc2UgMikKZm9yIChyIGluIHJjKSBtZDRbW3Bhc3RlKCJOUk1TRSIsIHIpXV0gPC0gZm10X21kKHQ0W1tyXV0sIDMpCm1kNFtbIkF2Zy4gcmFuayJdXSA8LSBmbXRfbWQodDQkcmFuaywgMik7IG1kNFtbIlRpbWUgKHMpIl1dIDwtIGZtdF9udW0odDQkc2VjLCAzKQp0eCA8LSBsYXBwbHkocGMsIGZ1bmN0aW9uKHApIGZtdF90ZXgodDRbW3BdXSwgaWYgKHAgPT0gIkNPIikgMyBlbHNlIDIpKTsgdHIgPC0gbGFwcGx5KHJjLCBmdW5jdGlvbihyKSBmbXRfdGV4KHQ0W1tyXV0sIDMpKQpyb3dzNCA8LSBzYXBwbHkoc2VxX2xlbihucm93KHQ0KSksIGZ1bmN0aW9uKGkpIHBhc3RlMChwYXN0ZShjKAogIGlmICh0NCRtZXRob2RbaV0gPT0gIlNlYXMtQVIiKSAiXFx0ZXh0YmZ7U2Vhcy1BUiAocHJvcG9zZWQpfSIgZWxzZSBhcy5jaGFyYWN0ZXIodDQkbWV0aG9kW2ldKSwKICBzYXBwbHkodHgsIGBbYCwgaSksIHNhcHBseSh0ciwgYFtgLCBpKSwgZm10X3RleCh0NCRyYW5rLCAyKVtpXSwgZm10X251bSh0NCRzZWNbaV0sIDMpKSwgY29sbGFwc2UgPSAiICYgIiksICIgXFxcXCIpKQp3cml0ZV90ZXgoYygiXFxiZWdpbnt0YWJsZSp9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgc3ByaW50ZigiXFxjYXB0aW9ue0ltcHV0YXRpb24gYWNjdXJhY3kgdW5kZXIgYXJ0aWZpY2lhbCBibG9jayBtaXNzaW5nbmVzcy4gTGVmdDogUk1TRSBhdCBTVUZ+MSBpbiBvcmlnaW5hbCB1bml0cywgYXZlcmFnZWQgb3ZlciB0aGUgdGhyZWUgZ2FwIHJlZ2ltZXMsIHR3byBtaXNzaW5nIHJhdGVzIGFuZCB0aHJlZSByZXBsaWNhdGVzLiBNaWRkbGU6IE5STVNFIChSTVNFIGRpdmlkZWQgYnkgdGhlIHN0YW5kYXJkIGRldmlhdGlvbiBvZiB0aGUgc2VyaWVzKSBhdmVyYWdlZCBvdmVyIGFsbCAxNSBzZXJpZXMgKHRocmVlIHN0YXRpb25zICRcXHRpbWVzJCBmaXZlIHBvbGx1dGFudHMpIGZvciBlYWNoIGdhcCByZWdpbWUuIFJpZ2h0OiBhdmVyYWdlIEZyaWVkbWFuIHJhbmsgb3ZlciB0aGUgJWQgc2NlbmFyaW9zIGFuZCBtZWFuIGNvbXB1dGluZyB0aW1lIHBlciBpbXB1dGF0aW9uIG9mIGEgZnVsbCBzZXJpZXMgKFIsIG9uZSBDUFUgY29yZSkuIEJlc3QgdmFsdWVzIGluIGJvbGQufSIsIG5yb3coUG0pKSwKICAiXFxsYWJlbHt0YWI6aW1wfSIsICJcXHNldGxlbmd0aHtcXHRhYmNvbHNlcH17NC4ycHR9IiwgIlxcYmVnaW57dGFidWxhcn17bHJycnJycnJycnJ9IiwgIlxcdG9wcnVsZSIsCiAgIiAmIFxcbXVsdGljb2x1bW57NX17Y317Uk1TRSBhdCBTVUZ+MX0gJiBcXG11bHRpY29sdW1uezN9e2N9e05STVNFLCBhbGwgc3RhdGlvbnN9ICYgJiBcXFxcIiwKICAiXFxjbWlkcnVsZShscil7Mi02fVxcY21pZHJ1bGUobHIpezctOX0iLAogICJNZXRob2QgJiBQTSRfezEwfSQgJiBOTyRfMiQgJiBTTyRfMiQgJiBDTyAmIE8kXzMkICYgU2hvcnQgJiBNZWRpdW0gJiBMb25nICYgQXZnLiByYW5rICYgVGltZSAocylcXFxcIiwgIlxcbWlkcnVsZSIsCiAgcm93czQsICJcXGJvdHRvbXJ1bGUiLCAiXFxlbmR7dGFidWxhcn0iLCAiXFxlbmR7dGFibGUqfSIpLCAidGFiX2ltcHV0YXRpb24iKQprbml0cjo6a2FibGUobWQ0LCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9IGNhcCgiVGFibGUgNSIsICJSTVNFIGF0IFNVRiAxLCBOUk1TRSBvZiBhbGwgc3RhdGlvbnMgYnkgZ2FwIHJlZ2ltZSwgYXZlcmFnZSByYW5rIGFuZCB0aW1lIikpCmBgYAoKIyMgTlJNU0UgYnkgZ2FwIHJlZ2ltZSBhbmQgbWlzc2luZyByYXRlCgpFYWNoIHJvdyBpcyBhIG1ldGhvZCAob3JkZXJlZCBieSBvdmVyYWxsIE5STVNFLCBiZXN0IGF0IHRoZSB0b3ApLCBlYWNoIHBhbmVsIGEgZ2FwIHJlZ2ltZTsgb3BlbiBhbmQKZmlsbGVkIGNpcmNsZXMgYXJlIHRoZSAxMCUgYW5kIDMwJSBtaXNzaW5nIHJhdGVzLCBhbmQgU2Vhcy1BUiBpcyBkcmF3biBpbiByZWQuCgpgYGB7ciBmaWc1LCBmaWcud2lkdGg9Ny4xNiwgZmlnLmhlaWdodD0yLjZ9Cm9yZDUgPC0gbWFpbiB8PiBncm91cF9ieShtZXRob2QpIHw+IHN1bW1hcmlzZSh2ID0gbWVhbihucm1zZSksIC5ncm91cHMgPSAiZHJvcCIpIHw+IGFycmFuZ2UoZGVzYyh2KSkgfD4gcHVsbChtZXRob2QpCmQ1IDwtIG1haW4gfD4gZ3JvdXBfYnkobWV0aG9kLCByZWdpbWUsIHJhdGUpIHw+IHN1bW1hcmlzZShucm1zZSA9IG1lYW4obnJtc2UpLCAuZ3JvdXBzID0gImRyb3AiKSB8PgogIG11dGF0ZShyZWdpbWUgPSBmYWN0b3IocmVnaW1lLCBsZXZlbHMgPSBSRUdfTkFNRVMsIGxhYmVscyA9IGMoIlNob3J0IGdhcHMgKDEtNCBzdGVwcykiLCAiTWVkaXVtIGdhcHMgKDUtNDggc3RlcHMpIiwgIkxvbmcgZ2FwcyAoNDktMzM2IHN0ZXBzKSIpKSwKICAgICAgICAgcmF0ZSA9IGZhY3RvcihzcHJpbnRmKCIlZCUlIiwgcm91bmQoMTAwICogcmF0ZSkpKSwgbWV0aG9kID0gZmFjdG9yKG1ldGhvZCwgbGV2ZWxzID0gYXMuY2hhcmFjdGVyKG9yZDUpKSwKICAgICAgICAgc2Vhc2FyID0gbWV0aG9kID09ICJTZWFzLUFSIikKcDUgPC0gZ2dwbG90KGQ1LCBhZXMobnJtc2UsIG1ldGhvZCkpICsKICBnZW9tX2xpbmUoYWVzKGdyb3VwID0gbWV0aG9kKSwgY29sb3VyID0gImdyZXk3NSIsIGxpbmV3aWR0aCA9IDAuNSkgKwogIGdlb21fcG9pbnQoYWVzKHNoYXBlID0gcmF0ZSwgY29sb3VyID0gc2Vhc2FyKSwgc2l6ZSA9IDEuNywgc3Ryb2tlID0gMC42KSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKGBGQUxTRWAgPSAiZ3JleTIwIiwgYFRSVUVgID0gIiNjMDM5MmIiKSwgZ3VpZGUgPSAibm9uZSIpICsKICBzY2FsZV9zaGFwZV9tYW51YWwodmFsdWVzID0gYygxLCAxNiksIG5hbWUgPSAiQWRkaXRpb25hbCBtaXNzaW5nIHJhdGUiKSArCiAgZmFjZXRfd3JhcCh+cmVnaW1lLCBucm93ID0gMSkgKyBsYWJzKHggPSAiTlJNU0UgKG1lYW4gb3ZlciBzZXJpZXMpIiwgeSA9IE5VTEwpICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIiwgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKGNvbG91ciA9ICJncmV5OTIiKSkKcHJpbnQocDUpOyBzYXZlX2ZpZyhwNSwgImZpZzA1X2FjY3VyYWN5X2J5X3JlZ2ltZSIsIDcuMTYsIDIuNikKYGBgCgojIyBSYW5rcywgRnJpZWRtYW4gdGVzdCBhbmQgTmVtZW55aSBjcml0aWNhbCBkaWZmZXJlbmNlCgpXaXRoaW4gZWFjaCBzY2VuYXJpbyAkaiQgdGhlIG1ldGhvZHMgcmVjZWl2ZSByYW5rcyAkcl97aWp9JCAoMSA9IHNtYWxsZXN0IE5STVNFKSBhbmQKJFxiYXIgcl9pPVxmcmFjMU5cc3VtX2ogcl97aWp9JC4gVGhlIEZyaWVkbWFuIHRlc3QgY2hlY2tzICRIXzAkOiBhbGwgbWV0aG9kcyBhcmUgZXF1aXZhbGVudC4gVHdvCmF2ZXJhZ2UgcmFua3MgZGlmZmVyIHNpZ25pZmljYW50bHkgKE5lbWVueWkpIHdoZW4gdGhlaXIgZGlmZmVyZW5jZSBleGNlZWRzCgokJApcbWF0aHJte0NEfT1xX3tcYWxwaGF9XHNxcnR7XGZyYWN7ayhrKzEpfXs2Tn19LFxxcXVhZCBrPTEwLFwgTj0yNzAsXCBxX3swLjA1fT0zLjE2NCAuCiQkCgpgYGB7ciBmaWc2LCBmaWcud2lkdGg9My41LCBmaWcuaGVpZ2h0PTIuNCwgb3V0LndpZHRoPSc2MCUnfQpmciA8LSBmcmllZG1hbi50ZXN0KFBtKQprXyA8LSBuY29sKFBtKTsgTl8gPC0gbnJvdyhQbSk7IENEIDwtIDMuMTY0ICogc3FydChrXyAqIChrXyArIDEpIC8gKDYgKiBOXykpICAgICAjIHFfMC4wNSA9IDMuMTY0IGZvciBrID0gMTAKZm9yIChtbSBpbiBuYW1lcyhhdmdfcmFuaykpIGtleShwYXN0ZTAoInJhbmtfIiwgbW0pLCBhdmdfcmFua1tbbW1dXSkKa2V5KCJmcmllZG1hbl9jaGkyIiwgdW5uYW1lKGZyJHN0YXRpc3RpYykpOyBrZXkoImZyaWVkbWFuX3AiLCBmciRwLnZhbHVlKTsga2V5KCJuZW1lbnlpX2NkIiwgQ0QpOyBrZXkoIm5fc2NlbmFyaW9zIiwgTl8pCmNhdChzcHJpbnRmKCJGcmllZG1hbiBjaGleMiglZCkgPSAlLjFmLCBwID0gJS4yZyB8IE5lbWVueWkgQ0QgPSAlLjNmIChrID0gJWQsIE4gPSAlZClcbiIsIGtfIC0gMSwgZnIkc3RhdGlzdGljLCBmciRwLnZhbHVlLCBDRCwga18sIE5fKSkKZDYgPC0gZGF0YS5mcmFtZShtZXRob2QgPSBuYW1lcyhhdmdfcmFuayksIHJhbmsgPSBhdmdfcmFuaykgfD4gYXJyYW5nZShyYW5rKSB8PiBtdXRhdGUoeSA9IHJldihzZXFfYWxvbmcocmFuaykpKQpwNiA8LSBnZ3Bsb3QoZDYsIGFlcyhyYW5rLCB5KSkgKwogIGFubm90YXRlKCJyZWN0IiwgeG1pbiA9IG1pbihkNiRyYW5rKSwgeG1heCA9IG1pbihkNiRyYW5rKSArIENELCB5bWluID0gLUluZiwgeW1heCA9IEluZiwgZmlsbCA9ICIjYzAzOTJiIiwgYWxwaGEgPSAuMDgpICsKICBhbm5vdGF0ZSgic2VnbWVudCIsIHggPSBtaW4oZDYkcmFuayksIHhlbmQgPSBtaW4oZDYkcmFuaykgKyBDRCwgeSA9IG5yb3coZDYpICsgLjgsIHllbmQgPSBucm93KGQ2KSArIC44LCBsaW5ld2lkdGggPSAwLjgpICsKICBhbm5vdGF0ZSgidGV4dCIsIHggPSBtaW4oZDYkcmFuayksIHkgPSBucm93KGQ2KSArIDEuMzUsIGxhYmVsID0gc3ByaW50ZigiQ0QgPSAlLjJmIiwgQ0QpLCBoanVzdCA9IDAsIHNpemUgPSAyLjQsIGZhbWlseSA9IEZPTlRfVEVYVCkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG91ciA9IG1ldGhvZCksIHNpemUgPSAxLjgsIHNob3cubGVnZW5kID0gRkFMU0UpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsID0gc3ByaW50ZigiJXMgKCUuMmYpIiwgbWV0aG9kLCByYW5rKSksIGhqdXN0ID0gLTAuMTUsIHNpemUgPSAyLjQsIGZhbWlseSA9IEZPTlRfVEVYVCkgKwogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gQ09MKSArIHNjYWxlX3hfY29udGludW91cyhsaW1pdHMgPSBjKDEsIDEyKSwgYnJlYWtzID0gc2VxKDIsIDEwLCAyKSkgKwogIHNjYWxlX3lfY29udGludW91cyhsaW1pdHMgPSBjKDAuNSwgbnJvdyhkNikgKyAxLjYpKSArCiAgbGFicyh4ID0gIkF2ZXJhZ2UgcmFuayBvdmVyIHRoZSBpbXB1dGF0aW9uIHNjZW5hcmlvcyAoMSA9IGJlc3QpIiwgeSA9IE5VTEwpICsKICB0aGVtZShheGlzLnRleHQueSA9IGVsZW1lbnRfYmxhbmsoKSwgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9ibGFuaygpKQpwcmludChwNik7IHNhdmVfZmlnKHA2LCAiZmlnMDZfcmFua3MiLCAzLjUsIDIuNCkKYGBgCgojIyBQYWlyZWQgV2lsY294b24gdGVzdHMgd2l0aCBIb2xtIGNvcnJlY3Rpb24KCkZvciBlYWNoIGNvbXBldGl0b3IgJGMkIHRoZSBkaWZmZXJlbmNlcyAkXERlbHRhX2o9XG1hdGhybXtOUk1TRX1faihcbWF0aHJte1NlYXMtQVJ9KS1cbWF0aHJte05STVNFfV9qKGMpJAphcmUgdGVzdGVkIHdpdGggdGhlIHR3by1zaWRlZCBXaWxjb3hvbiBzaWduZWQtcmFuayB0ZXN0IChub3JtYWwgYXBwcm94aW1hdGlvbiB3aXRob3V0IGNvbnRpbnVpdHkKY29ycmVjdGlvbiksIGFuZCB0aGUgbmluZSAkcCQtdmFsdWVzIGFyZSBhZGp1c3RlZCB3aXRoIEhvbG0ncyBtZXRob2QuIE5lZ2F0aXZlICRcRGVsdGEkIGZhdm91cnMgU2Vhcy1BUi4KCmBgYHtyIHRhYmxlNX0KdDUgPC0gYmluZF9yb3dzKGxhcHBseShzZXRkaWZmKE1FVEhPRF9PUkRFUiwgIlNlYXMtQVIiKSwgZnVuY3Rpb24oY2MpIHsKICBkIDwtIFBtWywgIlNlYXMtQVIiXSAtIFBtWywgY2NdCiAgZGF0YS5mcmFtZShjb21wZXRpdG9yID0gY2MsIG1lZGlhbl9kaWZmID0gbWVkaWFuKGQpLCB3aW5zID0gMTAwICogbWVhbihkIDwgMCksCiAgICAgICAgICAgICBwID0gd2lsY294LnRlc3QoUG1bLCAiU2Vhcy1BUiJdLCBQbVssIGNjXSwgcGFpcmVkID0gVFJVRSwgZXhhY3QgPSBGQUxTRSwgY29ycmVjdCA9IEZBTFNFKSRwLnZhbHVlKQp9KSkKdDUkcF9ob2xtIDwtIHAuYWRqdXN0KHQ1JHAsIG1ldGhvZCA9ICJob2xtIikKd3JpdGUuY3N2KHQ1LCBvdXRfZmlsZSgiZXhwMV90ZXN0cy5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCnB0eHQgPC0gZnVuY3Rpb24ocCkgaWZlbHNlKHAgPCAxZS00LCAiJDwxMF57LTR9JCIsIGZvcm1hdEMocCwgZm9ybWF0ID0gImYiLCBkaWdpdHMgPSA0KSkKd3JpdGVfdGV4KGMoIlxcYmVnaW57dGFibGV9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgc3ByaW50ZigiXFxjYXB0aW9ue1BhaXJlZCBjb21wYXJpc29uIG9mIFNlYXMtQVIgd2l0aCBlYWNoIGNvbXBldGluZyBpbXB1dGF0aW9uIG1ldGhvZCBvdmVyIHRoZSAlZCBtYXNraW5nIHNjZW5hcmlvczogbWVkaWFuIE5STVNFIGRpZmZlcmVuY2UgKG5lZ2F0aXZlIGZhdm91cnMgU2Vhcy1BUiksIHBlcmNlbnRhZ2Ugb2Ygc2NlbmFyaW9zIGluIHdoaWNoIFNlYXMtQVIgaXMgbW9yZSBhY2N1cmF0ZSwgYW5kIEhvbG0tYWRqdXN0ZWQgV2lsY294b24gc2lnbmVkLXJhbmsgJHAkLXZhbHVlcy4gRnJpZWRtYW4gdGVzdDogJFxcY2hpXjJfeyVkfT0lLjFmJCwgJHA8MTBeey00fSQufSIsIE5fLCBrXyAtIDEsIGZyJHN0YXRpc3RpYyksCiAgIlxcbGFiZWx7dGFiOnRlc3RzfSIsICJcXHNldGxlbmd0aHtcXHRhYmNvbHNlcH17NXB0fSIsICJcXGJlZ2lue3RhYnVsYXJ9e2xycnJ9IiwgIlxcdG9wcnVsZSIsCiAgIkNvbXBldGl0b3IgJiBNZWRpYW4gJFxcRGVsdGEkTlJNU0UgJiBTZWFzLUFSIHdpbnMgKFxcJSkgJiAkcF97XFxtYXRocm17SG9sbX19JFxcXFwiLCAiXFxtaWRydWxlIiwKICBzcHJpbnRmKCIlcyAmICUrLjRmICYgJS4xZiAmICVzIFxcXFwiLCB0NSRjb21wZXRpdG9yLCB0NSRtZWRpYW5fZGlmZiwgdDUkd2lucywgcHR4dCh0NSRwX2hvbG0pKSwKICAiXFxib3R0b21ydWxlIiwgIlxcZW5ke3RhYnVsYXJ9IiwgIlxcZW5ke3RhYmxlfSIpLCAidGFiX3Rlc3RzIikKZm9yIChpIGluIHNlcV9sZW4obnJvdyh0NSkpKSB7IGtleShwYXN0ZTAoIndpbF9tZWRkaWZmXyIsIHQ1JGNvbXBldGl0b3JbaV0pLCB0NSRtZWRpYW5fZGlmZltpXSk7IGtleShwYXN0ZTAoIndpbF93aW5zXyIsIHQ1JGNvbXBldGl0b3JbaV0pLCB0NSR3aW5zW2ldKSB9CmtuaXRyOjprYWJsZSh0NSB8PiB0cmFuc211dGUoQ29tcGV0aXRvciA9IGNvbXBldGl0b3IsIGBNZWRpYW4gzpROUk1TRWAgPSBtZWRpYW5fZGlmZiwgYFNlYXMtQVIgd2lucyAoJSlgID0gd2lucywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBgcCAoSG9sbSlgID0gaWZlbHNlKHBfaG9sbSA8IDFlLTQsICI8IDFlLTQiLCBmb3JtYXRDKHBfaG9sbSwgZm9ybWF0ID0gImciLCBkaWdpdHMgPSAzKSkpLAogICAgICAgICAgICAgZGlnaXRzID0gNCwgY2FwdGlvbiA9IGNhcCgiVGFibGUgNiIsICJTZWFzLUFSIHZlcnN1cyBlYWNoIGNvbXBldGl0b3IgKFdpbGNveG9uIHNpZ25lZC1yYW5rIHRlc3QsIEhvbG0gY29ycmVjdGlvbikiKSkKYGBgCgojIyBHYXAgbGVuZ3RoLCBtaXNzaW5nIHJhdGUgYW5kIHN0YXRpb24KCmBgYHtyIHJlZ2ltZS1hbmFseXNpc30KcmVnX3RhYiA8LSBtYWluIHw+IGdyb3VwX2J5KHJlZ2ltZSwgbWV0aG9kKSB8PiBzdW1tYXJpc2UobnJtc2UgPSBtZWFuKG5ybXNlKSwgLmdyb3VwcyA9ICJkcm9wIikKcmVsIDwtIGJpbmRfcm93cyhsYXBwbHkoUkVHX05BTUVTLCBmdW5jdGlvbihyKSB7CiAgZyA8LSByZWdfdGFiIHw+IGZpbHRlcihyZWdpbWUgPT0gcik7IGNvbXAgPC0gZyB8PiBmaWx0ZXIobWV0aG9kICE9ICJTZWFzLUFSIikgfD4gc2xpY2VfbWluKG5ybXNlLCBuID0gMSkKICBzZWFzYXIgPC0gZyRucm1zZVtnJG1ldGhvZCA9PSAiU2Vhcy1BUiJdOyBzZWwgPC0gZ3JlcGwocGFzdGUwKCJcXHwiLCByLCAiXFx8IiksIFB3JHNjZW5hcmlvKQogIGRhdGEuZnJhbWUocmVnaW1lID0gciwgYmVzdCA9IGFzLmNoYXJhY3Rlcihjb21wJG1ldGhvZCksIG5ybXNlX2Jlc3QgPSBjb21wJG5ybXNlLCBucm1zZV9zZWFzYXIgPSBzZWFzYXIsCiAgICAgICAgICAgICByZWxfcGN0ID0gMTAwICogKHNlYXNhciAvIGNvbXAkbnJtc2UgLSAxKSwKICAgICAgICAgICAgIGJlYXRzX2thbG1hbiA9IDEwMCAqIG1lYW4oUG1bc2VsLCAiU2Vhcy1BUiJdIDwgUG1bc2VsLCAiS0FMTUFOIl0pLCBiZWF0c19saSA9IDEwMCAqIG1lYW4oUG1bc2VsLCAiU2Vhcy1BUiJdIDwgUG1bc2VsLCAiTEkiXSksCiAgICAgICAgICAgICBiZWF0c19zZWFkZWMgPSAxMDAgKiBtZWFuKFBtW3NlbCwgIlNlYXMtQVIiXSA8IFBtW3NlbCwgIlNFQURFQyJdKSkKfSkpCndyaXRlLmNzdihyZWwsIG91dF9maWxlKCJleHAxX3JlZ2ltZS5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCmZvciAoaSBpbiBzZXFfbGVuKG5yb3cocmVsKSkpIGZvciAoY2MgaW4gbmFtZXMocmVsKVstMV0pIGtleShwYXN0ZTAoInJlZ2ltZV8iLCByZWwkcmVnaW1lW2ldLCAiXyIsIGNjKSwgcmVsW1tjY11dW2ldKQpieV9yYXRlIDwtIG1haW4gfD4gZmlsdGVyKG1ldGhvZCA9PSAiU2Vhcy1BUiIpIHw+IGdyb3VwX2J5KHJhdGUpIHw+IHN1bW1hcmlzZShucm1zZSA9IG1lYW4obnJtc2UpKQprZXkoInNlYXNhcl9ucm1zZV9yYXRlMTAiLCBieV9yYXRlJG5ybXNlW2J5X3JhdGUkcmF0ZSA9PSAwLjFdKTsga2V5KCJzZWFzYXJfbnJtc2VfcmF0ZTMwIiwgYnlfcmF0ZSRucm1zZVtieV9yYXRlJHJhdGUgPT0gMC4zXSkKc3RfcmFuayA8LSBzYXBwbHkoU1RBVElPTlMsIGZ1bmN0aW9uKHN0KSB7IHNlbCA8LSBzdGFydHNXaXRoKFB3JHNjZW5hcmlvLCBwYXN0ZTAoc3QsICJ8IikpCiAgaWYgKCFhbnkoc2VsKSkgcmV0dXJuKHJlcChOQSwgbGVuZ3RoKE1FVEhPRF9PUkRFUikpKTsgY29sTWVhbnModChhcHBseShQbVtzZWwsICwgZHJvcCA9IEZBTFNFXSwgMSwgcmFuaykpKSB9KQpyb3duYW1lcyhzdF9yYW5rKSA8LSBNRVRIT0RfT1JERVIKZm9yIChzdCBpbiBTVEFUSU9OUykga2V5KHBhc3RlMCgic2Vhc2FyX3JhbmtfIiwgZ3N1YigiICIsICIiLCBzdCkpLCBzdF9yYW5rWyJTZWFzLUFSIiwgc3RdKQpzdWYgPC0gdDQgfD4gZmlsdGVyKG1ldGhvZCAhPSAiU2Vhcy1BUiIpCmZvciAocCBpbiBwYykgewogIGIgPC0gc3VmJG1ldGhvZFt3aGljaC5taW4oc3VmW1twXV0pXTsga2V5KHBhc3RlMCgic3VmMV9iZXN0XyIsIHApLCBhcy5jaGFyYWN0ZXIoYikpOyBrZXkocGFzdGUwKCJzdWYxX2Jlc3R2YWxfIiwgcCksIG1pbihzdWZbW3BdXSkpCiAga2V5KHBhc3RlMCgic3VmMV9zZWFzYXJfIiwgcCksIHQ0W1twXV1bdDQkbWV0aG9kID09ICJTZWFzLUFSIl0pOyBrZXkocGFzdGUwKCJzdWYxX3JlbF8iLCBwKSwgMTAwICogKDEgLSB0NFtbcF1dW3Q0JG1ldGhvZCA9PSAiU2Vhcy1BUiJdIC8gbWluKHN1ZltbcF1dKSkpCiAga2V5KHBhc3RlMCgic3VmMV92c19zc21lZGlhbl8iLCBwKSwgMTAwICogKDEgLSB0NFtbcF1dW3Q0JG1ldGhvZCA9PSAiU2Vhcy1BUiJdIC8gdDRbW3BdXVt0NCRtZXRob2QgPT0gIlNTLU1FRElBTiJdKSkKfQprbml0cjo6a2FibGUocmVsLCBkaWdpdHMgPSBjKDAsIDAsIDQsIDQsIDEsIDEsIDEsIDEpLAogIGNvbC5uYW1lcyA9IGMoIkdhcCByZWdpbWUiLCAiQmVzdCBjb21wZXRpdG9yIiwgIk5STVNFIGJlc3QgY29tcGV0aXRvciIsICJOUk1TRSBTZWFzLUFSIiwgIlNlYXMtQVIgdnMgYmVzdCAoJSkiLAogICAgICAgICAgICAgICAgIlNlYXMtQVIgYmVhdHMgS0FMTUFOICglKSIsICJTZWFzLUFSIGJlYXRzIExJICglKSIsICJTZWFzLUFSIGJlYXRzIFNFQURFQyAoJSkiKSwKICBjYXB0aW9uID0gIlNlYXMtQVIgdmVyc3VzIHRoZSBiZXN0IGNvbXBldGl0b3IgaW4gZWFjaCBnYXAgcmVnaW1lIikKa25pdHI6OmthYmxlKGRhdGEuZnJhbWUobWV0aG9kID0gTUVUSE9EX09SREVSLCBzdF9yYW5rLCBjaGVjay5uYW1lcyA9IEZBTFNFKSwgZGlnaXRzID0gMiwgY2FwdGlvbiA9ICJBdmVyYWdlIHJhbmsgcGVyIHN0YXRpb24iKQpgYGAKCiMjIEFibGF0aW9uIHN0dWR5CgpFYWNoIHZhcmlhbnQgcmVtb3ZlcyBvbmUgY29tcG9uZW50IG9mIFNlYXMtQVI6IG5vIGxvZyB0cmFuc2Zvcm0gKCR6PXkvbSQpLCBkYWlseSBjb21wb25lbnQgb25seQooJFdfdFxlcXVpdjAkKSwgc3RhdGljIHByb2ZpbGVzICgkaF9EPWhfVz1caW5mdHkkKSwgbm8gcmVtYWluZGVyIG1vZGVsICgkXGhhdCBSX3Q9MCQpLCBhbmQgbGluZWFyCmludGVycG9sYXRpb24gb2YgdGhlIHJlbWFpbmRlciBpbnN0ZWFkIG9mIHRoZSBBUigxKSBicmlkZ2UuCgpgYGB7ciB0YWJsZTZ9CkFCTF9PUkRFUiA8LSBjKCJTZWFzLUFSIiwgIncvbyBsb2cgdHJhbnNmb3JtIiwgIncvbyB3ZWVrbHkgY29tcC4iLCAic3RhdGljIHByb2ZpbGVzIiwgInJlbWFpbmRlciA9IDAiLCAicmVtYWluZGVyIExJIikKQUJMX1RFWCA8LSBjKGBTZWFzLUFSYCA9ICJGdWxsIFNlYXMtQVIiLCBgdy9vIGxvZyB0cmFuc2Zvcm1gID0gIm5vIGxvZyB0cmFuc2Zvcm0gKCR6PXkvbSQpIiwKICAgICAgICAgICAgIGB3L28gd2Vla2x5IGNvbXAuYCA9ICJkYWlseSBjb21wb25lbnQgb25seSAoJFdfdFxcZXF1aXYwJCkiLCBgc3RhdGljIHByb2ZpbGVzYCA9ICJzdGF0aWMgcHJvZmlsZXMgKCRoX0Q9aF9XPVxcaW5mdHkkKSIsCiAgICAgICAgICAgICBgcmVtYWluZGVyID0gMGAgPSAibm8gcmVtYWluZGVyIG1vZGVsICgkXFxoYXQgUl90PTAkKSIsIGByZW1haW5kZXIgTElgID0gInJlbWFpbmRlciBieSBsaW5lYXIgaW50ZXJwb2xhdGlvbiIpCmFiIDwtIHJlczEgfD4gZmlsdGVyKGtpbmQgPT0gImFibGF0aW9uIiB8IG1ldGhvZCA9PSAiU2Vhcy1BUiIpCnQ2IDwtIGFiIHw+IGdyb3VwX2J5KG1ldGhvZCwgcmVnaW1lKSB8PiBzdW1tYXJpc2UodiA9IG1lYW4obnJtc2UpLCAuZ3JvdXBzID0gImRyb3AiKSB8PgogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSByZWdpbWUsIHZhbHVlc19mcm9tID0gdikgfD4KICBsZWZ0X2pvaW4oYWIgfD4gZ3JvdXBfYnkobWV0aG9kKSB8PiBzdW1tYXJpc2UoQWxsID0gbWVhbihucm1zZSkpLCBieSA9ICJtZXRob2QiKSB8PgogIG11dGF0ZShtZXRob2QgPSBmYWN0b3IobWV0aG9kLCBsZXZlbHMgPSBBQkxfT1JERVIpKSB8PiBhcnJhbmdlKG1ldGhvZCkKd3JpdGUuY3N2KHQ2LCBvdXRfZmlsZSgiZXhwMV9hYmxhdGlvbi5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCmNjNiA8LSBjKGludGVyc2VjdChSRUdfTkFNRVMsIG5hbWVzKHQ2KSksICJBbGwiKQpmb3IgKGkgaW4gc2VxX2xlbihucm93KHQ2KSkpIGZvciAoY2MgaW4gY2M2KSBrZXkocGFzdGUwKCJhYmxfIiwgZ3N1YigiW15BLVphLXowLTldIiwgIiIsIGFzLmNoYXJhY3Rlcih0NiRtZXRob2RbaV0pKSwgIl8iLCBjYyksIHQ2W1tjY11dW2ldKQp0eDYgPC0gbGFwcGx5KGNjNiwgZnVuY3Rpb24oY2MpIGZtdF90ZXgodDZbW2NjXV0sIDQpKQp3cml0ZV90ZXgoYygiXFxiZWdpbnt0YWJsZX1bIXRdIiwgIlxcY2VudGVyaW5nIiwKICAiXFxjYXB0aW9ue0FibGF0aW9uIHN0dWR5OiBtZWFuIE5STVNFIG92ZXIgdGhlIDE1IHNlcmllcyAodGhyZWUgc3RhdGlvbnMgJFxcdGltZXMkIGZpdmUgcG9sbHV0YW50cykgYnkgZ2FwIHJlZ2ltZS4gQmVzdCB2YWx1ZXMgaW4gYm9sZC59IiwKICAiXFxsYWJlbHt0YWI6YWJsYXRpb259IiwgIlxcc2V0bGVuZ3Roe1xcdGFiY29sc2VwfXszLjVwdH0iLCAiXFxiZWdpbnt0YWJ1bGFyfXtscnJycn0iLCAiXFx0b3BydWxlIiwKICAiVmFyaWFudCAmIFNob3J0ICYgTWVkaXVtICYgTG9uZyAmIEFsbFxcXFwiLCAiXFxtaWRydWxlIiwKICBzYXBwbHkoc2VxX2xlbihucm93KHQ2KSksIGZ1bmN0aW9uKGkpIHBhc3RlMChwYXN0ZShjKEFCTF9URVhbW2FzLmNoYXJhY3Rlcih0NiRtZXRob2RbaV0pXV0sIHNhcHBseSh0eDYsIGBbYCwgaSkpLCBjb2xsYXBzZSA9ICIgJiAiKSwgIiBcXFxcIikpLAogICJcXGJvdHRvbXJ1bGUiLCAiXFxlbmR7dGFidWxhcn0iLCAiXFxlbmR7dGFibGV9IiksICJ0YWJfYWJsYXRpb24iKQptZDYgPC0gZGF0YS5mcmFtZShWYXJpYW50ID0gYXMuY2hhcmFjdGVyKHQ2JG1ldGhvZCksIGNoZWNrLm5hbWVzID0gRkFMU0UpCmZvciAoY2MgaW4gY2M2KSBtZDZbW2NjXV0gPC0gZm10X21kKHQ2W1tjY11dLCA0KQprbml0cjo6a2FibGUobWQ2LCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9IGNhcCgiVGFibGUgNyIsICJBYmxhdGlvbiBzdHVkeSAobWVhbiBOUk1TRSkiKSkKYGBgCgojIyBBZGRpdGlvbmFsIGNoZWNrOiB0aGUgb3JpZ2luYWwgYGltcHV0ZVRTOjpuYV9zZWFkZWNgCgpgYGB7ciBuYS1zZWFkZWN9CmlmIChhbnkocmVzMSRraW5kID09ICJleHRyYSIpKSB7CiAgZXggPC0gcmVzMSB8PiBmaWx0ZXIoa2luZCA9PSAiZXh0cmEiKSB8PiBtdXRhdGUoc2NlbmFyaW8gPSBwYXN0ZShzdGF0aW9uLCBwb2xsdXRhbnQsIHJlZ2ltZSwgcmF0ZSwgcmVwLCBzZXAgPSAifCIpKQogIGpqIDwtIGV4IHw+IHNlbGVjdChzY2VuYXJpbywgZXggPSBucm1zZSkgfD4KICAgIGlubmVyX2pvaW4obWFpbiB8PiBmaWx0ZXIobWV0aG9kID09ICJTZWFzLUFSIikgfD4gc2VsZWN0KHNjZW5hcmlvLCBzZWFzYXIgPSBucm1zZSksIGJ5ID0gInNjZW5hcmlvIikgfD4KICAgIGlubmVyX2pvaW4obWFpbiB8PiBmaWx0ZXIobWV0aG9kID09ICJTRUFERUMiKSB8PiBzZWxlY3Qoc2NlbmFyaW8sIHNlYWRlYyA9IG5ybXNlKSwgYnkgPSAic2NlbmFyaW8iKQogIHd4IDwtIHdpbGNveC50ZXN0KGpqJHNlYXNhciwgamokZXgsIHBhaXJlZCA9IFRSVUUsIGV4YWN0ID0gRkFMU0UsIGNvcnJlY3QgPSBGQUxTRSkKICBrZXkoIm5hc2VhZGVjX25ybXNlIiwgbWVhbihqaiRleCkpOyBrZXkoInNlYWRlY19ucm1zZSIsIG1lYW4oamokc2VhZGVjKSk7IGtleSgic2Vhc2FyX25ybXNlIiwgbWVhbihqaiRzZWFzYXIpKQogIGtleSgibmFzZWFkZWNfc2Vhc2FyX3dpbnMiLCAxMDAgKiBtZWFuKGpqJHNlYXNhciA8IGpqJGV4KSk7IGtleSgibmFzZWFkZWNfcCIsIHd4JHAudmFsdWUpCiAgY2F0KHNwcmludGYoIk1lYW4gTlJNU0U6IG5hX3NlYWRlYyAocm9idXN0IFNUTCkgPSAlLjRmIHwgU0VBREVDIChjbGFzc2ljYWwpID0gJS40ZiB8IFNlYXMtQVIgPSAlLjRmXG4iLCBtZWFuKGpqJGV4KSwgbWVhbihqaiRzZWFkZWMpLCBtZWFuKGpqJHNlYXNhcikpKQogIGNhdChzcHJpbnRmKCJTZWFzLUFSIGlzIG1vcmUgYWNjdXJhdGUgdGhhbiBuYV9zZWFkZWMgaW4gJS4xZiUlIG9mIHRoZSBzY2VuYXJpb3MgKFdpbGNveG9uIHAgPSAlLjJnKVxuIiwgMTAwICogbWVhbihqaiRzZWFzYXIgPCBqaiRleCksIHd4JHAudmFsdWUpKQp9IGVsc2UgY2F0KCJpbXB1dGVUUyBpcyBub3QgYXZhaWxhYmxlOyBjaGVjayBza2lwcGVkLlxuIikKYGBgCgojIyBJbGx1c3RyYXRpb24gb2YgYSByZW1vdmVkIGJsb2NrCgpBIGZvdXItZGF5IE5PJF8yJCBibG9jayBpcyBjaG9zZW4gYXMgdGhlIGJsb2NrIHN0YXJ0aW5nIGF0IDAwOjMwLCBiZXR3ZWVuIDcgTWF5IGFuZCB0aGUgZW5kIG9mIE9jdG9iZXIsCndpdGggdGhlIGZld2VzdCBtaXNzaW5nIHZhbHVlcy4gSXQgaXMgcmVtb3ZlZCBhbmQgaW1wdXRlZCBieSBmaXZlIG1ldGhvZHMuIEVhY2ggcGFuZWwgc2hvd3MgdGhlIHRydWUKc2VyaWVzIChncmV5KSBhbmQgb25lIGltcHV0YXRpb24gKGNvbG91cikgaW5zaWRlIHRoZSBzaGFkZWQgZ2FwLCB3aXRoIHRoZSBSTVNFIG92ZXIgdGhlIGJsb2NrLgoKYGBge3IgZmlnNywgZmlnLndpZHRoPTcuMTYsIGZpZy5oZWlnaHQ9NC4yfQp4NyA8LSBnZXRfc2VyaWVzKCJTVUYgMSIsICJOTzIiKTsgTDcgPC0gNDggKiA0OyBjdHggPC0gNDggKiAyCmkwIDwtIHdoaWNoKFRTID49IGFzLlBPU0lYY3QoIjIwMTgtMDUtMDcgMDA6MzAiLCB0eiA9ICJVVEMiKSlbMV0gLSAxICAgICAgICAgICAgICMgMC1iYXNlZCBzdGFydApjYW5kIDwtIHNlcShpMCwgMzA0ICogNDggLSBMNyAtIGN0eCAtIDEsIGJ5ID0gNDgpCmkwIDwtIGNhbmRbd2hpY2gubWluKHNhcHBseShjYW5kLCBmdW5jdGlvbihrKSBzdW0oaXMubmEoeDdbKGsgKyAxKTooayArIEw3KV0pKSkpXQpibGsgPC0gKGkwICsgMSk6KGkwICsgTDcpOyB4bTcgPC0geDc7IHhtN1tibGtdIDwtIE5BOyBva2IgPC0gIWlzLm5hKHg3W2Jsa10pOyBzbCA8LSAoaTAgKyAxIC0gY3R4KTooaTAgKyBMNyArIGN0eCkKTTcgPC0gYygiTEkiLCAiS0FMTUFOIiwgIlNFQURFQyIsICJNU1RMLUxJIiwgIlNlYXMtQVIiKQpmNyA8LSBjKE1FVEhPRFNbYygiTEkiLCAiS0FMTUFOIiwgIlNFQURFQyIsICJNU1RMLUxJIildLAogICAgICAgIGxpc3QoYFNlYXMtQVJgID0gZnVuY3Rpb24odikgaW1wdXRlX3NlYXNhcih2LCBCV1tbIlNVRiAxIl1dJGJ3X2RheSwgQldbWyJTVUYgMSJdXSRid193ZWVrKSkpCmltcDcgPC0gbGFwcGx5KGY3LCBmdW5jdGlvbihmKSBmKHhtNykpOyBybTcgPC0gc2FwcGx5KGltcDcsIGZ1bmN0aW9uKHkpIHJtc2UoeVtibGtdW29rYl0sIHg3W2Jsa11bb2tiXSkpCmZvciAobW0gaW4gTTcpIGtleShwYXN0ZTAoImV4YW1wbGVfcm1zZV8iLCBtbSksIHJtN1tbbW1dXSkKbGFiNyA8LSBzZXROYW1lcyhzcHJpbnRmKCIlcyAoUk1TRSAlLjFmKSIsIE03LCBybTdbTTddKSwgTTcpCmQ3IDwtIGJpbmRfcm93cyhsYXBwbHkoTTcsIGZ1bmN0aW9uKG1tKSBkYXRhLmZyYW1lKG1ldGhvZCA9IG1tLCB0ID0gVFNbc2xdLCB5ID0gaWZlbHNlKHNsICVpbiUgYmxrLCBpbXA3W1ttbV1dW3NsXSwgTkEpKSkpIHw+CiAgbXV0YXRlKHBhbmVsID0gZmFjdG9yKGxhYjdbbWV0aG9kXSwgbGV2ZWxzID0gbGFiNykpCnRydXRoNyA8LSB0aWR5cjo6Y3Jvc3NpbmcocGFuZWwgPSBmYWN0b3IobGFiNywgbGV2ZWxzID0gbGFiNyksIGRhdGEuZnJhbWUodCA9IFRTW3NsXSwgeSA9IHg3W3NsXSkpCnA3IDwtIGdncGxvdCgpICsgYW5ub3RhdGUoInJlY3QiLCB4bWluID0gVFNbbWluKGJsayldLCB4bWF4ID0gVFNbbWF4KGJsayldLCB5bWluID0gLUluZiwgeW1heCA9IEluZiwgZmlsbCA9ICJncmV5OTMiKSArCiAgZ2VvbV9saW5lKGRhdGEgPSB0cnV0aDcsIGFlcyh0LCB5KSwgY29sb3VyID0gImdyZXkzMCIsIGxpbmV3aWR0aCA9IDAuMywgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV9saW5lKGRhdGEgPSBkNywgYWVzKHQsIHksIGNvbG91ciA9IG1ldGhvZCksIGxpbmV3aWR0aCA9IDAuNiwgbmEucm0gPSBUUlVFKSArCiAgZmFjZXRfd3JhcCh+cGFuZWwsIG5jb2wgPSAxKSArIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gQ09MLCBndWlkZSA9ICJub25lIikgKwogIHNjYWxlX3hfZGF0ZXRpbWUoZGF0ZV9icmVha3MgPSAiMSBkYXkiLCBkYXRlX2xhYmVscyA9ICIlZCAlYiIpICsKICBsYWJzKHggPSBOVUxMLCB5ID0gZXhwcmVzc2lvbihOT1syXSB+ICJbXHUwMGI1Zy9tIl4zICogIl0iKSkKcHJpbnQocDcpOyBzYXZlX2ZpZyhwNywgImZpZzA3X2V4YW1wbGVfaW1wdXRhdGlvbiIsIDcuMTYsIDQuMikKa2V5KCJleGFtcGxlX2Jsb2NrIiwgcGFzdGUoZm9ybWF0KFRTW21pbihibGspXSwgIiVkICViIiksICItIiwgZm9ybWF0KFRTW21heChibGspXSwgIiVkICViIikpKQpgYGAKCiMjIFNlYXMtQVIgZGVjb21wb3NpdGlvbgoKRm91ciB3ZWVrcyBvZiBsb2ctc2NhbGVkIE5PJF8yJCBhdCBTVUYgMSBmcm9tIDYgQXVndXN0IDIwMTgsIHNwbGl0IGludG8gaXRzIGZvdXIgY29tcG9uZW50cy4gR2FwcyBsb25nZXIgdGhhbgp0d28gaG91cnMgYXJlIHNoYWRlZCByZWQ7IGluc2lkZSB0aGVtIHRoZSBpbXB1dGVkICR6X3QkIGFuZCB0aGUgQVIoMSkgYnJpZGdlIGZvciAkUl90JCBhcmUgZHJhd24gaW4gcmVkIHdpdGggYQpwb2ludHdpc2UgOTUlIGJhbmQgZnJvbSB0aGUgY2xvc2VkLWZvcm0gY29uZGl0aW9uYWwgdmFyaWFuY2Ugb2YgdGhlIGJyaWRnZS4gV2Vla2VuZHMgYXJlIHNoYWRlZCBncmV5LCB3aGljaApzaG93cyB3aGVyZSB0aGUgd2Vla2x5IGRldmlhdGlvbiAkV190JCBkZXBhcnRzIGZyb20gemVyby4KCmBgYHtyIGZpZzgsIGZpZy53aWR0aD0zLjUsIGZpZy5oZWlnaHQ9NC42LCBvdXQud2lkdGg9JzYwJSd9CiMgU2Vhcy1BUiBkZWNvbXBvc2l0aW9uIG9mIHRoZSBsb2ctc2NhbGVkIE5PMiBzZXJpZXMgb2YgU1VGIDEgb3ZlciBmb3VyIHdlZWtzLiBHYXBzIGxvbmdlciB0aGFuIHR3byBob3VycwojIGFyZSBzaGFkZWQgcmVkOyB0aGUgaW1wdXRlZCB6X3QgYW5kIHRoZSBBUigxKSBicmlkZ2UgZm9yIFJfdCBhcmUgZHJhd24gaW4gcmVkIHdpdGggYSBwb2ludHdpc2UgOTUlIGJhbmQgZnJvbSB0aGUKIyBjbG9zZWQtZm9ybSBjb25kaXRpb25hbCB2YXJpYW5jZSBvZiB0aGUgYnJpZGdlOyB3ZWVrZW5kcyBhcmUgc2hhZGVkIGdyZXkuCmRlYyA8LSBpbXB1dGVfc2Vhc2FyKGdldF9zZXJpZXMoIlNVRiAxIiwgIk5PMiIpLCBCV1tbIlNVRiAxIl1dJGJ3X2RheSwgQldbWyJTVUYgMSJdXSRid193ZWVrLCByZXR1cm5fY29tcG9uZW50cyA9IFRSVUUpCndpbiA8LSB3aGljaChUUyA+PSBhcy5QT1NJWGN0KCIyMDE4LTA4LTA2IDAwOjMwIiwgdHogPSAiVVRDIikpWzFdICsgMDooNDggKiAyOCAtIDEpCm1pc3MgPC0gaXMubmEoZGVjJHpbd2luXSk7IHpmaWxsIDwtIChkZWMkY29tcCR0cmVuZCArIGRlYyRjb21wJGRhaWx5ICsgZGVjJGNvbXAkd2Vla2x5ICsgZGVjJHJlbWFpbmRlcl9pbXApW3dpbl0KIyBjb25kaXRpb25hbCBzdGFuZGFyZCBkZXZpYXRpb24gb2YgdGhlIGJyaWRnZTogZ2FtbWEwICgxIC0gcGhpXjJrKSgxIC0gcGhpXjIobi1rKSkgLyAoMSAtIHBoaV4ybikKciA8LSBkZWMkY29tcCRyZW1haW5kZXI7IG4gPC0gbGVuZ3RoKHIpOyBwciA8LSAhaXMubmEoclstMV0pICYgIWlzLm5hKHJbLW5dKQpwaGkgPC0gbWluKDAuOTk5LCBtYXgoMCwgc3VtKHJbLTFdW3ByXSAqIHJbLW5dW3ByXSkgLyBzdW0oclstbl1bcHJdXjIpKSkKZzAgPC0gbWVhbigoclstMV1bcHJdIC0gcGhpICogclstbl1bcHJdKV4yKSAvICgxIC0gcGhpXjIpCnNkX2IgPC0gcmVwKE5BX3JlYWxfLCBuKTsgcnIgPC0gcmxlKGlzLm5hKHIpKTsgZSA8LSBjdW1zdW0ocnIkbGVuZ3Rocyk7IHMgPC0gZSAtIHJyJGxlbmd0aHMgKyAxCmZvciAoayBpbiB3aGljaChyciR2YWx1ZXMpKSB7CiAgYSA8LSBzW2tdIC0gMTsgYiA8LSBlW2tdICsgMTsgaSA8LSBzW2tdOmVba10KICBzZF9iW2ldIDwtIGlmIChhID49IDEgJiYgYiA8PSBuKSBzcXJ0KGcwICogKDEgLSBwaGleKDIgKiAoaSAtIGEpKSkgKiAoMSAtIHBoaV4oMiAqIChiIC0gaSkpKSAvICgxIC0gcGhpXigyICogKGIgLSBhKSkpKSBlbHNlCiAgICAgICAgICAgICBpZiAoYSA+PSAxKSBzcXJ0KGcwICogKDEgLSBwaGleKDIgKiAoaSAtIGEpKSkpIGVsc2Ugc3FydChnMCAqICgxIC0gcGhpXigyICogKGIgLSBpKSkpKQp9CkxBQjggPC0gYyh6ID0gImF0b3Aoelt0XSwgJ2xvZyBzY2FsZScpIiwgVCA9ICJhdG9wKFRbdF0sICd0cmVuZCcpIiwgRCA9ICJhdG9wKERbdF0sICdkYWlseScpIiwKICAgICAgICAgIFcgPSAiYXRvcChXW3RdLCAnd2Vla2x5JykiLCBSID0gImF0b3AoUlt0XSwgJ3JlbWFpbmRlcicpIikKQ09MOCA8LSBzZXROYW1lcyhjKCIjMmMzZTUwIiwgIiMxZjYxOGQiLCAiIzE0OGY3NyIsICIjY2E2ZjFlIiwgIiM3ZDNjOTgiKSwgTEFCOCkKcGYgPC0gZnVuY3Rpb24oaykgZmFjdG9yKHVubmFtZShMQUI4W2tdKSwgbGV2ZWxzID0gTEFCOCkKZF9vYnMgPC0gYmluZF9yb3dzKGRhdGEuZnJhbWUocGFuZWwgPSBwZigieiIpLCB0ID0gVFNbd2luXSwgdiA9IGRlYyR6W3dpbl0pLAogICAgICAgICAgICAgICAgICAgZGF0YS5mcmFtZShwYW5lbCA9IHBmKCJUIiksIHQgPSBUU1t3aW5dLCB2ID0gZGVjJGNvbXAkdHJlbmRbd2luXSksCiAgICAgICAgICAgICAgICAgICBkYXRhLmZyYW1lKHBhbmVsID0gcGYoIkQiKSwgdCA9IFRTW3dpbl0sIHYgPSBkZWMkY29tcCRkYWlseVt3aW5dKSwKICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUocGFuZWwgPSBwZigiVyIpLCB0ID0gVFNbd2luXSwgdiA9IGRlYyRjb21wJHdlZWtseVt3aW5dKSwKICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUocGFuZWwgPSBwZigiUiIpLCB0ID0gVFNbd2luXSwgdiA9IGRlYyRjb21wJHJlbWFpbmRlclt3aW5dKSkKZF9pbXAgPC0gYmluZF9yb3dzKGRhdGEuZnJhbWUocGFuZWwgPSBwZigieiIpLCB0ID0gVFNbd2luXSwgdiA9IGlmZWxzZShtaXNzLCB6ZmlsbCwgTkEpLCBzZCA9IHNkX2Jbd2luXSksCiAgICAgICAgICAgICAgICAgICBkYXRhLmZyYW1lKHBhbmVsID0gcGYoIlIiKSwgdCA9IFRTW3dpbl0sIHYgPSBpZmVsc2UobWlzcywgZGVjJHJlbWFpbmRlcl9pbXBbd2luXSwgTkEpLCBzZCA9IHNkX2Jbd2luXSkpIHw+CiAgbXV0YXRlKGxvID0gdiAtIDEuOTYgKiBzZCwgaGkgPSB2ICsgMS45NiAqIHNkKQpydyA8LSBybGUobWlzcyk7IGV3IDwtIGN1bXN1bShydyRsZW5ndGhzKTsgc3cgPC0gZXcgLSBydyRsZW5ndGhzICsgMTsga3cgPC0gd2hpY2gocnckdmFsdWVzKQpnYXBzOCA8LSBkYXRhLmZyYW1lKHhtaW4gPSBUU1t3aW5dW3N3W2t3XV0gLSA5MDAsIHhtYXggPSBUU1t3aW5dW2V3W2t3XV0gKyA5MDAsIGxlbiA9IHJ3JGxlbmd0aHNba3ddKQpnbCA8LSBnYXBzOFt3aGljaC5tYXgoZ2FwczgkbGVuKSwgXQpnbF9sYWIgPC0gZGF0YS5mcmFtZShwYW5lbCA9IHBmKCJ6IiksIHQgPSBnbCR4bWluICsgKGdsJHhtYXggLSBnbCR4bWluKSAvIDIsCiAgICAgICAgICAgICAgICAgICAgIGxhYiA9IGlmIChnbCRsZW4gPj0gNDgpIHNwcmludGYoIiUuMWYtZGF5IGdhcCIsIGdsJGxlbiAvIDQ4KSBlbHNlIHNwcmludGYoIiVnLWggZ2FwIiwgZ2wkbGVuIC8gMikpCmRkIDwtIHNlcShhcy5EYXRlKG1pbihUU1t3aW5dKSksIGFzLkRhdGUobWF4KFRTW3dpbl0pKSwgYnkgPSAiZGF5Iik7IHNhdCA8LSBkZFtmb3JtYXQoZGQsICIldSIpID09ICI2Il0Kd2tlbmQgPC0gZGF0YS5mcmFtZSh4bWluID0gcG1heChhcy5QT1NJWGN0KHBhc3RlKHNhdCwgIjAwOjAwIiksIHR6ID0gIlVUQyIpLCBtaW4oVFNbd2luXSkpLAogICAgICAgICAgICAgICAgICAgIHhtYXggPSBwbWluKGFzLlBPU0lYY3QocGFzdGUoc2F0ICsgMiwgIjAwOjAwIiksIHR6ID0gIlVUQyIpLCBtYXgoVFNbd2luXSkpKQpwOCA8LSBnZ3Bsb3QoKSArCiAgZ2VvbV9yZWN0KGRhdGEgPSB3a2VuZCwgYWVzKHhtaW4gPSB4bWluLCB4bWF4ID0geG1heCwgeW1pbiA9IC1JbmYsIHltYXggPSBJbmYpLCBmaWxsID0gImdyZXk5MyIpICsKICBnZW9tX3JlY3QoZGF0YSA9IGdhcHM4W2dhcHM4JGxlbiA+IDQsIF0sIGFlcyh4bWluID0geG1pbiwgeG1heCA9IHhtYXgsIHltaW4gPSAtSW5mLCB5bWF4ID0gSW5mKSwgZmlsbCA9ICIjZmFkYmQ4IikgKwogIGdlb21faGxpbmUoZGF0YSA9IGRhdGEuZnJhbWUocGFuZWwgPSBwZihjKCJEIiwgIlciLCAiUiIpKSwgeSA9IDApLCBhZXMoeWludGVyY2VwdCA9IHkpLCBjb2xvdXIgPSAiZ3JleTUwIiwKICAgICAgICAgICAgIGxpbmV3aWR0aCA9IDAuMiwgbGluZXR5cGUgPSAiMjIiKSArCiAgZ2VvbV9yaWJib24oZGF0YSA9IGZpbHRlcihkX29icywgcGFuZWwgJWluJSBwZihjKCJEIiwgIlciKSkpLCBhZXModCwgeW1pbiA9IHBtaW4odiwgMCksIHltYXggPSBwbWF4KHYsIDApLCBmaWxsID0gcGFuZWwpLAogICAgICAgICAgICAgIGFscGhhID0gMC4yNSwgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV9yaWJib24oZGF0YSA9IGRfaW1wLCBhZXModCwgeW1pbiA9IGxvLCB5bWF4ID0gaGkpLCBmaWxsID0gIiNlNzRjM2MiLCBhbHBoYSA9IDAuMjUpICsgICAjIE5BIHJvd3Mgc3BsaXQgdGhlIGJhbmQKICBnZW9tX2xpbmUoZGF0YSA9IGRfb2JzLCBhZXModCwgdiwgY29sb3VyID0gcGFuZWwsIGxpbmV3aWR0aCA9IHBhbmVsID09IExBQjhbWyJUIl1dKSwgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV9saW5lKGRhdGEgPSBkX2ltcCwgYWVzKHQsIHYpLCBjb2xvdXIgPSAiI2MwMzkyYiIsIGxpbmV3aWR0aCA9IDAuNSwgbmEucm0gPSBUUlVFKSArCiAgZ2VvbV90ZXh0KGRhdGEgPSBnbF9sYWIsIGFlcyh0LCBJbmYsIGxhYmVsID0gbGFiKSwgdmp1c3QgPSAxLjUsIHNpemUgPSAyLjMsIGZhbWlseSA9IEZPTlRfVEVYVCwgY29sb3VyID0gIiM5MjJiMjEiKSArCiAgZmFjZXRfZ3JpZChwYW5lbCB+IC4sIHNjYWxlcyA9ICJmcmVlX3kiLCBzd2l0Y2ggPSAieSIsIGxhYmVsbGVyID0gbGFiZWxfcGFyc2VkKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBDT0w4LCBndWlkZSA9ICJub25lIikgKyBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBDT0w4LCBndWlkZSA9ICJub25lIikgKwogIHNjYWxlX2xpbmV3aWR0aF9tYW51YWwodmFsdWVzID0gYyhgRkFMU0VgID0gMC4zLCBgVFJVRWAgPSAwLjcpLCBndWlkZSA9ICJub25lIikgKwogIHNjYWxlX3hfZGF0ZXRpbWUoYnJlYWtzID0gYXMuUE9TSVhjdChjKCIyMDE4LTA4LTEzIiwgIjIwMTgtMDgtMjAiLCAiMjAxOC0wOC0yNyIpLCB0eiA9ICJVVEMiKSwgZGF0ZV9sYWJlbHMgPSAiJWQgJWIiLAogICAgICAgICAgICAgICAgICAgZXhwYW5kID0gYygwLCAwKSkgKwogIHNjYWxlX3lfY29udGludW91cyhuLmJyZWFrcyA9IDMsIGV4cGFuZCA9IGV4cGFuc2lvbihtdWx0ID0gYygwLjA2LCAwLjE0KSkpICsKICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgdGl0bGUgPSBleHByZXNzaW9uKHpbdF0gPT0gVFt0XSArIERbdF0gKyBXW3RdICsgUlt0XSksCiAgICAgICBzdWJ0aXRsZSA9ICJyZWQ6IGdhcHMgaW1wdXRlZCBieSBTZWFzLUFSLCB3aXRoIDk1JSBiYW5kOyBncmV5OiB3ZWVrZW5kcyIpICsKICB0aGVtZShzdHJpcC5wbGFjZW1lbnQgPSAib3V0c2lkZSIsIHN0cmlwLnRleHQueS5sZWZ0ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCwgc2l6ZSA9IDcuNSksCiAgICAgICAgcGFuZWwuZ3JpZC5taW5vciA9IGVsZW1lbnRfYmxhbmsoKSwgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKGxpbmV3aWR0aCA9IDAuMTUsIGNvbG91ciA9ICJncmV5ODgiKSwKICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2xpbmUobGluZXdpZHRoID0gMC4yLCBjb2xvdXIgPSAiZ3JleTgyIiksIHBhbmVsLnNwYWNpbmcueSA9IHVuaXQoNSwgInB0IiksCiAgICAgICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplID0gOSwgaGp1c3QgPSAwLjUpLCBwbG90LnN1YnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSA3LCBoanVzdCA9IDAuNSwgY29sb3VyID0gImdyZXkzMCIpLAogICAgICAgIGF4aXMudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gNi44KSwgcGxvdC5tYXJnaW4gPSBtYXJnaW4oNCwgOCwgNCwgMikpCnByaW50KHA4KTsgc2F2ZV9maWcocDgsICJmaWcwOF9zZWFzYXJfY29tcG9uZW50cyIsIDMuNSwgNC42KQpgYGAKCiMjIENvbXB1dGluZyB0aW1lCgpgYGB7ciB0aW1pbmd9CmZvciAobW0gaW4gTUVUSE9EX09SREVSKSBrZXkocGFzdGUwKCJzZWNfIiwgbW0pLCB0NCRzZWNbdDQkbWV0aG9kID09IG1tXSkKa25pdHI6OmthYmxlKHQ0IHw+IHRyYW5zbXV0ZShtZXRob2QsIHNlY29uZHMgPSBzZWMpLCBkaWdpdHMgPSAzLCBjYXB0aW9uID0gIk1lYW4gY29tcHV0aW5nIHRpbWUgcGVyIGltcHV0YXRpb24gb2YgYSBmdWxsIHNlcmllcyAoUiwgb25lIENQVSBjb3JlKSIpCmBgYAoKIyBGb3JlY2FzdGluZyBtb2RlbHMgYW5kIHByb3RvY29sCgojIyBEYXRhIHNwbGl0LCBvcmlnaW5zIGFuZCBjYXVzYWwgaW1wdXRhdGlvbgoKQW4gb3JpZ2luICRcdGF1JCBpcyB0aGUgbnVtYmVyIG9mIG9ic2VydmF0aW9ucyBhdmFpbGFibGUgd2hlbiB0aGUgZm9yZWNhc3QgaXMgaXNzdWVkIChhdCBtaWRuaWdodCk7CnRoZSB0YXJnZXRzIGFyZSAkeV97XHRhdSsxfSxcZG90cyx5X3tcdGF1KzQ4fSQgKGRheS1haGVhZCwgaG9yaXpvbnMgJGg9MSxcZG90cyw0OCQpLgoKKiAqKlRlc3Q6KiogMeKAkzIwIERlY2VtYmVyIDIwMTgsICRcdGF1X2o9MTZ7LH0wMzIrNDgoai0xKSQsICRqPTEsXGRvdHMsMjAkICg5NjAgdGFyZ2V0IHZhbHVlcykuCiogKipWYWxpZGF0aW9uOioqIG9yaWdpbnMgaW4gTm92ZW1iZXIgKCQxNHssfTU5MlxsZVx0YXVcbGUxNXssfTk4NCQsIGV2ZXJ5IDIgc3RlcHMpIGZvciBlYXJseSBzdG9wcGluZzsKICB0aGUgbG9zcyB1c2VzICoqb2JzZXJ2ZWQgdGFyZ2V0cyBvbmx5KiouCiogKipUcmFpbmluZzoqKiBvcmlnaW5zIGZyb20gMTUgSmFudWFyeSB0byAzMSBPY3RvYmVyICgkNjcyXGxlXHRhdVxsZTE0eyx9NTQ0JCwgZXZlcnkgNiBzdGVwcywgMjMxMyBvcmlnaW5zKS4KClRocmVlIHJ1bGVzIHByZXZlbnQgbGVha2FnZTogKDEpIHRoZSBoaXN0b3J5IHVzZWQgZm9yIHRyYWluaW5nIGlzIGltcHV0ZWQgd2l0aCB0aGUgZGF0YSBiZWZvcmUKMSBEZWNlbWJlciBvbmx5LCAkXHRpbGRlIHk9XG1hdGhybXtJbXB9KHlfezE6MTYwMzJ9KSQ7ICgyKSBhdCBldmVyeSB0ZXN0IG9yaWdpbiB0aGUgaW1wdXRhdGlvbiBpcyByZS1ydW4KY2F1c2FsbHksICRcdGlsZGUgeV57KFx0YXUpfT1cbWF0aHJte0ltcH0oeV97MTpcdGF1fSkkOyAoMykgdmFsaWRhdGlvbiBhbmQgdGVzdCBlcnJvcnMgYXJlIGNvbXB1dGVkIG9uCmdlbnVpbmVseSBvYnNlcnZlZCB2YWx1ZXMgb25seSwgd2hpbGUgdGhlIHRyYWluaW5nIHRhcmdldHMgYXJlIHRoZSBpbXB1dGVkIHNlcmllcy4KCmBgYHtyIGZvcmVjYXN0LXNldHVwfQpIIDwtIDQ4TDsgTF9TRVEgPC0gNDhMOyBURVNUX1NUQVJUIDwtIDMzNEwgKiA0OEw7IFZBTF9TVEFSVCA8LSAzMDRMICogNDhMCk1JTl9PUklHSU4gPC0gMkwgKiBTMjsgTl9URVNUX0RBWVMgPC0gMjBMCnRlc3Rfb3JpZ2lucyA8LSBURVNUX1NUQVJUICsgNDhMICogKDA6KE5fVEVTVF9EQVlTIC0gMSkpCnRyX29yaWcgPC0gc2VxKE1JTl9PUklHSU4sIFZBTF9TVEFSVCAtIEgsIGJ5ID0gNikKdmFfb3JpZyA8LSBzZXEoVkFMX1NUQVJULCBURVNUX1NUQVJUIC0gSCwgYnkgPSAyKQpob2QgPC0gZnVuY3Rpb24oaikgaiAlJSA0OEwgICAgICAgICAgICAgICAgICAgICAgIyBqID0gMC1iYXNlZCBpbmRleDsgMCA9IDAwOjMwCmRvdyA8LSBmdW5jdGlvbihqKSAoaiAlLyUgNDhMKSAlJSA3TCAgICAgICAgICAgICAjIDAgPSBNb25kYXkKY2F0KCJ0cmFpbmluZyBvcmlnaW5zOiIsIGxlbmd0aCh0cl9vcmlnKSwgInwgdmFsaWRhdGlvbiBvcmlnaW5zOiIsIGxlbmd0aCh2YV9vcmlnKSwgInwgdGVzdCBvcmlnaW5zOiIsIGxlbmd0aCh0ZXN0X29yaWdpbnMpLAogICAgIigiLCBmb3JtYXQoVFNbVEVTVF9TVEFSVCArIDFdKSwgInRvIiwgZm9ybWF0KFRTW1RFU1RfU1RBUlQgKyA5NjBdKSwgIilcbiIpCmBgYAoKIyMgU2Vhc29uYWwgbmFpdmUgYW5kIFRTUiAoRXEuIDEwKQoKKipTZWFzb25hbCBuYWl2ZToqKiAkXGhhdCB5X3tcdGF1K2h9PVx0aWxkZSB5XnsoXHRhdSl9X3tcdGF1K2gtMzM2fSQuICoqVFNSKiogKGFkZGl0aXZlIGRvdWJsZS1zZWFzb25hbAp0aW1lIHNlcmllcyByZWdyZXNzaW9uKToKCiQkCnlfdD1cYmV0YV8wK1xiZXRhXzF0K1xzdW1fe2k9Mn1eezQ4fVxhbHBoYV9pSF97aSx0fStcc3VtX3tqPTJ9Xns3fVxnYW1tYV9qRV97aix0fStlX3QsIFx0YWd7MTB9CiQkCgp3aXRoIGhhbGYtaG91ciBkdW1taWVzICRIX3tpLHR9JCBhbmQgd2Vla2RheSBkdW1taWVzICRFX3tqLHR9JCAoYmFzZSBjYXRlZ29yaWVzIDAwOjMwIGFuZCBNb25kYXkpLApmaXR0ZWQgYnkgbGVhc3Qgc3F1YXJlcyBvbiB0aGUgd2hvbGUgaW1wdXRlZCBoaXN0b3J5LiBUaW1lIGlzIHNjYWxlZCBhcyAkdC8xMF40JCBmb3IgbnVtZXJpY2FsCnN0YWJpbGl0eTsgdGhlIHNjYWxpbmcgZG9lcyBub3QgY2hhbmdlIHRoZSBmb3JlY2FzdHMuCgpgYGB7ciBtb2RlbHMtc3RhdH0KdHNyX2Rlc2lnbiA8LSBmdW5jdGlvbihqKSB7CiAgbiA8LSBsZW5ndGgoaik7IGhoIDwtIGhvZChqKTsgZGQgPC0gZG93KGopCiAgWGggPC0gbWF0cml4KDAsIG4sIDQ3KTsgWGhbY2JpbmQod2hpY2goaGggPiAwKSwgaGhbaGggPiAwXSldIDwtIDEKICBYZCA8LSBtYXRyaXgoMCwgbiwgNik7ICBYZFtjYmluZCh3aGljaChkZCA+IDApLCBkZFtkZCA+IDBdKV0gPC0gMQogIGNiaW5kKDEsIGogLyAxZTQsIFhoLCBYZCkKfQp0c3JfZml0ICA8LSBmdW5jdGlvbih5LCBqKSBxci5jb2VmKHFyKHRzcl9kZXNpZ24oaikpLCB5KQp0c3JfcHJlZCA8LSBmdW5jdGlvbihiZXRhLCBqKSB7IGIgPC0gYmV0YTsgYltpcy5uYShiKV0gPC0gMDsgYXMubnVtZXJpYyh0c3JfZGVzaWduKGopICUqJSBiKSB9CmBgYAoKIyMgTGlnaHRHQk0gKGRpcmVjdCBtdWx0aS1ob3Jpem9uIG1vZGVsKQoKRWFjaCAob3JpZ2luICRcdGF1JCwgaG9yaXpvbiAkaCQpIHBhaXIgaXMgb25lIHJvdyB3aXRoIDE0IGZlYXR1cmVzOiAkaCQsIGhhbGYtaG91ciBhbmQgd2Vla2RheSBvZiB0aGUKdGFyZ2V0LCB0aGUgbGFncyAxLCAyLCAzLCA2IGFuZCAxMiBhdCB0aGUgb3JpZ2luLCB0aGUgdmFsdWVzIDQ4LCA5NiwgMzM2IGFuZCA2NzIgc3RlcHMgYmVmb3JlIHRoZQp0YXJnZXQsIGFuZCB0aGUgbWVhbnMgb2YgdGhlIGxhc3QgZGF5IGFuZCB3ZWVrLiBTZXR0aW5nczogbGVhcm5pbmcgcmF0ZSAwLjAzLCAzMSBsZWF2ZXMsIGF0IGxlYXN0IDUwCnNhbXBsZXMgcGVyIGxlYWYsIGJhZ2dpbmcgMC44LCBmZWF0dXJlIGZyYWN0aW9uIDAuOSwgJExfMiQgcGVuYWx0eSAxLCBlYXJseSBzdG9wcGluZyBhZnRlciAxMDAgcm91bmRzCihhdCBtb3N0IDIwMDAgdHJlZXMpIG9uIHRoZSBvYnNlcnZlZCB2YWxpZGF0aW9uIHRhcmdldHMuCgpgYGB7ciBtb2RlbHMtZ2JtfQpnYm1fcm93cyA8LSBmdW5jdGlvbih5LCB0YXUpIHsKICBoIDwtIDA6KEggLSAxKTsgdGd0IDwtIHRhdSArIGggICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgMC1iYXNlZCB0YXJnZXQgaW5kaWNlcwogIGNzIDwtIGMoMCwgY3Vtc3VtKHkpKTsgbTQ4IDwtIChjc1t0YXUgKyAxXSAtIGNzW3RhdSArIDEgLSA0OF0pIC8gNDg7IG0zMzYgPC0gKGNzW3RhdSArIDFdIC0gY3NbdGF1ICsgMSAtIDMzNl0pIC8gMzM2CiAgbGFnIDwtIGZ1bmN0aW9uKGspIHJlcCh5W3RhdSAtIGsgKyAxXSwgSCk7IHNsIDwtIGZ1bmN0aW9uKGspIHlbdGd0IC0gayArIDFdCiAgY2JpbmQoaCA9IGgsIGhvZCA9IGhvZCh0Z3QpLCBkb3cgPSBkb3codGd0KSwgbGFnMSA9IGxhZygxKSwgbGFnMiA9IGxhZygyKSwgbGFnMyA9IGxhZygzKSwgbGFnNiA9IGxhZyg2KSwKICAgICAgICBsYWcxMiA9IGxhZygxMiksIHNsYWc0OCA9IHNsKDQ4KSwgc2xhZzk2ID0gc2woOTYpLCBzbGFnMzM2ID0gc2woMzM2KSwgc2xhZzY3MiA9IHNsKDY3MiksCiAgICAgICAgbWVhbjQ4ID0gbTQ4LCBtZWFuMzM2ID0gbTMzNikKfQpidWlsZF9nYm0gPC0gZnVuY3Rpb24oeSwgeXRhcmdldCwgb3JpZ2lucykgbGlzdCgKICBYID0gZG8uY2FsbChyYmluZCwgbGFwcGx5KG9yaWdpbnMsIGZ1bmN0aW9uKHQpIGdibV9yb3dzKHksIHQpKSksCiAgWSA9IHVubGlzdChsYXBwbHkob3JpZ2lucywgZnVuY3Rpb24odCkgeXRhcmdldFsodCArIDEpOih0ICsgSCldKSkpCmZpdF9wcmVkaWN0X2dibSA8LSBmdW5jdGlvbih5X2hpc3QsIHhfaGlzdF9yYXcsIGhpc3RfYXQpIHsKICB0ciA8LSBidWlsZF9nYm0oeV9oaXN0LCB5X2hpc3QsIHRyX29yaWcpOyB2YSA8LSBidWlsZF9nYm0oeV9oaXN0LCB4X2hpc3RfcmF3LCB2YV9vcmlnKTsgb2sgPC0gIWlzLm5hKHZhJFkpCiAgZHRyIDwtIGxpZ2h0Z2JtOjpsZ2IuRGF0YXNldCh0ciRYLCBsYWJlbCA9IHRyJFkpCiAgZHZhIDwtIGxpZ2h0Z2JtOjpsZ2IuRGF0YXNldC5jcmVhdGUudmFsaWQoZHRyLCB2YSRYW29rLCAsIGRyb3AgPSBGQUxTRV0sIGxhYmVsID0gdmEkWVtva10pCiAgcHJtIDwtIGxpc3Qob2JqZWN0aXZlID0gInJlZ3Jlc3Npb24iLCBsZWFybmluZ19yYXRlID0gMC4wMywgbnVtX2xlYXZlcyA9IDMxTCwgbWluX2RhdGFfaW5fbGVhZiA9IDUwTCwKICAgICAgICAgICAgICBiYWdnaW5nX2ZyYWN0aW9uID0gMC44LCBiYWdnaW5nX2ZyZXEgPSAxTCwgZmVhdHVyZV9mcmFjdGlvbiA9IDAuOSwgbGFtYmRhX2wyID0gMSwKICAgICAgICAgICAgICBudW1fdGhyZWFkcyA9IDFMLCB2ZXJib3NlID0gLTFMLCBzZWVkID0gMUwpCiAgZ2IgPC0gbGlnaHRnYm06OmxnYi50cmFpbihwYXJhbXMgPSBwcm0sIGRhdGEgPSBkdHIsIG5yb3VuZHMgPSAyMDAwTCwgdmFsaWRzID0gbGlzdCh2YWxpZCA9IGR2YSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSAxMDBMLCB2ZXJib3NlID0gLTFMKQogIFh0ZSA8LSBkby5jYWxsKHJiaW5kLCBsYXBwbHkoc2VxX2Fsb25nKHRlc3Rfb3JpZ2lucyksIGZ1bmN0aW9uKGspIGdibV9yb3dzKGhpc3RfYXRbW2tdXSwgdGVzdF9vcmlnaW5zW2tdKSkpCiAgbWF0cml4KHByZWRpY3QoZ2IsIFh0ZSksIG5yb3cgPSBOX1RFU1RfREFZUywgYnlyb3cgPSBUUlVFKQp9CmBgYAoKIyMgTmV1cmFsIG5ldHdvcmtzOiBNTFAsIExTVE0sIFRDTiBhbmQgdGhlIFRTUuKAk0xTVE0gaHlicmlkCgpBbGwgbmV0d29ya3MgdXNlIHN0YW5kYXJkaXNlZCBkYXRhIGFuZCB0aGUgc2FtZSBpbnB1dHM6IHRoZSBzZXF1ZW5jZQokXG1hdGhiZiBzX1x0YXU9KFx0aWxkZSB5X3tcdGF1LTQ3fSxcZG90cyxcdGlsZGUgeV9cdGF1KSQgYW5kIHRoZSBzaWRlIHZlY3RvcgokXG1hdGhiZiB2X1x0YXU9KFx0aWxkZSB5X3tcdGF1LTMzNX0sXGRvdHMsXHRpbGRlIHlfe1x0YXUtMjg4fSxcc2luXHRmcmFjezJccGkgaF9cdGF1fXs0OH0sXGNvc1x0ZnJhY3syXHBpIGhfXHRhdX17NDh9LFxzaW5cdGZyYWN7MlxwaSBkX1x0YXV9ezd9LFxjb3NcdGZyYWN7MlxwaSBkX1x0YXV9ezd9KSQKKHRoZSBzYW1lIHBlcmlvZCBvbmUgd2VlayBlYXJsaWVyIGFuZCBjYWxlbmRhciBjb2RlcyBvZiB0aGUgb3JpZ2luKS4KCiogKipNTFA6KiogJFxoYXR7XG1hdGhiZiB5fT1cbWF0aGJmIFdfM1wsXG1hdGhybXtSZUxVfShcbWF0aGJmIFdfMlwsXG1hdGhybXtSZUxVfShcbWF0aGJmIFdfMVtcbWF0aGJmIHNfXHRhdTtcbWF0aGJmIHZfXHRhdV0rXG1hdGhiZiBiXzEpK1xtYXRoYmYgYl8yKStcbWF0aGJmIGJfMyQgKDEyOCBhbmQgNjQgdW5pdHMpLgoqICoqTFNUTToqKiA2NCB1bml0cywgJFxtYXRoYmYgY190PVxtYXRoYmYgZl90XG9kb3RcbWF0aGJmIGNfe3QtMX0rXG1hdGhiZiBpX3Rcb2RvdFx0aWxkZXtcbWF0aGJmIGN9X3QkIGFuZAogICRcbWF0aGJmIGhfdD1cbWF0aGJmIG9fdFxvZG90XHRhbmgoXG1hdGhiZiBjX3QpJCB3aXRoIHNpZ21vaWQgZ2F0ZXMgJFxtYXRoYmYgaV90LFxtYXRoYmYgZl90LFxtYXRoYmYgb190JDsKICAkXG1hdGhiZiBoX3s0OH0kIGlzIGNvbmNhdGVuYXRlZCB3aXRoICRcbWF0aGJmIHZfXHRhdSQsIGZvbGxvd2VkIGJ5IERlbnNlIDY0IChSZUxVKSBhbmQgRGVuc2UgNDguCiogKipUQ046KiogZml2ZSBjYXVzYWwgZGlsYXRlZCBjb252b2x1dGlvbnMgJChGKl9keCkodCk9XHN1bV97aT0wfV57Mn1mKGkpXCx4X3t0LWRcLGl9JCwKICAkZFxpblx7MSwyLDQsOCwxNlx9JCwgMzIgZmlsdGVycyAocmVjZXB0aXZlIGZpZWxkIDYzKSwgdGhlbiBhcyBhYm92ZS4KKiAqKlRTUuKAk0xTVE06KiogdGhlIExTVE0gZm9yZWNhc3RzIHRoZSBUU1IgcmVzaWR1YWxzLCAkXGhhdCB5X3tcdGF1K2h9PVxoYXQgTF97XHRhdStofStcaGF0IE5fe1x0YXUraH0kLgoKTG9zczogbWFza2VkIG1lYW4gc3F1YXJlZCBlcnJvciBvdmVyIG9ic2VydmVkIHRhcmdldHMsCiRcbWF0aGNhbCBMPVxzdW1fe2ksaH1tX3tpaH0oeV97aWh9LVxoYXQgeV97aWh9KV4yL1xzdW1fe2ksaH1tX3tpaH0kLiBBZGFtIChsZWFybmluZyByYXRlCiQyXHRpbWVzMTBeey0zfSQpLCBiYXRjaCAyNTYsIGF0IG1vc3QgMjAgZXBvY2hzLCBlYXJseSBzdG9wcGluZyB3aXRoIHBhdGllbmNlIDMgYW5kIGJlc3Qgd2VpZ2h0cwpyZXN0b3JlZDsgdHdvIHNlZWRzLCBmb3JlY2FzdHMgYXZlcmFnZWQuIFdpdGhvdXQgYGtlcmFzM2Agb25seSBhbiBNTFAgZnJvbSBgbm5ldGAgaXMgYXZhaWxhYmxlLgoKYGBge3IgbW9kZWxzLW5ufQpvcmlnaW5fY2FsIDwtIGZ1bmN0aW9uKHRhdSkgeyB0IDwtIHRhdSAtIDE7IGEgPC0gMiAqIHBpICogaG9kKHQpIC8gNDg7IGIgPC0gMiAqIHBpICogZG93KHQpIC8gNwogIGMoc2luKGEpLCBjb3MoYSksIHNpbihiKSwgY29zKGIpKSB9Cm5uX2lucHV0cyA8LSBmdW5jdGlvbih5LCB0YXUpIGxpc3Qoc2VxID0geVsodGF1IC0gTF9TRVEgKyAxKTp0YXVdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNpZGUgPSBjKHlbKHRhdSAtIFMyICsgMSk6KHRhdSAtIFMyICsgSCldLCBvcmlnaW5fY2FsKHRhdSkpKQpidWlsZF9ubl9zYW1wbGVzIDwtIGZ1bmN0aW9uKHksIHl0YXJnZXQsIG9yaWdpbnMpIHsKICBuIDwtIGxlbmd0aChvcmlnaW5zKTsgU0VRIDwtIGFycmF5KDAsIGMobiwgTF9TRVEsIDEpKTsgU0lERSA8LSBtYXRyaXgoMCwgbiwgSCArIDQpOyBZIDwtIG1hdHJpeCgwLCBuLCBIKQogIGZvciAoaSBpbiBzZXFfbGVuKG4pKSB7IHRhdSA8LSBvcmlnaW5zW2ldOyBpbnAgPC0gbm5faW5wdXRzKHksIHRhdSkKICAgIFNFUVtpLCAsIDFdIDwtIGlucCRzZXE7IFNJREVbaSwgXSA8LSBpbnAkc2lkZTsgWVtpLCBdIDwtIHl0YXJnZXRbKHRhdSArIDEpOih0YXUgKyBIKV0gfQogIGxpc3Qoc2VxID0gU0VRLCBzaWRlID0gU0lERSwgeSA9IFkpCn0Kc3RhY2tfdGVzdCA8LSBmdW5jdGlvbihsc3QpIGxpc3Qoc2VxID0gYXJyYXkodChzYXBwbHkobHN0LCBgW1tgLCAic2VxIikpLCBjKGxlbmd0aChsc3QpLCBMX1NFUSwgMSkpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaWRlID0gdChzYXBwbHkobHN0LCBgW1tgLCAic2lkZSIpKSkKaWYgKEhBU19LRVJBUykgewogIGxpYnJhcnkoa2VyYXMzKQogIG1hc2tlZF9tc2UgPC0gZnVuY3Rpb24oeV90cnVlLCB5X3ByZWQpIHsgICAgICAgICMgbWVhbiBzcXVhcmVkIGVycm9yIG92ZXIgb2JzZXJ2ZWQgdGFyZ2V0cyBvbmx5CiAgICBtYXNrIDwtIG9wX2xvZ2ljYWxfbm90KG9wX2lzbmFuKHlfdHJ1ZSkpCiAgICBkaWZmIDwtIG9wX3doZXJlKG1hc2ssIHlfdHJ1ZSAtIHlfcHJlZCwgb3BfemVyb3NfbGlrZSh5X3ByZWQpKQogICAgb3Bfc3VtKG9wX3NxdWFyZShkaWZmKSkgLyBvcF9tYXhpbXVtKG9wX3N1bShvcF9jYXN0KG1hc2ssICJmbG9hdDMyIikpLCAxKQogIH0KICBidWlsZF9ubiA8LSBmdW5jdGlvbihraW5kLCB1bml0cyA9IDY0LCBsciA9IDJlLTMpIHsKICAgIGEgPC0ga2VyYXNfaW5wdXQoc2hhcGUgPSBjKExfU0VRLCAxTCkpOyBiIDwtIGtlcmFzX2lucHV0KHNoYXBlID0gYyhIICsgNEwpKQogICAgaWYgKGtpbmQgPT0gIk1MUCIpIHsKICAgICAgeiA8LSBsYXllcl9jb25jYXRlbmF0ZShsaXN0KGxheWVyX2ZsYXR0ZW4oYSksIGIpKSB8PgogICAgICAgIGxheWVyX2RlbnNlKDEyOCwgYWN0aXZhdGlvbiA9ICJyZWx1IikgfD4gbGF5ZXJfZGVuc2UoNjQsIGFjdGl2YXRpb24gPSAicmVsdSIpCiAgICB9IGVsc2UgewogICAgICBpZiAoa2luZCAlaW4lIGMoIkxTVE0iLCAiVFNSLUxTVE0iKSkgaCA8LSBhIHw+IGxheWVyX2xzdG0odW5pdHMpCiAgICAgIGlmIChraW5kID09ICJUQ04iKSB7CiAgICAgICAgaCA8LSBhCiAgICAgICAgZm9yIChkIGluIGMoMSwgMiwgNCwgOCwgMTYpKSBoIDwtIGggfD4gbGF5ZXJfY29udl8xZCgzMiwgMywgZGlsYXRpb25fcmF0ZSA9IGQsIHBhZGRpbmcgPSAiY2F1c2FsIiwgYWN0aXZhdGlvbiA9ICJyZWx1IikKICAgICAgICBoIDwtIGggfD4gbGF5ZXJfY3JvcHBpbmdfMWQoY3JvcHBpbmcgPSBjKExfU0VRIC0gMUwsIDBMKSkgfD4gbGF5ZXJfZmxhdHRlbigpICAgIyBsYXN0IHRpbWUgc3RlcAogICAgICB9CiAgICAgIHogPC0gbGF5ZXJfY29uY2F0ZW5hdGUobGlzdChoLCBiKSkgfD4gbGF5ZXJfZGVuc2UoNjQsIGFjdGl2YXRpb24gPSAicmVsdSIpCiAgICB9CiAgICBtIDwtIGtlcmFzX21vZGVsKGxpc3QoYSwgYiksIHogfD4gbGF5ZXJfZGVuc2UoSCkpCiAgICBtIHw+IGNvbXBpbGUob3B0aW1pemVyID0gb3B0aW1pemVyX2FkYW0obHIpLCBsb3NzID0gbWFza2VkX21zZSkKICAgIG0KICB9CiAgZml0X3ByZWRpY3Rfbm4gPC0gZnVuY3Rpb24oa2luZCwgdHIsIHZhLCB0ZSwgc2VlZCwgZXBvY2hzID0gMjAsIHBhdGllbmNlID0gMykgewogICAgY2xlYXJfc2Vzc2lvbigpOyBzZXRfcmFuZG9tX3NlZWQoc2VlZCkKICAgIG0gPC0gYnVpbGRfbm4oa2luZCkKICAgIG0gfD4gZml0KGxpc3QodHIkc2VxLCB0ciRzaWRlKSwgdHIkeSwgdmFsaWRhdGlvbl9kYXRhID0gbGlzdChsaXN0KHZhJHNlcSwgdmEkc2lkZSksIHZhJHkpLAogICAgICAgICAgICAgZXBvY2hzID0gZXBvY2hzLCBiYXRjaF9zaXplID0gMjU2LCB2ZXJib3NlID0gMCwKICAgICAgICAgICAgIGNhbGxiYWNrcyA9IGxpc3QoY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSBwYXRpZW5jZSwgcmVzdG9yZV9iZXN0X3dlaWdodHMgPSBUUlVFKSkpCiAgICBtIHw+IHByZWRpY3QobGlzdCh0ZSRzZXEsIHRlJHNpZGUpLCB2ZXJib3NlID0gMCkKICB9CiAgTk5fTU9ERUxTIDwtIGMoIk1MUCIsICJMU1RNIiwgIlRDTiIsICJUU1ItTFNUTSIpCn0gZWxzZSB7CiAgZml0X3ByZWRpY3Rfbm4gPC0gZnVuY3Rpb24oa2luZCwgdHIsIHZhLCB0ZSwgc2VlZCwgZXBvY2hzID0gTkEsIHBhdGllbmNlID0gTkEpIHsgICAjIGZhbGxiYWNrIHdpdGhvdXQga2VyYXMzCiAgICBzZXQuc2VlZChzZWVkKQogICAgWHRyIDwtIGNiaW5kKG1hdHJpeCh0ciRzZXEsIG5yb3codHIkeSkpLCB0ciRzaWRlKTsgWHRlIDwtIGNiaW5kKG1hdHJpeCh0ZSRzZXEsIG5yb3codGUkc2lkZSkpLCB0ZSRzaWRlKQogICAgZml0IDwtIG5uZXQ6Om5uZXQoWHRyLCB0ciR5LCBzaXplID0gMTIsIGxpbm91dCA9IFRSVUUsIGRlY2F5ID0gMWUtMywgbWF4aXQgPSAzMDAsIE1heE5XdHMgPSAxMDAwMCwgdHJhY2UgPSBGQUxTRSkKICAgIHByZWRpY3QoZml0LCBYdGUpCiAgfQogIE5OX01PREVMUyA8LSAiTUxQIgp9CmBgYAoKIyMgT25lIGNvbXBsZXRlIHJ1bjogZnJvbSBpbXB1dGF0aW9uIHRvIGZvcmVjYXN0cwoKYHJ1bl9mb3JlY2FzdCgpYCBhcHBsaWVzIHRoZSBwcm90b2NvbCBmb3Igb25lIHBvbGx1dGFudCBhbmQgb25lIGltcHV0YXRpb24gbWV0aG9kIGFuZCByZXR1cm5zIHRoZQpmb3JlY2FzdHMgb2YgYWxsIG1vZGVscyAodHJ1bmNhdGVkIGF0IHplcm8pIGFuZCB0aGUgUk1TRSBvZiBldmVyeSBzZWVkLgoKYGBge3IgcnVuLWZvcmVjYXN0fQpydW5fZm9yZWNhc3QgPC0gZnVuY3Rpb24oeF9yYXcsIGltcHV0ZV9mbiwgc2VlZHMgPSBTRUVEUykgewogIHlfaGlzdCAgPC0gaW1wdXRlX2ZuKHhfcmF3WzE6VEVTVF9TVEFSVF0pICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBydWxlIDEKICBoaXN0X2F0IDwtIGxhcHBseSh0ZXN0X29yaWdpbnMsIGZ1bmN0aW9uKHRhdSkgaW1wdXRlX2ZuKHhfcmF3WzE6dGF1XSkpICAgICAgICAjIHJ1bGUgMiAoY2F1c2FsKQogIHlfdHJ1ZSAgPC0gdChzYXBwbHkodGVzdF9vcmlnaW5zLCBmdW5jdGlvbih0YXUpIHhfcmF3Wyh0YXUgKyAxKToodGF1ICsgSCldKSkgICMgTkEgPSBub3Qgb2JzZXJ2ZWQgKHJ1bGUgMykKICB4X2hpc3RfcmF3IDwtIHhfcmF3WzE6VEVTVF9TVEFSVF0KICBvdXQgPC0gbGlzdCgpOyBzZWVkX3Jvd3MgPC0gbGlzdCgpCiAgb3V0JFNOQUlWRSA8LSB0KHNhcHBseShzZXFfYWxvbmcodGVzdF9vcmlnaW5zKSwgZnVuY3Rpb24oaykgewogICAgdGF1IDwtIHRlc3Rfb3JpZ2luc1trXTsgaGlzdF9hdFtba11dWyh0YXUgLSBTMiArIDEpOih0YXUgLSBTMiArIEgpXSB9KSkKICBqX2hpc3QgPC0gMDooVEVTVF9TVEFSVCAtIDEpOyBiZXRhIDwtIHRzcl9maXQoeV9oaXN0LCBqX2hpc3QpCiAgb3V0JFRTUiA8LSB0KHNhcHBseSh0ZXN0X29yaWdpbnMsIGZ1bmN0aW9uKHRhdSkgdHNyX3ByZWQoYmV0YSwgdGF1ICsgMDooSCAtIDEpKSkpCiAgaWYgKEhBU19MR0IpIG91dCRMaWdodEdCTSA8LSBmaXRfcHJlZGljdF9nYm0oeV9oaXN0LCB4X2hpc3RfcmF3LCBoaXN0X2F0KQogIG11IDwtIG1lYW4oeV9oaXN0WzE6VkFMX1NUQVJUXSk7IHNkdiA8LSBzZCh5X2hpc3RbMTpWQUxfU1RBUlRdKSAgICAgICAgICAgICAgICMgc3RhbmRhcmRpc2F0aW9uCiAgdHJzIDwtIGJ1aWxkX25uX3NhbXBsZXMoKHlfaGlzdCAtIG11KSAvIHNkdiwgKHlfaGlzdCAtIG11KSAvIHNkdiwgdHJfb3JpZykKICB2YXMgPC0gYnVpbGRfbm5fc2FtcGxlcygoeV9oaXN0IC0gbXUpIC8gc2R2LCAoeF9oaXN0X3JhdyAtIG11KSAvIHNkdiwgdmFfb3JpZykKICB0ZXMgPC0gc3RhY2tfdGVzdChsYXBwbHkoc2VxX2Fsb25nKHRlc3Rfb3JpZ2lucyksIGZ1bmN0aW9uKGspIG5uX2lucHV0cygoaGlzdF9hdFtba11dIC0gbXUpIC8gc2R2LCB0ZXN0X29yaWdpbnNba10pKSkKICByX2hpc3QgPC0geV9oaXN0IC0gdHNyX3ByZWQoYmV0YSwgal9oaXN0KTsgcl9zZCA8LSBzZChyX2hpc3RbMTpWQUxfU1RBUlRdKSAgICAjIFRTUiByZXNpZHVhbHMKICBmb3IgKGtpbmQgaW4gTk5fTU9ERUxTKSB7CiAgICBwcmVkcyA8LSBsYXBwbHkoc2VlZHMsIGZ1bmN0aW9uKHMpIHsKICAgICAgaWYgKGtpbmQgPT0gIlRTUi1MU1RNIikgewogICAgICAgIHRyciA8LSBidWlsZF9ubl9zYW1wbGVzKHJfaGlzdCAvIHJfc2QsIHJfaGlzdCAvIHJfc2QsIHRyX29yaWcpCiAgICAgICAgdmFyIDwtIGJ1aWxkX25uX3NhbXBsZXMocl9oaXN0IC8gcl9zZCwgKHhfaGlzdF9yYXcgLSB0c3JfcHJlZChiZXRhLCBqX2hpc3QpKSAvIHJfc2QsIHZhX29yaWcpCiAgICAgICAgdGVyIDwtIHN0YWNrX3Rlc3QobGFwcGx5KHNlcV9hbG9uZyh0ZXN0X29yaWdpbnMpLCBmdW5jdGlvbihrKSB7CiAgICAgICAgICB0YXUgPC0gdGVzdF9vcmlnaW5zW2tdOyBubl9pbnB1dHMoKGhpc3RfYXRbW2tdXSAtIHRzcl9wcmVkKGJldGEsIDA6KHRhdSAtIDEpKSkgLyByX3NkLCB0YXUpIH0pKQogICAgICAgIGZpdF9wcmVkaWN0X25uKGtpbmQsIHRyciwgdmFyLCB0ZXIsIHMpICogcl9zZCArIG91dCRUU1IKICAgICAgfSBlbHNlIGZpdF9wcmVkaWN0X25uKGtpbmQsIHRycywgdmFzLCB0ZXMsIHMpICogc2R2ICsgbXUKICAgIH0pCiAgICBmb3IgKGkgaW4gc2VxX2Fsb25nKHNlZWRzKSkgc2VlZF9yb3dzW1tsZW5ndGgoc2VlZF9yb3dzKSArIDFdXSA8LSBkYXRhLmZyYW1lKAogICAgICBtb2RlbCA9IGtpbmQsIHNlZWQgPSBzZWVkc1tpXSwgcm1zZSA9IHNxcnQobWVhbigocG1heChwcmVkc1tbaV1dLCAwKSAtIHlfdHJ1ZSleMiwgbmEucm0gPSBUUlVFKSkpCiAgICBvdXRbW2tpbmRdXSA8LSBSZWR1Y2UoYCtgLCBwcmVkcykgLyBsZW5ndGgocHJlZHMpCiAgfQogIGZjIDwtIGJpbmRfcm93cyhsYXBwbHkobmFtZXMob3V0KSwgZnVuY3Rpb24obW4pIGRhdGEuZnJhbWUobW9kZWwgPSBtbiwKICAgIG9yaWdpbiA9IHJlcCh0ZXN0X29yaWdpbnMsIGVhY2ggPSBIKSwgaCA9IHJlcCgxOkgsIE5fVEVTVF9EQVlTKSwKICAgIHkgPSBhcy52ZWN0b3IodCh5X3RydWUpKSwgeWhhdCA9IHBtYXgoYXMudmVjdG9yKHQob3V0W1ttbl1dKSksIDApKSkpCiAgbGlzdChmYyA9IGZjLCBzZWVkcyA9IGJpbmRfcm93cyhzZWVkX3Jvd3MpKQp9CmBgYAoKIyMgRGllYm9sZOKAk01hcmlhbm8gdGVzdCAoRXEuIDExKQoKV2l0aCAkZF90PWVfezF0fV4yLWVfezJ0fV4yJCAoU2Vhcy1BUiBoaXN0b3J5IG1pbnVzIGFsdGVybmF0aXZlIGhpc3RvcnkpLAoKJCQKXG1hdGhybXtETX09XHNxcnR7XGZyYWN7TisxLTJoK2goaC0xKS9OfXtOfX1cO1xmcmFje1xiYXIgZH17XHNxcnR7XHdpZGVoYXR7XG1hdGhybXtWYXJ9fShcYmFyIGQpfX0sXHFxdWFkClx3aWRlaGF0e1xtYXRocm17VmFyfX0oXGJhciBkKT1cZnJhYzFOXEJpZyhcaGF0XGdhbW1hXzArMlxzdW1fe2s9MX1ee2gtMX1caGF0XGdhbW1hX2tcQmlnKSwgXHRhZ3sxMX0KJCQKCndpdGggc2FtcGxlIGF1dG9jb3ZhcmlhbmNlcyAkXGhhdFxnYW1tYV9rJCBvZiAkZF90JCwgJGg9NDgkLCB0aGUgSGFydmV54oCTTGV5Ym91cm5l4oCTTmV3Ym9sZCBmYWN0b3IgYW5kIGEKJHRfe04tMX0kIHJlZmVyZW5jZSBkaXN0cmlidXRpb247IG5lZ2F0aXZlIHZhbHVlcyBmYXZvdXIgU2Vhcy1BUi4gSWYgdGhlIHZhcmlhbmNlIGVzdGltYXRlIGlzIG5vdCBwb3NpdGl2ZSwKJFxoYXRcZ2FtbWFfMC9OJCBpcyB1c2VkLiBUaGlzIGVxdWFscyBgZm9yZWNhc3Q6OmRtLnRlc3QodmFyZXN0aW1hdG9yID0gImFjZiIpYCAoY2hlY2tlZCBpbiBTZWN0aW9uIDguNCkuCgpgYGB7ciBkbS10ZXN0fQpkbV90ZXN0IDwtIGZ1bmN0aW9uKGUxLCBlMiwgaCA9IDQ4LCBwb3dlciA9IDIpIHsgICAjIEVxLiAoMTEpCiAgb2sgPC0gIShpcy5uYShlMSkgfCBpcy5uYShlMikpOyBkIDwtIGFicyhlMVtva10pXnBvd2VyIC0gYWJzKGUyW29rXSlecG93ZXIKICBuIDwtIGxlbmd0aChkKTsgZGMgPC0gZCAtIG1lYW4oZCkKICBnYW0gPC0gc2FwcGx5KDA6KGggLSAxKSwgZnVuY3Rpb24oaykgc3VtKGRjWyhrICsgMSk6bl0gKiBkY1sxOihuIC0gayldKSAvIG4pCiAgdiA8LSAoZ2FtWzFdICsgMiAqIHN1bShnYW1bLTFdKSkgLyBuCiAgaWYgKHYgPD0gMCkgdiA8LSBnYW1bMV0gLyBuCiAgc3RhdCA8LSBtZWFuKGQpIC8gc3FydCh2KSAqIHNxcnQoKG4gKyAxIC0gMiAqIGggKyBoICogKGggLSAxKSAvIG4pIC8gbikKICBjKHN0YXQgPSBzdGF0LCBwID0gMiAqIHB0KC1hYnMoc3RhdCksIGRmID0gbiAtIDEpKQp9CmBgYAoKIyBFeHBlcmltZW50IDI6IGltcGFjdCBvZiBpbXB1dGF0aW9uIG9uIGZvcmVjYXN0aW5nCgojIyBSdW5uaW5nIHRoZSBleHBlcmltZW50CgpGaXZlIGltcHV0YXRpb25zIGFyZSBjb21wYXJlZCBmb3Igc2V2ZW4gbW9kZWxzIGF0IFNVRiAxOiBNRURJQU4gKGEgY29tbW9uIHByYWN0aWNhbCBkZWZhdWx0KSwgTEkKKHRoZSBnZW5lcmljIGRlZmF1bHQpLCBTRUFERUMgKHRoZSBzdHJvbmdlc3QgY29tcGV0aXRvciBpbiBFeHBlcmltZW50IDEpLCBLQUxNQU4gKHN0YXRlLXNwYWNlCnNtb290aGluZykgYW5kIFNlYXMtQVIgKGJhbmR3aWR0aHMgb2YgU1VGIDEgZnJvbSBFeHBlcmltZW50IDApLiBGb3JlY2FzdHMgYXJlIHN0b3JlZCBwZXIgcG9sbHV0YW50IGFuZAppbXB1dGF0aW9uLCBzbyBhbiBpbnRlcnJ1cHRlZCBydW4gcmVzdW1lcyB3aGVyZSBpdCBzdG9wcGVkLgoKYGBge3IgZXhwMi1ydW59CklNUF9GQyA8LSBjKCJNRURJQU4iLCAiTEkiLCAiU0VBREVDIiwgIktBTE1BTiIsICJTZWFzLUFSIikKTU9ERUxTIDwtIGMoIlNOQUlWRSIsICJUU1IiLCAiTGlnaHRHQk0iLCAiTUxQIiwgIkxTVE0iLCAiVENOIiwgIlRTUi1MU1RNIikKTU9ERUxfVEVYIDwtIGMoU05BSVZFID0gIlNlYXNvbmFsIG5hXFxcIml2ZSIsIFRTUiA9ICJUU1IiLCBMaWdodEdCTSA9ICJMaWdodEdCTSIsIE1MUCA9ICJNTFAiLCBMU1RNID0gIkxTVE0iLAogICAgICAgICAgICAgICBUQ04gPSAiVENOIiwgYFRTUi1MU1RNYCA9ICJUU1ItLUxTVE0iKQppbXB1dGVyX2ZvciA8LSBmdW5jdGlvbihpbXApIHN3aXRjaChpbXAsIE1FRElBTiA9IE1FVEhPRFMkTUVESUFOLCBMSSA9IE1FVEhPRFMkTEksIFNFQURFQyA9IE1FVEhPRFMkU0VBREVDLAogIEtBTE1BTiA9IE1FVEhPRFMkS0FMTUFOLCBgU2Vhcy1BUmAgPSBmdW5jdGlvbih2KSBpbXB1dGVfc2Vhc2FyKHYsIEJXW1siU1VGIDEiXV0kYndfZGF5LCBCV1tbIlNVRiAxIl1dJGJ3X3dlZWspKQpkaXIyIDwtIG91dF9maWxlKGlmIChRVUlDSykgImV4cDJfcXVpY2siIGVsc2UgImV4cDIiKTsgZGlyLmNyZWF0ZShkaXIyLCBzaG93V2FybmluZ3MgPSBGQUxTRSkKcG9sc19mYyA8LSBpZiAoUVVJQ0spICJOTzIiIGVsc2UgUE9MUzsgaW1wc19mYyA8LSBpZiAoUVVJQ0spIGMoIk1FRElBTiIsICJTZWFzLUFSIikgZWxzZSBJTVBfRkMKaWYgKCEoSEFTX0tFUkFTICYmIEhBU19MR0IpKSBjYXQoIk5vdGU6IHRoZSBmdWxsIG1vZGVsIHNldCBuZWVkcyBsaWdodGdibSBhbmQga2VyYXMzOyBtaXNzaW5nIG1vZGVscyBhcmUgc2tpcHBlZC5cbiIpCmZvciAocCBpbiBwb2xzX2ZjKSBmb3IgKGltcCBpbiBpbXBzX2ZjKSB7CiAgZmYgPC0gZmlsZS5wYXRoKGRpcjIsIHNwcmludGYoImZjXyVzXyVzLmNzdiIsIHAsIGltcCkpCiAgaWYgKGlzVFJVRShQJHVzZV9jYWNoZSkgJiYgZmlsZS5leGlzdHMoZmYpKSBuZXh0CiAgdDAgPC0gU3lzLnRpbWUoKQogIHJlcyA8LSBydW5fZm9yZWNhc3QoZ2V0X3NlcmllcygiU1VGIDEiLCBwKSwgaW1wdXRlcl9mb3IoaW1wKSwgc2VlZHMgPSBpZiAoUVVJQ0spIFNFRURTWzFdIGVsc2UgU0VFRFMpCiAgd3JpdGUuY3N2KHJlcyRmYyB8PiBtdXRhdGUocG9sbHV0YW50ID0gcCwgaW1wdXRhdGlvbiA9IGltcCksIGZmLCByb3cubmFtZXMgPSBGQUxTRSkKICB3cml0ZS5jc3YocmVzJHNlZWRzIHw+IG11dGF0ZShwb2xsdXRhbnQgPSBwLCBpbXB1dGF0aW9uID0gaW1wKSwgc3ViKCJmY18iLCAic2VlZHNfIiwgZmYpLCByb3cubmFtZXMgPSBGQUxTRSkKICBjYXQocCwgaW1wLCAiZmluaXNoZWQgaW4iLCByb3VuZChhcy5udW1lcmljKFN5cy50aW1lKCkgLSB0MCwgdW5pdHMgPSAic2VjcyIpKSwgInNcbiIpCn0KZmNfYWxsIDwtIGJpbmRfcm93cyhsYXBwbHkobGlzdC5maWxlcyhkaXIyLCBwYXR0ZXJuID0gIl5mY18uKlxcLmNzdiQiLCBmdWxsLm5hbWVzID0gVFJVRSksIHJlYWQuY3N2KSkgfD4KICBzZWxlY3QocG9sbHV0YW50LCBpbXB1dGF0aW9uLCBtb2RlbCwgb3JpZ2luLCBoLCB5LCB5aGF0KQpzZWVkX2ZpbGVzIDwtIGxpc3QuZmlsZXMoZGlyMiwgcGF0dGVybiA9ICJec2VlZHNfLipcXC5jc3YkIiwgZnVsbC5uYW1lcyA9IFRSVUUpCnNlZWRzX2FsbCA8LSBpZiAobGVuZ3RoKHNlZWRfZmlsZXMpKSBiaW5kX3Jvd3MobGFwcGx5KHNlZWRfZmlsZXMsIHJlYWQuY3N2KSkgZWxzZSBOVUxMCmNhdCgiRXhwZXJpbWVudCAyOiIsIG5yb3coZmNfYWxsKSwgImZvcmVjYXN0IHJvd3MgfCBtb2RlbHM6IiwgcGFzdGUoaW50ZXJzZWN0KE1PREVMUywgdW5pcXVlKGZjX2FsbCRtb2RlbCkpLCBjb2xsYXBzZSA9ICIsICIpLCAiXG4iKQpgYGAKCiMjIFJNU0UgYW5kIE1BRSBvbiB0aGUgdGVzdCBkYXlzCgokXG1hdGhybXtSTVNFfT1cYmlnKFxmcmFjezF9e3xcbWF0aGNhbCBUfH1cc3VtX3soXHRhdSxoKVxpblxtYXRoY2FsIFR9KFxoYXQgeV97XHRhdStofS15X3tcdGF1K2h9KV4yXGJpZyleezEvMn0kCm92ZXIgdGhlIHBhaXJzICRcbWF0aGNhbCBUJCB3aG9zZSB0YXJnZXQgaXMgb2JzZXJ2ZWQuIEJvbGQgbWFya3MgdGhlIGJlc3QgaW1wdXRhdGlvbiBmb3IgZWFjaCBtb2RlbC4KCmBgYHtyIHRhYmxlN30Kc2MgPC0gZmNfYWxsIHw+IGZpbHRlcighaXMubmEoeSkpIHw+IGdyb3VwX2J5KHBvbGx1dGFudCwgbW9kZWwsIGltcHV0YXRpb24pIHw+CiAgc3VtbWFyaXNlKFJNU0UgPSBzcXJ0KG1lYW4oKHloYXQgLSB5KV4yKSksIE1BRSA9IG1lYW4oYWJzKHloYXQgLSB5KSksIG4gPSBuKCksIC5ncm91cHMgPSAiZHJvcCIpCndyaXRlLmNzdihzYywgb3V0X2ZpbGUoImV4cDJfc2NvcmVzLmNzdiIpLCByb3cubmFtZXMgPSBGQUxTRSkKd2lkZTcgPC0gZnVuY3Rpb24obWV0cmljKSBzYyB8PiBzZWxlY3QocG9sbHV0YW50LCBtb2RlbCwgaW1wdXRhdGlvbiwgdiA9IGFsbF9vZihtZXRyaWMpKSB8PgogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSBpbXB1dGF0aW9uLCB2YWx1ZXNfZnJvbSA9IHYpIHw+CiAgbXV0YXRlKHBvbGx1dGFudCA9IGZhY3Rvcihwb2xsdXRhbnQsIGxldmVscyA9IFBPTFMpLCBtb2RlbCA9IGZhY3Rvcihtb2RlbCwgbGV2ZWxzID0gTU9ERUxTKSkgfD4gYXJyYW5nZShwb2xsdXRhbnQsIG1vZGVsKQptZDcgPC0gZnVuY3Rpb24obWV0cmljKSB7CiAgdyA8LSB3aWRlNyhtZXRyaWMpOyBpYyA8LSBpbnRlcnNlY3QoSU1QX0ZDLCBuYW1lcyh3KSk7IE0gPC0gYXMubWF0cml4KHdbLCBpY10pCiAgTWYgPC0gZG8uY2FsbChyYmluZCwgbGFwcGx5KHNlcV9sZW4obnJvdyhNKSksIGZ1bmN0aW9uKGkpIGZtdF9tZChNW2ksIF0sIGlmICh3JHBvbGx1dGFudFtpXSA9PSAiQ08iKSAzIGVsc2UgMikpKQogIGNvbG5hbWVzKE1mKSA8LSBpYzsgZGF0YS5mcmFtZShQb2xsdXRhbnQgPSBhcy5jaGFyYWN0ZXIodyRwb2xsdXRhbnQpLCBNb2RlbCA9IGFzLmNoYXJhY3Rlcih3JG1vZGVsKSwgTWYsIGNoZWNrLm5hbWVzID0gRkFMU0UpCn0KdzcgPC0gd2lkZTcoIlJNU0UiKTsgaWM3IDwtIGludGVyc2VjdChJTVBfRkMsIG5hbWVzKHc3KSk7IHJvd3M3IDwtIGMoKQpmb3IgKHAgaW4gaW50ZXJzZWN0KFBPTFMsIGFzLmNoYXJhY3Rlcih3NyRwb2xsdXRhbnQpKSkgewogIHcgPC0gdzcgfD4gZmlsdGVyKHBvbGx1dGFudCA9PSBwKTsgTSA8LSBhcy5tYXRyaXgod1ssIGljN10pOyBiZXN0X2FsbCA8LSBtaW4oTSwgbmEucm0gPSBUUlVFKTsgZGcgPC0gaWYgKHAgPT0gIkNPIikgMyBlbHNlIDIKICBmb3IgKGkgaW4gc2VxX2xlbihucm93KHcpKSkgewogICAgY2VsbHMgPC0gc2FwcGx5KHNlcV9hbG9uZyhpYzcpLCBmdW5jdGlvbihqKSB7IHYgPC0gTVtpLCBqXTsgcyA8LSBmbXRfbnVtKHYsIGRnKQogICAgICBpZiAoYWJzKHYgLSBtaW4oTVtpLCBdLCBuYS5ybSA9IFRSVUUpKSA8IDFlLTEyKSBzIDwtIHBhc3RlMCgiXFx0ZXh0YmZ7IiwgcywgIn0iKQogICAgICBpZiAoYWJzKHYgLSBiZXN0X2FsbCkgPCAxZS0xMikgcyA8LSBwYXN0ZTAoIlxcdW5kZXJsaW5leyIsIHMsICJ9Iik7IHMgfSkKICAgIGxlYWQgPC0gaWYgKGkgPT0gMSkgc3ByaW50ZigiXFxtdWx0aXJvd3slZH17Kn17JXN9IiwgbnJvdyh3KSwgUE9MX1RFWFtbcF1dKSBlbHNlICIiCiAgICByb3dzNyA8LSBjKHJvd3M3LCBwYXN0ZTAocGFzdGUoYyhsZWFkLCBNT0RFTF9URVhbW2FzLmNoYXJhY3Rlcih3JG1vZGVsW2ldKV1dLCBjZWxscyksIGNvbGxhcHNlID0gIiAmICIpLCAiIFxcXFwiKSkKICB9CiAgcm93czcgPC0gYyhyb3dzNywgIlxcbWlkcnVsZSIpCn0Kd3JpdGVfdGV4KGMoIlxcYmVnaW57dGFibGV9WyF0XSIsICJcXGNlbnRlcmluZyIsCiAgc3ByaW50ZigiXFxjYXB0aW9ue0RheS1haGVhZCBmb3JlY2FzdCBSTVNFIG9uIHRoZSAyMCB0ZXN0IGRheXMgKDEtLTIwIERlYyAyMDE4LCBvYnNlcnZlZCB2YWx1ZXMgb25seSkgZm9yIG1vZGVscyB0cmFpbmVkIG9uIGhpc3RvcmllcyBpbXB1dGVkIGJ5ICVkIG1ldGhvZHMuIEJvbGQ6IGJlc3QgaW1wdXRhdGlvbiBmb3IgZWFjaCBtb2RlbDsgdW5kZXJsaW5lZDogYmVzdCBtb2RlbC0taW1wdXRhdGlvbiBwYWlyIGZvciBlYWNoIHBvbGx1dGFudC59IiwgbGVuZ3RoKGljNykpLAogICJcXGxhYmVse3RhYjpmb3JlY2FzdH0iLCAiXFxzZXRsZW5ndGh7XFx0YWJjb2xzZXB9ezIuNnB0fSIsIHNwcmludGYoIlxcYmVnaW57dGFidWxhcn17bGwlc30iLCBzdHJyZXAoInIiLCBsZW5ndGgoaWM3KSkpLCAiXFx0b3BydWxlIiwKICBwYXN0ZTAoIiAmIE1vZGVsICYgIiwgcGFzdGUoaWM3LCBjb2xsYXBzZSA9ICIgJiAiKSwgIlxcXFwiKSwgIlxcbWlkcnVsZSIsIGhlYWQocm93czcsIC0xKSwKICAiXFxib3R0b21ydWxlIiwgIlxcZW5ke3RhYnVsYXJ9IiwgIlxcZW5ke3RhYmxlfSIpLCAidGFiX2ZvcmVjYXN0IikKa25pdHI6OmthYmxlKG1kNygiUk1TRSIpLCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9IGNhcCgiVGFibGUgOCIsICJEYXktYWhlYWQgUk1TRSBvbiB0aGUgMjAgdGVzdCBkYXlzIChvYnNlcnZlZCB2YWx1ZXMpIikpCmtuaXRyOjprYWJsZShtZDcoIk1BRSIpLCBhbGlnbiA9ICJyIiwgY2FwdGlvbiA9ICJEYXktYWhlYWQgTUFFIG9uIHRoZSAyMCB0ZXN0IGRheXMgKG9ic2VydmVkIHZhbHVlcykiKQpiZXN0cCA8LSBzYyB8PiBncm91cF9ieShwb2xsdXRhbnQpIHw+IHNsaWNlX21pbihSTVNFLCBuID0gMSwgd2l0aF90aWVzID0gRkFMU0UpIHw+IHVuZ3JvdXAoKSB8PgogIG11dGF0ZShwb2xsdXRhbnQgPSBmYWN0b3IocG9sbHV0YW50LCBsZXZlbHMgPSBQT0xTKSkgfD4gYXJyYW5nZShwb2xsdXRhbnQpCmZvciAoaSBpbiBzZXFfbGVuKG5yb3coYmVzdHApKSkgeyBwIDwtIGFzLmNoYXJhY3RlcihiZXN0cCRwb2xsdXRhbnRbaV0pCiAga2V5KHBhc3RlMCgiYmVzdHBhaXJfIiwgcCksIHBhc3RlKGJlc3RwJG1vZGVsW2ldLCBiZXN0cCRpbXB1dGF0aW9uW2ldKSk7IGtleShwYXN0ZTAoImJlc3RwYWlyX3Jtc2VfIiwgcCksIGJlc3RwJFJNU0VbaV0pIH0Ka25pdHI6OmthYmxlKGJlc3RwLCBkaWdpdHMgPSAzLCBjYXB0aW9uID0gIkJlc3QgbW9kZWwtaW1wdXRhdGlvbiBwYWlyIGZvciBlYWNoIHBvbGx1dGFudCIpCmBgYAoKIyMgQ2hhbmdlIHJlbGF0aXZlIHRvIG1lZGlhbiBpbXB1dGF0aW9uCgokXERlbHRhX3twLG0saX09MTAwXGJpZyhcbWF0aHJte1JNU0V9X3twLG0saX0vXG1hdGhybXtSTVNFfV97cCxtLFxtYXRocm17TUVESUFOfX0tMVxiaWcpJCBmb3IgcG9sbHV0YW50ICRwJCwKbW9kZWwgJG0kIGFuZCBpbXB1dGF0aW9uICRpJDsgYmx1ZSBjZWxscyBtZWFuIGEgbG93ZXIgZXJyb3IgdGhhbiB3aXRoIGEgbWVkaWFuLWltcHV0ZWQgaGlzdG9yeS4KCmBgYHtyIGZpZzksIGZpZy53aWR0aD03LjE2LCBmaWcuaGVpZ2h0PTIuM30KQUJCUiA8LSBjKE1FRElBTiA9ICJNRUQiLCBMSSA9ICJMSSIsIFNFQURFQyA9ICJTRUEiLCBLQUxNQU4gPSAiS0FMIiwgYFNlYXMtQVJgID0gIlMtQVIiKQpkOSA8LSBzYyB8PiBncm91cF9ieShwb2xsdXRhbnQsIG1vZGVsKSB8PiBtdXRhdGUoY2hhbmdlID0gMTAwICogKFJNU0UgLyBSTVNFW2ltcHV0YXRpb24gPT0gIk1FRElBTiJdIC0gMSkpIHw+IHVuZ3JvdXAoKSB8PgogIG11dGF0ZShwb2xsdXRhbnQgPSBmYWN0b3IocG9sbHV0YW50LCBsZXZlbHMgPSBQT0xTLCBsYWJlbHMgPSBjKCJQTVsxMF0iLCAiTk9bMl0iLCAiU09bMl0iLCAiQ08iLCAiT1szXSIpKSwKICAgICAgICAgbW9kZWwgPSBmYWN0b3IobW9kZWwsIGxldmVscyA9IHJldihNT0RFTFMpLCBsYWJlbHMgPSByZXYoYygiU2Vhc29uYWwgbmFpdmUiLCBNT0RFTFNbLTFdKSkpLAogICAgICAgICBpbXB1dGF0aW9uID0gZmFjdG9yKEFCQlJbaW1wdXRhdGlvbl0sIGxldmVscyA9IEFCQlIpKQpwOSA8LSBnZ3Bsb3QoZDksIGFlcyhpbXB1dGF0aW9uLCBtb2RlbCwgZmlsbCA9IHBtYXgocG1pbihjaGFuZ2UsIDMwKSwgLTMwKSkpICsgZ2VvbV90aWxlKGNvbG91ciA9ICJ3aGl0ZSIpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsID0gc3ByaW50ZigiJSsuMWYiLCBjaGFuZ2UpKSwgc2l6ZSA9IDEuOSwgZmFtaWx5ID0gRk9OVF9URVhUKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudDIobG93ID0gIiMyMTY2YWMiLCBoaWdoID0gIiNiMjE4MmIiLCBsaW1pdHMgPSBjKC0zMCwgMzApLCBuYW1lID0gIlJNU0UgY2hhbmdlXG52cyBNRURJQU4gKCUpIikgKwogIGZhY2V0X3dyYXAofnBvbGx1dGFudCwgbnJvdyA9IDEsIGxhYmVsbGVyID0gbGFiZWxfcGFyc2VkKSArIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMKSArCiAgdGhlbWUocGFuZWwuZ3JpZCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDYpLCBsZWdlbmQua2V5LmhlaWdodCA9IHVuaXQoMC41LCAiY20iKSkKcHJpbnQocDkpOyBzYXZlX2ZpZyhwOSwgImZpZzA5X2ZvcmVjYXN0X2hlYXRtYXAiLCA3LjE2LCAyLjMpCmBgYAoKIyMgRGllYm9sZOKAk01hcmlhbm8gdGVzdHMKCkVhY2ggY2VsbCBjb3VudHMgdGhlIHNldmVuIG1vZGVscyBhcyBXL1QvTDogU2Vhcy1BUiBzaWduaWZpY2FudGx5IGJldHRlciAvIG5vIHNpZ25pZmljYW50IGRpZmZlcmVuY2UgLwpzaWduaWZpY2FudGx5IHdvcnNlIGF0IHRoZSA1JSBsZXZlbC4gVGhlIGZpcnN0IGxpbmUgY2hlY2tzIGBkbV90ZXN0KClgIGFnYWluc3QgYGZvcmVjYXN0OjpkbS50ZXN0KClgLgoKYGBge3IgdGFibGU4fQpleF9yZWYgPC0gZmNfYWxsIHw+IGZpbHRlcihwb2xsdXRhbnQgPT0gIk5PMiIsIG1vZGVsID09ICJNTFAiLCBpbXB1dGF0aW9uID09ICJTZWFzLUFSIikgfD4gYXJyYW5nZShvcmlnaW4sIGgpCmV4X290aCA8LSBmY19hbGwgfD4gZmlsdGVyKHBvbGx1dGFudCA9PSAiTk8yIiwgbW9kZWwgPT0gIk1MUCIsIGltcHV0YXRpb24gPT0gIk1FRElBTiIpIHw+IGFycmFuZ2Uob3JpZ2luLCBoKQppZiAobnJvdyhleF9yZWYpICYmIG5yb3coZXhfb3RoKSkgewogIG9rIDwtICFpcy5uYShleF9yZWYkeSk7IGUxIDwtIChleF9yZWYkeWhhdCAtIGV4X3JlZiR5KVtva107IGUyIDwtIChleF9vdGgkeWhhdCAtIGV4X290aCR5KVtva10KICBtaW5lIDwtIGRtX3Rlc3QoZTEsIGUyLCBoID0gNDgpOyBmZG0gPC0gc3VwcHJlc3NXYXJuaW5ncyhmb3JlY2FzdDo6ZG0udGVzdChlMSwgZTIsIGggPSA0OCwgcG93ZXIgPSAyLCB2YXJlc3RpbWF0b3IgPSAiYWNmIikpCiAgY2F0KHNwcmludGYoIkNoZWNrIChOTzIsIE1MUCwgU2Vhcy1BUiB2cyBNRURJQU4pOiBkbV90ZXN0IERNID0gJS40ZiAocCA9ICUuM2cpIHwgZm9yZWNhc3Q6OmRtLnRlc3QgRE0gPSAlLjRmIChwID0gJS4zZylcbiIsCiAgICAgICAgICAgICAgbWluZVtbInN0YXQiXV0sIG1pbmVbWyJwIl1dLCBmZG0kc3RhdGlzdGljLCBmZG0kcC52YWx1ZSkpCn0KZG0gPC0gYmluZF9yb3dzKGxhcHBseShzcGxpdChmY19hbGwsIGxpc3QoZmNfYWxsJHBvbGx1dGFudCwgZmNfYWxsJG1vZGVsKSwgZHJvcCA9IFRSVUUpLCBmdW5jdGlvbihnKSB7CiAgcmVmIDwtIGcgfD4gZmlsdGVyKGltcHV0YXRpb24gPT0gIlNlYXMtQVIiKSB8PiBhcnJhbmdlKG9yaWdpbiwgaCkKICBpZiAoIW5yb3cocmVmKSkgcmV0dXJuKE5VTEwpCiAgYmluZF9yb3dzKGxhcHBseShzZXRkaWZmKHVuaXF1ZShnJGltcHV0YXRpb24pLCAiU2Vhcy1BUiIpLCBmdW5jdGlvbihpbXApIHsKICAgIG90aCA8LSBnIHw+IGZpbHRlcihpbXB1dGF0aW9uID09IGltcCkgfD4gYXJyYW5nZShvcmlnaW4sIGgpCiAgICByIDwtIGRtX3Rlc3QocmVmJHloYXQgLSByZWYkeSwgb3RoJHloYXQgLSBvdGgkeSwgaCA9IDQ4KQogICAgZGF0YS5mcmFtZShwb2xsdXRhbnQgPSByZWYkcG9sbHV0YW50WzFdLCBtb2RlbCA9IHJlZiRtb2RlbFsxXSwgY29tcGV0aXRvciA9IGltcCwgRE0gPSByW1sic3RhdCJdXSwgcCA9IHJbWyJwIl1dKQogIH0pKQp9KSkKd3JpdGUuY3N2KGRtLCBvdXRfZmlsZSgiZXhwMl9kbS5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCnd0bF9mIDwtIGZ1bmN0aW9uKGQpIGQgfD4gc3VtbWFyaXNlKFcgPSBzdW0ocCA8IC4wNSAmIERNIDwgMCksIEwgPSBzdW0ocCA8IC4wNSAmIERNID4gMCksIG4gPSBuKCksIC5ncm91cHMgPSAiZHJvcCIpIHw+CiAgbXV0YXRlKFQgPSBuIC0gVyAtIEwsIGNlbGwgPSBzcHJpbnRmKCIlZC8lZC8lZCIsIFcsIFQsIEwpKQp3dGwgPC0gYmluZF9yb3dzKGRtIHw+IGdyb3VwX2J5KHBvbGx1dGFudCwgY29tcGV0aXRvcikgfD4gd3RsX2YoKSwgZG0gfD4gZ3JvdXBfYnkoY29tcGV0aXRvcikgfD4gd3RsX2YoKSB8PiBtdXRhdGUocG9sbHV0YW50ID0gIlRvdGFsIikpCndyaXRlLmNzdih3dGwsIG91dF9maWxlKCJleHAyX3d0bC5jc3YiKSwgcm93Lm5hbWVzID0gRkFMU0UpCmZvciAoaSBpbiB3aGljaCh3dGwkcG9sbHV0YW50ID09ICJUb3RhbCIpKSBrZXkocGFzdGUwKCJkbV90b3RhbF8iLCB3dGwkY29tcGV0aXRvcltpXSksIHd0bCRjZWxsW2ldKQpjb21wX29yZGVyIDwtIGludGVyc2VjdChzZXRkaWZmKElNUF9GQywgIlNlYXMtQVIiKSwgdW5pcXVlKGRtJGNvbXBldGl0b3IpKQp0OCA8LSB3dGwgfD4gc2VsZWN0KHBvbGx1dGFudCwgY29tcGV0aXRvciwgY2VsbCkgfD4gcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IGNvbXBldGl0b3IsIHZhbHVlc19mcm9tID0gY2VsbCkgfD4KICBtdXRhdGUocG9sbHV0YW50ID0gZmFjdG9yKHBvbGx1dGFudCwgbGV2ZWxzID0gYyhQT0xTLCAiVG90YWwiKSkpIHw+IGFycmFuZ2UocG9sbHV0YW50KQpyb3dzOCA8LSBzYXBwbHkoc2VxX2xlbihucm93KHQ4KSksIGZ1bmN0aW9uKGkpIHsgcCA8LSBhcy5jaGFyYWN0ZXIodDgkcG9sbHV0YW50W2ldKQogIHBhc3RlMChwYXN0ZShjKGlmIChwID09ICJUb3RhbCIpICJcXHRleHRpdHtUb3RhbH0iIGVsc2UgUE9MX1RFWFtbcF1dLCB1bmxpc3QodDhbaSwgY29tcF9vcmRlcl0pKSwgY29sbGFwc2UgPSAiICYgIiksICIgXFxcXCIpIH0pCndyaXRlX3RleChjKCJcXGJlZ2lue3RhYmxlfVshdF0iLCAiXFxjZW50ZXJpbmciLAogICJcXGNhcHRpb257RGllYm9sZC0tTWFyaWFubyB0ZXN0cyAoc3F1YXJlZCBsb3NzLCAkaD00OCQsIEhhcnZleS0tTGV5Ym91cm5lLS1OZXdib2xkIGNvcnJlY3Rpb24pIGNvbXBhcmluZyBmb3JlY2FzdHMgb2J0YWluZWQgd2l0aCBTZWFzLUFSLWltcHV0ZWQgaGlzdG9yaWVzIGFnYWluc3QgZWFjaCBhbHRlcm5hdGl2ZSBpbXB1dGF0aW9uLiBFYWNoIGNlbGwgY291bnRzIHRoZSBzZXZlbiBmb3JlY2FzdGluZyBtb2RlbHMgYXMgVy9UL0w6IFNlYXMtQVIgc2lnbmlmaWNhbnRseSBiZXR0ZXIgLyBubyBzaWduaWZpY2FudCBkaWZmZXJlbmNlIC8gc2lnbmlmaWNhbnRseSB3b3JzZSBhdCB0aGUgNVxcJSBsZXZlbC59IiwKICAiXFxsYWJlbHt0YWI6ZG19IiwgIlxcc2V0bGVuZ3Roe1xcdGFiY29sc2VwfXs1cHR9Iiwgc3ByaW50ZigiXFxiZWdpbnt0YWJ1bGFyfXtsJXN9Iiwgc3RycmVwKCJyIiwgbGVuZ3RoKGNvbXBfb3JkZXIpKSksICJcXHRvcHJ1bGUiLAogIHBhc3RlMCgiUG9sbHV0YW50ICYgIiwgcGFzdGUocGFzdGUoInZzIiwgY29tcF9vcmRlciksIGNvbGxhcHNlID0gIiAmICIpLCAiXFxcXCIpLCAiXFxtaWRydWxlIiwKICBoZWFkKHJvd3M4LCAtMSksICJcXG1pZHJ1bGUiLCB0YWlsKHJvd3M4LCAxKSwgIlxcYm90dG9tcnVsZSIsICJcXGVuZHt0YWJ1bGFyfSIsICJcXGVuZHt0YWJsZX0iKSwgInRhYl9kbSIpCmtuaXRyOjprYWJsZSh0OFssIGMoInBvbGx1dGFudCIsIGNvbXBfb3JkZXIpXSwgY29sLm5hbWVzID0gYygiUG9sbHV0YW50IiwgcGFzdGUoInZzIiwgY29tcF9vcmRlcikpLAogICAgICAgICAgICAgY2FwdGlvbiA9IGNhcCgiVGFibGUgOSIsICJEaWVib2xkLU1hcmlhbm8gdGVzdHM6IFNlYXMtQVIgdnMgZWFjaCBhbHRlcm5hdGl2ZSAoVy9UL0wgb3ZlciB0aGUgc2V2ZW4gbW9kZWxzKSIpKQpgYGAKCiMjIFN1bW1hcnkgb3ZlciBwb2xsdXRhbnRzIGFuZCBtb2RlbHMKCmBgYHtyIGV4cDItc3VtbWFyeX0KTTcgPC0gYXMubWF0cml4KHc3WywgaWM3XSk7IGJlc3RfaW1wIDwtIGljN1thcHBseShNNywgMSwgd2hpY2gubWluKV0Kcms3IDwtIGNvbE1lYW5zKHQoYXBwbHkoTTcsIDEsIHJhbmspKSkKY2hnIDwtIGNvbE1lYW5zKDEwMCAqIChNNyAvIE03WywgIk1FRElBTiJdIC0gMSkpCnN1bW0gPC0gZGF0YS5mcmFtZShpbXB1dGF0aW9uID0gaWM3LCBiZXN0X2ZvciA9IGFzLmludGVnZXIodGFibGUoZmFjdG9yKGJlc3RfaW1wLCBsZXZlbHMgPSBpYzcpKSksIGF2Z19yYW5rID0gcms3LCBtZWFuX2NoYW5nZV92c19tZWRpYW4gPSBjaGcpCndyaXRlLmNzdihzdW1tLCBvdXRfZmlsZSgiZXhwMl9zdW1tYXJ5LmNzdiIpLCByb3cubmFtZXMgPSBGQUxTRSkKZm9yIChpIGluIHNlcV9sZW4obnJvdyhzdW1tKSkpIHsga2V5KHBhc3RlMCgiZmNfYmVzdF8iLCBzdW1tJGltcHV0YXRpb25baV0pLCBzdW1tJGJlc3RfZm9yW2ldKTsga2V5KHBhc3RlMCgiZmNfcmFua18iLCBzdW1tJGltcHV0YXRpb25baV0pLCBzdW1tJGF2Z19yYW5rW2ldKQogIGtleShwYXN0ZTAoImZjX2NoZ18iLCBzdW1tJGltcHV0YXRpb25baV0pLCBzdW1tJG1lYW5fY2hhbmdlX3ZzX21lZGlhbltpXSkgfQpyZWw3IDwtIHc3OyBmb3IgKGNjIGluIGljNykgcmVsN1tbY2NdXSA8LSAxMDAgKiAodzdbW2NjXV0gLyB3NyRNRURJQU4gLSAxKQpjaGdfcCA8LSByZWw3IHw+IGdyb3VwX2J5KHBvbGx1dGFudCkgfD4gc3VtbWFyaXNlKGFjcm9zcyhhbGxfb2YoaWM3KSwgbWVhbiksIC5ncm91cHMgPSAiZHJvcCIpCmJlc3RjIDwtIHc3IHw+IG11dGF0ZShiZXN0ID0gYmVzdF9pbXApIHw+IGNvdW50KHBvbGx1dGFudCwgYmVzdCkgfD4gcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IGJlc3QsIHZhbHVlc19mcm9tID0gbiwgdmFsdWVzX2ZpbGwgPSAwKQp3cml0ZS5jc3YoY2hnX3AsIG91dF9maWxlKCJleHAyX2NoYW5nZV9ieV9wb2xsdXRhbnQuY3N2IiksIHJvdy5uYW1lcyA9IEZBTFNFKTsgd3JpdGUuY3N2KGJlc3RjLCBvdXRfZmlsZSgiZXhwMl9iZXN0Y291bnRfYnlfcG9sbHV0YW50LmNzdiIpLCByb3cubmFtZXMgPSBGQUxTRSkKZm9yIChpIGluIHNlcV9sZW4obnJvdyhjaGdfcCkpKSBmb3IgKGNjIGluIGljNykga2V5KHBhc3RlMCgiY2hnXyIsIGNoZ19wJHBvbGx1dGFudFtpXSwgIl8iLCBjYyksIGNoZ19wW1tjY11dW2ldKQprbml0cjo6a2FibGUoc3VtbSwgZGlnaXRzID0gMiwgY29sLm5hbWVzID0gYygiSW1wdXRhdGlvbiIsICJCZXN0IGZvciAocGFpcnMpIiwgIkF2ZXJhZ2UgcmFuayIsICJNZWFuIFJNU0UgY2hhbmdlIHZzIE1FRElBTiAoJSkiKSwKICAgICAgICAgICAgIGNhcHRpb24gPSBzcHJpbnRmKCJTdW1tYXJ5IG92ZXIgdGhlICVkIHBvbGx1dGFudC1tb2RlbCBwYWlycyIsIG5yb3coTTcpKSkKa25pdHI6OmthYmxlKGNoZ19wLCBkaWdpdHMgPSAyLCBjYXB0aW9uID0gIk1lYW4gUk1TRSBjaGFuZ2UgdnMgTUVESUFOICglKSBieSBwb2xsdXRhbnQiKQprbml0cjo6a2FibGUoYmVzdGMsIGNhcHRpb24gPSAiTnVtYmVyIG9mIG1vZGVscyBmb3Igd2hpY2ggZWFjaCBpbXB1dGF0aW9uIGlzIGJlc3QsIGJ5IHBvbGx1dGFudCIpCmBgYAoKIyMgRXhhbXBsZSBmb3JlY2FzdHMgZm9yIE5PJF8yJAoKVGhlIGJlc3QgbW9kZWwgd2l0aCBhIFNlYXMtQVIgaGlzdG9yeSBpcyBzaG93biBmb3IgdGhlIGZpcnN0IGZvdXIgdGVzdCBkYXlzLCBvbmUgcGFuZWwgcGVyIGltcHV0YXRpb246Cm9ic2VydmVkIHZhbHVlcyBpbiBncmV5LCBmb3JlY2FzdHMgaW4gY29sb3VyLCBhbmQgdGhlIHRlc3QgUk1TRSBvZiB0aGF0IG1vZGVs4oCTaW1wdXRhdGlvbiBwYWlyIGluIHRoZQpwYW5lbCB0aXRsZS4KCmBgYHtyIGZpZzEwLCBmaWcud2lkdGg9Ny4xNiwgZmlnLmhlaWdodD00LjJ9CmJlc3RfbSA8LSBzYyB8PiBmaWx0ZXIocG9sbHV0YW50ID09ICJOTzIiLCBpbXB1dGF0aW9uID09ICJTZWFzLUFSIikgfD4gc2xpY2VfbWluKFJNU0UsIG4gPSAxLCB3aXRoX3RpZXMgPSBGQUxTRSkgfD4gcHVsbChtb2RlbCkKcm0xMCA8LSBzYyB8PiBmaWx0ZXIocG9sbHV0YW50ID09ICJOTzIiLCBtb2RlbCA9PSBiZXN0X20pIHw+IG11dGF0ZShpbXB1dGF0aW9uID0gZmFjdG9yKGltcHV0YXRpb24sIGxldmVscyA9IElNUF9GQykpIHw+IGFycmFuZ2UoaW1wdXRhdGlvbikKbGFiMTAgPC0gc2V0TmFtZXMoc3ByaW50ZigiJXMgdHJhaW5lZCBvbiBhICVzLWltcHV0ZWQgaGlzdG9yeSAodGVzdCBSTVNFICUuMmYpIiwgYmVzdF9tLCBybTEwJGltcHV0YXRpb24sIHJtMTAkUk1TRSksIGFzLmNoYXJhY3RlcihybTEwJGltcHV0YXRpb24pKQprZXkoImZpZzEwX21vZGVsIiwgYmVzdF9tKQpkMTAgPC0gZmNfYWxsIHw+IGZpbHRlcihwb2xsdXRhbnQgPT0gIk5PMiIsIG1vZGVsID09IGJlc3RfbSwgb3JpZ2luIDwgbWluKG9yaWdpbikgKyA0ICogNDgpIHw+CiAgbXV0YXRlKHRpbWUgPSBUU1tvcmlnaW4gKyBoXSwgcGFuZWwgPSBmYWN0b3IobGFiMTBbaW1wdXRhdGlvbl0sIGxldmVscyA9IGxhYjEwKSkKcDEwIDwtIGdncGxvdChkMTAsIGFlcyh0aW1lKSkgKyBnZW9tX2xpbmUoYWVzKHkgPSB5KSwgY29sb3VyID0gImdyZXkzMCIsIGxpbmV3aWR0aCA9IDAuMzUsIG5hLnJtID0gVFJVRSkgKwogIGdlb21fbGluZShhZXMoeSA9IHloYXQsIGNvbG91ciA9IGltcHV0YXRpb24pLCBsaW5ld2lkdGggPSAwLjYpICsgZmFjZXRfd3JhcCh+cGFuZWwsIG5jb2wgPSAxKSArCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBDT0xbSU1QX0ZDXSwgZ3VpZGUgPSAibm9uZSIpICsKICBzY2FsZV94X2RhdGV0aW1lKGRhdGVfYnJlYWtzID0gIjEgZGF5IiwgZGF0ZV9sYWJlbHMgPSAiJWQgJWIiKSArCiAgbGFicyh4ID0gTlVMTCwgeSA9IGV4cHJlc3Npb24oTk9bMl0gfiAiWyIgKiBtdSAqICJnL20iXjMgKiAiXSIpKQpwcmludChwMTApOyBzYXZlX2ZpZyhwMTAsICJmaWcxMF9mb3JlY2FzdF9leGFtcGxlIiwgNy4xNiwgNC4yKQpgYGAKCgoKCgoKCg==