Problem description

library(ggplot2)
library(ggtrace)
library(palmerpenguins)
library(patchwork)

Suppose a facetted layout with 3 panels, a discrete x scale and continuous y scale. The distribution of x-categories are uneven (“sparse”) across panels.

facetted_sparseX_plot <- ggplot(penguins, aes(species)) +
  facet_grid(~ island, scales = "free_x", space = "free")
facetted_sparseX_plot

And suppose you want to plot the proportion of each penguin species (x) within each island (facet). Using after_stat(prop) just gets you a constant prop = 1 but you can achieve this within-facet normalization with an additional group = 1:

(facetted_sparseX_plot +
  geom_bar(aes(y = after_stat(prop)))) +
(facetted_sparseX_plot +
  geom_bar(aes(y = after_stat(prop), group = 1)))

Problem is that you can’t additionally color code by species using the fill aesthetic - it gives a cryptic warning:

problem_plot <- facetted_sparseX_plot +
  geom_bar(aes(y = after_stat(prop), group = 1, fill = species))
problem_plot
## Warning: The following aesthetics were dropped during statistical transformation: fill
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
## The following aesthetics were dropped during statistical transformation: fill
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?

Note that a minimal reprex doesn’t need the faceting part, but I’m using that more complex example for demo purposes:

ggplot(penguins, aes(x = species)) +
  geom_bar(aes(y = after_stat(prop), group = 1, fill = species))
## Warning: The following aesthetics were dropped during statistical transformation: fill
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?

There are lots of moving parts to this, so I’m walking through my thought process step by step.

Issues for consideration

Issue 1 - is this off-label usage?

The first thing to consider is of course whether it’s intended to use something like aes(y = after_stat(prop), fill = species, group = 1) for geom_bar().

ggplot docs for geom_bar/stat_count have no example of after_stat(prop) but there is a clear parallel in the docs for (the confusingly named) geom_count, where the docs use the conjunction of after_stat(prop) and group = 1 to achieve the effect of normalizing within facet (vs. group)

d <- ggplot(diamonds, aes(x = cut, y = clarity))
geom_count_ex1 <- d +
  geom_count(aes(size = after_stat(prop)))
geom_count_ex2 <- d +
  geom_count(aes(size = after_stat(prop), group = 1)) +
  scale_size_area(max_size = 10)
geom_count_ex1 + geom_count_ex2

Modifying this geom_count() code with something like aes(fill = <x-var>) is not documented but works like as one would expect:

d +
  geom_count(aes(size = after_stat(prop), group = 1, fill = cut), shape = 21) +
  scale_size_area(max_size = 10)

So it seems like people should be able to write code like geom_bar() code with aes(group = 1, fill = <some-discrete-var>. If not, then we need to be careful (e.g., write better error msg) about the fact that people will pick up this pattern from seeing how geom_count() works, for example.

Assuming this is indeed a bug, here’s an investigation of why:

Issue 2 - is this about how geom_bar is inferring grouping structure?

This was my first guess given the warning message.

But curiously, you get this warning even when you swap out fill = with label =. We shouldn’t see a grouping-related warning since the label aesthetic is ignored as a possible grouping variable at the ggplot2:::add_group level, but we see the same warning again:

facetted_sparseX_plot +
  aes(y = after_stat(prop), group = 1) +
  geom_bar() +
  stat_count(geom = "label", aes(label = species))
## Warning: The following aesthetics were dropped during statistical transformation: label
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
## The following aesthetics were dropped during statistical transformation: label
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
## Warning: Removed 4 rows containing missing values (`geom_label()`).

So even if this isn’t a bug, it’s at best an incorrect/uninformative warning because you’re hard-coding the grouping structure with aes(group = 1), and the plot does correctly reflect this fact (it just gets the non-grouping aesthetic wrong for some reason). Furthermore, the fill aesthetic actually does not get dropped contrary to what the warning suggests - fill persists in the layer_data where you just get a partial fill scale for the third panel:

layer_data(problem_plot)
## Warning: The following aesthetics were dropped during statistical transformation: fill
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
## The following aesthetics were dropped during statistical transformation: fill
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
##      fill         y count      prop x flipped_aes group PANEL ymin      ymax
## 1  grey50 0.2619048    44 0.2619048 1       FALSE     1     1    0 0.2619048
## 2  grey50 0.7380952   124 0.7380952 2       FALSE     1     1    0 0.7380952
## 3  grey50 0.4516129    56 0.4516129 1       FALSE     1     2    0 0.4516129
## 4  grey50 0.5483871    68 0.5483871 2       FALSE     1     2    0 0.5483871
## 5 #F8766D 1.0000000    52 1.0000000 1       FALSE     1     3    0 1.0000000
##   xmin xmax colour linewidth linetype alpha
## 1 0.55 1.45     NA       0.5        1    NA
## 2 1.55 2.45     NA       0.5        1    NA
## 3 0.55 1.45     NA       0.5        1    NA
## 4 1.55 2.45     NA       0.5        1    NA
## 5 0.55 1.45     NA       0.5        1    NA

Issue 3 - where does this happen?

Unfortunately, it comes down to Stat$compute_panel and since parent ggproto methods have a big inertia to being edited, idk if it’s worth “fixing” at all. In any case, the problem boils down to how “non_constant_columns” are handled:

(warning suppressed from this point on)

Consider the data for the first panel at the stat compute step. What we want is for the panel-level stat data to look something like this, where the fill column retains the “x-to-fill” correspondence:

count prop x ... group   fill PANEL
   44  ... 1 ...     1 Adelie     1
  124  ... 2 ...     1 Gentoo     1

But here’s what happens instead inside $compute_panel. First it sends off the data to $compute_group for the split-apply-combine, and then it identifies whether there are “non constant columns” from the combined data. These happen in two assignment calls to the internal stats variable.

ggbody(Stat$compute_panel)[c(4,6)]
## [[1]]
## stats <- lapply(groups, function(group) {
##     self$compute_group(data = group, scales = scales, ...)
## })
## 
## [[2]]
## stats <- mapply(function(new, old) {
##     if (empty(new)) 
##         return(data_frame0())
##     old <- old[, !(names(old) %in% names(new)), drop = FALSE]
##     non_constant <- vapply(old, function(x) length(unique0(x)) > 
##         1, logical(1L))
##     non_constant_columns <<- c(non_constant_columns, names(old)[non_constant])
##     vec_cbind(new, old[rep(1, nrow(new)), , drop = FALSE])
## }, stats, groups, SIMPLIFY = FALSE)

In the case of the first panel of problem_plot, the group-level split-apply-combine works fine and calculates the necessary stat variables, but then in the clean-up stage, fill is recognized as a non-constant variable (and forced to repeat just the first value, “Adelie”):

ggtrace_inspect_vars(problem_plot, Stat$compute_panel, vars = "stats")
## $Step5
## $Step5$`1`
##   count      prop x width flipped_aes
## 1    44 0.2619048 1   0.9       FALSE
## 2   124 0.7380952 2   0.9       FALSE
## 
## 
## $Step7
## $Step7$`1`
##     count      prop x width flipped_aes group   fill PANEL
## 1      44 0.2619048 1   0.9       FALSE     1 Adelie     1
## 1.1   124 0.7380952 2   0.9       FALSE     1 Adelie     1

Once fill is flagged as a non constant column, it gets dropped from the data right before the data is returned from $compute_panel:

ggbody(Stat$compute_panel)[[length(ggbody(Stat$compute_panel))]]
## data_new[, !names(data_new) %in% non_constant_columns, drop = FALSE]

In contrast, data for the third panel works because there’s only 1 value, so the fill column cannot be “non constant” by definition. This is why the fill column is dropped by $compute_panel for the first panel (and the second panel too) but retained in the last:

ggtrace_inspect_return(problem_plot, Stat$compute_panel, cond = 1)
##     count      prop x width flipped_aes group PANEL
## 1      44 0.2619048 1   0.9       FALSE     1     1
## 1.1   124 0.7380952 2   0.9       FALSE     1     1
ggtrace_inspect_return(problem_plot, Stat$compute_panel, cond = 3)
##   count prop x width flipped_aes group   fill PANEL
## 1    52    1 1   0.9       FALSE     1 Adelie     3

Then when Stat$compute_layer combines the panel-level data, you get a bunch of NA values in fill, which causes the visual bug down the line.

Fixes?

One way is to give StatCount it’s own $compute_panel method. This is actually precisely why this behavior doesn’t replicate for geom_count(): its default StatSum actually extends $compute_panel instead of $compute_group to handle this more graciously:

StatSum$compute_panel
## <ggproto method>
##   <Wrapper function>
##     function (...) 
## compute_panel(...)
## 
##   <Inner function (f)>
##     function (data, scales) 
## {
##     if (is.null(data$weight)) 
##         data$weight <- 1
##     group_by <- setdiff(intersect(names(data), ggplot_global$all_aesthetics), 
##         "weight")
##     counts <- count(data, group_by, wt_var = "weight")
##     counts <- rename(counts, c(freq = "n"))
##     counts$prop <- stats::ave(counts$n, counts$group, FUN = prop.table)
##     counts
## }

So a compromise to editing Stat$compute_panel could be that for StatCount, count is calculated at the level of $compute_group but prop is calculated at the level of $compute_panel. This design is actually not new - StatYdensity extends both compute methods to “calculate” by group and “normalize” by panel.

But this still needs to happen earlier than the “non constant column” check. One option is to remove that check entirely, although unlike StatSum, StatCount does drop an aesthetic ("weight").

Here’s one attempt that minimizes changes in the code. Specifically, I edit just the mapply() portion of the Stat$compute_panel for a new StatCount2$compute_panel:

Original Stat$compute_panel’s mapply() line:
stats <- mapply(function(new, old) {
  if (empty(new)) return(data_frame0())
  old <- old[, !(names(old) %in% names(new)), drop = FALSE]
  non_constant <- vapply(old, function(x) length(unique0(x)) > 1, logical(1L))
  non_constant_columns <<- c(non_constant_columns, names(old)[non_constant])
  vec_cbind(
    new,
    old[rep(1, nrow(new)), , drop = FALSE]
  )
}, stats, groups, SIMPLIFY = FALSE)
New StatCount2$compute_panel’s mapply() line:

I have no idea about how you’re supposed to do low-level things with vctrs and things - probably needs refactoring but this is functional.

stats <- mapply(function(new, old) {
  if (empty(new)) return(data_frame0())
  # Edit 1: Identify exceptions where a variable is constant *within values of x*
  within_x_constant <- vapply(old, function(x) nlevels(interaction(x, old$x, drop = TRUE)) == max(old$x), logical(1L))
  # Edit 2: Columns to keep
  kept <- unique(old[, within_x_constant])
  # Edit 3: Don't test non-constant-ness of `kept` columns
  old <- old[, !(names(old) %in% c(names(new), names(kept))), drop = FALSE]
  non_constant <- vapply(old, function(x) length(unique0(x)) > 1, logical(1L))
  non_constant_columns <<- c(non_constant_columns, names(old)[non_constant])
  # Edit 4: Add `kept` columns
  vec_cbind(
    new,
    kept[, setdiff(names(kept), "x"), drop = FALSE],
    old[rep(1, nrow(new)), setdiff(names(old), names(kept)), drop = FALSE],
  )
}, stats, groups, SIMPLIFY = FALSE)
Full StatCount2 extension:
StatCount2 <- ggproto(
  "StatCount2", StatCount,
  compute_panel = function (self, data, scales, ...) {
    if (ggplot2:::empty(data)) 
      return(ggplot2:::data_frame0())
    groups <- split(data, data$group)
    stats <- lapply(groups, function(group) {
      self$compute_group(data = group, scales = scales, ...)
    })
    non_constant_columns <- character(0)
    
    stats <- mapply(function(new, old) {
      if (ggplot2:::empty(new)) return(ggplot2:::data_frame0())
      # Edit 1: Identify exceptions where a variable is constant *within values of x*
      within_x_constant <- vapply(old, function(x) nlevels(interaction(x, old$x, drop = TRUE)) == max(old$x), logical(1L))
      # Edit 2: Columns to keep
      kept <- unique(old[, within_x_constant])
      # Edit 3: Don't test non-constant-ness of `kept` columns
      old <- old[, !(names(old) %in% c(names(new), names(kept))), drop = FALSE]
      non_constant <- vapply(old, function(x) length(unique0(x)) > 1, logical(1L))
      non_constant_columns <<- c(non_constant_columns, names(old)[non_constant])
      # Edit 4: Add `kept` columns
      vctrs::vec_cbind(
        new,
        kept[, setdiff(names(kept), "x"), drop = FALSE],
        old[rep(1, nrow(new)), setdiff(names(old), names(kept)), drop = FALSE],
      )
    }, stats, groups, SIMPLIFY = FALSE)
    
    non_constant_columns <- ggplot2:::unique0(non_constant_columns)
    dropped <- non_constant_columns[!non_constant_columns %in% self$dropped_aes]
    if (length(dropped) > 0) {
      cli::cli_warn(c(
        "The following aesthetics were dropped during statistical transformation: {.field {glue_collapse(dropped, sep = ', ')}}",
        "i" = "This can happen when ggplot fails to infer the correct grouping structure in the data.",
        "i" = "Did you forget to specify a {.code group} aesthetic or to convert a numerical variable into a factor?"
      ))
    }

    # Finally, combine the results and drop columns that are not constant.
    data_new <- ggplot2:::vec_rbind0(!!!stats)
    data_new[, !names(data_new) %in% non_constant_columns, drop = FALSE]
  }
)


Check that StatCount2 behaves as it should with a new $compute_panel:

facetted_sparseX_plot +
  geom_bar(
    aes(y = after_stat(prop), group = 1, fill = species),
    stat = "count2"
  )

LS0tCnRpdGxlOiAic3RhdCBjb3VudCBwcm9ibGVtIgphdXRob3I6ICJKdW5lIENob2UiCm91dHB1dDoKICBodG1sX2RvY3VtZW50OgogICAgY29kZV9kb3dubG9hZDogVFJVRQogICAgdG9jOiB0cnVlCiAgICB0b2NfZmxvYXQ6IHRydWUKLS0tCgojIyBQcm9ibGVtIGRlc2NyaXB0aW9uCgpgYGB7cn0KbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGdndHJhY2UpCmxpYnJhcnkocGFsbWVycGVuZ3VpbnMpCmxpYnJhcnkocGF0Y2h3b3JrKQpgYGAKClN1cHBvc2UgYSBmYWNldHRlZCBsYXlvdXQgd2l0aCAzIHBhbmVscywgYSBkaXNjcmV0ZSB4IHNjYWxlIGFuZCBjb250aW51b3VzIHkgc2NhbGUuClRoZSBkaXN0cmlidXRpb24gb2YgeC1jYXRlZ29yaWVzIGFyZSB1bmV2ZW4gKCJzcGFyc2UiKSBhY3Jvc3MgcGFuZWxzLgoKYGBge3J9CmZhY2V0dGVkX3NwYXJzZVhfcGxvdCA8LSBnZ3Bsb3QocGVuZ3VpbnMsIGFlcyhzcGVjaWVzKSkgKwogIGZhY2V0X2dyaWQofiBpc2xhbmQsIHNjYWxlcyA9ICJmcmVlX3giLCBzcGFjZSA9ICJmcmVlIikKZmFjZXR0ZWRfc3BhcnNlWF9wbG90CmBgYAoKQW5kIHN1cHBvc2UgeW91IHdhbnQgdG8gcGxvdCB0aGUgcHJvcG9ydGlvbiBvZiBlYWNoIHBlbmd1aW4gc3BlY2llcyAoeCkgd2l0aGluIGVhY2ggaXNsYW5kIChmYWNldCkuClVzaW5nIGBhZnRlcl9zdGF0KHByb3ApYCBqdXN0IGdldHMgeW91IGEgY29uc3RhbnQgYHByb3AgPSAxYCBidXQgeW91IGNhbiBhY2hpZXZlIHRoaXMgd2l0aGluLWZhY2V0IG5vcm1hbGl6YXRpb24gd2l0aCBhbiBhZGRpdGlvbmFsIGBncm91cCA9IDFgOgoKYGBge3J9CihmYWNldHRlZF9zcGFyc2VYX3Bsb3QgKwogIGdlb21fYmFyKGFlcyh5ID0gYWZ0ZXJfc3RhdChwcm9wKSkpKSArCihmYWNldHRlZF9zcGFyc2VYX3Bsb3QgKwogIGdlb21fYmFyKGFlcyh5ID0gYWZ0ZXJfc3RhdChwcm9wKSwgZ3JvdXAgPSAxKSkpCmBgYAoKUHJvYmxlbSBpcyB0aGF0IHlvdSBjYW4ndCBhZGRpdGlvbmFsbHkgY29sb3IgY29kZSBieSBzcGVjaWVzIHVzaW5nIHRoZSBgZmlsbGAgYWVzdGhldGljIC0gaXQgZ2l2ZXMgYSBjcnlwdGljIHdhcm5pbmc6CgpgYGB7cn0KcHJvYmxlbV9wbG90IDwtIGZhY2V0dGVkX3NwYXJzZVhfcGxvdCArCiAgZ2VvbV9iYXIoYWVzKHkgPSBhZnRlcl9zdGF0KHByb3ApLCBncm91cCA9IDEsIGZpbGwgPSBzcGVjaWVzKSkKcHJvYmxlbV9wbG90CmBgYAoKTm90ZSB0aGF0IGEgbWluaW1hbCByZXByZXggZG9lc24ndCBuZWVkIHRoZSBmYWNldGluZyBwYXJ0LCBidXQgSSdtIHVzaW5nIHRoYXQgbW9yZSBjb21wbGV4IGV4YW1wbGUgZm9yIGRlbW8gcHVycG9zZXM6IApgYGB7cn0KZ2dwbG90KHBlbmd1aW5zLCBhZXMoeCA9IHNwZWNpZXMpKSArCiAgZ2VvbV9iYXIoYWVzKHkgPSBhZnRlcl9zdGF0KHByb3ApLCBncm91cCA9IDEsIGZpbGwgPSBzcGVjaWVzKSkKYGBgCgpUaGVyZSBhcmUgbG90cyBvZiBtb3ZpbmcgcGFydHMgdG8gdGhpcywgc28gSSdtIHdhbGtpbmcgdGhyb3VnaCBteSB0aG91Z2h0IHByb2Nlc3Mgc3RlcCBieSBzdGVwLgoKIyMgSXNzdWVzIGZvciBjb25zaWRlcmF0aW9uCgojIyMgSXNzdWUgMSAtIGlzIHRoaXMgb2ZmLWxhYmVsIHVzYWdlPwoKVGhlIGZpcnN0IHRoaW5nIHRvIGNvbnNpZGVyIGlzIG9mIGNvdXJzZSB3aGV0aGVyIGl0J3MgaW50ZW5kZWQgdG8gdXNlIHNvbWV0aGluZyBsaWtlIGBhZXMoeSA9IGFmdGVyX3N0YXQocHJvcCksIGZpbGwgPSBzcGVjaWVzLCBncm91cCA9IDEpYCBmb3IgYGdlb21fYmFyKClgLgoKZ2dwbG90IGRvY3MgZm9yIGBnZW9tX2JhcmAvYHN0YXRfY291bnRgIGhhdmUgbm8gZXhhbXBsZSBvZiBgYWZ0ZXJfc3RhdChwcm9wKWAgYnV0IHRoZXJlIGlzIGEgY2xlYXIgcGFyYWxsZWwgaW4gdGhlIGRvY3MgZm9yICh0aGUgY29uZnVzaW5nbHkgbmFtZWQpIGBnZW9tX2NvdW50YCwgd2hlcmUgdGhlIGRvY3MgdXNlIHRoZSBjb25qdW5jdGlvbiBvZiBgYWZ0ZXJfc3RhdChwcm9wKWAgYW5kIGBncm91cCA9IDFgIHRvIGFjaGlldmUgdGhlIGVmZmVjdCBvZiBub3JtYWxpemluZyB3aXRoaW4gZmFjZXQgKHZzLiBncm91cCkKCmBgYHtyfQpkIDwtIGdncGxvdChkaWFtb25kcywgYWVzKHggPSBjdXQsIHkgPSBjbGFyaXR5KSkKZ2VvbV9jb3VudF9leDEgPC0gZCArCiAgZ2VvbV9jb3VudChhZXMoc2l6ZSA9IGFmdGVyX3N0YXQocHJvcCkpKQpnZW9tX2NvdW50X2V4MiA8LSBkICsKICBnZW9tX2NvdW50KGFlcyhzaXplID0gYWZ0ZXJfc3RhdChwcm9wKSwgZ3JvdXAgPSAxKSkgKwogIHNjYWxlX3NpemVfYXJlYShtYXhfc2l6ZSA9IDEwKQpnZW9tX2NvdW50X2V4MSArIGdlb21fY291bnRfZXgyCmBgYAoKTW9kaWZ5aW5nIHRoaXMgYGdlb21fY291bnQoKWAgY29kZSB3aXRoIHNvbWV0aGluZyBsaWtlIGBhZXMoZmlsbCA9IDx4LXZhcj4pYCBpcyBub3QgZG9jdW1lbnRlZCBidXQgd29ya3MgbGlrZSBhcyBvbmUgd291bGQgZXhwZWN0OgoKYGBge3J9CmQgKwogIGdlb21fY291bnQoYWVzKHNpemUgPSBhZnRlcl9zdGF0KHByb3ApLCBncm91cCA9IDEsIGZpbGwgPSBjdXQpLCBzaGFwZSA9IDIxKSArCiAgc2NhbGVfc2l6ZV9hcmVhKG1heF9zaXplID0gMTApCmBgYAoKU28gaXQgc2VlbXMgbGlrZSBwZW9wbGUgKnNob3VsZCogYmUgYWJsZSB0byB3cml0ZSBjb2RlIGxpa2UgYGdlb21fYmFyKClgIGNvZGUgd2l0aCBgYWVzKGdyb3VwID0gMSwgZmlsbCA9IDxzb21lLWRpc2NyZXRlLXZhcj5gLiBJZiBub3QsIHRoZW4gd2UgbmVlZCB0byBiZSBjYXJlZnVsIChlLmcuLCB3cml0ZSBiZXR0ZXIgZXJyb3IgbXNnKSBhYm91dCB0aGUgZmFjdCB0aGF0IHBlb3BsZSB3aWxsIHBpY2sgdXAgdGhpcyBwYXR0ZXJuIGZyb20gc2VlaW5nIGhvdyBgZ2VvbV9jb3VudCgpYCB3b3JrcywgZm9yIGV4YW1wbGUuCgpBc3N1bWluZyB0aGlzIGlzIGluZGVlZCBhIGJ1ZywgaGVyZSdzIGFuIGludmVzdGlnYXRpb24gb2Ygd2h5OgoKIyMjIElzc3VlIDIgLSBpcyB0aGlzIGFib3V0IGhvdyBnZW9tX2JhciBpcyBpbmZlcnJpbmcgZ3JvdXBpbmcgc3RydWN0dXJlPwoKVGhpcyB3YXMgbXkgZmlyc3QgZ3Vlc3MgZ2l2ZW4gdGhlIHdhcm5pbmcgbWVzc2FnZS4KCkJ1dCBjdXJpb3VzbHksIHlvdSBnZXQgdGhpcyB3YXJuaW5nIGV2ZW4gd2hlbiB5b3Ugc3dhcCBvdXQgYGZpbGwgPWAgd2l0aCBgbGFiZWwgPWAuCldlIHNob3VsZG4ndCBzZWUgYSBncm91cGluZy1yZWxhdGVkIHdhcm5pbmcgc2luY2UgdGhlIGBsYWJlbGAgYWVzdGhldGljIGlzIGlnbm9yZWQgYXMgYSBwb3NzaWJsZSBncm91cGluZyB2YXJpYWJsZSBhdCB0aGUgYGdncGxvdDI6OjphZGRfZ3JvdXBgIGxldmVsLCBidXQgd2Ugc2VlIHRoZSBzYW1lIHdhcm5pbmcgYWdhaW46CgpgYGB7cn0KZmFjZXR0ZWRfc3BhcnNlWF9wbG90ICsKICBhZXMoeSA9IGFmdGVyX3N0YXQocHJvcCksIGdyb3VwID0gMSkgKwogIGdlb21fYmFyKCkgKwogIHN0YXRfY291bnQoZ2VvbSA9ICJsYWJlbCIsIGFlcyhsYWJlbCA9IHNwZWNpZXMpKQpgYGAKClNvIGV2ZW4gaWYgdGhpcyBpc24ndCBhIGJ1ZywgaXQncyBhdCBiZXN0IGFuIGluY29ycmVjdC91bmluZm9ybWF0aXZlIHdhcm5pbmcgYmVjYXVzZSB5b3UncmUgaGFyZC1jb2RpbmcgdGhlIGdyb3VwaW5nIHN0cnVjdHVyZSB3aXRoIGBhZXMoZ3JvdXAgPSAxKWAsIGFuZCB0aGUgcGxvdCAqZG9lcyogY29ycmVjdGx5IHJlZmxlY3QgdGhpcyBmYWN0IChpdCBqdXN0IGdldHMgdGhlIG5vbi1ncm91cGluZyBhZXN0aGV0aWMgd3JvbmcgZm9yIHNvbWUgcmVhc29uKS4KRnVydGhlcm1vcmUsIHRoZSBgZmlsbGAgYWVzdGhldGljIGFjdHVhbGx5ICpkb2VzIG5vdCBnZXQgZHJvcHBlZCogY29udHJhcnkgdG8gd2hhdCB0aGUgd2FybmluZyBzdWdnZXN0cyAtIGBmaWxsYCBwZXJzaXN0cyBpbiB0aGUgbGF5ZXJfZGF0YSB3aGVyZSB5b3UganVzdCBnZXQgYSBwYXJ0aWFsIGBmaWxsYCBzY2FsZSBmb3IgdGhlIHRoaXJkIHBhbmVsOgoKYGBge3IsIG1lc3NhZ2UgPSBGQUxTRX0KbGF5ZXJfZGF0YShwcm9ibGVtX3Bsb3QpCmBgYAoKIyMjIElzc3VlIDMgLSB3aGVyZSBkb2VzIHRoaXMgaGFwcGVuPwoKVW5mb3J0dW5hdGVseSwgaXQgY29tZXMgZG93biB0byBgU3RhdCRjb21wdXRlX3BhbmVsYCBhbmQgc2luY2UgcGFyZW50IGdncHJvdG8gbWV0aG9kcyBoYXZlIGEgYmlnIGluZXJ0aWEgdG8gYmVpbmcgZWRpdGVkLCBpZGsgaWYgaXQncyB3b3J0aCAiZml4aW5nIiBhdCBhbGwuCkluIGFueSBjYXNlLCB0aGUgcHJvYmxlbSBib2lscyBkb3duIHRvIGhvdyAiYG5vbl9jb25zdGFudF9jb2x1bW5zYCIgYXJlIGhhbmRsZWQ6Cgood2FybmluZyBzdXBwcmVzc2VkIGZyb20gdGhpcyBwb2ludCBvbikKCkNvbnNpZGVyIHRoZSBkYXRhIGZvciB0aGUgZmlyc3QgcGFuZWwgYXQgdGhlIHN0YXQgY29tcHV0ZSBzdGVwLgpXaGF0IHdlIHdhbnQgaXMgZm9yIHRoZSBwYW5lbC1sZXZlbCBzdGF0IGRhdGEgdG8gbG9vayBzb21ldGhpbmcgbGlrZSB0aGlzLCB3aGVyZSB0aGUgZmlsbCBjb2x1bW4gcmV0YWlucyB0aGUgIngtdG8tZmlsbCIgY29ycmVzcG9uZGVuY2U6CgpgYGB7ciwgZXZhbD1GQUxTRSwgY2xhc3Mtb3V0cHV0OiAiciBzb3VyY2VDb2RlIn0KY291bnQgcHJvcCB4IC4uLiBncm91cCAgIGZpbGwgUEFORUwKICAgNDQgIC4uLiAxIC4uLiAgICAgMSBBZGVsaWUgICAgIDEKICAxMjQgIC4uLiAyIC4uLiAgICAgMSBHZW50b28gICAgIDEKYGBgCgpCdXQgaGVyZSdzIHdoYXQgaGFwcGVucyBpbnN0ZWFkIGluc2lkZSBgJGNvbXB1dGVfcGFuZWxgLgpGaXJzdCBpdCBzZW5kcyBvZmYgdGhlIGRhdGEgdG8gYCRjb21wdXRlX2dyb3VwYCBmb3IgdGhlIHNwbGl0LWFwcGx5LWNvbWJpbmUsIGFuZCB0aGVuIGl0IGlkZW50aWZpZXMgd2hldGhlciB0aGVyZSBhcmUgIm5vbiBjb25zdGFudCBjb2x1bW5zIiBmcm9tIHRoZSBjb21iaW5lZCBkYXRhLgpUaGVzZSBoYXBwZW4gaW4gdHdvIGFzc2lnbm1lbnQgY2FsbHMgdG8gdGhlIGludGVybmFsIGBzdGF0c2AgdmFyaWFibGUuCgpgYGB7cn0KZ2dib2R5KFN0YXQkY29tcHV0ZV9wYW5lbClbYyg0LDYpXQpgYGAKCkluIHRoZSBjYXNlIG9mIHRoZSBmaXJzdCBwYW5lbCBvZiBgcHJvYmxlbV9wbG90YCwgdGhlIGdyb3VwLWxldmVsIHNwbGl0LWFwcGx5LWNvbWJpbmUgd29ya3MgZmluZSBhbmQgY2FsY3VsYXRlcyB0aGUgbmVjZXNzYXJ5IHN0YXQgdmFyaWFibGVzLCBidXQgdGhlbiBpbiB0aGUgY2xlYW4tdXAgc3RhZ2UsIGBmaWxsYCBpcyByZWNvZ25pemVkIGFzIGEgbm9uLWNvbnN0YW50IHZhcmlhYmxlIChhbmQgZm9yY2VkIHRvIHJlcGVhdCBqdXN0IHRoZSBmaXJzdCB2YWx1ZSwgIkFkZWxpZSIpOgoKYGBge3IsIHdhcm5pbmc9RkFMU0V9CmdndHJhY2VfaW5zcGVjdF92YXJzKHByb2JsZW1fcGxvdCwgU3RhdCRjb21wdXRlX3BhbmVsLCB2YXJzID0gInN0YXRzIikKYGBgCgpPbmNlIGBmaWxsYCBpcyBmbGFnZ2VkIGFzIGEgbm9uIGNvbnN0YW50IGNvbHVtbiwgaXQgZ2V0cyBkcm9wcGVkIGZyb20gdGhlIGRhdGEgcmlnaHQgYmVmb3JlIHRoZSBkYXRhIGlzIHJldHVybmVkIGZyb20gYCRjb21wdXRlX3BhbmVsYDoKCmBgYHtyfQpnZ2JvZHkoU3RhdCRjb21wdXRlX3BhbmVsKVtbbGVuZ3RoKGdnYm9keShTdGF0JGNvbXB1dGVfcGFuZWwpKV1dCmBgYAoKSW4gY29udHJhc3QsIGRhdGEgZm9yIHRoZSB0aGlyZCBwYW5lbCB3b3JrcyBiZWNhdXNlIHRoZXJlJ3Mgb25seSAxIHZhbHVlLCBzbyB0aGUgYGZpbGxgIGNvbHVtbiBjYW5ub3QgYmUgIm5vbiBjb25zdGFudCIgYnkgZGVmaW5pdGlvbi4KVGhpcyBpcyB3aHkgdGhlIGBmaWxsYCBjb2x1bW4gaXMgZHJvcHBlZCBieSBgJGNvbXB1dGVfcGFuZWxgIGZvciB0aGUgZmlyc3QgcGFuZWwgKGFuZCB0aGUgc2Vjb25kIHBhbmVsIHRvbykgYnV0IHJldGFpbmVkIGluIHRoZSBsYXN0OgoKYGBge3IsIHdhcm5pbmcgPSBGQUxTRX0KZ2d0cmFjZV9pbnNwZWN0X3JldHVybihwcm9ibGVtX3Bsb3QsIFN0YXQkY29tcHV0ZV9wYW5lbCwgY29uZCA9IDEpCmdndHJhY2VfaW5zcGVjdF9yZXR1cm4ocHJvYmxlbV9wbG90LCBTdGF0JGNvbXB1dGVfcGFuZWwsIGNvbmQgPSAzKQpgYGAKClRoZW4gd2hlbiBgU3RhdCRjb21wdXRlX2xheWVyYCBjb21iaW5lcyB0aGUgcGFuZWwtbGV2ZWwgZGF0YSwgeW91IGdldCBhIGJ1bmNoIG9mIGBOQWAgdmFsdWVzIGluIGBmaWxsYCwgd2hpY2ggY2F1c2VzIHRoZSB2aXN1YWwgYnVnIGRvd24gdGhlIGxpbmUuCgojIyBGaXhlcz8KCk9uZSB3YXkgaXMgdG8gZ2l2ZSBgU3RhdENvdW50YCBpdCdzIG93biBgJGNvbXB1dGVfcGFuZWxgIG1ldGhvZC4KVGhpcyBpcyBhY3R1YWxseSBwcmVjaXNlbHkgd2h5IHRoaXMgYmVoYXZpb3IgZG9lc24ndCByZXBsaWNhdGUgZm9yIGBnZW9tX2NvdW50KClgOiBpdHMgZGVmYXVsdCBgU3RhdFN1bWAgYWN0dWFsbHkgZXh0ZW5kcyBgJGNvbXB1dGVfcGFuZWxgIGluc3RlYWQgb2YgYCRjb21wdXRlX2dyb3VwYCB0byBoYW5kbGUgdGhpcyBtb3JlIGdyYWNpb3VzbHk6CgpgYGB7cn0KU3RhdFN1bSRjb21wdXRlX3BhbmVsCmBgYAoKU28gYSBjb21wcm9taXNlIHRvIGVkaXRpbmcgYFN0YXQkY29tcHV0ZV9wYW5lbGAgY291bGQgYmUgdGhhdCBmb3IgYFN0YXRDb3VudGAsIGBjb3VudGAgaXMgY2FsY3VsYXRlZCBhdCB0aGUgbGV2ZWwgb2YgYCRjb21wdXRlX2dyb3VwYCBidXQgYHByb3BgIGlzIGNhbGN1bGF0ZWQgYXQgdGhlIGxldmVsIG9mIGAkY29tcHV0ZV9wYW5lbGAuClRoaXMgZGVzaWduIGlzIGFjdHVhbGx5IG5vdCBuZXcgLSBgU3RhdFlkZW5zaXR5YCBleHRlbmRzIGJvdGggY29tcHV0ZSBtZXRob2RzIHRvICJjYWxjdWxhdGUiIGJ5IGdyb3VwIGFuZCAibm9ybWFsaXplIiBieSBwYW5lbC4KCkJ1dCB0aGlzIHN0aWxsIG5lZWRzIHRvIGhhcHBlbiBlYXJsaWVyIHRoYW4gdGhlICJub24gY29uc3RhbnQgY29sdW1uIiBjaGVjay4KT25lIG9wdGlvbiBpcyB0byByZW1vdmUgdGhhdCBjaGVjayBlbnRpcmVseSwgYWx0aG91Z2ggdW5saWtlIGBTdGF0U3VtYCwgYFN0YXRDb3VudGAgKmRvZXMqIGRyb3AgYW4gYWVzdGhldGljIChgIndlaWdodCJgKS4KCkhlcmUncyBvbmUgYXR0ZW1wdCB0aGF0IG1pbmltaXplcyBjaGFuZ2VzIGluIHRoZSBjb2RlLgpTcGVjaWZpY2FsbHksIEkgZWRpdCBqdXN0IHRoZSBgbWFwcGx5KClgIHBvcnRpb24gb2YgdGhlIGBTdGF0JGNvbXB1dGVfcGFuZWxgIGZvciBhIG5ldyBgU3RhdENvdW50MiRjb21wdXRlX3BhbmVsYDoKCjxkZXRhaWxzPgoKPHN1bW1hcnk+T3JpZ2luYWwgYFN0YXQkY29tcHV0ZV9wYW5lbGAncyBgbWFwcGx5KClgIGxpbmU6PC9zdW1tYXJ5PgoKYGBge3IsIGV2YWwgPSBGQUxTRX0Kc3RhdHMgPC0gbWFwcGx5KGZ1bmN0aW9uKG5ldywgb2xkKSB7CiAgaWYgKGVtcHR5KG5ldykpIHJldHVybihkYXRhX2ZyYW1lMCgpKQogIG9sZCA8LSBvbGRbLCAhKG5hbWVzKG9sZCkgJWluJSBuYW1lcyhuZXcpKSwgZHJvcCA9IEZBTFNFXQogIG5vbl9jb25zdGFudCA8LSB2YXBwbHkob2xkLCBmdW5jdGlvbih4KSBsZW5ndGgodW5pcXVlMCh4KSkgPiAxLCBsb2dpY2FsKDFMKSkKICBub25fY29uc3RhbnRfY29sdW1ucyA8PC0gYyhub25fY29uc3RhbnRfY29sdW1ucywgbmFtZXMob2xkKVtub25fY29uc3RhbnRdKQogIHZlY19jYmluZCgKICAgIG5ldywKICAgIG9sZFtyZXAoMSwgbnJvdyhuZXcpKSwgLCBkcm9wID0gRkFMU0VdCiAgKQp9LCBzdGF0cywgZ3JvdXBzLCBTSU1QTElGWSA9IEZBTFNFKQpgYGAKCjwvZGV0YWlscz4KCjxkZXRhaWxzPgoKPHN1bW1hcnk+TmV3IGBTdGF0Q291bnQyJGNvbXB1dGVfcGFuZWxgJ3MgYG1hcHBseSgpYCBsaW5lOjwvc3VtbWFyeT4KCkkgaGF2ZSBubyBpZGVhIGFib3V0IGhvdyB5b3UncmUgc3VwcG9zZWQgdG8gZG8gbG93LWxldmVsIHRoaW5ncyB3aXRoIHZjdHJzIGFuZCB0aGluZ3MgLSBwcm9iYWJseSBuZWVkcyByZWZhY3RvcmluZyBidXQgdGhpcyBpcyBmdW5jdGlvbmFsLgoKYGBge3Igc3RhdHMtbWFwcGx5LCBldmFsID0gRkFMU0V9CnN0YXRzIDwtIG1hcHBseShmdW5jdGlvbihuZXcsIG9sZCkgewogIGlmIChlbXB0eShuZXcpKSByZXR1cm4oZGF0YV9mcmFtZTAoKSkKICAjIEVkaXQgMTogSWRlbnRpZnkgZXhjZXB0aW9ucyB3aGVyZSBhIHZhcmlhYmxlIGlzIGNvbnN0YW50ICp3aXRoaW4gdmFsdWVzIG9mIHgqCiAgd2l0aGluX3hfY29uc3RhbnQgPC0gdmFwcGx5KG9sZCwgZnVuY3Rpb24oeCkgbmxldmVscyhpbnRlcmFjdGlvbih4LCBvbGQkeCwgZHJvcCA9IFRSVUUpKSA9PSBtYXgob2xkJHgpLCBsb2dpY2FsKDFMKSkKICAjIEVkaXQgMjogQ29sdW1ucyB0byBrZWVwCiAga2VwdCA8LSB1bmlxdWUob2xkWywgd2l0aGluX3hfY29uc3RhbnRdKQogICMgRWRpdCAzOiBEb24ndCB0ZXN0IG5vbi1jb25zdGFudC1uZXNzIG9mIGBrZXB0YCBjb2x1bW5zCiAgb2xkIDwtIG9sZFssICEobmFtZXMob2xkKSAlaW4lIGMobmFtZXMobmV3KSwgbmFtZXMoa2VwdCkpKSwgZHJvcCA9IEZBTFNFXQogIG5vbl9jb25zdGFudCA8LSB2YXBwbHkob2xkLCBmdW5jdGlvbih4KSBsZW5ndGgodW5pcXVlMCh4KSkgPiAxLCBsb2dpY2FsKDFMKSkKICBub25fY29uc3RhbnRfY29sdW1ucyA8PC0gYyhub25fY29uc3RhbnRfY29sdW1ucywgbmFtZXMob2xkKVtub25fY29uc3RhbnRdKQogICMgRWRpdCA0OiBBZGQgYGtlcHRgIGNvbHVtbnMKICB2ZWNfY2JpbmQoCiAgICBuZXcsCiAgICBrZXB0Wywgc2V0ZGlmZihuYW1lcyhrZXB0KSwgIngiKSwgZHJvcCA9IEZBTFNFXSwKICAgIG9sZFtyZXAoMSwgbnJvdyhuZXcpKSwgc2V0ZGlmZihuYW1lcyhvbGQpLCBuYW1lcyhrZXB0KSksIGRyb3AgPSBGQUxTRV0sCiAgKQp9LCBzdGF0cywgZ3JvdXBzLCBTSU1QTElGWSA9IEZBTFNFKQpgYGAKCjwvZGV0YWlscz4KCjxkZXRhaWxzPgoKPHN1bW1hcnk+RnVsbCBgU3RhdENvdW50MmAgZXh0ZW5zaW9uOjwvc3VtbWFyeT4KCmBgYHtyfQpTdGF0Q291bnQyIDwtIGdncHJvdG8oCiAgIlN0YXRDb3VudDIiLCBTdGF0Q291bnQsCiAgY29tcHV0ZV9wYW5lbCA9IGZ1bmN0aW9uIChzZWxmLCBkYXRhLCBzY2FsZXMsIC4uLikgewogICAgaWYgKGdncGxvdDI6OjplbXB0eShkYXRhKSkgCiAgICAgIHJldHVybihnZ3Bsb3QyOjo6ZGF0YV9mcmFtZTAoKSkKICAgIGdyb3VwcyA8LSBzcGxpdChkYXRhLCBkYXRhJGdyb3VwKQogICAgc3RhdHMgPC0gbGFwcGx5KGdyb3VwcywgZnVuY3Rpb24oZ3JvdXApIHsKICAgICAgc2VsZiRjb21wdXRlX2dyb3VwKGRhdGEgPSBncm91cCwgc2NhbGVzID0gc2NhbGVzLCAuLi4pCiAgICB9KQogICAgbm9uX2NvbnN0YW50X2NvbHVtbnMgPC0gY2hhcmFjdGVyKDApCiAgICAKICAgIHN0YXRzIDwtIG1hcHBseShmdW5jdGlvbihuZXcsIG9sZCkgewogICAgICBpZiAoZ2dwbG90Mjo6OmVtcHR5KG5ldykpIHJldHVybihnZ3Bsb3QyOjo6ZGF0YV9mcmFtZTAoKSkKICAgICAgIyBFZGl0IDE6IElkZW50aWZ5IGV4Y2VwdGlvbnMgd2hlcmUgYSB2YXJpYWJsZSBpcyBjb25zdGFudCAqd2l0aGluIHZhbHVlcyBvZiB4KgogICAgICB3aXRoaW5feF9jb25zdGFudCA8LSB2YXBwbHkob2xkLCBmdW5jdGlvbih4KSBubGV2ZWxzKGludGVyYWN0aW9uKHgsIG9sZCR4LCBkcm9wID0gVFJVRSkpID09IG1heChvbGQkeCksIGxvZ2ljYWwoMUwpKQogICAgICAjIEVkaXQgMjogQ29sdW1ucyB0byBrZWVwCiAgICAgIGtlcHQgPC0gdW5pcXVlKG9sZFssIHdpdGhpbl94X2NvbnN0YW50XSkKICAgICAgIyBFZGl0IDM6IERvbid0IHRlc3Qgbm9uLWNvbnN0YW50LW5lc3Mgb2YgYGtlcHRgIGNvbHVtbnMKICAgICAgb2xkIDwtIG9sZFssICEobmFtZXMob2xkKSAlaW4lIGMobmFtZXMobmV3KSwgbmFtZXMoa2VwdCkpKSwgZHJvcCA9IEZBTFNFXQogICAgICBub25fY29uc3RhbnQgPC0gdmFwcGx5KG9sZCwgZnVuY3Rpb24oeCkgbGVuZ3RoKHVuaXF1ZTAoeCkpID4gMSwgbG9naWNhbCgxTCkpCiAgICAgIG5vbl9jb25zdGFudF9jb2x1bW5zIDw8LSBjKG5vbl9jb25zdGFudF9jb2x1bW5zLCBuYW1lcyhvbGQpW25vbl9jb25zdGFudF0pCiAgICAgICMgRWRpdCA0OiBBZGQgYGtlcHRgIGNvbHVtbnMKICAgICAgdmN0cnM6OnZlY19jYmluZCgKICAgICAgICBuZXcsCiAgICAgICAga2VwdFssIHNldGRpZmYobmFtZXMoa2VwdCksICJ4IiksIGRyb3AgPSBGQUxTRV0sCiAgICAgICAgb2xkW3JlcCgxLCBucm93KG5ldykpLCBzZXRkaWZmKG5hbWVzKG9sZCksIG5hbWVzKGtlcHQpKSwgZHJvcCA9IEZBTFNFXSwKICAgICAgKQogICAgfSwgc3RhdHMsIGdyb3VwcywgU0lNUExJRlkgPSBGQUxTRSkKICAgIAogICAgbm9uX2NvbnN0YW50X2NvbHVtbnMgPC0gZ2dwbG90Mjo6OnVuaXF1ZTAobm9uX2NvbnN0YW50X2NvbHVtbnMpCiAgICBkcm9wcGVkIDwtIG5vbl9jb25zdGFudF9jb2x1bW5zWyFub25fY29uc3RhbnRfY29sdW1ucyAlaW4lIHNlbGYkZHJvcHBlZF9hZXNdCiAgICBpZiAobGVuZ3RoKGRyb3BwZWQpID4gMCkgewogICAgICBjbGk6OmNsaV93YXJuKGMoCiAgICAgICAgIlRoZSBmb2xsb3dpbmcgYWVzdGhldGljcyB3ZXJlIGRyb3BwZWQgZHVyaW5nIHN0YXRpc3RpY2FsIHRyYW5zZm9ybWF0aW9uOiB7LmZpZWxkIHtnbHVlX2NvbGxhcHNlKGRyb3BwZWQsIHNlcCA9ICcsICcpfX0iLAogICAgICAgICJpIiA9ICJUaGlzIGNhbiBoYXBwZW4gd2hlbiBnZ3Bsb3QgZmFpbHMgdG8gaW5mZXIgdGhlIGNvcnJlY3QgZ3JvdXBpbmcgc3RydWN0dXJlIGluIHRoZSBkYXRhLiIsCiAgICAgICAgImkiID0gIkRpZCB5b3UgZm9yZ2V0IHRvIHNwZWNpZnkgYSB7LmNvZGUgZ3JvdXB9IGFlc3RoZXRpYyBvciB0byBjb252ZXJ0IGEgbnVtZXJpY2FsIHZhcmlhYmxlIGludG8gYSBmYWN0b3I/IgogICAgICApKQogICAgfQoKICAgICMgRmluYWxseSwgY29tYmluZSB0aGUgcmVzdWx0cyBhbmQgZHJvcCBjb2x1bW5zIHRoYXQgYXJlIG5vdCBjb25zdGFudC4KICAgIGRhdGFfbmV3IDwtIGdncGxvdDI6Ojp2ZWNfcmJpbmQwKCEhIXN0YXRzKQogICAgZGF0YV9uZXdbLCAhbmFtZXMoZGF0YV9uZXcpICVpbiUgbm9uX2NvbnN0YW50X2NvbHVtbnMsIGRyb3AgPSBGQUxTRV0KICB9CikKYGBgCgo8L2RldGFpbHM+Cgo8YnI+CgpDaGVjayB0aGF0IGBTdGF0Q291bnQyYCBiZWhhdmVzIGFzIGl0IHNob3VsZCB3aXRoIGEgbmV3IGAkY29tcHV0ZV9wYW5lbGA6CgpgYGB7cn0KZmFjZXR0ZWRfc3BhcnNlWF9wbG90ICsKICBnZW9tX2JhcigKICAgIGFlcyh5ID0gYWZ0ZXJfc3RhdChwcm9wKSwgZ3JvdXAgPSAxLCBmaWxsID0gc3BlY2llcyksCiAgICBzdGF0ID0gImNvdW50MiIKICApCmBgYAo=