Introduction and Summary of Data Set
The following describes my path toward building a movie recommendation system that ultimately resulted in the Shiny app located here: Movie Recommendations on Shiny
This data set (ml-latest-small) describes 5-star rating and free-text tagging activity from MovieLens, a movie recommendation service. It contains 100004 ratings and 1296 tag applications across 9125 movies. These data were created by 671 users between January 09, 1995 and October 16, 2016. This data set was generated on October 17, 2016.
Users were selected at random for inclusion. All selected users had rated at least 20 movies. No demographic information is included. Each user is represented by an id, and no other information is provided.
The data are contained in the files links.csv, movies.csv, ratings.csv and tags.csv. More details about the contents and use of all these files follows.
This is a development data set. As such, it may change over time and is not an appropriate data set for shared research results. See available benchmark data sets if that is your intent.
This and other GroupLens data sets are publicly available for download at http://grouplens.org/data sets/.
recommenderlab
To work with recommenderlab package, data firstly is needed to be converted to sparse format. A sparse matrix is a matrix where most of the elements are zero. In the case of our data set, while there are many users and many movies, the number of user/movie ratings is relatively few.
# transforming numeric IDs into strings so that sparseMatrix function does not fill in missing
# ID numbers and thus preserving correct dimensions
i = paste0('u', ratings$userId)
j = paste0('m', ratings$movieId)
x = ratings$rating
df = data.frame(i, j, x, stringsAsFactors = T)
# interesting that as.integer works on character vector
sparse_matrix = sparseMatrix(as.integer(df$i), as.integer(df$j), x = df$x)
colnames(sparse_matrix) = levels(df$j)
rownames(sparse_matrix) = levels(df$i)
# create recommenderLab real rating object
real_ratings = new('realRatingMatrix', data = sparse_matrix)
Most Popular
For a given user who has not yet rated an item, the most popular approach will predict a rating based on the average rating for that item based on those who have rated it. Thus, all users who have not rated said item will receive the same predicted rating.
# create Recommender object for popular model
model_popular = Recommender(real_ratings, method = 'POPULAR', param = list(normalize = 'center'))
# create prediction object
pred_popular = predict(model_popular, real_ratings[1:5], type = 'ratings')
as(pred_popular, 'matrix')[, 1:5]
m1 m10 m100 m100017 m100032
u1 2.775976 2.448398 2.345312 1.329026 1.928235
u10 3.921628 3.594050 3.490964 2.474678 3.073887
u100 NA 3.298398 3.195312 2.179026 2.778235
u101 4.125976 3.798398 3.695312 2.679026 3.278235
u102 4.200902 3.873324 3.770238 2.753952 3.353162
# evaluate accuracy of popular model
e_popular = evaluationScheme(real_ratings, method = 'split', train = 0.8, given = -5)
mode_popular = Recommender(getData(e_popular, 'train'), method = 'POPULAR', param = list(normalize = 'center'))
pred_popular = predict(model_popular, getData(e_popular, 'known'), type = 'ratings')
rmse_popular = calcPredictionAccuracy(pred_popular, getData(e_popular, 'unknown'))
rmse_popular
RMSE MSE MAE
0.9248621 0.8553699 0.6941487
User-Based Collaborative Filtering (UBCF)
Again for a given user who has not yet rated an item, UBCF is based on identifying the most similar users to our particular user. It then calculates the average rating (or averaged rating weighted by user similarity) of said item that these similar users have assigned to it.
Various similarity measures can be used, e.g. Pearson correlation, Cosine similarity, etc. Also, the n number of similar users can be optimized via cross-validation or similar approaches.
Because UBCF depends on user similarity, it must recalculate item ratings whenever the user’s preferences change. As a result, it can be too slow for to make real time item recommendations when the number of users and items is substantial.
# create Recommender object for ubcf model
model_ubcf = Recommender(real_ratings, method = 'UBCF', param = list(normalize = 'center'))
# create prediction object
pred_ubcf = predict(model_ubcf, real_ratings[1:5], type = 'ratings')
as(pred_ubcf, 'matrix')[, 1:5]
m1 m10 m100 m100017 m100032
u1 2.559320 2.544006 2.550000 2.550000 2.550000
u10 3.787169 3.626540 3.653646 3.695652 3.695652
u100 NA 3.350874 3.354726 3.400000 3.400000
u101 4.045674 3.905234 3.900000 3.900000 3.900000
u102 4.073754 3.863099 3.958329 3.974926 3.974926
# evaluate accuracy of ubcf model
e_ubcf = evaluationScheme(real_ratings, method = 'split', train = 0.8, given = -5)
mode_ubcf = Recommender(getData(e_ubcf, 'train'), method = 'ubcf', param = list(normalize = 'center'))
pred_ubcf = predict(model_ubcf, getData(e_ubcf, 'known'), type = 'ratings')
rmse_ubcf = calcPredictionAccuracy(pred_ubcf, getData(e_ubcf, 'unknown'))
rmse_ubcf
RMSE MSE MAE
0.7017058 0.4923910 0.5312428
Item-Based Collaborative Filtering (IBCF)
Similar to UBCF, IBCF is also based on similarity. But in the case of IBCF, the similarity in question is the similarity between items. IBCF calculates a similarity measure for all items based on existing user ratings. Then as a user peruses a new item, the algorithm can recommend similar items.
Again similar to UBCF, IBCF can use similar similarity measures and optimization techniques to determine the n number of items to use.
Unlike UBCF, however, the resource-intensive process of calculating similarity between items can be done offline. This allows IBCF to be much more responsive in making item recommendations to users and can be utilized in a real time setting. Amazon uses a custom IBCF approach.
With the full data set, the process takes hours to run, so I’ve not included the output here.
# create Recommender object for ibcf model
#model_ibcf = Recommender(real_ratings, method = 'IBCF', param = list(normalize = 'center'))
# create prediction object
#pred_ibcf = predict(model_ibcf, real_ratings[1:5], type = 'ratings')
#as(pred_ibcf, 'matrix')[, 1:5]
# evaluate accuracy of ibcf model
#e_ibcf = evaluationScheme(real_ratings, method = 'split', train = 0.8, given = -5)
#mode_ibcf = Recommender(getData(e_ibcf, 'train'), method = 'ibcf', param = list(normalize = 'center'))
#pred_ibcf = predict(model_ibcf, getData(e_ibcf, 'known'), type = 'ratings')
#rmse_ibcf = calcPredictionAccuracy(pred_ibcf, getData(e_ibcf, 'unknown'))
#rmse_ibcf
Singular Value Decomposition (SVD)
Singular value decomposition is essentially trying to reduce a rank R matrix to a rank K matrix by taking a list of R unique vectors and approximating them as a linear combination of K unique vectors (Quora).
# create Recommender object for svd model
model_svd = Recommender(real_ratings, method = 'SVD', param = list(normalize = 'center'))
# create prediction object
pred_svd = predict(model_svd, real_ratings[1:5], type = 'ratings')
as(pred_svd, 'matrix')[, 1:5]
m1 m10 m100 m100017 m100032
u1 2.543127 2.550196 2.550261 2.549503 2.549747
u10 3.726763 3.683090 3.689763 3.693689 3.694652
u100 NA 3.377720 3.394836 3.400426 3.400217
u101 4.045254 3.860013 3.884195 3.898862 3.899420
u102 4.418523 3.667622 3.850810 3.955026 3.964792
# evaluate accuracy of svd model
e_svd = evaluationScheme(real_ratings, method = 'split', train = 0.8, given = -5)
mode_svd = Recommender(getData(e_svd, 'train'), method = 'svd', param = list(normalize = 'center'))
pred_svd = predict(model_svd, getData(e_svd, 'known'), type = 'ratings')
rmse_svd = calcPredictionAccuracy(pred_svd, getData(e_svd, 'unknown'))
rmse_svd
RMSE MSE MAE
0.9047596 0.8185900 0.7085536
Algorithm Comparison
# combine rmse outputs
comparison = rbind(rmse_popular, rmse_ubcf, rmse_svd)
comparison = data.frame(comparison, row.names = NULL)
comparison = cbind(model = c('popular', 'ubcf', 'svd'), comparison)
comparison %>% gather('measure', 'value', -1) %>%
ggplot(aes(x = measure, y = value, fill = model)) +
geom_bar(stat = 'identity', position = position_dodge())

In this particular instance, the UBCF model produces smaller errors than the other models on the test data. Since we are not using too much data, we will use UBCF to build our recommendation engine down below.
Recommend Movies
Static Movie Recommendations
In the example below, I’ve added some movies and ratings and generated some movie recommendations using UBCF.
# find movies based on genre and year
#movies %>% filter(str_detect(genres, "Animation") & year == 2014)
# create custom user ratings
custom_ratings_df = data.frame(title = c('The Secret Life of Pets (2016)', 'Kung Fu Panda 3 (2016)', 'Zootopia (2016)', 'Inside Out (2015)',
'Minions (2015)', 'The Good Dinosaur (2015)', 'Hotel Transylvania 2 (2015)', 'The Lego Movie (2014)',
'Mr. Peabody & Sherman (2014)', 'How to Train Your Dragon 2 (2014)', 'Big Hero 6 (2014)',
'Song of the Sea (2014)', 'Paperman (2012)', 'Grand Budapest Hotel, The (2014)',
"King's Speech, The (2010)", 'How to Train Your Dragon (2010)', 'Avengers, The (2012)',
'The Imitation Game (2014)'),
rating = c(3.5, 3.5, 5.0, 4.0, 3.0, 1.0, 5.0, 1.0, 1.0, 4.5, 5.0, 5.0, 5.0, 5.0, 5.0, 4.5, 1.0, 4.0))
# rating = c(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1))
# add movieId
custom_ratings = custom_ratings_df %>% left_join(movies, by = 'title') %>%
mutate(i = 'uCustom', j = paste0('m', movieId), x = rating) %>% select(i, j, x)
joining character vector and factor, coercing into character vector
#custom_ratings
custom_df = rbind(df, custom_ratings)
custom_sparse_matrix = sparseMatrix(as.integer(custom_df$i), as.integer(custom_df$j), x = custom_df$x)
colnames(custom_sparse_matrix) = levels(custom_df$j)
rownames(custom_sparse_matrix) = levels(custom_df$i)
# check custom user ratings
check = data.frame(custom_sparse_matrix[custom_sparse_matrix@Dimnames[[1]] == 'uCustom',])
check$movieId = rownames(check)
colnames(check)[1] = 'rating'
#check[check$rating != 0,]
# create real rating object
custom_real_ratings = new('realRatingMatrix', data = custom_sparse_matrix)
# make prediction using ubcf model
custom_ubcf = predict(model_ubcf, n = 20, custom_real_ratings)
custom_ubcf = as(custom_ubcf, 'list')$uCustom
custom_ubcf = data.frame(rank = 1:10, movieId = as.integer(str_replace(custom_ubcf, 'm', '')))
custom_ubcf %>% left_join(movies, by = 'movieId')
Shiny App for Movie Recommendations
The method above of entering movies and ratings is a bit cumbersome, so I created a Shiny implementation of the code above to make the process more seamless.
It can be found here: Movie Recommendations on Shiny
LS0tCnRpdGxlOiAiRGF0YTYwNy1GaW5hbFByb2plY3QiCm91dHB1dDogCiAgaHRtbF9ub3RlYm9vazoKICAgIHRoZW1lOiB5ZXRpCiAgICB0b2M6IHRydWUKICAgIHRvY19mbG9hdDogdHJ1ZQogICAgY29kZV9mb2xkaW5nOiBzaG93Ci0tLQoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShyZWNvbW1lbmRlcmxhYikKbGlicmFyeShkcGx5cikKbGlicmFyeSh0aWR5cikKbGlicmFyeShzdHJpbmdyKQpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkoZ2d0aGVtZXMpCmxpYnJhcnkoZ3JpZEV4dHJhKQoKc2V0d2QoIn4vR29vZ2xlIERyaXZlL0NVTlkvZ2l0L0RBVEE2MDcvUHJvamVjdEZpbmFsIikKI3NldHdkKCJDOi9Vc2Vycy9iaGFvL0dvb2dsZSBEcml2ZS9DVU5ZL2dpdC9EQVRBNjA3L1Byb2plY3RGaW5hbCIpCnBhdGhfbmFtZSA9ICJ+L0dvb2dsZSBEcml2ZS9DVU5ZL2dpdC9EQVRBNjA3L1Byb2plY3RGaW5hbCIKI3BhdGhfbmFtZSA9ICJDOi9Vc2Vycy9iaGFvL0dvb2dsZSBEcml2ZS9DVU5ZL2dpdC9EQVRBNjA3L1Byb2plY3RGaW5hbCIKcmF0aW5ncyA9IHJlYWQuY3N2KHBhc3RlMChwYXRoX25hbWUsICcvbWwtbGF0ZXN0LXNtYWxsL3JhdGluZ3MuY3N2JyksIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkKbW92aWVzID0gcmVhZC5jc3YocGFzdGUwKHBhdGhfbmFtZSwgJy9tbC1sYXRlc3Qtc21hbGwvbW92aWVzLmNzdicpLCBzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UpCgojIGNyZWF0ZSB5ZWFyIGNvbHVtbgptb3ZpZXMgPSBtb3ZpZXMgJT4lIG11dGF0ZSh5ZWFyID0gYXMuaW50ZWdlcihzdHJfZXh0cmFjdCh0aXRsZSwgIihbWzpkaWdpdDpdXXs0fSkiKSkpCgojIGxpbWl0IHRvIG1vcmUgcmVjZW50IG1vdmllcwpyYXRpbmdzID0gcmF0aW5ncyAlPiUgCiAgbGVmdF9qb2luKG1vdmllcywgYnkgPSAnbW92aWVJZCcpICU+JQogIHNlbGVjdCh1c2VySWQsIG1vdmllSWQsIHJhdGluZywgdGltZXN0YW1wKQpgYGAKCiNJbnRyb2R1Y3Rpb24gYW5kIFN1bW1hcnkgb2YgRGF0YSBTZXQgIAoKVGhlIGZvbGxvd2luZyBkZXNjcmliZXMgbXkgcGF0aCB0b3dhcmQgYnVpbGRpbmcgYSBtb3ZpZSByZWNvbW1lbmRhdGlvbiBzeXN0ZW0gdGhhdCB1bHRpbWF0ZWx5IHJlc3VsdGVkIGluIHRoZSBTaGlueSBhcHAgbG9jYXRlZCBoZXJlOiA8YSBocmVmID0gImh0dHBzOi8vYnJ1Y2VoYW8uc2hpbnlhcHBzLmlvL3Byb2plY3RmaW5hbC8iPk1vdmllIFJlY29tbWVuZGF0aW9ucyBvbiBTaGlueTwvYT4gIAoKVGhpcyBkYXRhIHNldCAobWwtbGF0ZXN0LXNtYWxsKSBkZXNjcmliZXMgNS1zdGFyIHJhdGluZyBhbmQgZnJlZS10ZXh0IHRhZ2dpbmcgYWN0aXZpdHkgZnJvbSBNb3ZpZUxlbnMsIGEgbW92aWUgcmVjb21tZW5kYXRpb24gc2VydmljZS4gSXQgY29udGFpbnMgMTAwMDA0IHJhdGluZ3MgYW5kIDEyOTYgdGFnIGFwcGxpY2F0aW9ucyBhY3Jvc3MgOTEyNSBtb3ZpZXMuIFRoZXNlIGRhdGEgd2VyZSBjcmVhdGVkIGJ5IDY3MSB1c2VycyBiZXR3ZWVuIEphbnVhcnkgMDksIDE5OTUgYW5kIE9jdG9iZXIgMTYsIDIwMTYuIFRoaXMgZGF0YSBzZXQgd2FzIGdlbmVyYXRlZCBvbiBPY3RvYmVyIDE3LCAyMDE2LgoKVXNlcnMgd2VyZSBzZWxlY3RlZCBhdCByYW5kb20gZm9yIGluY2x1c2lvbi4gQWxsIHNlbGVjdGVkIHVzZXJzIGhhZCByYXRlZCBhdCBsZWFzdCAyMCBtb3ZpZXMuIE5vIGRlbW9ncmFwaGljIGluZm9ybWF0aW9uIGlzIGluY2x1ZGVkLiBFYWNoIHVzZXIgaXMgcmVwcmVzZW50ZWQgYnkgYW4gaWQsIGFuZCBubyBvdGhlciBpbmZvcm1hdGlvbiBpcyBwcm92aWRlZC4KClRoZSBkYXRhIGFyZSBjb250YWluZWQgaW4gdGhlIGZpbGVzIGxpbmtzLmNzdiwgbW92aWVzLmNzdiwgcmF0aW5ncy5jc3YgYW5kIHRhZ3MuY3N2LiBNb3JlIGRldGFpbHMgYWJvdXQgdGhlIGNvbnRlbnRzIGFuZCB1c2Ugb2YgYWxsIHRoZXNlIGZpbGVzIGZvbGxvd3MuCgpUaGlzIGlzIGEgZGV2ZWxvcG1lbnQgZGF0YSBzZXQuIEFzIHN1Y2gsIGl0IG1heSBjaGFuZ2Ugb3ZlciB0aW1lIGFuZCBpcyBub3QgYW4gYXBwcm9wcmlhdGUgZGF0YSBzZXQgZm9yIHNoYXJlZCByZXNlYXJjaCByZXN1bHRzLiBTZWUgYXZhaWxhYmxlIGJlbmNobWFyayBkYXRhIHNldHMgaWYgdGhhdCBpcyB5b3VyIGludGVudC4KClRoaXMgYW5kIG90aGVyIEdyb3VwTGVucyBkYXRhIHNldHMgYXJlIHB1YmxpY2x5IGF2YWlsYWJsZSBmb3IgZG93bmxvYWQgYXQgaHR0cDovL2dyb3VwbGVucy5vcmcvZGF0YSBzZXRzLy4KCiNWaXN1YWxpemF0aW9uIG9mIERhdGEgU2V0IyAgCgpgYGB7cn0KcmF0aW5ncyAlPiUgZ2dwbG90KGFlcyh4ID0gcmF0aW5nKSkgKyAKICBnZW9tX2hpc3RvZ3JhbShiaW5zID0gMTApICsgCiAgZ2d0aXRsZSgnSGlzdG9ncmFtIG9mIE1vdmllIFJhdGluZ3MnKSAKCnAxID0gcmF0aW5ncyAlPiUgZ3JvdXBfYnkodXNlcklkKSAlPiUKICBzdW1tYXJpc2UoYXZnUmF0aW5nID0gbWVhbihyYXRpbmcpLCBuUmF0aW5ncyA9IG4oKSkgJT4lCiAgZ2dwbG90KGFlcyh4ID0gYXZnUmF0aW5nKSkgKwogIGdlb21faGlzdG9ncmFtKCkgKwogIHhsYWIoJ1VzZXIgQXZlcmFnZSBSYXRpbmcnKQoKcDIgPSByYXRpbmdzICU+JSBncm91cF9ieSh1c2VySWQpICU+JQogIHN1bW1hcmlzZShhdmdSYXRpbmcgPSBtZWFuKHJhdGluZyksIG5SYXRpbmdzID0gbigpKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBuUmF0aW5ncykpICsKICBnZW9tX2hpc3RvZ3JhbSgpICsKICB4bGFiKCdOdW1iZXIgb2YgUmF0ZWQgTW92aWVzJykgKwogIHlsYWIoJ1VzZXJzJykKCnAzID0gcmF0aW5ncyAlPiUgZ3JvdXBfYnkobW92aWVJZCkgJT4lCiAgc3VtbWFyaXNlKGF2Z1JhdGluZyA9IG1lYW4ocmF0aW5nKSwgblJhdGluZ3MgPSBuKCkpICU+JQogIGdncGxvdChhZXMoeCA9IGF2Z1JhdGluZykpICsKICBnZW9tX2hpc3RvZ3JhbSgpICsKICB4bGFiKCdNb3ZpZSBBdmVyYWdlIFJhdGluZycpCgpwNCA9IHJhdGluZ3MgJT4lIGdyb3VwX2J5KG1vdmllSWQpICU+JQogIHN1bW1hcmlzZShhdmdSYXRpbmcgPSBtZWFuKHJhdGluZyksIG5SYXRpbmdzID0gbigpKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBuUmF0aW5ncykpICsKICBnZW9tX2hpc3RvZ3JhbSgpICsKICB4bGFiKCdOdW1iZXIgb2YgUmF0aW5ncyBwZXIgTW92aWUnKSArCiAgeWxhYignTW92aWVzJykKCmdyaWQuYXJyYW5nZShwMSwgcDMsIHAyLCBwNCwgbmNvbCA9IDIpCmBgYAoKI1R5cGVzIG9mIFJlY29tbWVuZGVyIE1ldGhvZHMgIAoKV2Ugd2lsbCBleHBsb3JlIGZvdXIgZGlmZmVyZW50IHJlY29tbWVuZGF0aW9uIGFsZ29yaXRobXM6ICAKCjEuIE1vc3QgUG9wdWxhciBNZXRob2QgKFBvcHVsYXIpICAKMi4gVXNlciBCYXNlZCBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyAoVUJDRikgIAozLiBJdGVtIEJhc2VkIENvbGxhYm9yYXRpdmUgRmlsdGVyaW5nIChJQkNGKSAgCjQuIFNpbmd1bGFyIFZhbHVlIERlY29tcG9zaXRpb24gKFNWRCkgIAoKRnVydGhlcm1vcmUsIHdlIHdpbGwgdXNlIHRoZSBgcmVjb21tZW5kZXJsYWJgIHBhY2thZ2UgdG8gaW1wbGVtZW50IGVhY2ggYWxnb3JpdGhtLiAgCgojcmVjb21tZW5kZXJsYWIgIAoKVG8gd29yayB3aXRoIHJlY29tbWVuZGVybGFiIHBhY2thZ2UsIGRhdGEgZmlyc3RseSBpcyBuZWVkZWQgdG8gYmUgY29udmVydGVkIHRvIHNwYXJzZSBmb3JtYXQuIEEgc3BhcnNlIG1hdHJpeCBpcyBhIG1hdHJpeCB3aGVyZSBtb3N0IG9mIHRoZSBlbGVtZW50cyBhcmUgemVyby4gSW4gdGhlIGNhc2Ugb2Ygb3VyIGRhdGEgc2V0LCB3aGlsZSB0aGVyZSBhcmUgbWFueSB1c2VycyBhbmQgbWFueSBtb3ZpZXMsIHRoZSBudW1iZXIgb2YgdXNlci9tb3ZpZSByYXRpbmdzIGlzIHJlbGF0aXZlbHkgZmV3LiAgCgpgYGB7cn0KIyB0cmFuc2Zvcm1pbmcgbnVtZXJpYyBJRHMgaW50byBzdHJpbmdzIHNvIHRoYXQgc3BhcnNlTWF0cml4IGZ1bmN0aW9uIGRvZXMgbm90IGZpbGwgaW4gbWlzc2luZwojIElEIG51bWJlcnMgYW5kIHRodXMgcHJlc2VydmluZyBjb3JyZWN0IGRpbWVuc2lvbnMKaSA9IHBhc3RlMCgndScsIHJhdGluZ3MkdXNlcklkKQpqID0gcGFzdGUwKCdtJywgcmF0aW5ncyRtb3ZpZUlkKQp4ID0gcmF0aW5ncyRyYXRpbmcKCmRmID0gZGF0YS5mcmFtZShpLCBqLCB4LCBzdHJpbmdzQXNGYWN0b3JzID0gVCkKCiMgaW50ZXJlc3RpbmcgdGhhdCBhcy5pbnRlZ2VyIHdvcmtzIG9uIGNoYXJhY3RlciB2ZWN0b3IKc3BhcnNlX21hdHJpeCA9IHNwYXJzZU1hdHJpeChhcy5pbnRlZ2VyKGRmJGkpLCBhcy5pbnRlZ2VyKGRmJGopLCB4ID0gZGYkeCkKY29sbmFtZXMoc3BhcnNlX21hdHJpeCkgPSBsZXZlbHMoZGYkaikKcm93bmFtZXMoc3BhcnNlX21hdHJpeCkgPSBsZXZlbHMoZGYkaSkKCiMgY3JlYXRlIHJlY29tbWVuZGVyTGFiIHJlYWwgcmF0aW5nIG9iamVjdApyZWFsX3JhdGluZ3MgPSBuZXcoJ3JlYWxSYXRpbmdNYXRyaXgnLCBkYXRhID0gc3BhcnNlX21hdHJpeCkKYGBgCgojI01vc3QgUG9wdWxhciAgCkZvciBhIGdpdmVuIHVzZXIgd2hvIGhhcyBub3QgeWV0IHJhdGVkIGFuIGl0ZW0sIHRoZSBtb3N0IHBvcHVsYXIgYXBwcm9hY2ggd2lsbCBwcmVkaWN0IGEgcmF0aW5nIGJhc2VkIG9uIHRoZSBhdmVyYWdlIHJhdGluZyBmb3IgdGhhdCBpdGVtIGJhc2VkIG9uIHRob3NlIHdobyBoYXZlIHJhdGVkIGl0LiBUaHVzLCBhbGwgdXNlcnMgd2hvIGhhdmUgbm90IHJhdGVkIHNhaWQgaXRlbSB3aWxsIHJlY2VpdmUgdGhlIHNhbWUgcHJlZGljdGVkIHJhdGluZy4gIAoKYGBge3J9CiMgY3JlYXRlIFJlY29tbWVuZGVyIG9iamVjdCBmb3IgcG9wdWxhciBtb2RlbAptb2RlbF9wb3B1bGFyID0gUmVjb21tZW5kZXIocmVhbF9yYXRpbmdzLCBtZXRob2QgPSAnUE9QVUxBUicsIHBhcmFtID0gbGlzdChub3JtYWxpemUgPSAnY2VudGVyJykpCgojIGNyZWF0ZSBwcmVkaWN0aW9uIG9iamVjdApwcmVkX3BvcHVsYXIgPSBwcmVkaWN0KG1vZGVsX3BvcHVsYXIsIHJlYWxfcmF0aW5nc1sxOjVdLCB0eXBlID0gJ3JhdGluZ3MnKQphcyhwcmVkX3BvcHVsYXIsICdtYXRyaXgnKVssIDE6NV0KCiMgZXZhbHVhdGUgYWNjdXJhY3kgb2YgcG9wdWxhciBtb2RlbAplX3BvcHVsYXIgPSBldmFsdWF0aW9uU2NoZW1lKHJlYWxfcmF0aW5ncywgbWV0aG9kID0gJ3NwbGl0JywgdHJhaW4gPSAwLjgsIGdpdmVuID0gLTUpCm1vZGVfcG9wdWxhciA9IFJlY29tbWVuZGVyKGdldERhdGEoZV9wb3B1bGFyLCAndHJhaW4nKSwgbWV0aG9kID0gJ1BPUFVMQVInLCBwYXJhbSA9IGxpc3Qobm9ybWFsaXplID0gJ2NlbnRlcicpKQpwcmVkX3BvcHVsYXIgPSBwcmVkaWN0KG1vZGVsX3BvcHVsYXIsIGdldERhdGEoZV9wb3B1bGFyLCAna25vd24nKSwgdHlwZSA9ICdyYXRpbmdzJykKcm1zZV9wb3B1bGFyID0gY2FsY1ByZWRpY3Rpb25BY2N1cmFjeShwcmVkX3BvcHVsYXIsIGdldERhdGEoZV9wb3B1bGFyLCAndW5rbm93bicpKQpybXNlX3BvcHVsYXIKYGBgCgojI1VzZXItQmFzZWQgQ29sbGFib3JhdGl2ZSBGaWx0ZXJpbmcgKFVCQ0YpICAKCkFnYWluIGZvciBhIGdpdmVuIHVzZXIgd2hvIGhhcyBub3QgeWV0IHJhdGVkIGFuIGl0ZW0sIFVCQ0YgaXMgYmFzZWQgb24gaWRlbnRpZnlpbmcgdGhlIG1vc3Qgc2ltaWxhciB1c2VycyB0byBvdXIgcGFydGljdWxhciB1c2VyLiBJdCB0aGVuIGNhbGN1bGF0ZXMgdGhlIGF2ZXJhZ2UgcmF0aW5nIChvciBhdmVyYWdlZCByYXRpbmcgd2VpZ2h0ZWQgYnkgdXNlciBzaW1pbGFyaXR5KSBvZiBzYWlkIGl0ZW0gdGhhdCB0aGVzZSBzaW1pbGFyIHVzZXJzIGhhdmUgYXNzaWduZWQgdG8gaXQuICAKClZhcmlvdXMgc2ltaWxhcml0eSBtZWFzdXJlcyBjYW4gYmUgdXNlZCwgZS5nLiBQZWFyc29uIGNvcnJlbGF0aW9uLCBDb3NpbmUgc2ltaWxhcml0eSwgZXRjLiBBbHNvLCB0aGUgbiBudW1iZXIgb2Ygc2ltaWxhciB1c2VycyBjYW4gYmUgb3B0aW1pemVkIHZpYSBjcm9zcy12YWxpZGF0aW9uIG9yIHNpbWlsYXIgYXBwcm9hY2hlcy4gIAoKQmVjYXVzZSBVQkNGIGRlcGVuZHMgb24gdXNlciBzaW1pbGFyaXR5LCBpdCBtdXN0IHJlY2FsY3VsYXRlIGl0ZW0gcmF0aW5ncyB3aGVuZXZlciB0aGUgdXNlcidzIHByZWZlcmVuY2VzIGNoYW5nZS4gQXMgYSByZXN1bHQsIGl0IGNhbiBiZSB0b28gc2xvdyBmb3IgdG8gbWFrZSByZWFsIHRpbWUgaXRlbSByZWNvbW1lbmRhdGlvbnMgd2hlbiB0aGUgbnVtYmVyIG9mIHVzZXJzIGFuZCBpdGVtcyBpcyBzdWJzdGFudGlhbC4gIAoKYGBge3J9CiMgY3JlYXRlIFJlY29tbWVuZGVyIG9iamVjdCBmb3IgdWJjZiBtb2RlbAptb2RlbF91YmNmID0gUmVjb21tZW5kZXIocmVhbF9yYXRpbmdzLCBtZXRob2QgPSAnVUJDRicsIHBhcmFtID0gbGlzdChub3JtYWxpemUgPSAnY2VudGVyJykpCgojIGNyZWF0ZSBwcmVkaWN0aW9uIG9iamVjdApwcmVkX3ViY2YgPSBwcmVkaWN0KG1vZGVsX3ViY2YsIHJlYWxfcmF0aW5nc1sxOjVdLCB0eXBlID0gJ3JhdGluZ3MnKQphcyhwcmVkX3ViY2YsICdtYXRyaXgnKVssIDE6NV0KCiMgZXZhbHVhdGUgYWNjdXJhY3kgb2YgdWJjZiBtb2RlbAplX3ViY2YgPSBldmFsdWF0aW9uU2NoZW1lKHJlYWxfcmF0aW5ncywgbWV0aG9kID0gJ3NwbGl0JywgdHJhaW4gPSAwLjgsIGdpdmVuID0gLTUpCm1vZGVfdWJjZiA9IFJlY29tbWVuZGVyKGdldERhdGEoZV91YmNmLCAndHJhaW4nKSwgbWV0aG9kID0gJ3ViY2YnLCBwYXJhbSA9IGxpc3Qobm9ybWFsaXplID0gJ2NlbnRlcicpKQpwcmVkX3ViY2YgPSBwcmVkaWN0KG1vZGVsX3ViY2YsIGdldERhdGEoZV91YmNmLCAna25vd24nKSwgdHlwZSA9ICdyYXRpbmdzJykKcm1zZV91YmNmID0gY2FsY1ByZWRpY3Rpb25BY2N1cmFjeShwcmVkX3ViY2YsIGdldERhdGEoZV91YmNmLCAndW5rbm93bicpKQpybXNlX3ViY2YKYGBgCgojI0l0ZW0tQmFzZWQgQ29sbGFib3JhdGl2ZSBGaWx0ZXJpbmcgKElCQ0YpICAKClNpbWlsYXIgdG8gVUJDRiwgSUJDRiBpcyBhbHNvIGJhc2VkIG9uIHNpbWlsYXJpdHkuIEJ1dCBpbiB0aGUgY2FzZSBvZiBJQkNGLCB0aGUgc2ltaWxhcml0eSBpbiBxdWVzdGlvbiBpcyB0aGUgc2ltaWxhcml0eSBiZXR3ZWVuIGl0ZW1zLiBJQkNGIGNhbGN1bGF0ZXMgYSBzaW1pbGFyaXR5IG1lYXN1cmUgZm9yIGFsbCBpdGVtcyBiYXNlZCBvbiBleGlzdGluZyB1c2VyIHJhdGluZ3MuIFRoZW4gYXMgYSB1c2VyIHBlcnVzZXMgYSBuZXcgaXRlbSwgdGhlIGFsZ29yaXRobSBjYW4gcmVjb21tZW5kIHNpbWlsYXIgaXRlbXMuICAKCkFnYWluIHNpbWlsYXIgdG8gVUJDRiwgSUJDRiBjYW4gdXNlIHNpbWlsYXIgc2ltaWxhcml0eSBtZWFzdXJlcyBhbmQgb3B0aW1pemF0aW9uIHRlY2huaXF1ZXMgdG8gZGV0ZXJtaW5lIHRoZSBuIG51bWJlciBvZiBpdGVtcyB0byB1c2UuICAKClVubGlrZSBVQkNGLCBob3dldmVyLCB0aGUgcmVzb3VyY2UtaW50ZW5zaXZlIHByb2Nlc3Mgb2YgY2FsY3VsYXRpbmcgc2ltaWxhcml0eSBiZXR3ZWVuIGl0ZW1zIGNhbiBiZSBkb25lIG9mZmxpbmUuIFRoaXMgYWxsb3dzIElCQ0YgdG8gYmUgbXVjaCBtb3JlIHJlc3BvbnNpdmUgaW4gbWFraW5nIGl0ZW0gcmVjb21tZW5kYXRpb25zIHRvIHVzZXJzIGFuZCBjYW4gYmUgdXRpbGl6ZWQgaW4gYSByZWFsIHRpbWUgc2V0dGluZy4gQW1hem9uIHVzZXMgYSBjdXN0b20gSUJDRiBhcHByb2FjaC4gIAoKV2l0aCB0aGUgZnVsbCBkYXRhIHNldCwgdGhlIHByb2Nlc3MgdGFrZXMgaG91cnMgdG8gcnVuLCBzbyBJJ3ZlIG5vdCBpbmNsdWRlZCB0aGUgb3V0cHV0IGhlcmUuICAKCmBgYHtyfQojIGNyZWF0ZSBSZWNvbW1lbmRlciBvYmplY3QgZm9yIGliY2YgbW9kZWwKI21vZGVsX2liY2YgPSBSZWNvbW1lbmRlcihyZWFsX3JhdGluZ3MsIG1ldGhvZCA9ICdJQkNGJywgcGFyYW0gPSBsaXN0KG5vcm1hbGl6ZSA9ICdjZW50ZXInKSkKCiMgY3JlYXRlIHByZWRpY3Rpb24gb2JqZWN0CiNwcmVkX2liY2YgPSBwcmVkaWN0KG1vZGVsX2liY2YsIHJlYWxfcmF0aW5nc1sxOjVdLCB0eXBlID0gJ3JhdGluZ3MnKQojYXMocHJlZF9pYmNmLCAnbWF0cml4JylbLCAxOjVdCgojIGV2YWx1YXRlIGFjY3VyYWN5IG9mIGliY2YgbW9kZWwKI2VfaWJjZiA9IGV2YWx1YXRpb25TY2hlbWUocmVhbF9yYXRpbmdzLCBtZXRob2QgPSAnc3BsaXQnLCB0cmFpbiA9IDAuOCwgZ2l2ZW4gPSAtNSkKI21vZGVfaWJjZiA9IFJlY29tbWVuZGVyKGdldERhdGEoZV9pYmNmLCAndHJhaW4nKSwgbWV0aG9kID0gJ2liY2YnLCBwYXJhbSA9IGxpc3Qobm9ybWFsaXplID0gJ2NlbnRlcicpKQojcHJlZF9pYmNmID0gcHJlZGljdChtb2RlbF9pYmNmLCBnZXREYXRhKGVfaWJjZiwgJ2tub3duJyksIHR5cGUgPSAncmF0aW5ncycpCiNybXNlX2liY2YgPSBjYWxjUHJlZGljdGlvbkFjY3VyYWN5KHByZWRfaWJjZiwgZ2V0RGF0YShlX2liY2YsICd1bmtub3duJykpCiNybXNlX2liY2YKYGBgCgojI1Npbmd1bGFyIFZhbHVlIERlY29tcG9zaXRpb24gKFNWRCkgIAoKU2luZ3VsYXIgdmFsdWUgZGVjb21wb3NpdGlvbiBpcyBlc3NlbnRpYWxseSB0cnlpbmcgdG8gcmVkdWNlIGEgcmFuayBSIG1hdHJpeCB0byBhIHJhbmsgSyBtYXRyaXggYnkgdGFraW5nIGEgbGlzdCBvZiBSIHVuaXF1ZSB2ZWN0b3JzIGFuZCBhcHByb3hpbWF0aW5nIHRoZW0gYXMgYSBsaW5lYXIgY29tYmluYXRpb24gb2YgSyB1bmlxdWUgdmVjdG9ycyAoPGEgaHJlZiA9ICdodHRwczovL3d3dy5xdW9yYS5jb20vV2hhdC1pcy1hbi1pbnR1aXRpdmUtZXhwbGFuYXRpb24tb2Ytc2luZ3VsYXItdmFsdWUtZGVjb21wb3NpdGlvbi1TVkQnPlF1b3JhPC9hPikuICAKCmBgYHtyfQojIGNyZWF0ZSBSZWNvbW1lbmRlciBvYmplY3QgZm9yIHN2ZCBtb2RlbAptb2RlbF9zdmQgPSBSZWNvbW1lbmRlcihyZWFsX3JhdGluZ3MsIG1ldGhvZCA9ICdTVkQnLCBwYXJhbSA9IGxpc3Qobm9ybWFsaXplID0gJ2NlbnRlcicpKQoKIyBjcmVhdGUgcHJlZGljdGlvbiBvYmplY3QKcHJlZF9zdmQgPSBwcmVkaWN0KG1vZGVsX3N2ZCwgcmVhbF9yYXRpbmdzWzE6NV0sIHR5cGUgPSAncmF0aW5ncycpCmFzKHByZWRfc3ZkLCAnbWF0cml4JylbLCAxOjVdCgojIGV2YWx1YXRlIGFjY3VyYWN5IG9mIHN2ZCBtb2RlbAplX3N2ZCA9IGV2YWx1YXRpb25TY2hlbWUocmVhbF9yYXRpbmdzLCBtZXRob2QgPSAnc3BsaXQnLCB0cmFpbiA9IDAuOCwgZ2l2ZW4gPSAtNSkKbW9kZV9zdmQgPSBSZWNvbW1lbmRlcihnZXREYXRhKGVfc3ZkLCAndHJhaW4nKSwgbWV0aG9kID0gJ3N2ZCcsIHBhcmFtID0gbGlzdChub3JtYWxpemUgPSAnY2VudGVyJykpCnByZWRfc3ZkID0gcHJlZGljdChtb2RlbF9zdmQsIGdldERhdGEoZV9zdmQsICdrbm93bicpLCB0eXBlID0gJ3JhdGluZ3MnKQpybXNlX3N2ZCA9IGNhbGNQcmVkaWN0aW9uQWNjdXJhY3kocHJlZF9zdmQsIGdldERhdGEoZV9zdmQsICd1bmtub3duJykpCnJtc2Vfc3ZkCmBgYAoKIyNBbGdvcml0aG0gQ29tcGFyaXNvbiAgCgpgYGB7cn0KIyBjb21iaW5lIHJtc2Ugb3V0cHV0cwpjb21wYXJpc29uID0gcmJpbmQocm1zZV9wb3B1bGFyLCBybXNlX3ViY2YsIHJtc2Vfc3ZkKQpjb21wYXJpc29uID0gZGF0YS5mcmFtZShjb21wYXJpc29uLCByb3cubmFtZXMgPSBOVUxMKQpjb21wYXJpc29uID0gY2JpbmQobW9kZWwgPSBjKCdwb3B1bGFyJywgJ3ViY2YnLCAnc3ZkJyksIGNvbXBhcmlzb24pCmNvbXBhcmlzb24gJT4lIGdhdGhlcignbWVhc3VyZScsICd2YWx1ZScsIC0xKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBtZWFzdXJlLCB5ID0gdmFsdWUsIGZpbGwgPSBtb2RlbCkpICsKICBnZW9tX2JhcihzdGF0ID0gJ2lkZW50aXR5JywgcG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgpKQpgYGAKCkluIHRoaXMgcGFydGljdWxhciBpbnN0YW5jZSwgdGhlIFVCQ0YgbW9kZWwgcHJvZHVjZXMgc21hbGxlciBlcnJvcnMgdGhhbiB0aGUgb3RoZXIgbW9kZWxzIG9uIHRoZSB0ZXN0IGRhdGEuIFNpbmNlIHdlIGFyZSBub3QgdXNpbmcgdG9vIG11Y2ggZGF0YSwgd2Ugd2lsbCB1c2UgVUJDRiB0byBidWlsZCBvdXIgcmVjb21tZW5kYXRpb24gZW5naW5lIGRvd24gYmVsb3cuICAKCiNSZWNvbW1lbmQgTW92aWVzICAKCiMjU3RhdGljIE1vdmllIFJlY29tbWVuZGF0aW9ucyAgICAKCkluIHRoZSBleGFtcGxlIGJlbG93LCBJJ3ZlIGFkZGVkIHNvbWUgbW92aWVzIGFuZCByYXRpbmdzIGFuZCBnZW5lcmF0ZWQgc29tZSBtb3ZpZSByZWNvbW1lbmRhdGlvbnMgdXNpbmcgVUJDRi4gIAoKYGBge3J9CiMgZmluZCBtb3ZpZXMgYmFzZWQgb24gZ2VucmUgYW5kIHllYXIKI21vdmllcyAlPiUgZmlsdGVyKHN0cl9kZXRlY3QoZ2VucmVzLCAiQW5pbWF0aW9uIikgJiB5ZWFyID09IDIwMTQpCgojIGNyZWF0ZSBjdXN0b20gdXNlciByYXRpbmdzCmN1c3RvbV9yYXRpbmdzX2RmID0gZGF0YS5mcmFtZSh0aXRsZSA9IGMoJ1RoZSBTZWNyZXQgTGlmZSBvZiBQZXRzICgyMDE2KScsICdLdW5nIEZ1IFBhbmRhIDMgKDIwMTYpJywgJ1pvb3RvcGlhICgyMDE2KScsICdJbnNpZGUgT3V0ICgyMDE1KScsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ01pbmlvbnMgKDIwMTUpJywgJ1RoZSBHb29kIERpbm9zYXVyICgyMDE1KScsICdIb3RlbCBUcmFuc3lsdmFuaWEgMiAoMjAxNSknLCAnVGhlIExlZ28gTW92aWUgKDIwMTQpJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnTXIuIFBlYWJvZHkgJiBTaGVybWFuICgyMDE0KScsICdIb3cgdG8gVHJhaW4gWW91ciBEcmFnb24gMiAoMjAxNCknLCAnQmlnIEhlcm8gNiAoMjAxNCknLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICdTb25nIG9mIHRoZSBTZWEgKDIwMTQpJywgJ1BhcGVybWFuICgyMDEyKScsICdHcmFuZCBCdWRhcGVzdCBIb3RlbCwgVGhlICgyMDE0KScsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIktpbmcncyBTcGVlY2gsIFRoZSAoMjAxMCkiLCAnSG93IHRvIFRyYWluIFlvdXIgRHJhZ29uICgyMDEwKScsICdBdmVuZ2VycywgVGhlICgyMDEyKScsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ1RoZSBJbWl0YXRpb24gR2FtZSAoMjAxNCknKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhdGluZyA9IGMoMy41LCAzLjUsIDUuMCwgNC4wLCAzLjAsIDEuMCwgNS4wLCAxLjAsIDEuMCwgNC41LCA1LjAsIDUuMCwgNS4wLCA1LjAsIDUuMCwgNC41LCAxLjAsIDQuMCkpCiMgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmF0aW5nID0gYygxLDEsMSwxLDEsMSwxLDEsMSwxLDEsMSwxLDEsMSwxLDEsMSkpCiMgYWRkIG1vdmllSWQKY3VzdG9tX3JhdGluZ3MgPSBjdXN0b21fcmF0aW5nc19kZiAlPiUgbGVmdF9qb2luKG1vdmllcywgYnkgPSAndGl0bGUnKSAlPiUKICBtdXRhdGUoaSA9ICd1Q3VzdG9tJywgaiA9IHBhc3RlMCgnbScsIG1vdmllSWQpLCB4ID0gcmF0aW5nKSAlPiUgc2VsZWN0KGksIGosIHgpCiNjdXN0b21fcmF0aW5ncwoKY3VzdG9tX2RmID0gcmJpbmQoZGYsIGN1c3RvbV9yYXRpbmdzKQpjdXN0b21fc3BhcnNlX21hdHJpeCA9IHNwYXJzZU1hdHJpeChhcy5pbnRlZ2VyKGN1c3RvbV9kZiRpKSwgYXMuaW50ZWdlcihjdXN0b21fZGYkaiksIHggPSBjdXN0b21fZGYkeCkKY29sbmFtZXMoY3VzdG9tX3NwYXJzZV9tYXRyaXgpID0gbGV2ZWxzKGN1c3RvbV9kZiRqKQpyb3duYW1lcyhjdXN0b21fc3BhcnNlX21hdHJpeCkgPSBsZXZlbHMoY3VzdG9tX2RmJGkpCgojIGNoZWNrIGN1c3RvbSB1c2VyIHJhdGluZ3MKY2hlY2sgPSBkYXRhLmZyYW1lKGN1c3RvbV9zcGFyc2VfbWF0cml4W2N1c3RvbV9zcGFyc2VfbWF0cml4QERpbW5hbWVzW1sxXV0gPT0gJ3VDdXN0b20nLF0pCmNoZWNrJG1vdmllSWQgPSByb3duYW1lcyhjaGVjaykKY29sbmFtZXMoY2hlY2spWzFdID0gJ3JhdGluZycKI2NoZWNrW2NoZWNrJHJhdGluZyAhPSAwLF0KCiMgY3JlYXRlIHJlYWwgcmF0aW5nIG9iamVjdApjdXN0b21fcmVhbF9yYXRpbmdzID0gbmV3KCdyZWFsUmF0aW5nTWF0cml4JywgZGF0YSA9IGN1c3RvbV9zcGFyc2VfbWF0cml4KQoKIyBtYWtlIHByZWRpY3Rpb24gdXNpbmcgdWJjZiBtb2RlbApjdXN0b21fdWJjZiA9IHByZWRpY3QobW9kZWxfdWJjZiwgbiA9IDIwLCBjdXN0b21fcmVhbF9yYXRpbmdzKQpjdXN0b21fdWJjZiA9IGFzKGN1c3RvbV91YmNmLCAnbGlzdCcpJHVDdXN0b20KY3VzdG9tX3ViY2YgPSBkYXRhLmZyYW1lKHJhbmsgPSAxOjEwLCBtb3ZpZUlkID0gYXMuaW50ZWdlcihzdHJfcmVwbGFjZShjdXN0b21fdWJjZiwgJ20nLCAnJykpKQpjdXN0b21fdWJjZiAlPiUgbGVmdF9qb2luKG1vdmllcywgYnkgPSAnbW92aWVJZCcpCmBgYAoKIyNTaGlueSBBcHAgZm9yIE1vdmllIFJlY29tbWVuZGF0aW9ucyAgCgpUaGUgbWV0aG9kIGFib3ZlIG9mIGVudGVyaW5nIG1vdmllcyBhbmQgcmF0aW5ncyBpcyBhIGJpdCBjdW1iZXJzb21lLCBzbyBJIGNyZWF0ZWQgYSBTaGlueSBpbXBsZW1lbnRhdGlvbiBvZiB0aGUgY29kZSBhYm92ZSB0byBtYWtlIHRoZSBwcm9jZXNzIG1vcmUgc2VhbWxlc3MuICAKCkl0IGNhbiBiZSBmb3VuZCBoZXJlOiA8YSBocmVmID0gImh0dHBzOi8vYnJ1Y2VoYW8uc2hpbnlhcHBzLmlvL3Byb2plY3RmaW5hbC8iPk1vdmllIFJlY29tbWVuZGF0aW9ucyBvbiBTaGlueTwvYT4gIAoKCgo=