• 1 getting started
  • 2 Custom functions
  • 3 Packages
  • 4 Input
  • 5 Fetch publications from OpenAlex (Part 1)
    • 5.1 Data wrangling
  • 6 Fetch publications from OpenAlex (Part 2)
    • 6.1 Data wrangling

This script uses OpenAlex to download paper DOIs and associated topics.

1 getting started

2 Custom functions

rm(list=ls())

fsave <- function(x, file = NULL, location = "./data/processed/") {
    ifelse(!dir.exists("data"), dir.create("data"), FALSE)
    ifelse(!dir.exists("data/processed"), dir.create("data/processed"), FALSE)
    if (is.null(file))
        file = deparse(substitute(x))
    datename <- substr(gsub("[:-]", "", Sys.time()), 1, 8)
    totalname <- paste(location, file, "_", datename,  ".rda", sep = "")
    save(x, file = totalname)  #need to fix if file is reloaded as input name, not as x. 
}

3 Packages

Load packages with groundhog, such that we can replicate.

library(groundhog)
packages_to_load <- c("readr", "dplyr", "openalexR",
                      "tidyr", "tidyverse", "tictoc",
                      "reshape", "data.table", "stringdist")
groundhog.library(packages_to_load, date = "2024-07-01")

4 Input

Load the publications from NARCIS and some data manipulation and bugfixes.

load("./data/processed/20240701df_pubs_topic.rda")

# how many papers?
print(paste0("Do we need to fetch ", nrow(x), " works from OpenAlex?"))

# how many papers with DOI?
print(paste0("No, we actually need to fetch ", nrow(x[!is.na(x$DOI), ]), " works from OpenAlex"))

# prep data such that we can fetch from AO
fetch <- x[!is.na(x$DOI), ]

#bugfixes, took me way too long to find out what was wrong...
fetch$DOI <- ifelse(fetch$DOI == "10.1016/j.jclinepi.2016.09,007", "10.1016/j.jclinepi.2016.09.007", fetch$DOI)
fetch$DOI <- ifelse(fetch$DOI == "10.1016/j.medengphy.2015.03,018", "10.1016/j.medengphy.2015.03.018", fetch$DOI)

#didn't do this before hand, should've done it
nrow(unique(fetch[, c("DOI")]))

5 Fetch publications from OpenAlex (Part 1)

We will use DOI in part 1. Some checks before we attempt the entire fetch.

tic()
onethousand <- oa_fetch(
  entity = "works",
  doi = c(unlist(fetch[c(1:1000), c("DOI")])),
  options = list(select = c("doi", "id", "counts_by_year", "publication_year", "title", "concepts", "topics")),
  verbose = FALSE
)
toc()
# 2.2 seconds for 100
# 28 seconds for 1000
# scales pretty linearly? No reason not to.
# on the 1000 about 8 that weren't tracked, or <1%
print(object.size(onethousand), units="Mb")
rm(onethousand)

# Some expectation management
cat(paste0("We need about ", ((174000/1000)*28)/60," minutes to fetch the 174K DOIs. \nObject size will be about ", 9.6*174, " Mbs. \nExpectation is to have <1% missings."))

And now the entire dataset of DOIs we have from NARCIS. I dropped authorships and some other columns that made the object unreasonably large. This object should be about 1.6Gb. And call polite, chunk it up and store it in lists.

# And now we attempt to get them all

# chunk them up for a loop
dois <- c(unlist(fetch[, c("DOI")]))
chunk_number <- 500       
dois <- split(dois,          
          cut(seq_along(dois),
              chunk_number,
          labels = FALSE))


# list to store
works_from_dois <- list()
#load("/Users/u351132/surfdrive/Shared/DDDCovid_surf/data/processed/20240703df_oa_pubs.rda")

tic()
for (i in 1:length(dois)) {
  
  works_from_dois[[i]] <- oa_fetch(
    entity = "works",
    doi = dois[[i]],
    options = list(select = c("doi", "id", "counts_by_year", "publication_year", "title", "concepts", "topics")),
    verbose = FALSE)
  
}
toc()

Save the data that we fetch from OpenAlex.

# save data
fsave(works_from_dois, "df_oa_pubs.rda")

df <- bind_rows(works_from_dois)
fsave(df, "df_oa_pubs_df.rda")

5.1 Data wrangling

Now some crucial data manipulation. We first get topics from the DF we extract from OpenAlex. Then we drop those for which we don’t have any topics. We then attach DOIs and extract columns in which we’re interested. After that, we cast it into a dataframe, drop some columns, only keep the topic that loads best on the paper, keep unique papers, and cast it to a wide dataset.

# 117K unique dois out of 171K fetched dois
nrow(unique(df[,c("doi")]))

# get topics element from df
topics <- df[, c("topics", "doi")]
topics <- topics %>% unnest(topics)
tops <- topics[topics$i == 1, ] # get only the topic that loads heaviest on paper
tops <- data.table(tops) # set to data table
tops <- unique(tops) # keep unique papers

# by topic?
topic <- tops[tops$name == "topic", c("score", "display_name", "doi")]
topic <- topic %>% group_by(doi) %>% top_n(1, score)

# by subfield?
subfield <- tops[tops$name == "subfield", c("score", "display_name", "doi")]
subfield <- subfield %>% group_by(doi) %>% top_n(1, score)

# by field?
fields <- tops[tops$name == "field", c("score", "display_name", "doi")]
fields <- fields %>% group_by(doi) %>% top_n(1, score)

# by domain?
domains <- tops[tops$name == "domain", c("score", "display_name", "doi")]
domains <- domains %>% group_by(doi) %>% top_n(1, score)

# clunky, but it works, join them together
paper_fields <- left_join(domains[, c("display_name", "doi")], fields[, c("display_name", "doi")], by = c("doi" = "doi"))
names(paper_fields) <- c("domain", "doi", "field")
paper_fields <- left_join(paper_fields[, c("domain", "doi", "field")], subfield[, c("display_name", "doi")], by = c("doi" = "doi"))
colnames(paper_fields)[4] <- "subfield"
paper_fields <- left_join(paper_fields[, c("domain", "doi", "field", "subfield")], topic[, c("display_name", "doi")], by = c("doi" = "doi"))
colnames(paper_fields)[5] <- "topic"
paper_fields <- paper_fields[, c("doi", "domain", "field", "subfield", "topic")]

# did everything check out? No missings, exact number of unique cases
nrow(paper_fields[is.na("doi"), ])
nrow(paper_fields[is.na("domain"), ])
nrow(paper_fields[is.na("field"), ])
nrow(paper_fields[is.na("subfield"), ])
nrow(paper_fields[is.na("topic"), ])
nrow(unique(paper_fields))

Now we want to know whether it worked

# did it work?
fcheck <- function(df1, df2, iter) {
  
  x1 <- df1[iter,]
  x2 <- unique(df2[df2$doi == as.character(df1[iter, c("doi")]), c("topics")] %>% unnest(topics))
  x2 <- x2 %>% top_n(1, score)
  x2 <- x2[x2$i == 1, c("name", "display_name")]
  
  return(c(x2[1,2] == x1[1,5],
           x2[2,2] == x1[1,4],
           x2[3,2] == x1[1,3],
           x2[4,2] == x1[1,2]))
  
}

fcheck(paper_fields, df, 8000)
fcheck(paper_fields, df, 25000)

# check! it worked, dataframe and curation I made corresponds with what we fetched from OA

Some data information. We have 206K papers from NARCIS. Of which 174K have a DOI. Of which we can fetch 171K papers from OpenAlex that have a topic load. Ultimately, these are 116K unique papers with a topic load.

#re-introduce the bugs such that we can match to our OA papers
paper_fields$doi <- ifelse(paper_fields$doi == "10.1016/j.jclinepi.2016.09.007", "10.1016/j.jclinepi.2016.09,007", paper_fields$doi)
paper_fields$doi <- ifelse(paper_fields$doi == "10.1016/j.medengphy.2015.03.018", "10.1016/j.medengphy.2015.03,018", paper_fields$doi)

# seperate data
colnames(paper_fields)[3] <- "fields"
fsave(paper_fields, "df_oa_paper_fields.rda")

# match to NARCIS
paper_fields$doi <- gsub("https://doi.org/", "", paper_fields$doi)


x <- left_join(x, paper_fields, by = c("DOI" = "doi"))

# so we have for 172K out of 206K papers domains and topics.
nrow(x[!is.na(x$domain), ])

# how many?
length(table(x$domain))
length(table(x$fields))
length(table(x$subfield))

fsave(x, "tempfields.rda")

6 Fetch publications from OpenAlex (Part 2)

In part 2 we look up those works without DOI and match on title.

We first count the number of hits (such that we don’t download 100k results for a short title). We then download those with <51 hits. We then first consider those with similar publication years and then look for the title match with the nearest edit distances. If that edit distance is >.xx, we consider that lookup title the one we found and matched in OpenAlex.

# and titles, about 28K unique ones
fetch_titles <- x[is.na(x$DOI), ]
fetch_titles <- fetch_titles[, c("Titel_ori", "pub_year")]
nrow(fetch_titles)
fetch_titles <- data.frame(unique(fetch_titles))
fetch_titles$oa_lookup <- gsub('[[:punct:] ]+',' ', fetch_titles$Titel_ori)
fetch_titles$oa_lookup <- gsub("OR", "or", fetch_titles$oa_lookup) # OR is seen as an OR statement break the for loop below

# polite pool
polite <- "bas.hofstra@ru.nl"


# I first count the number of hits, such that we don't have to download MANY results per title
#count_per_title <- list()
tic()
for (i in 27375:nrow(fetch_titles)) {

  count_per_title[[i]] <- oa_fetch(
      entity = "works",
      title.search = fetch_titles[i, c("oa_lookup")],
      count_only = TRUE,
      verbose = FALSE, 
      mailto = polite)
  
    # we attach lookup title and pub year to use later
    count_per_title[[i]] <- data.frame(count_per_title[[i]])
    count_per_title[[i]]["oa_lookup"] <- fetch_titles[i, c("oa_lookup")]
    count_per_title[[i]]["pub_year"] <- fetch_titles[i, c("pub_year")]

}
toc()
fsave(count_per_title, "count_per_title.rda")

So now that we have our counts, we can download the actual metadata.

# once we have the count per lookup title, we download only those with less than 51 hits (i.e., 1 page of)
#works_from_titles <- list()
tic()
for (i in 1:length(count_per_title)) {
  
  if(count_per_title[[i]]["count"] > 0 & count_per_title[[i]]["count"] <51){
    
    works_from_titles[[i]] <- oa_fetch(
      entity = "works",
      title.search = count_per_title[[i]][, c("oa_lookup")],
      options = list(select = c("doi", "id", "counts_by_year", 
                                "publication_year", "title", "concepts", 
                                "topics")),
      verbose = FALSE, 
      mailto = polite)
  
      # we attach lookup title and pub year
      works_from_titles[[i]]["oa_lookup"] <- count_per_title[[i]][, c("oa_lookup")]
      works_from_titles[[i]]["pub_year"] <- count_per_title[[i]][, c("pub_year")]
  }
}
toc()
fsave(works_from_titles, "works_from_titles.rda")

#works_from_titles <- x

Note that some title searched have multiple hits, so we first select those with equal pub years within lookup titles, after which we calculate an edit distance score. We keep the title combos (between OA titles and our lookup titles) with the lowest edit distance.

6.1 Data wrangling

works_from_titles <- x
hit_from_title <- list()
for (i in 1:length(works_from_titles)) {
  
  if(!is.null(works_from_titles[[i]])) {
    
    hit_from_title[[i]] <- works_from_titles[[i]][, c("title", "publication_year", "topics", "oa_lookup", "pub_year")]
    # if we don't do this, data blow up quite a bit
    hit_from_title[[i]] <- hit_from_title[[i]][hit_from_title[[i]]$publication_year == hit_from_title[[i]]$pub_year,]
    
  }
}

# bind rows, find title match closest (edit distance?) within lookup title
works_df <- bind_rows(hit_from_title)
works_df$ld <- stringdist(tolower(gsub('[[:punct:] ]+',' ', works_df$title)), tolower(works_df$oa_lookup), method = "lv")

# then choose lowest distance!
works_df2 <- works_df %>% group_by(oa_lookup) %>% top_n(-1, ld)
length(unique(works_df2$oa_lookup)) == nrow(works_df2)

#multiple lowest? sample
works_df2 <- works_df2 %>% group_by(oa_lookup) %>% slice_sample(n=1)
nrow(works_df2) == length(unique(works_df$oa_lookup))

#only keep those with edit distance <10, from eyeballing, pretty similar!
plot(density(works_df2$ld))
works_df2 <- works_df2[works_df2$ld < 10, ]

# 117K unique dois out of 171K fetched dois


# get topics element from df
topics <- works_df2[, c("topics", "oa_lookup")]
topics <- topics %>% unnest(topics)
tops <- topics[topics$i == 1, ] # get only the topic that loads heaviest on paper
tops <- data.table(tops) # set to data table
tops <- unique(tops) # keep unique papers

# by topic?
topic <- tops[tops$name == "topic", c("score", "display_name", "oa_lookup")]
topic <- topic %>% group_by(oa_lookup) %>% top_n(1, score)

# by subfield?
subfield <- tops[tops$name == "subfield", c("score", "display_name", "oa_lookup")]
subfield <- subfield %>% group_by(oa_lookup) %>% top_n(1, score)

# by field?
fields <- tops[tops$name == "field", c("score", "display_name", "oa_lookup")]
fields <- fields %>% group_by(oa_lookup) %>% top_n(1, score)

# by domain?
domains <- tops[tops$name == "domain", c("score", "display_name", "oa_lookup")]
domains <- domains %>% group_by(oa_lookup) %>% top_n(1, score)

# clunky, but it works, join them together
paper_fields <- left_join(domains[, c("display_name", "oa_lookup")], fields[, c("display_name", "oa_lookup")], by = c("oa_lookup" = "oa_lookup"))
names(paper_fields) <- c("domain", "oa_lookup", "field")
paper_fields <- left_join(paper_fields[, c("domain", "oa_lookup", "field")], subfield[, c("display_name", "oa_lookup")], by = c("oa_lookup" = "oa_lookup"))
colnames(paper_fields)[4] <- "subfield"
paper_fields <- left_join(paper_fields[, c("domain", "oa_lookup", "field", "subfield")], topic[, c("display_name", "oa_lookup")], by = c("oa_lookup" = "oa_lookup"))
colnames(paper_fields)[5] <- "topic"
paper_fields <- paper_fields[, c("oa_lookup", "domain", "field", "subfield", "topic")]
names(paper_fields) <- c("oa_lookup", "domain1", "field1", "subfield1", "topic1")

# did everything check out? No missings, exact number of unique cases
nrow(paper_fields[is.na("oa_lookup"), ])
nrow(paper_fields[is.na("domain1"), ])
nrow(paper_fields[is.na("field1"), ])
nrow(paper_fields[is.na("subfield1"), ])
nrow(paper_fields[is.na("topic1"), ])
nrow(unique(paper_fields))

And match back to original data.

# same operations to get to the oa_lookup variable to match on
x$oa_lookup <- ifelse(is.na(x$DOI), gsub('[[:punct:] ]+',' ', x$Titel_ori), NA)
x$oa_lookup <- ifelse(!is.na(x$oa_lookup), gsub("OR", "or", x$oa_lookup), NA) # OR is seen as an OR statement break the for loop below
nrow(x[!is.na(x$oa_lookup), ])

x <- left_join(x, paper_fields, by = c("oa_lookup" = "oa_lookup"))

a <- nrow(x[!is.na(x$domain),])
x$domain <- ifelse(is.na(x$domain) & !is.na(x$domain1), x$domain1, x$domain)
b <- nrow(x[!is.na(x$domain),])



x$fields <- ifelse(is.na(x$fields) & !is.na(x$field1), x$field1, x$fields)
x$subfield <- ifelse(is.na(x$subfield) & !is.na(x$subfield1), x$subfield1, x$subfield)
x$topic <- ifelse(is.na(x$topic) & !is.na(x$topic1), x$topic1, x$topic)

a
b
b/nrow(x)
fsave(x, "linked_pubs_oa_fields.rda")

LS0tDQp0aXRsZTogIkZldGNoaW5nIE9BIHRvcGljcyINCmJpYmxpb2dyYXBoeTogcmVmZXJlbmNlcy5iaWINCi0tLQ0KDQpUaGlzIHNjcmlwdCB1c2VzIE9wZW5BbGV4IHRvIGRvd25sb2FkIHBhcGVyIERPSXMgYW5kIGFzc29jaWF0ZWQgdG9waWNzLg0KDQojIGdldHRpbmcgc3RhcnRlZCAgDQoNCmBgYHtyLCBnbG9iYWxzZXR0aW5ncywgZWNobz1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoa25pdHIpDQpvcHRzX2NodW5rJHNldCh0aWR5Lm9wdHM9bGlzdCh3aWR0aC5jdXRvZmY9MTAwKSx0aWR5PVRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgZXZhbCA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsY29tbWVudCA9ICIjPiIsIGNhY2hlPVRSVUUsIGNsYXNzLnNvdXJjZT1jKCJ0ZXN0IiksIGNsYXNzLm91dHB1dD1jKCJ0ZXN0MiIpLCBjYWNoZS5sYXp5ID0gRkFMU0UpDQpvcHRpb25zKHdpZHRoID0gMTAwKQ0KcmdsOjpzZXR1cEtuaXRyKCkNCg0KY29sb3JpemUgPC0gZnVuY3Rpb24oeCwgY29sb3IpIHtzcHJpbnRmKCI8c3BhbiBzdHlsZT0nY29sb3I6ICVzOyc+JXM8L3NwYW4+IiwgY29sb3IsIHgpIH0NCg0KYGBgDQoNCmBgYHtyIGtsaXBweSwgZWNobz1GQUxTRSwgaW5jbHVkZT1UUlVFLCBldmFsID0gVH0NCiMgaW5zdGFsbC5wYWNrYWdlcygicmVtb3RlcyIpDQojcmVtb3Rlczo6aW5zdGFsbF9naXRodWIoInJsZXN1ci9rbGlwcHkiKQ0KI2tsaXBweTo6a2xpcHB5KHBvc2l0aW9uID0gYygndG9wJywgJ3JpZ2h0JykpDQoja2xpcHB5OjprbGlwcHkoY29sb3IgPSAnZGFya3JlZCcpDQoja2xpcHB5OjprbGlwcHkodG9vbHRpcF9tZXNzYWdlID0gJ0NsaWNrIHRvIGNvcHknLCB0b29sdGlwX3N1Y2Nlc3MgPSAnRG9uZScpDQpgYGANCg0KIyBDdXN0b20gZnVuY3Rpb25zDQoNCmBgYHtyLCByZXN1bHRzPSdoaWRlJ30NCnJtKGxpc3Q9bHMoKSkNCg0KZnNhdmUgPC0gZnVuY3Rpb24oeCwgZmlsZSA9IE5VTEwsIGxvY2F0aW9uID0gIi4vZGF0YS9wcm9jZXNzZWQvIikgew0KICAgIGlmZWxzZSghZGlyLmV4aXN0cygiZGF0YSIpLCBkaXIuY3JlYXRlKCJkYXRhIiksIEZBTFNFKQ0KICAgIGlmZWxzZSghZGlyLmV4aXN0cygiZGF0YS9wcm9jZXNzZWQiKSwgZGlyLmNyZWF0ZSgiZGF0YS9wcm9jZXNzZWQiKSwgRkFMU0UpDQogICAgaWYgKGlzLm51bGwoZmlsZSkpDQogICAgICAgIGZpbGUgPSBkZXBhcnNlKHN1YnN0aXR1dGUoeCkpDQogICAgZGF0ZW5hbWUgPC0gc3Vic3RyKGdzdWIoIls6LV0iLCAiIiwgU3lzLnRpbWUoKSksIDEsIDgpDQogICAgdG90YWxuYW1lIDwtIHBhc3RlKGxvY2F0aW9uLCBmaWxlLCAiXyIsIGRhdGVuYW1lLCAgIi5yZGEiLCBzZXAgPSAiIikNCiAgICBzYXZlKHgsIGZpbGUgPSB0b3RhbG5hbWUpICAjbmVlZCB0byBmaXggaWYgZmlsZSBpcyByZWxvYWRlZCBhcyBpbnB1dCBuYW1lLCBub3QgYXMgeC4gDQp9DQoNCg0KYGBgDQoNCiMgUGFja2FnZXMgDQoNCkxvYWQgcGFja2FnZXMgd2l0aCBncm91bmRob2csIHN1Y2ggdGhhdCB3ZSBjYW4gcmVwbGljYXRlLg0KDQpgYGB7ciBtZXNzYWdlID0gIEYsIHdhcm5pbmcgPSBGfQ0KbGlicmFyeShncm91bmRob2cpDQpwYWNrYWdlc190b19sb2FkIDwtIGMoInJlYWRyIiwgImRwbHlyIiwgIm9wZW5hbGV4UiIsDQogICAgICAgICAgICAgICAgICAgICAgInRpZHlyIiwgInRpZHl2ZXJzZSIsICJ0aWN0b2MiLA0KICAgICAgICAgICAgICAgICAgICAgICJyZXNoYXBlIiwgImRhdGEudGFibGUiLCAic3RyaW5nZGlzdCIpDQpncm91bmRob2cubGlicmFyeShwYWNrYWdlc190b19sb2FkLCBkYXRlID0gIjIwMjQtMDctMDEiKQ0KYGBgDQoNCi0tLSANCg0KIyBJbnB1dA0KDQpMb2FkIHRoZSBwdWJsaWNhdGlvbnMgZnJvbSBOQVJDSVMgYW5kIHNvbWUgZGF0YSBtYW5pcHVsYXRpb24gYW5kIGJ1Z2ZpeGVzLg0KDQpgYGB7ciBtZXNzYWdlID0gRiwgd2FybmluZyA9IEYsICB9DQpsb2FkKCIuL2RhdGEvcHJvY2Vzc2VkLzIwMjQwNzAxZGZfcHVic190b3BpYy5yZGEiKQ0KDQojIGhvdyBtYW55IHBhcGVycz8NCnByaW50KHBhc3RlMCgiRG8gd2UgbmVlZCB0byBmZXRjaCAiLCBucm93KHgpLCAiIHdvcmtzIGZyb20gT3BlbkFsZXg/IikpDQoNCiMgaG93IG1hbnkgcGFwZXJzIHdpdGggRE9JPw0KcHJpbnQocGFzdGUwKCJObywgd2UgYWN0dWFsbHkgbmVlZCB0byBmZXRjaCAiLCBucm93KHhbIWlzLm5hKHgkRE9JKSwgXSksICIgd29ya3MgZnJvbSBPcGVuQWxleCIpKQ0KDQojIHByZXAgZGF0YSBzdWNoIHRoYXQgd2UgY2FuIGZldGNoIGZyb20gQU8NCmZldGNoIDwtIHhbIWlzLm5hKHgkRE9JKSwgXQ0KDQojYnVnZml4ZXMsIHRvb2sgbWUgd2F5IHRvbyBsb25nIHRvIGZpbmQgb3V0IHdoYXQgd2FzIHdyb25nLi4uDQpmZXRjaCRET0kgPC0gaWZlbHNlKGZldGNoJERPSSA9PSAiMTAuMTAxNi9qLmpjbGluZXBpLjIwMTYuMDksMDA3IiwgIjEwLjEwMTYvai5qY2xpbmVwaS4yMDE2LjA5LjAwNyIsIGZldGNoJERPSSkNCmZldGNoJERPSSA8LSBpZmVsc2UoZmV0Y2gkRE9JID09ICIxMC4xMDE2L2oubWVkZW5ncGh5LjIwMTUuMDMsMDE4IiwgIjEwLjEwMTYvai5tZWRlbmdwaHkuMjAxNS4wMy4wMTgiLCBmZXRjaCRET0kpDQoNCiNkaWRuJ3QgZG8gdGhpcyBiZWZvcmUgaGFuZCwgc2hvdWxkJ3ZlIGRvbmUgaXQNCm5yb3codW5pcXVlKGZldGNoWywgYygiRE9JIildKSkNCmBgYA0KDQojIEZldGNoIHB1YmxpY2F0aW9ucyBmcm9tIE9wZW5BbGV4IChQYXJ0IDEpDQoNCldlIHdpbGwgdXNlIERPSSBpbiBwYXJ0IDEuIA0KU29tZSBjaGVja3MgYmVmb3JlIHdlIGF0dGVtcHQgdGhlIGVudGlyZSBmZXRjaC4NCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGLCBldmFsID0gRn0NCg0KdGljKCkNCm9uZXRob3VzYW5kIDwtIG9hX2ZldGNoKA0KICBlbnRpdHkgPSAid29ya3MiLA0KICBkb2kgPSBjKHVubGlzdChmZXRjaFtjKDE6MTAwMCksIGMoIkRPSSIpXSkpLA0KICBvcHRpb25zID0gbGlzdChzZWxlY3QgPSBjKCJkb2kiLCAiaWQiLCAiY291bnRzX2J5X3llYXIiLCAicHVibGljYXRpb25feWVhciIsICJ0aXRsZSIsICJjb25jZXB0cyIsICJ0b3BpY3MiKSksDQogIHZlcmJvc2UgPSBGQUxTRQ0KKQ0KdG9jKCkNCiMgMi4yIHNlY29uZHMgZm9yIDEwMA0KIyAyOCBzZWNvbmRzIGZvciAxMDAwDQojIHNjYWxlcyBwcmV0dHkgbGluZWFybHk/IE5vIHJlYXNvbiBub3QgdG8uDQojIG9uIHRoZSAxMDAwIGFib3V0IDggdGhhdCB3ZXJlbid0IHRyYWNrZWQsIG9yIDwxJQ0KcHJpbnQob2JqZWN0LnNpemUob25ldGhvdXNhbmQpLCB1bml0cz0iTWIiKQ0Kcm0ob25ldGhvdXNhbmQpDQoNCiMgU29tZSBleHBlY3RhdGlvbiBtYW5hZ2VtZW50DQpjYXQocGFzdGUwKCJXZSBuZWVkIGFib3V0ICIsICgoMTc0MDAwLzEwMDApKjI4KS82MCwiIG1pbnV0ZXMgdG8gZmV0Y2ggdGhlIDE3NEsgRE9Jcy4gXG5PYmplY3Qgc2l6ZSB3aWxsIGJlIGFib3V0ICIsIDkuNioxNzQsICIgTWJzLiBcbkV4cGVjdGF0aW9uIGlzIHRvIGhhdmUgPDElIG1pc3NpbmdzLiIpKQ0KDQoNCmBgYA0KDQpBbmQgbm93IHRoZSBlbnRpcmUgZGF0YXNldCBvZiBET0lzIHdlIGhhdmUgZnJvbSBOQVJDSVMuIEkgZHJvcHBlZCBhdXRob3JzaGlwcyBhbmQgc29tZSBvdGhlciBjb2x1bW5zIHRoYXQgbWFkZSB0aGUgb2JqZWN0IHVucmVhc29uYWJseSBsYXJnZS4gVGhpcyBvYmplY3Qgc2hvdWxkIGJlIGFib3V0IDEuNkdiLiBBbmQgY2FsbCBwb2xpdGUsIGNodW5rIGl0IHVwIGFuZCBzdG9yZSBpdCBpbiBsaXN0cy4NCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGLCBldmFsID0gRn0NCiMgQW5kIG5vdyB3ZSBhdHRlbXB0IHRvIGdldCB0aGVtIGFsbA0KDQojIGNodW5rIHRoZW0gdXAgZm9yIGEgbG9vcA0KZG9pcyA8LSBjKHVubGlzdChmZXRjaFssIGMoIkRPSSIpXSkpDQpjaHVua19udW1iZXIgPC0gNTAwICAgICAgIA0KZG9pcyA8LSBzcGxpdChkb2lzLCAgICAgICAgICANCiAgICAgICAgICBjdXQoc2VxX2Fsb25nKGRvaXMpLA0KICAgICAgICAgICAgICBjaHVua19udW1iZXIsDQogICAgICAgICAgbGFiZWxzID0gRkFMU0UpKQ0KDQoNCiMgbGlzdCB0byBzdG9yZQ0Kd29ya3NfZnJvbV9kb2lzIDwtIGxpc3QoKQ0KI2xvYWQoIi9Vc2Vycy91MzUxMTMyL3N1cmZkcml2ZS9TaGFyZWQvREREQ292aWRfc3VyZi9kYXRhL3Byb2Nlc3NlZC8yMDI0MDcwM2RmX29hX3B1YnMucmRhIikNCg0KdGljKCkNCmZvciAoaSBpbiAxOmxlbmd0aChkb2lzKSkgew0KICANCiAgd29ya3NfZnJvbV9kb2lzW1tpXV0gPC0gb2FfZmV0Y2goDQogICAgZW50aXR5ID0gIndvcmtzIiwNCiAgICBkb2kgPSBkb2lzW1tpXV0sDQogICAgb3B0aW9ucyA9IGxpc3Qoc2VsZWN0ID0gYygiZG9pIiwgImlkIiwgImNvdW50c19ieV95ZWFyIiwgInB1YmxpY2F0aW9uX3llYXIiLCAidGl0bGUiLCAiY29uY2VwdHMiLCAidG9waWNzIikpLA0KICAgIHZlcmJvc2UgPSBGQUxTRSkNCiAgDQp9DQp0b2MoKQ0KDQoNCmBgYA0KDQpTYXZlIHRoZSBkYXRhIHRoYXQgd2UgZmV0Y2ggZnJvbSBPcGVuQWxleC4gDQoNCmBgYHtyIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRiwgZXZhbCA9IEZ9DQoNCiMgc2F2ZSBkYXRhDQpmc2F2ZSh3b3Jrc19mcm9tX2RvaXMsICJkZl9vYV9wdWJzLnJkYSIpDQoNCmRmIDwtIGJpbmRfcm93cyh3b3Jrc19mcm9tX2RvaXMpDQpmc2F2ZShkZiwgImRmX29hX3B1YnNfZGYucmRhIikNCg0KYGBgDQoNCiMjIERhdGEgd3JhbmdsaW5nICANCg0KTm93IHNvbWUgY3J1Y2lhbCBkYXRhIG1hbmlwdWxhdGlvbi4gV2UgZmlyc3QgZ2V0IHRvcGljcyBmcm9tIHRoZSBERiB3ZSBleHRyYWN0IGZyb20gT3BlbkFsZXguIFRoZW4gd2UgZHJvcCB0aG9zZSBmb3Igd2hpY2ggd2UgZG9uJ3QgaGF2ZSBhbnkgdG9waWNzLiBXZSB0aGVuIGF0dGFjaCBET0lzIGFuZCBleHRyYWN0IGNvbHVtbnMgaW4gd2hpY2ggd2UncmUgaW50ZXJlc3RlZC4gQWZ0ZXIgdGhhdCwgd2UgY2FzdCBpdCBpbnRvIGEgZGF0YWZyYW1lLCBkcm9wIHNvbWUgY29sdW1ucywgb25seSBrZWVwIHRoZSB0b3BpYyB0aGF0IGxvYWRzIGJlc3Qgb24gdGhlIHBhcGVyLCBrZWVwIHVuaXF1ZSBwYXBlcnMsIGFuZCBjYXN0IGl0IHRvIGEgd2lkZSBkYXRhc2V0Lg0KDQoNCmBgYHtyIGluY2x1ZGUgPSBGLCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEYgIH0NCmxvYWQoIi4vZGF0YS9wcm9jZXNzZWQvMjAyNDA3MDNkZl9vYV9wdWJzX2RmLnJkYSIpDQpsb2FkKCIuL2RhdGEvcHJvY2Vzc2VkLzIwMjQwNzAxZGZfcHVic190b3BpYy5yZGEiKQ0KYGBgDQoNCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGICB9DQoNCiMgMTE3SyB1bmlxdWUgZG9pcyBvdXQgb2YgMTcxSyBmZXRjaGVkIGRvaXMNCm5yb3codW5pcXVlKGRmWyxjKCJkb2kiKV0pKQ0KDQojIGdldCB0b3BpY3MgZWxlbWVudCBmcm9tIGRmDQp0b3BpY3MgPC0gZGZbLCBjKCJ0b3BpY3MiLCAiZG9pIildDQp0b3BpY3MgPC0gdG9waWNzICU+JSB1bm5lc3QodG9waWNzKQ0KdG9wcyA8LSB0b3BpY3NbdG9waWNzJGkgPT0gMSwgXSAjIGdldCBvbmx5IHRoZSB0b3BpYyB0aGF0IGxvYWRzIGhlYXZpZXN0IG9uIHBhcGVyDQp0b3BzIDwtIGRhdGEudGFibGUodG9wcykgIyBzZXQgdG8gZGF0YSB0YWJsZQ0KdG9wcyA8LSB1bmlxdWUodG9wcykgIyBrZWVwIHVuaXF1ZSBwYXBlcnMNCg0KIyBieSB0b3BpYz8NCnRvcGljIDwtIHRvcHNbdG9wcyRuYW1lID09ICJ0b3BpYyIsIGMoInNjb3JlIiwgImRpc3BsYXlfbmFtZSIsICJkb2kiKV0NCnRvcGljIDwtIHRvcGljICU+JSBncm91cF9ieShkb2kpICU+JSB0b3BfbigxLCBzY29yZSkNCg0KIyBieSBzdWJmaWVsZD8NCnN1YmZpZWxkIDwtIHRvcHNbdG9wcyRuYW1lID09ICJzdWJmaWVsZCIsIGMoInNjb3JlIiwgImRpc3BsYXlfbmFtZSIsICJkb2kiKV0NCnN1YmZpZWxkIDwtIHN1YmZpZWxkICU+JSBncm91cF9ieShkb2kpICU+JSB0b3BfbigxLCBzY29yZSkNCg0KIyBieSBmaWVsZD8NCmZpZWxkcyA8LSB0b3BzW3RvcHMkbmFtZSA9PSAiZmllbGQiLCBjKCJzY29yZSIsICJkaXNwbGF5X25hbWUiLCAiZG9pIildDQpmaWVsZHMgPC0gZmllbGRzICU+JSBncm91cF9ieShkb2kpICU+JSB0b3BfbigxLCBzY29yZSkNCg0KIyBieSBkb21haW4/DQpkb21haW5zIDwtIHRvcHNbdG9wcyRuYW1lID09ICJkb21haW4iLCBjKCJzY29yZSIsICJkaXNwbGF5X25hbWUiLCAiZG9pIildDQpkb21haW5zIDwtIGRvbWFpbnMgJT4lIGdyb3VwX2J5KGRvaSkgJT4lIHRvcF9uKDEsIHNjb3JlKQ0KDQojIGNsdW5reSwgYnV0IGl0IHdvcmtzLCBqb2luIHRoZW0gdG9nZXRoZXINCnBhcGVyX2ZpZWxkcyA8LSBsZWZ0X2pvaW4oZG9tYWluc1ssIGMoImRpc3BsYXlfbmFtZSIsICJkb2kiKV0sIGZpZWxkc1ssIGMoImRpc3BsYXlfbmFtZSIsICJkb2kiKV0sIGJ5ID0gYygiZG9pIiA9ICJkb2kiKSkNCm5hbWVzKHBhcGVyX2ZpZWxkcykgPC0gYygiZG9tYWluIiwgImRvaSIsICJmaWVsZCIpDQpwYXBlcl9maWVsZHMgPC0gbGVmdF9qb2luKHBhcGVyX2ZpZWxkc1ssIGMoImRvbWFpbiIsICJkb2kiLCAiZmllbGQiKV0sIHN1YmZpZWxkWywgYygiZGlzcGxheV9uYW1lIiwgImRvaSIpXSwgYnkgPSBjKCJkb2kiID0gImRvaSIpKQ0KY29sbmFtZXMocGFwZXJfZmllbGRzKVs0XSA8LSAic3ViZmllbGQiDQpwYXBlcl9maWVsZHMgPC0gbGVmdF9qb2luKHBhcGVyX2ZpZWxkc1ssIGMoImRvbWFpbiIsICJkb2kiLCAiZmllbGQiLCAic3ViZmllbGQiKV0sIHRvcGljWywgYygiZGlzcGxheV9uYW1lIiwgImRvaSIpXSwgYnkgPSBjKCJkb2kiID0gImRvaSIpKQ0KY29sbmFtZXMocGFwZXJfZmllbGRzKVs1XSA8LSAidG9waWMiDQpwYXBlcl9maWVsZHMgPC0gcGFwZXJfZmllbGRzWywgYygiZG9pIiwgImRvbWFpbiIsICJmaWVsZCIsICJzdWJmaWVsZCIsICJ0b3BpYyIpXQ0KDQojIGRpZCBldmVyeXRoaW5nIGNoZWNrIG91dD8gTm8gbWlzc2luZ3MsIGV4YWN0IG51bWJlciBvZiB1bmlxdWUgY2FzZXMNCm5yb3cocGFwZXJfZmllbGRzW2lzLm5hKCJkb2kiKSwgXSkNCm5yb3cocGFwZXJfZmllbGRzW2lzLm5hKCJkb21haW4iKSwgXSkNCm5yb3cocGFwZXJfZmllbGRzW2lzLm5hKCJmaWVsZCIpLCBdKQ0KbnJvdyhwYXBlcl9maWVsZHNbaXMubmEoInN1YmZpZWxkIiksIF0pDQpucm93KHBhcGVyX2ZpZWxkc1tpcy5uYSgidG9waWMiKSwgXSkNCm5yb3codW5pcXVlKHBhcGVyX2ZpZWxkcykpDQoNCmBgYA0KDQpOb3cgd2Ugd2FudCB0byBrbm93IHdoZXRoZXIgaXQgd29ya2VkDQoNCmBgYHtyIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRiAgfQ0KIyBkaWQgaXQgd29yaz8NCmZjaGVjayA8LSBmdW5jdGlvbihkZjEsIGRmMiwgaXRlcikgew0KICANCiAgeDEgPC0gZGYxW2l0ZXIsXQ0KICB4MiA8LSB1bmlxdWUoZGYyW2RmMiRkb2kgPT0gYXMuY2hhcmFjdGVyKGRmMVtpdGVyLCBjKCJkb2kiKV0pLCBjKCJ0b3BpY3MiKV0gJT4lIHVubmVzdCh0b3BpY3MpKQ0KICB4MiA8LSB4MiAlPiUgdG9wX24oMSwgc2NvcmUpDQogIHgyIDwtIHgyW3gyJGkgPT0gMSwgYygibmFtZSIsICJkaXNwbGF5X25hbWUiKV0NCiAgDQogIHJldHVybihjKHgyWzEsMl0gPT0geDFbMSw1XSwNCiAgICAgICAgICAgeDJbMiwyXSA9PSB4MVsxLDRdLA0KICAgICAgICAgICB4MlszLDJdID09IHgxWzEsM10sDQogICAgICAgICAgIHgyWzQsMl0gPT0geDFbMSwyXSkpDQogIA0KfQ0KDQpmY2hlY2socGFwZXJfZmllbGRzLCBkZiwgODAwMCkNCmZjaGVjayhwYXBlcl9maWVsZHMsIGRmLCAyNTAwMCkNCg0KIyBjaGVjayEgaXQgd29ya2VkLCBkYXRhZnJhbWUgYW5kIGN1cmF0aW9uIEkgbWFkZSBjb3JyZXNwb25kcyB3aXRoIHdoYXQgd2UgZmV0Y2hlZCBmcm9tIE9BDQoNCg0KYGBgDQoNClNvbWUgZGF0YSBpbmZvcm1hdGlvbi4gV2UgaGF2ZSAyMDZLIHBhcGVycyBmcm9tIE5BUkNJUy4gT2Ygd2hpY2ggMTc0SyBoYXZlIGEgRE9JLiBPZiB3aGljaCB3ZSBjYW4gZmV0Y2ggMTcxSyBwYXBlcnMgZnJvbSBPcGVuQWxleCB0aGF0IGhhdmUgYSB0b3BpYyBsb2FkLiBVbHRpbWF0ZWx5LCB0aGVzZSBhcmUgMTE2SyB1bmlxdWUgcGFwZXJzIHdpdGggYSB0b3BpYyBsb2FkLiANCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGICB9DQoNCiNyZS1pbnRyb2R1Y2UgdGhlIGJ1Z3Mgc3VjaCB0aGF0IHdlIGNhbiBtYXRjaCB0byBvdXIgT0EgcGFwZXJzDQpwYXBlcl9maWVsZHMkZG9pIDwtIGlmZWxzZShwYXBlcl9maWVsZHMkZG9pID09ICIxMC4xMDE2L2ouamNsaW5lcGkuMjAxNi4wOS4wMDciLCAiMTAuMTAxNi9qLmpjbGluZXBpLjIwMTYuMDksMDA3IiwgcGFwZXJfZmllbGRzJGRvaSkNCnBhcGVyX2ZpZWxkcyRkb2kgPC0gaWZlbHNlKHBhcGVyX2ZpZWxkcyRkb2kgPT0gIjEwLjEwMTYvai5tZWRlbmdwaHkuMjAxNS4wMy4wMTgiLCAiMTAuMTAxNi9qLm1lZGVuZ3BoeS4yMDE1LjAzLDAxOCIsIHBhcGVyX2ZpZWxkcyRkb2kpDQoNCiMgc2VwZXJhdGUgZGF0YQ0KY29sbmFtZXMocGFwZXJfZmllbGRzKVszXSA8LSAiZmllbGRzIg0KZnNhdmUocGFwZXJfZmllbGRzLCAiZGZfb2FfcGFwZXJfZmllbGRzLnJkYSIpDQoNCiMgbWF0Y2ggdG8gTkFSQ0lTDQpwYXBlcl9maWVsZHMkZG9pIDwtIGdzdWIoImh0dHBzOi8vZG9pLm9yZy8iLCAiIiwgcGFwZXJfZmllbGRzJGRvaSkNCg0KDQp4IDwtIGxlZnRfam9pbih4LCBwYXBlcl9maWVsZHMsIGJ5ID0gYygiRE9JIiA9ICJkb2kiKSkNCg0KIyBzbyB3ZSBoYXZlIGZvciAxNzJLIG91dCBvZiAyMDZLIHBhcGVycyBkb21haW5zIGFuZCB0b3BpY3MuDQpucm93KHhbIWlzLm5hKHgkZG9tYWluKSwgXSkNCg0KIyBob3cgbWFueT8NCmxlbmd0aCh0YWJsZSh4JGRvbWFpbikpDQpsZW5ndGgodGFibGUoeCRmaWVsZHMpKQ0KbGVuZ3RoKHRhYmxlKHgkc3ViZmllbGQpKQ0KDQpmc2F2ZSh4LCAidGVtcGZpZWxkcy5yZGEiKQ0KYGBgDQoNCiMgRmV0Y2ggcHVibGljYXRpb25zIGZyb20gT3BlbkFsZXggKFBhcnQgMikgIA0KDQpJbiBwYXJ0IDIgd2UgbG9vayB1cCB0aG9zZSB3b3JrcyB3aXRob3V0IERPSSBhbmQgbWF0Y2ggb24gdGl0bGUuIA0KDQpXZSBmaXJzdCBjb3VudCB0aGUgbnVtYmVyIG9mIGhpdHMgKHN1Y2ggdGhhdCB3ZSBkb24ndCBkb3dubG9hZCAxMDBrIHJlc3VsdHMgZm9yIGEgc2hvcnQgdGl0bGUpLiBXZSB0aGVuIGRvd25sb2FkIHRob3NlIHdpdGggPDUxIGhpdHMuIFdlIHRoZW4gZmlyc3QgY29uc2lkZXIgdGhvc2Ugd2l0aCBzaW1pbGFyIHB1YmxpY2F0aW9uIHllYXJzIGFuZCB0aGVuIGxvb2sgZm9yIHRoZSB0aXRsZSBtYXRjaCB3aXRoIHRoZSBuZWFyZXN0IGVkaXQgZGlzdGFuY2VzLiBJZiB0aGF0IGVkaXQgZGlzdGFuY2UgaXMgPi54eCwgd2UgY29uc2lkZXIgdGhhdCBsb29rdXAgdGl0bGUgdGhlIG9uZSB3ZSBmb3VuZCBhbmQgbWF0Y2hlZCBpbiBPcGVuQWxleC4NCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGLCBldmFsID0gRn0NCiMgYW5kIHRpdGxlcywgYWJvdXQgMjhLIHVuaXF1ZSBvbmVzDQpmZXRjaF90aXRsZXMgPC0geFtpcy5uYSh4JERPSSksIF0NCmZldGNoX3RpdGxlcyA8LSBmZXRjaF90aXRsZXNbLCBjKCJUaXRlbF9vcmkiLCAicHViX3llYXIiKV0NCm5yb3coZmV0Y2hfdGl0bGVzKQ0KZmV0Y2hfdGl0bGVzIDwtIGRhdGEuZnJhbWUodW5pcXVlKGZldGNoX3RpdGxlcykpDQpmZXRjaF90aXRsZXMkb2FfbG9va3VwIDwtIGdzdWIoJ1tbOnB1bmN0Ol0gXSsnLCcgJywgZmV0Y2hfdGl0bGVzJFRpdGVsX29yaSkNCmZldGNoX3RpdGxlcyRvYV9sb29rdXAgPC0gZ3N1YigiT1IiLCAib3IiLCBmZXRjaF90aXRsZXMkb2FfbG9va3VwKSAjIE9SIGlzIHNlZW4gYXMgYW4gT1Igc3RhdGVtZW50IGJyZWFrIHRoZSBmb3IgbG9vcCBiZWxvdw0KDQojIHBvbGl0ZSBwb29sDQpwb2xpdGUgPC0gImJhcy5ob2ZzdHJhQHJ1Lm5sIg0KDQoNCiMgSSBmaXJzdCBjb3VudCB0aGUgbnVtYmVyIG9mIGhpdHMsIHN1Y2ggdGhhdCB3ZSBkb24ndCBoYXZlIHRvIGRvd25sb2FkIE1BTlkgcmVzdWx0cyBwZXIgdGl0bGUNCiNjb3VudF9wZXJfdGl0bGUgPC0gbGlzdCgpDQp0aWMoKQ0KZm9yIChpIGluIDI3Mzc1Om5yb3coZmV0Y2hfdGl0bGVzKSkgew0KDQogIGNvdW50X3Blcl90aXRsZVtbaV1dIDwtIG9hX2ZldGNoKA0KICAgICAgZW50aXR5ID0gIndvcmtzIiwNCiAgICAgIHRpdGxlLnNlYXJjaCA9IGZldGNoX3RpdGxlc1tpLCBjKCJvYV9sb29rdXAiKV0sDQogICAgICBjb3VudF9vbmx5ID0gVFJVRSwNCiAgICAgIHZlcmJvc2UgPSBGQUxTRSwgDQogICAgICBtYWlsdG8gPSBwb2xpdGUpDQogIA0KICAgICMgd2UgYXR0YWNoIGxvb2t1cCB0aXRsZSBhbmQgcHViIHllYXIgdG8gdXNlIGxhdGVyDQogICAgY291bnRfcGVyX3RpdGxlW1tpXV0gPC0gZGF0YS5mcmFtZShjb3VudF9wZXJfdGl0bGVbW2ldXSkNCiAgICBjb3VudF9wZXJfdGl0bGVbW2ldXVsib2FfbG9va3VwIl0gPC0gZmV0Y2hfdGl0bGVzW2ksIGMoIm9hX2xvb2t1cCIpXQ0KICAgIGNvdW50X3Blcl90aXRsZVtbaV1dWyJwdWJfeWVhciJdIDwtIGZldGNoX3RpdGxlc1tpLCBjKCJwdWJfeWVhciIpXQ0KDQp9DQp0b2MoKQ0KZnNhdmUoY291bnRfcGVyX3RpdGxlLCAiY291bnRfcGVyX3RpdGxlLnJkYSIpDQpgYGANCg0KU28gbm93IHRoYXQgd2UgaGF2ZSBvdXIgY291bnRzLCB3ZSBjYW4gZG93bmxvYWQgdGhlIGFjdHVhbCBtZXRhZGF0YS4NCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGLCBldmFsID0gRn0NCiMgb25jZSB3ZSBoYXZlIHRoZSBjb3VudCBwZXIgbG9va3VwIHRpdGxlLCB3ZSBkb3dubG9hZCBvbmx5IHRob3NlIHdpdGggbGVzcyB0aGFuIDUxIGhpdHMgKGkuZS4sIDEgcGFnZSBvZikNCiN3b3Jrc19mcm9tX3RpdGxlcyA8LSBsaXN0KCkNCnRpYygpDQpmb3IgKGkgaW4gMTpsZW5ndGgoY291bnRfcGVyX3RpdGxlKSkgew0KICANCiAgaWYoY291bnRfcGVyX3RpdGxlW1tpXV1bImNvdW50Il0gPiAwICYgY291bnRfcGVyX3RpdGxlW1tpXV1bImNvdW50Il0gPDUxKXsNCiAgICANCiAgICB3b3Jrc19mcm9tX3RpdGxlc1tbaV1dIDwtIG9hX2ZldGNoKA0KICAgICAgZW50aXR5ID0gIndvcmtzIiwNCiAgICAgIHRpdGxlLnNlYXJjaCA9IGNvdW50X3Blcl90aXRsZVtbaV1dWywgYygib2FfbG9va3VwIildLA0KICAgICAgb3B0aW9ucyA9IGxpc3Qoc2VsZWN0ID0gYygiZG9pIiwgImlkIiwgImNvdW50c19ieV95ZWFyIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNhdGlvbl95ZWFyIiwgInRpdGxlIiwgImNvbmNlcHRzIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0b3BpY3MiKSksDQogICAgICB2ZXJib3NlID0gRkFMU0UsIA0KICAgICAgbWFpbHRvID0gcG9saXRlKQ0KICANCiAgICAgICMgd2UgYXR0YWNoIGxvb2t1cCB0aXRsZSBhbmQgcHViIHllYXINCiAgICAgIHdvcmtzX2Zyb21fdGl0bGVzW1tpXV1bIm9hX2xvb2t1cCJdIDwtIGNvdW50X3Blcl90aXRsZVtbaV1dWywgYygib2FfbG9va3VwIildDQogICAgICB3b3Jrc19mcm9tX3RpdGxlc1tbaV1dWyJwdWJfeWVhciJdIDwtIGNvdW50X3Blcl90aXRsZVtbaV1dWywgYygicHViX3llYXIiKV0NCiAgfQ0KfQ0KdG9jKCkNCmZzYXZlKHdvcmtzX2Zyb21fdGl0bGVzLCAid29ya3NfZnJvbV90aXRsZXMucmRhIikNCg0KI3dvcmtzX2Zyb21fdGl0bGVzIDwtIHgNCmBgYA0KDQpOb3RlIHRoYXQgc29tZSB0aXRsZSBzZWFyY2hlZCBoYXZlIG11bHRpcGxlIGhpdHMsIHNvIHdlIGZpcnN0IHNlbGVjdCB0aG9zZSB3aXRoIGVxdWFsIHB1YiB5ZWFycyB3aXRoaW4gbG9va3VwIHRpdGxlcywgYWZ0ZXIgd2hpY2ggd2UgY2FsY3VsYXRlIGFuIGVkaXQgZGlzdGFuY2Ugc2NvcmUuIFdlIGtlZXAgdGhlIHRpdGxlIGNvbWJvcyAoYmV0d2VlbiBPQSB0aXRsZXMgYW5kIG91ciBsb29rdXAgdGl0bGVzKSB3aXRoIHRoZSBsb3dlc3QgZWRpdCBkaXN0YW5jZS4NCg0KIyMgRGF0YSB3cmFuZ2xpbmcNCg0KYGBge3IgaW5jbHVkZSA9IEYsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRiAgfQ0KbG9hZCgiLi9kYXRhL3Byb2Nlc3NlZC8yMDI0MDcxMXdvcmtzX2Zyb21fdGl0bGVzLnJkYSIpDQpgYGANCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGLCAgfQ0Kd29ya3NfZnJvbV90aXRsZXMgPC0geA0KaGl0X2Zyb21fdGl0bGUgPC0gbGlzdCgpDQpmb3IgKGkgaW4gMTpsZW5ndGgod29ya3NfZnJvbV90aXRsZXMpKSB7DQogIA0KICBpZighaXMubnVsbCh3b3Jrc19mcm9tX3RpdGxlc1tbaV1dKSkgew0KICAgIA0KICAgIGhpdF9mcm9tX3RpdGxlW1tpXV0gPC0gd29ya3NfZnJvbV90aXRsZXNbW2ldXVssIGMoInRpdGxlIiwgInB1YmxpY2F0aW9uX3llYXIiLCAidG9waWNzIiwgIm9hX2xvb2t1cCIsICJwdWJfeWVhciIpXQ0KICAgICMgaWYgd2UgZG9uJ3QgZG8gdGhpcywgZGF0YSBibG93IHVwIHF1aXRlIGEgYml0DQogICAgaGl0X2Zyb21fdGl0bGVbW2ldXSA8LSBoaXRfZnJvbV90aXRsZVtbaV1dW2hpdF9mcm9tX3RpdGxlW1tpXV0kcHVibGljYXRpb25feWVhciA9PSBoaXRfZnJvbV90aXRsZVtbaV1dJHB1Yl95ZWFyLF0NCiAgICANCiAgfQ0KfQ0KDQojIGJpbmQgcm93cywgZmluZCB0aXRsZSBtYXRjaCBjbG9zZXN0IChlZGl0IGRpc3RhbmNlPykgd2l0aGluIGxvb2t1cCB0aXRsZQ0Kd29ya3NfZGYgPC0gYmluZF9yb3dzKGhpdF9mcm9tX3RpdGxlKQ0Kd29ya3NfZGYkbGQgPC0gc3RyaW5nZGlzdCh0b2xvd2VyKGdzdWIoJ1tbOnB1bmN0Ol0gXSsnLCcgJywgd29ya3NfZGYkdGl0bGUpKSwgdG9sb3dlcih3b3Jrc19kZiRvYV9sb29rdXApLCBtZXRob2QgPSAibHYiKQ0KDQojIHRoZW4gY2hvb3NlIGxvd2VzdCBkaXN0YW5jZSENCndvcmtzX2RmMiA8LSB3b3Jrc19kZiAlPiUgZ3JvdXBfYnkob2FfbG9va3VwKSAlPiUgdG9wX24oLTEsIGxkKQ0KbGVuZ3RoKHVuaXF1ZSh3b3Jrc19kZjIkb2FfbG9va3VwKSkgPT0gbnJvdyh3b3Jrc19kZjIpDQoNCiNtdWx0aXBsZSBsb3dlc3Q/IHNhbXBsZQ0Kd29ya3NfZGYyIDwtIHdvcmtzX2RmMiAlPiUgZ3JvdXBfYnkob2FfbG9va3VwKSAlPiUgc2xpY2Vfc2FtcGxlKG49MSkNCm5yb3cod29ya3NfZGYyKSA9PSBsZW5ndGgodW5pcXVlKHdvcmtzX2RmJG9hX2xvb2t1cCkpDQoNCiNvbmx5IGtlZXAgdGhvc2Ugd2l0aCBlZGl0IGRpc3RhbmNlIDwxMCwgZnJvbSBleWViYWxsaW5nLCBwcmV0dHkgc2ltaWxhciENCnBsb3QoZGVuc2l0eSh3b3Jrc19kZjIkbGQpKQ0Kd29ya3NfZGYyIDwtIHdvcmtzX2RmMlt3b3Jrc19kZjIkbGQgPCAxMCwgXQ0KDQojIDExN0sgdW5pcXVlIGRvaXMgb3V0IG9mIDE3MUsgZmV0Y2hlZCBkb2lzDQoNCg0KIyBnZXQgdG9waWNzIGVsZW1lbnQgZnJvbSBkZg0KdG9waWNzIDwtIHdvcmtzX2RmMlssIGMoInRvcGljcyIsICJvYV9sb29rdXAiKV0NCnRvcGljcyA8LSB0b3BpY3MgJT4lIHVubmVzdCh0b3BpY3MpDQp0b3BzIDwtIHRvcGljc1t0b3BpY3MkaSA9PSAxLCBdICMgZ2V0IG9ubHkgdGhlIHRvcGljIHRoYXQgbG9hZHMgaGVhdmllc3Qgb24gcGFwZXINCnRvcHMgPC0gZGF0YS50YWJsZSh0b3BzKSAjIHNldCB0byBkYXRhIHRhYmxlDQp0b3BzIDwtIHVuaXF1ZSh0b3BzKSAjIGtlZXAgdW5pcXVlIHBhcGVycw0KDQojIGJ5IHRvcGljPw0KdG9waWMgPC0gdG9wc1t0b3BzJG5hbWUgPT0gInRvcGljIiwgYygic2NvcmUiLCAiZGlzcGxheV9uYW1lIiwgIm9hX2xvb2t1cCIpXQ0KdG9waWMgPC0gdG9waWMgJT4lIGdyb3VwX2J5KG9hX2xvb2t1cCkgJT4lIHRvcF9uKDEsIHNjb3JlKQ0KDQojIGJ5IHN1YmZpZWxkPw0Kc3ViZmllbGQgPC0gdG9wc1t0b3BzJG5hbWUgPT0gInN1YmZpZWxkIiwgYygic2NvcmUiLCAiZGlzcGxheV9uYW1lIiwgIm9hX2xvb2t1cCIpXQ0Kc3ViZmllbGQgPC0gc3ViZmllbGQgJT4lIGdyb3VwX2J5KG9hX2xvb2t1cCkgJT4lIHRvcF9uKDEsIHNjb3JlKQ0KDQojIGJ5IGZpZWxkPw0KZmllbGRzIDwtIHRvcHNbdG9wcyRuYW1lID09ICJmaWVsZCIsIGMoInNjb3JlIiwgImRpc3BsYXlfbmFtZSIsICJvYV9sb29rdXAiKV0NCmZpZWxkcyA8LSBmaWVsZHMgJT4lIGdyb3VwX2J5KG9hX2xvb2t1cCkgJT4lIHRvcF9uKDEsIHNjb3JlKQ0KDQojIGJ5IGRvbWFpbj8NCmRvbWFpbnMgPC0gdG9wc1t0b3BzJG5hbWUgPT0gImRvbWFpbiIsIGMoInNjb3JlIiwgImRpc3BsYXlfbmFtZSIsICJvYV9sb29rdXAiKV0NCmRvbWFpbnMgPC0gZG9tYWlucyAlPiUgZ3JvdXBfYnkob2FfbG9va3VwKSAlPiUgdG9wX24oMSwgc2NvcmUpDQoNCiMgY2x1bmt5LCBidXQgaXQgd29ya3MsIGpvaW4gdGhlbSB0b2dldGhlcg0KcGFwZXJfZmllbGRzIDwtIGxlZnRfam9pbihkb21haW5zWywgYygiZGlzcGxheV9uYW1lIiwgIm9hX2xvb2t1cCIpXSwgZmllbGRzWywgYygiZGlzcGxheV9uYW1lIiwgIm9hX2xvb2t1cCIpXSwgYnkgPSBjKCJvYV9sb29rdXAiID0gIm9hX2xvb2t1cCIpKQ0KbmFtZXMocGFwZXJfZmllbGRzKSA8LSBjKCJkb21haW4iLCAib2FfbG9va3VwIiwgImZpZWxkIikNCnBhcGVyX2ZpZWxkcyA8LSBsZWZ0X2pvaW4ocGFwZXJfZmllbGRzWywgYygiZG9tYWluIiwgIm9hX2xvb2t1cCIsICJmaWVsZCIpXSwgc3ViZmllbGRbLCBjKCJkaXNwbGF5X25hbWUiLCAib2FfbG9va3VwIildLCBieSA9IGMoIm9hX2xvb2t1cCIgPSAib2FfbG9va3VwIikpDQpjb2xuYW1lcyhwYXBlcl9maWVsZHMpWzRdIDwtICJzdWJmaWVsZCINCnBhcGVyX2ZpZWxkcyA8LSBsZWZ0X2pvaW4ocGFwZXJfZmllbGRzWywgYygiZG9tYWluIiwgIm9hX2xvb2t1cCIsICJmaWVsZCIsICJzdWJmaWVsZCIpXSwgdG9waWNbLCBjKCJkaXNwbGF5X25hbWUiLCAib2FfbG9va3VwIildLCBieSA9IGMoIm9hX2xvb2t1cCIgPSAib2FfbG9va3VwIikpDQpjb2xuYW1lcyhwYXBlcl9maWVsZHMpWzVdIDwtICJ0b3BpYyINCnBhcGVyX2ZpZWxkcyA8LSBwYXBlcl9maWVsZHNbLCBjKCJvYV9sb29rdXAiLCAiZG9tYWluIiwgImZpZWxkIiwgInN1YmZpZWxkIiwgInRvcGljIildDQpuYW1lcyhwYXBlcl9maWVsZHMpIDwtIGMoIm9hX2xvb2t1cCIsICJkb21haW4xIiwgImZpZWxkMSIsICJzdWJmaWVsZDEiLCAidG9waWMxIikNCg0KIyBkaWQgZXZlcnl0aGluZyBjaGVjayBvdXQ/IE5vIG1pc3NpbmdzLCBleGFjdCBudW1iZXIgb2YgdW5pcXVlIGNhc2VzDQpucm93KHBhcGVyX2ZpZWxkc1tpcy5uYSgib2FfbG9va3VwIiksIF0pDQpucm93KHBhcGVyX2ZpZWxkc1tpcy5uYSgiZG9tYWluMSIpLCBdKQ0KbnJvdyhwYXBlcl9maWVsZHNbaXMubmEoImZpZWxkMSIpLCBdKQ0KbnJvdyhwYXBlcl9maWVsZHNbaXMubmEoInN1YmZpZWxkMSIpLCBdKQ0KbnJvdyhwYXBlcl9maWVsZHNbaXMubmEoInRvcGljMSIpLCBdKQ0KbnJvdyh1bmlxdWUocGFwZXJfZmllbGRzKSkNCg0KYGBgDQoNCmBgYHtyIGluY2x1ZGUgPSBGLCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEYgIH0NCmxvYWQoIi4vZGF0YS9wcm9jZXNzZWQvMjAyNDA3MTF0ZW1wZmllbGRzLnJkYSIpDQpgYGANCg0KQW5kIG1hdGNoIGJhY2sgdG8gb3JpZ2luYWwgZGF0YS4NCg0KYGBge3IgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGICB9DQoNCiMgc2FtZSBvcGVyYXRpb25zIHRvIGdldCB0byB0aGUgb2FfbG9va3VwIHZhcmlhYmxlIHRvIG1hdGNoIG9uDQp4JG9hX2xvb2t1cCA8LSBpZmVsc2UoaXMubmEoeCRET0kpLCBnc3ViKCdbWzpwdW5jdDpdIF0rJywnICcsIHgkVGl0ZWxfb3JpKSwgTkEpDQp4JG9hX2xvb2t1cCA8LSBpZmVsc2UoIWlzLm5hKHgkb2FfbG9va3VwKSwgZ3N1YigiT1IiLCAib3IiLCB4JG9hX2xvb2t1cCksIE5BKSAjIE9SIGlzIHNlZW4gYXMgYW4gT1Igc3RhdGVtZW50IGJyZWFrIHRoZSBmb3IgbG9vcCBiZWxvdw0KbnJvdyh4WyFpcy5uYSh4JG9hX2xvb2t1cCksIF0pDQoNCnggPC0gbGVmdF9qb2luKHgsIHBhcGVyX2ZpZWxkcywgYnkgPSBjKCJvYV9sb29rdXAiID0gIm9hX2xvb2t1cCIpKQ0KDQphIDwtIG5yb3coeFshaXMubmEoeCRkb21haW4pLF0pDQp4JGRvbWFpbiA8LSBpZmVsc2UoaXMubmEoeCRkb21haW4pICYgIWlzLm5hKHgkZG9tYWluMSksIHgkZG9tYWluMSwgeCRkb21haW4pDQpiIDwtIG5yb3coeFshaXMubmEoeCRkb21haW4pLF0pDQoNCg0KDQp4JGZpZWxkcyA8LSBpZmVsc2UoaXMubmEoeCRmaWVsZHMpICYgIWlzLm5hKHgkZmllbGQxKSwgeCRmaWVsZDEsIHgkZmllbGRzKQ0KeCRzdWJmaWVsZCA8LSBpZmVsc2UoaXMubmEoeCRzdWJmaWVsZCkgJiAhaXMubmEoeCRzdWJmaWVsZDEpLCB4JHN1YmZpZWxkMSwgeCRzdWJmaWVsZCkNCngkdG9waWMgPC0gaWZlbHNlKGlzLm5hKHgkdG9waWMpICYgIWlzLm5hKHgkdG9waWMxKSwgeCR0b3BpYzEsIHgkdG9waWMpDQoNCmENCmINCmIvbnJvdyh4KQ0KYGBgDQoNCmBgYHtyIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRiAgfQ0KZnNhdmUoeCwgImxpbmtlZF9wdWJzX29hX2ZpZWxkcy5yZGEiKQ0KYGBgDQoNCi0tLSAgDQoNCmBgYHtyIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRiwgZXZhbCA9IEYsIGVjaG8gPSBGfQ0KeCAlPiUgDQogIGdncGxvdChhZXMoeCA9IGRvbWFpbikpICsNCiAgZ2VvbV9iYXIoKSArDQogICAgbGFicyh5ID0gIkZyZXF1ZW5jeSIsIHggPSAiRG9tYWluIikgKw0KICBnZ3RpdGxlKCJEb21haW4gb24gcGFwZXItYXV0aG9yIGxldmVsIikNCg0KDQp4ICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gZmllbGRzKSkgKw0KICBnZW9tX2JhcigpICsNCiAgICBsYWJzKHkgPSAiRnJlcXVlbmN5IiwgeCA9ICJGaWVsZCIpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA3MCwgIGhqdXN0PTEpKSArDQogIGdndGl0bGUoIkZpZWxkIG9uIHBhcGVyLWF1dGhvciBsZXZlbCIpDQoNCg0KYGBgDQoNCg0KDQpgYGB7ciBtZXNzYWdlID0gRiwgd2FybmluZyA9IEYsIGV2YWwgPSBGLCBlY2hvID0gRn0NCiMgRE9NQUlOIGxldmVsDQojIHBpY2sgYmVmb3JlIDIwMDAsIGRvbWFpbnMgYW5kIHBlcnNvbl9pZA0KZG9tYWluIDwtIGRhdGEuZnJhbWUodGFibGUoeFt4JHB1Yl95ZWFyIDwgMjAyMCwgYygiZG9tYWluIiwgInBlcnNvbl9pZCIpXSkpDQoNCiMgZ2V0IHRvcCBkb21haW4gcGVyIHBlcnNvbg0KZG9tYWluPC0gZG9tYWluICU+JSBncm91cF9ieShwZXJzb25faWQpICU+JSB0b3BfbigxLCBGcmVxKQ0KZG9tYWluJHBlcnNvbl9pZCA8LSBhcy5jaGFyYWN0ZXIoZG9tYWluJHBlcnNvbl9pZCkNCg0KIyBuID0gMTAwMDEgc2Nob2xhcnMNCm5yb3codW5pcXVlKGRvbWFpblssYygicGVyc29uX2lkIildKSkNCg0KIyBpcyB0aGF0IGNvcnJlY3Q/IFllcyENCm5yb3codW5pcXVlKGRvbWFpblssYygicGVyc29uX2lkIildKSkgPT0gbnJvdyh1bmlxdWUoeFssYygicGVyc29uX2lkIildKSkNCg0KIyByZW1vdmUgdGhvc2Ugd2l0aG91dCBhbnkgZG9tYWluDQpkb21haW4gPC0gZG9tYWluWyFkb21haW4kRnJlcSA9PSAwLCBdDQoNCiMgOTUzMiBmb2xrcywgc28gYWJvdXQgNTAwIHdobyBhcmUgc3BsaXQgZXZlbj8NCm5yb3codW5pcXVlKGRvbWFpblssYygicGVyc29uX2lkIildKSkgDQoNCiMgY2FuIGRvIG90aGVyIGRlY2lzaW9uIHJ1bGVzIGhlcmUsIG5vdyB3ZSBwaWNrIHJhbmRvbWx5IGZvciB0aG9zZSB3aXRoIG11bHRpcGxlIGRvbWFpbnMsIHNlZW1zIHJlYXNvbmFibGU/DQpkb21haW4gPC0gZG9tYWluICU+JSBncm91cF9ieShwZXJzb25faWQpICU+JSBzbGljZV9zYW1wbGUobj0xKQ0KZG9tYWluIDwtIGRvbWFpblssYygicGVyc29uX2lkIiwgImRvbWFpbiIpXQ0KDQpkb21haW4gJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBkb21haW4pKSArDQogIGdlb21fYmFyKCkgKw0KICAgIGxhYnMoeSA9ICJGcmVxdWVuY3kiLCB4ID0gIkRvbWFpbiIpICsNCiAgZ2d0aXRsZSgiRG9tYWluIG9uIGF1dGhvciBsZXZlbCIpDQoNCg0KYGBgDQoNCmBgYHtyIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRiwgZXZhbCA9IEYsIGVjaG8gPSBGfQ0KIyBGSUVMRCBsZXZlbA0KIyBwaWNrIGJlZm9yZSAyMDAwLCBkb21haW5zIGFuZCBwZXJzb25faWQNCmZpZWxkIDwtIGRhdGEuZnJhbWUodGFibGUoeFt4JHB1Yl95ZWFyIDwgMjAyMCwgYygiZmllbGRzIiwgInBlcnNvbl9pZCIpXSkpDQoNCiMgZ2V0IHRvcCBmaWVsZCBwZXIgcGVyc29uDQpmaWVsZDwtIGZpZWxkICU+JSBncm91cF9ieShwZXJzb25faWQpICU+JSB0b3BfbigxLCBGcmVxKQ0KZmllbGQkcGVyc29uX2lkIDwtIGFzLmNoYXJhY3RlcihmaWVsZCRwZXJzb25faWQpDQoNCiMgbiA9IDEwMDAxIHNjaG9sYXJzDQpucm93KHVuaXF1ZShmaWVsZFssYygicGVyc29uX2lkIildKSkNCg0KIyBpcyB0aGF0IGNvcnJlY3Q/IFllcyENCm5yb3codW5pcXVlKGZpZWxkWyxjKCJwZXJzb25faWQiKV0pKSA9PSBucm93KHVuaXF1ZSh4WyxjKCJwZXJzb25faWQiKV0pKQ0KDQojIHJlbW92ZSB0aG9zZSB3aXRob3V0IGFueSBmaWVsZA0KZmllbGQgPC0gZmllbGRbIWZpZWxkJEZyZXEgPT0gMCwgXQ0KDQojIDk1MzIgZm9sa3MsIHNvIGFib3V0IDEyMDAgd2hvIGFyZSBzcGxpdCBldmVuPw0KbnJvdyh1bmlxdWUoZmllbGRbLGMoInBlcnNvbl9pZCIpXSkpIA0KDQojIGNhbiBkbyBvdGhlciBkZWNpc2lvbiBydWxlcyBoZXJlLCBub3cgd2UgcGljayByYW5kb21seSBmb3IgdGhvc2Ugd2l0aCBtdWx0aXBsZSBkb21haW5zLCBzZWVtcyByZWFzb25hYmxlPw0KZmllbGQgPC0gZmllbGQgJT4lIGdyb3VwX2J5KHBlcnNvbl9pZCkgJT4lIHNsaWNlX3NhbXBsZShuPTEpDQpmaWVsZCA8LSBmaWVsZFssYygicGVyc29uX2lkIiwgImZpZWxkcyIpXQ0KDQoNCmZpZWxkICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gZmllbGRzKSkgKw0KICBnZW9tX2JhcigpICsNCiAgICBsYWJzKHkgPSAiRnJlcXVlbmN5IiwgeCA9ICJEb21haW4iKSArDQogIGdndGl0bGUoIkZpZWxkIG9uIGF1dGhvciBsZXZlbCIpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA3MCwgIGhqdXN0PTEpKQ0KDQoNCmBgYA==


Copyright © 2024- Jochem Tolsma