matrix-react-sdk
Version:
SDK for matrix.org using React
598 lines (578 loc) • 88.1 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.UploadCanceledError = void 0;
exports.uploadFile = uploadFile;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _matrix = require("matrix-js-sdk/src/matrix");
var _matrixEncryptAttachment = _interopRequireDefault(require("matrix-encrypt-attachment"));
var _pngChunksExtract = _interopRequireDefault(require("png-chunks-extract"));
var _logger = require("matrix-js-sdk/src/logger");
var _utils = require("matrix-js-sdk/src/utils");
var _dispatcher = _interopRequireDefault(require("./dispatcher/dispatcher"));
var _languageHandler = require("./languageHandler");
var _Modal = _interopRequireDefault(require("./Modal"));
var _Spinner = _interopRequireDefault(require("./components/views/elements/Spinner"));
var _actions = require("./dispatcher/actions");
var _RoomUpload = require("./models/RoomUpload");
var _SettingsStore = _interopRequireDefault(require("./settings/SettingsStore"));
var _sendTimePerformanceMetrics = require("./sendTimePerformanceMetrics");
var _RoomContext = require("./contexts/RoomContext");
var _Reply = require("./utils/Reply");
var _ErrorDialog = _interopRequireDefault(require("./components/views/dialogs/ErrorDialog"));
var _UploadFailureDialog = _interopRequireDefault(require("./components/views/dialogs/UploadFailureDialog"));
var _UploadConfirmDialog = _interopRequireDefault(require("./components/views/dialogs/UploadConfirmDialog"));
var _imageMedia = require("./utils/image-media");
var _SendMessageComposer = require("./components/views/rooms/SendMessageComposer");
var _localRoom = require("./utils/local-room");
var _SDKContext = require("./contexts/SDKContext");
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 New Vector Ltd
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
// scraped out of a macOS hidpi (5660ppm) screenshot png
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
class UploadCanceledError extends Error {}
exports.UploadCanceledError = UploadCanceledError;
/**
* Load a file into a newly created image element.
*
* @param {File} imageFile The file to load in an image element.
* @return {Promise} A promise that resolves with the html image element.
*/
async function loadImageElement(imageFile) {
// Load the file into an html element
const img = new Image();
const objectUrl = URL.createObjectURL(imageFile);
const imgPromise = new Promise((resolve, reject) => {
img.onload = function () {
URL.revokeObjectURL(objectUrl);
resolve(img);
};
img.onerror = function (e) {
reject(e);
};
});
img.src = objectUrl;
// check for hi-dpi PNGs and fudge display resolution as needed.
// this is mainly needed for macOS screencaps
let parsePromise = Promise.resolve(false);
if (imageFile.type === "image/png") {
// in practice macOS happens to order the chunks so they fall in
// the first 0x1000 bytes (thanks to a massive ICC header).
// Thus we could slice the file down to only sniff the first 0x1000
// bytes (but this makes extractPngChunks choke on the corrupt file)
const headers = imageFile; //.slice(0, 0x1000);
parsePromise = readFileAsArrayBuffer(headers).then(arrayBuffer => {
const buffer = new Uint8Array(arrayBuffer);
const chunks = (0, _pngChunksExtract.default)(buffer);
for (const chunk of chunks) {
if (chunk.name === "pHYs") {
if (chunk.data.byteLength !== PHYS_HIDPI.length) return false;
return chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
}
}
return false;
}).catch(e => {
console.error("Failed to parse PNG", e);
return false;
});
}
const [hidpi] = await Promise.all([parsePromise, imgPromise]);
const width = hidpi ? img.width >> 1 : img.width;
const height = hidpi ? img.height >> 1 : img.height;
return {
width,
height,
img
};
}
// Minimum size for image files before we generate a thumbnail for them.
const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB
const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10%
// We don't apply these thresholds to video thumbnails as a poster image is always useful
// and videos tend to be much larger.
// Image mime types for which to always include a thumbnail for even if it is larger than the input for wider support.
const ALWAYS_INCLUDE_THUMBNAIL = ["image/avif", "image/webp"];
/**
* Read the metadata for an image file and create and upload a thumbnail of the image.
*
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
* @param {String} roomId The ID of the room the image will be uploaded in.
* @param {File} imageFile The image to read and thumbnail.
* @return {Promise} A promise that resolves with the attachment info.
*/
async function infoForImageFile(matrixClient, roomId, imageFile) {
let thumbnailType = "image/png";
if (imageFile.type === "image/jpeg") {
thumbnailType = "image/jpeg";
}
const imageElement = await loadImageElement(imageFile);
const result = await (0, _imageMedia.createThumbnail)(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
const imageInfo = result.info;
// For lesser supported image types, always include the thumbnail even if it is larger
if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) {
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
if (
// image is small enough already
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||
// thumbnail is not sufficiently smaller than original
sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && sizeDifference <= imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT) {
delete imageInfo["thumbnail_info"];
return imageInfo;
}
}
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
imageInfo["thumbnail_url"] = uploadResult.url;
imageInfo["thumbnail_file"] = uploadResult.file;
return imageInfo;
}
/**
* Load a file into a newly created audio element and load the metadata
*
* @param {File} audioFile The file to load in an audio element.
* @return {Promise} A promise that resolves with the audio element.
*/
function loadAudioElement(audioFile) {
return new Promise((resolve, reject) => {
// Load the file into a html element
const audio = document.createElement("audio");
audio.preload = "metadata";
audio.muted = true;
const reader = new FileReader();
reader.onload = function (ev) {
audio.onloadedmetadata = async function () {
resolve(audio);
};
audio.onerror = function (e) {
reject(e);
};
audio.src = ev.target?.result;
};
reader.onerror = function (e) {
reject(e);
};
reader.readAsDataURL(audioFile);
});
}
/**
* Read the metadata for an audio file.
*
* @param {File} audioFile The audio to read.
* @return {Promise} A promise that resolves with the attachment info.
*/
async function infoForAudioFile(audioFile) {
const audio = await loadAudioElement(audioFile);
return {
duration: Math.ceil(audio.duration * 1000)
};
}
/**
* Load a file into a newly created video element and pull some strings
* in an attempt to guarantee the first frame will be showing.
*
* @param {File} videoFile The file to load in a video element.
* @return {Promise} A promise that resolves with the video element.
*/
function loadVideoElement(videoFile) {
return new Promise((resolve, reject) => {
// Load the file into a html element
const video = document.createElement("video");
video.preload = "metadata";
video.playsInline = true;
video.muted = true;
const reader = new FileReader();
reader.onload = function (ev) {
// Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = async function () {
resolve(video);
video.pause();
};
video.onerror = function (e) {
reject(e);
};
let dataUrl = ev.target?.result;
// Chrome chokes on quicktime but likes mp4, and `file.type` is
// read only, so do this horrible hack to unbreak quicktime
if (dataUrl?.startsWith("data:video/quicktime;")) {
dataUrl = dataUrl.replace("data:video/quicktime;", "data:video/mp4;");
}
video.src = dataUrl;
video.load();
video.play();
};
reader.onerror = function (e) {
reject(e);
};
reader.readAsDataURL(videoFile);
});
}
/**
* Read the metadata for a video file and create and upload a thumbnail of the video.
*
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
* @param {String} roomId The ID of the room the video will be uploaded to.
* @param {File} videoFile The video to read and thumbnail.
* @return {Promise} A promise that resolves with the attachment info.
*/
function infoForVideoFile(matrixClient, roomId, videoFile) {
const thumbnailType = "image/jpeg";
const videoInfo = {};
return loadVideoElement(videoFile).then(video => {
videoInfo.duration = Math.ceil(video.duration * 1000);
return (0, _imageMedia.createThumbnail)(video, video.videoWidth, video.videoHeight, thumbnailType);
}).then(result => {
Object.assign(videoInfo, result.info);
return uploadFile(matrixClient, roomId, result.thumbnail);
}).then(result => {
videoInfo.thumbnail_url = result.url;
videoInfo.thumbnail_file = result.file;
return videoInfo;
});
}
/**
* Read the file as an ArrayBuffer.
* @param {File} file The file to read
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
* is read.
*/
function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
resolve(e.target?.result);
};
reader.onerror = function (e) {
reject(e);
};
reader.readAsArrayBuffer(file);
});
}
/**
* Upload the file to the content repository.
* If the room is encrypted then encrypt the file before uploading.
*
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
* @param {String} roomId The ID of the room being uploaded to.
* @param {File} file The file to upload.
* @param {Function?} progressHandler optional callback to be called when a chunk of
* data is uploaded.
* @param {AbortController?} controller optional abortController to use for this upload.
* @return {Promise} A promise that resolves with an object.
* If the file is unencrypted then the object will have a "url" key.
* If the file is encrypted then the object will have a "file" key.
*/
async function uploadFile(matrixClient, roomId, file, progressHandler, controller) {
const abortController = controller ?? new AbortController();
// If the room is encrypted then encrypt the file before uploading it.
if (matrixClient.isRoomEncrypted(roomId)) {
// First read the file into memory.
const data = await readFileAsArrayBuffer(file);
if (abortController.signal.aborted) throw new UploadCanceledError();
// Then encrypt the file.
const encryptResult = await _matrixEncryptAttachment.default.encryptAttachment(data);
if (abortController.signal.aborted) throw new UploadCanceledError();
// Pass the encrypted data as a Blob to the uploader.
const blob = new Blob([encryptResult.data]);
const {
content_uri: url
} = await matrixClient.uploadContent(blob, {
progressHandler,
abortController,
includeFilename: false,
type: "application/octet-stream"
});
if (abortController.signal.aborted) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along with the information
// needed to decrypt the attachment and add it under a file key.
return {
file: _objectSpread(_objectSpread({}, encryptResult.info), {}, {
url
})
};
} else {
const {
content_uri: url
} = await matrixClient.uploadContent(file, {
progressHandler,
abortController
});
if (abortController.signal.aborted) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
return {
url
};
}
}
class ContentMessages {
constructor() {
(0, _defineProperty2.default)(this, "inprogress", []);
(0, _defineProperty2.default)(this, "mediaConfig", null);
}
sendStickerContentToRoom(url, roomId, threadId, info, text, matrixClient) {
return (0, _localRoom.doMaybeLocalRoomAction)(roomId, actualRoomId => matrixClient.sendStickerMessage(actualRoomId, threadId, url, info, text), matrixClient).catch(e => {
_logger.logger.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
throw e;
});
}
getUploadLimit() {
if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) {
return this.mediaConfig["m.upload.size"];
} else {
return null;
}
}
async sendContentListToRoom(files, roomId, relation, matrixClient, context = _RoomContext.TimelineRenderingType.Room) {
if (matrixClient.isGuest()) {
_dispatcher.default.dispatch({
action: "require_registration"
});
return;
}
const replyToEvent = _SDKContext.SdkContextClass.instance.roomViewStore.getQuotingEvent();
if (!this.mediaConfig) {
// hot-path optimization to not flash a spinner if we don't need to
const modal = _Modal.default.createDialog(_Spinner.default, undefined, "mx_Dialog_spinner");
await Promise.race([this.ensureMediaConfigFetched(matrixClient), modal.finished]);
if (!this.mediaConfig) {
// User cancelled by clicking away on the spinner
return;
} else {
modal.close();
}
}
const tooBigFiles = [];
const okFiles = [];
for (const file of files) {
if (this.isFileSizeAcceptable(file)) {
okFiles.push(file);
} else {
tooBigFiles.push(file);
}
}
if (tooBigFiles.length > 0) {
const {
finished
} = _Modal.default.createDialog(_UploadFailureDialog.default, {
badFiles: tooBigFiles,
totalFiles: files.length,
contentMessages: this
});
const [shouldContinue] = await finished;
if (!shouldContinue) return;
}
let uploadAll = false;
// Promise to complete before sending next file into room, used for synchronisation of file-sending
// to match the order the files were specified in
let promBefore = Promise.resolve();
for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i];
const loopPromiseBefore = promBefore;
if (!uploadAll) {
const {
finished
} = _Modal.default.createDialog(_UploadConfirmDialog.default, {
file,
currentIndex: i,
totalFiles: okFiles.length
});
const [shouldContinue, shouldUploadAll] = await finished;
if (!shouldContinue) break;
if (shouldUploadAll) {
uploadAll = true;
}
}
promBefore = (0, _localRoom.doMaybeLocalRoomAction)(roomId, actualRoomId => this.sendContentToRoom(file, actualRoomId, relation, matrixClient, replyToEvent ?? undefined, loopPromiseBefore), matrixClient);
}
if (replyToEvent) {
// Clear event being replied to
_dispatcher.default.dispatch({
action: "reply_to_event",
event: null,
context
});
}
// Focus the correct composer
_dispatcher.default.dispatch({
action: _actions.Action.FocusSendMessageComposer,
context
});
}
getCurrentUploads(relation) {
return this.inprogress.filter(roomUpload => {
const noRelation = !relation && !roomUpload.relation;
const matchingRelation = relation && roomUpload.relation && relation.rel_type === roomUpload.relation.rel_type && relation.event_id === roomUpload.relation.event_id;
return (noRelation || matchingRelation) && !roomUpload.cancelled;
});
}
cancelUpload(upload) {
upload.abort();
_dispatcher.default.dispatch({
action: _actions.Action.UploadCanceled,
upload
});
}
async sendContentToRoom(file, roomId, relation, matrixClient, replyToEvent, promBefore) {
const fileName = file.name || (0, _languageHandler._t)("common|attachment");
const content = {
body: fileName,
info: {
size: file.size
},
msgtype: _matrix.MsgType.File // set more specifically later
};
// Attach mentions, which really only applies if there's a replyToEvent.
(0, _SendMessageComposer.attachMentions)(matrixClient.getSafeUserId(), content, null, replyToEvent);
(0, _SendMessageComposer.attachRelation)(content, relation);
if (replyToEvent) {
(0, _Reply.addReplyToMessageContent)(content, replyToEvent, {
includeLegacyFallback: false
});
}
if (_SettingsStore.default.getValue("Performance.addSendMessageTimingMetadata")) {
(0, _sendTimePerformanceMetrics.decorateStartSendingTime)(content);
}
// if we have a mime type for the file, add it to the message metadata
if (file.type) {
content.info.mimetype = file.type;
}
const upload = new _RoomUpload.RoomUpload(roomId, fileName, relation, file.size);
this.inprogress.push(upload);
_dispatcher.default.dispatch({
action: _actions.Action.UploadStarted,
upload
});
function onProgress(progress) {
upload.onProgress(progress);
_dispatcher.default.dispatch({
action: _actions.Action.UploadProgress,
upload
});
}
try {
if (file.type.startsWith("image/")) {
content.msgtype = _matrix.MsgType.Image;
try {
const imageInfo = await infoForImageFile(matrixClient, roomId, file);
Object.assign(content.info, imageInfo);
} catch (e) {
if (e instanceof _matrix.HTTPError) {
// re-throw to main upload error handler
throw e;
}
// Otherwise we failed to thumbnail, fall back to uploading an m.file
_logger.logger.error(e);
content.msgtype = _matrix.MsgType.File;
}
} else if (file.type.indexOf("audio/") === 0) {
content.msgtype = _matrix.MsgType.Audio;
try {
const audioInfo = await infoForAudioFile(file);
Object.assign(content.info, audioInfo);
} catch (e) {
// Failed to process audio file, fall back to uploading an m.file
_logger.logger.error(e);
content.msgtype = _matrix.MsgType.File;
}
} else if (file.type.indexOf("video/") === 0) {
content.msgtype = _matrix.MsgType.Video;
try {
const videoInfo = await infoForVideoFile(matrixClient, roomId, file);
Object.assign(content.info, videoInfo);
} catch (e) {
// Failed to thumbnail, fall back to uploading an m.file
_logger.logger.error(e);
content.msgtype = _matrix.MsgType.File;
}
} else {
content.msgtype = _matrix.MsgType.File;
}
if (upload.cancelled) throw new UploadCanceledError();
const result = await uploadFile(matrixClient, roomId, file, onProgress, upload.abortController);
content.file = result.file;
content.url = result.url;
if (upload.cancelled) throw new UploadCanceledError();
// Await previous message being sent into the room
if (promBefore) await promBefore;
if (upload.cancelled) throw new UploadCanceledError();
const threadId = relation?.rel_type === _matrix.THREAD_RELATION_TYPE.name ? relation.event_id : null;
const response = await matrixClient.sendMessage(roomId, threadId ?? null, content);
if (_SettingsStore.default.getValue("Performance.addSendMessageTimingMetadata")) {
(0, _sendTimePerformanceMetrics.sendRoundTripMetric)(matrixClient, roomId, response.event_id);
}
_dispatcher.default.dispatch({
action: _actions.Action.UploadFinished,
upload
});
_dispatcher.default.dispatch({
action: "message_sent"
});
} catch (error) {
// 413: File was too big or upset the server in some way:
// clear the media size limit so we fetch it again next time we try to upload
if (error instanceof _matrix.HTTPError && error.httpStatus === 413) {
this.mediaConfig = null;
}
if (!upload.cancelled) {
let desc = (0, _languageHandler._t)("upload_failed_generic", {
fileName: upload.fileName
});
if (error instanceof _matrix.HTTPError && error.httpStatus === 413) {
desc = (0, _languageHandler._t)("upload_failed_size", {
fileName: upload.fileName
});
}
_Modal.default.createDialog(_ErrorDialog.default, {
title: (0, _languageHandler._t)("upload_failed_title"),
description: desc
});
_dispatcher.default.dispatch({
action: _actions.Action.UploadFailed,
upload,
error
});
}
} finally {
(0, _utils.removeElement)(this.inprogress, e => e.promise === upload.promise);
}
}
isFileSizeAcceptable(file) {
if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined && file.size > this.mediaConfig["m.upload.size"]) {
return false;
}
return true;
}
ensureMediaConfigFetched(matrixClient) {
if (this.mediaConfig !== null) return Promise.resolve();
_logger.logger.log("[Media Config] Fetching");
return matrixClient.getMediaConfig().then(config => {
_logger.logger.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
_logger.logger.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then(config => {
this.mediaConfig = config;
});
}
static sharedInstance() {
if (window.mxContentMessages === undefined) {
window.mxContentMessages = new ContentMessages();
}
return window.mxContentMessages;
}
}
exports.default = ContentMessages;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfbWF0cml4IiwicmVxdWlyZSIsIl9tYXRyaXhFbmNyeXB0QXR0YWNobWVudCIsIl9pbnRlcm9wUmVxdWlyZURlZmF1bHQiLCJfcG5nQ2h1bmtzRXh0cmFjdCIsIl9sb2dnZXIiLCJfdXRpbHMiLCJfZGlzcGF0Y2hlciIsIl9sYW5ndWFnZUhhbmRsZXIiLCJfTW9kYWwiLCJfU3Bpbm5lciIsIl9hY3Rpb25zIiwiX1Jvb21VcGxvYWQiLCJfU2V0dGluZ3NTdG9yZSIsIl9zZW5kVGltZVBlcmZvcm1hbmNlTWV0cmljcyIsIl9Sb29tQ29udGV4dCIsIl9SZXBseSIsIl9FcnJvckRpYWxvZyIsIl9VcGxvYWRGYWlsdXJlRGlhbG9nIiwiX1VwbG9hZENvbmZpcm1EaWFsb2ciLCJfaW1hZ2VNZWRpYSIsIl9TZW5kTWVzc2FnZUNvbXBvc2VyIiwiX2xvY2FsUm9vbSIsIl9TREtDb250ZXh0Iiwib3duS2V5cyIsImUiLCJyIiwidCIsIk9iamVjdCIsImtleXMiLCJnZXRPd25Qcm9wZXJ0eVN5bWJvbHMiLCJvIiwiZmlsdGVyIiwiZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yIiwiZW51bWVyYWJsZSIsInB1c2giLCJhcHBseSIsIl9vYmplY3RTcHJlYWQiLCJhcmd1bWVudHMiLCJsZW5ndGgiLCJmb3JFYWNoIiwiX2RlZmluZVByb3BlcnR5MiIsImRlZmF1bHQiLCJnZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3JzIiwiZGVmaW5lUHJvcGVydGllcyIsImRlZmluZVByb3BlcnR5IiwiUEhZU19ISURQSSIsIlVwbG9hZENhbmNlbGVkRXJyb3IiLCJFcnJvciIsImV4cG9ydHMiLCJsb2FkSW1hZ2VFbGVtZW50IiwiaW1hZ2VGaWxlIiwiaW1nIiwiSW1hZ2UiLCJvYmplY3RVcmwiLCJVUkwiLCJjcmVhdGVPYmplY3RVUkwiLCJpbWdQcm9taXNlIiwiUHJvbWlzZSIsInJlc29sdmUiLCJyZWplY3QiLCJvbmxvYWQiLCJyZXZva2VPYmplY3RVUkwiLCJvbmVycm9yIiwic3JjIiwicGFyc2VQcm9taXNlIiwidHlwZSIsImhlYWRlcnMiLCJyZWFkRmlsZUFzQXJyYXlCdWZmZXIiLCJ0aGVuIiwiYXJyYXlCdWZmZXIiLCJidWZmZXIiLCJVaW50OEFycmF5IiwiY2h1bmtzIiwiZXh0cmFjdFBuZ0NodW5rcyIsImNodW5rIiwibmFtZSIsImRhdGEiLCJieXRlTGVuZ3RoIiwiZXZlcnkiLCJ2YWwiLCJpIiwiY2F0Y2giLCJjb25zb2xlIiwiZXJyb3IiLCJoaWRwaSIsImFsbCIsIndpZHRoIiwiaGVpZ2h0IiwiSU1BR0VfU0laRV9USFJFU0hPTERfVEhVTUJOQUlMIiwiSU1BR0VfVEhVTUJOQUlMX01JTl9SRURVQ1RJT05fU0laRSIsIklNQUdFX1RIVU1CTkFJTF9NSU5fUkVEVUNUSU9OX1BFUkNFTlQiLCJBTFdBWVNfSU5DTFVERV9USFVNQk5BSUwiLCJpbmZvRm9ySW1hZ2VGaWxlIiwibWF0cml4Q2xpZW50Iiwicm9vbUlkIiwidGh1bWJuYWlsVHlwZSIsImltYWdlRWxlbWVudCIsInJlc3VsdCIsImNyZWF0ZVRodW1ibmFpbCIsImltYWdlSW5mbyIsImluZm8iLCJpbmNsdWRlcyIsInNpemVEaWZmZXJlbmNlIiwic2l6ZSIsInRodW1ibmFpbF9pbmZvIiwidXBsb2FkUmVzdWx0IiwidXBsb2FkRmlsZSIsInRodW1ibmFpbCIsInVybCIsImZpbGUiLCJsb2FkQXVkaW9FbGVtZW50IiwiYXVkaW9GaWxlIiwiYXVkaW8iLCJkb2N1bWVudCIsImNyZWF0ZUVsZW1lbnQiLCJwcmVsb2FkIiwibXV0ZWQiLCJyZWFkZXIiLCJGaWxlUmVhZGVyIiwiZXYiLCJvbmxvYWRlZG1ldGFkYXRhIiwidGFyZ2V0IiwicmVhZEFzRGF0YVVSTCIsImluZm9Gb3JBdWRpb0ZpbGUiLCJkdXJhdGlvbiIsIk1hdGgiLCJjZWlsIiwibG9hZFZpZGVvRWxlbWVudCIsInZpZGVvRmlsZSIsInZpZGVvIiwicGxheXNJbmxpbmUiLCJvbmxvYWRlZGRhdGEiLCJwYXVzZSIsImRhdGFVcmwiLCJzdGFydHNXaXRoIiwicmVwbGFjZSIsImxvYWQiLCJwbGF5IiwiaW5mb0ZvclZpZGVvRmlsZSIsInZpZGVvSW5mbyIsInZpZGVvV2lkdGgiLCJ2aWRlb0hlaWdodCIsImFzc2lnbiIsInRodW1ibmFpbF91cmwiLCJ0aHVtYm5haWxfZmlsZSIsInJlYWRBc0FycmF5QnVmZmVyIiwicHJvZ3Jlc3NIYW5kbGVyIiwiY29udHJvbGxlciIsImFib3J0Q29udHJvbGxlciIsIkFib3J0Q29udHJvbGxlciIsImlzUm9vbUVuY3J5cHRlZCIsInNpZ25hbCIsImFib3J0ZWQiLCJlbmNyeXB0UmVzdWx0IiwiZW5jcnlwdCIsImVuY3J5cHRBdHRhY2htZW50IiwiYmxvYiIsIkJsb2IiLCJjb250ZW50X3VyaSIsInVwbG9hZENvbnRlbnQiLCJpbmNsdWRlRmlsZW5hbWUiLCJDb250ZW50TWVzc2FnZXMiLCJjb25zdHJ1Y3RvciIsInNlbmRTdGlja2VyQ29udGVudFRvUm9vbSIsInRocmVhZElkIiwidGV4dCIsImRvTWF5YmVMb2NhbFJvb21BY3Rpb24iLCJhY3R1YWxSb29tSWQiLCJzZW5kU3RpY2tlck1lc3NhZ2UiLCJsb2dnZXIiLCJ3YXJuIiwiZ2V0VXBsb2FkTGltaXQiLCJtZWRpYUNvbmZpZyIsInVuZGVmaW5lZCIsInNlbmRDb250ZW50TGlzdFRvUm9vbSIsImZpbGVzIiwicmVsYXRpb24iLCJjb250ZXh0IiwiVGltZWxpbmVSZW5kZXJpbmdUeXBlIiwiUm9vbSIsImlzR3Vlc3QiLCJkaXMiLCJkaXNwYXRjaCIsImFjdGlvbiIsInJlcGx5VG9FdmVudCIsIlNka0NvbnRleHRDbGFzcyIsImluc3RhbmNlIiwicm9vbVZpZXdTdG9yZSIsImdldFF1b3RpbmdFdmVudCIsIm1vZGFsIiwiTW9kYWwiLCJjcmVhdGVEaWFsb2ciLCJTcGlubmVyIiwicmFjZSIsImVuc3VyZU1lZGlhQ29uZmlnRmV0Y2hlZCIsImZpbmlzaGVkIiwiY2xvc2UiLCJ0b29CaWdGaWxlcyIsIm9rRmlsZXMiLCJpc0ZpbGVTaXplQWNjZXB0YWJsZSIsIlVwbG9hZEZhaWx1cmVEaWFsb2ciLCJiYWRGaWxlcyIsInRvdGFsRmlsZXMiLCJjb250ZW50TWVzc2FnZXMiLCJzaG91bGRDb250aW51ZSIsInVwbG9hZEFsbCIsInByb21CZWZvcmUiLCJsb29wUHJvbWlzZUJlZm9yZSIsIlVwbG9hZENvbmZpcm1EaWFsb2ciLCJjdXJyZW50SW5kZXgiLCJzaG91bGRVcGxvYWRBbGwiLCJzZW5kQ29udGVudFRvUm9vbSIsImV2ZW50IiwiQWN0aW9uIiwiRm9jdXNTZW5kTWVzc2FnZUNvbXBvc2VyIiwiZ2V0Q3VycmVudFVwbG9hZHMiLCJpbnByb2dyZXNzIiwicm9vbVVwbG9hZCIsIm5vUmVsYXRpb24iLCJtYXRjaGluZ1JlbGF0aW9uIiwicmVsX3R5cGUiLCJldmVudF9pZCIsImNhbmNlbGxlZCIsImNhbmNlbFVwbG9hZCIsInVwbG9hZCIsImFib3J0IiwiVXBsb2FkQ2FuY2VsZWQiLCJmaWxlTmFtZSIsIl90IiwiY29udGVudCIsImJvZHkiLCJtc2d0eXBlIiwiTXNnVHlwZSIsIkZpbGUiLCJhdHRhY2hNZW50aW9ucyIsImdldFNhZmVVc2VySWQiLCJhdHRhY2hSZWxhdGlvbiIsImFkZFJlcGx5VG9NZXNzYWdlQ29udGVudCIsImluY2x1ZGVMZWdhY3lGYWxsYmFjayIsIlNldHRpbmdzU3RvcmUiLCJnZXRWYWx1ZSIsImRlY29yYXRlU3RhcnRTZW5kaW5nVGltZSIsIm1pbWV0eXBlIiwiUm9vbVVwbG9hZCIsIlVwbG9hZFN0YXJ0ZWQiLCJvblByb2dyZXNzIiwicHJvZ3Jlc3MiLCJVcGxvYWRQcm9ncmVzcyIsIkhUVFBFcnJvciIsImluZGV4T2YiLCJBdWRpbyIsImF1ZGlvSW5mbyIsIlZpZGVvIiwiVEhSRUFEX1JFTEFUSU9OX1RZUEUiLCJyZXNwb25zZSIsInNlbmRNZXNzYWdlIiwic2VuZFJvdW5kVHJpcE1ldHJpYyIsIlVwbG9hZEZpbmlzaGVkIiwiaHR0cFN0YXR1cyIsImRlc2MiLCJFcnJvckRpYWxvZyIsInRpdGxlIiwiZGVzY3JpcHRpb24iLCJVcGxvYWRGYWlsZWQiLCJyZW1vdmVFbGVtZW50IiwicHJvbWlzZSIsImxvZyIsImdldE1lZGlhQ29uZmlnIiwiY29uZmlnIiwic2hhcmVkSW5zdGFuY2UiLCJ3aW5kb3ciLCJteENvbnRlbnRNZXNzYWdlcyJdLCJzb3VyY2VzIjpbIi4uL3NyYy9Db250ZW50TWVzc2FnZXMudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLypcbkNvcHlyaWdodCAyMDI0IE5ldyBWZWN0b3IgTHRkLlxuQ29weXJpZ2h0IDIwMjAgVGhlIE1hdHJpeC5vcmcgRm91bmRhdGlvbiBDLkkuQy5cbkNvcHlyaWdodCAyMDE5IE5ldyBWZWN0b3IgTHRkXG5Db3B5cmlnaHQgMjAxNSwgMjAxNiBPcGVuTWFya2V0IEx0ZFxuXG5TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQUdQTC0zLjAtb25seSBPUiBHUEwtMy4wLW9ubHlcblBsZWFzZSBzZWUgTElDRU5TRSBmaWxlcyBpbiB0aGUgcmVwb3NpdG9yeSByb290IGZvciBmdWxsIGRldGFpbHMuXG4qL1xuXG5pbXBvcnQge1xuICAgIE1hdHJpeENsaWVudCxcbiAgICBNc2dUeXBlLFxuICAgIEhUVFBFcnJvcixcbiAgICBJRXZlbnRSZWxhdGlvbixcbiAgICBJU2VuZEV2ZW50UmVzcG9uc2UsXG4gICAgTWF0cml4RXZlbnQsXG4gICAgVXBsb2FkT3B0cyxcbiAgICBVcGxvYWRQcm9ncmVzcyxcbiAgICBUSFJFQURfUkVMQVRJT05fVFlQRSxcbn0gZnJvbSBcIm1hdHJpeC1qcy1zZGsvc3JjL21hdHJpeFwiO1xuaW1wb3J0IHtcbiAgICBJbWFnZUluZm8sXG4gICAgQXVkaW9JbmZvLFxuICAgIFZpZGVvSW5mbyxcbiAgICBFbmNyeXB0ZWRGaWxlLFxuICAgIE1lZGlhRXZlbnRDb250ZW50LFxuICAgIE1lZGlhRXZlbnRJbmZvLFxufSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvdHlwZXNcIjtcbmltcG9ydCBlbmNyeXB0IGZyb20gXCJtYXRyaXgtZW5jcnlwdC1hdHRhY2htZW50XCI7XG5pbXBvcnQgZXh0cmFjdFBuZ0NodW5rcyBmcm9tIFwicG5nLWNodW5rcy1leHRyYWN0XCI7XG5pbXBvcnQgeyBsb2dnZXIgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvbG9nZ2VyXCI7XG5pbXBvcnQgeyByZW1vdmVFbGVtZW50IH0gZnJvbSBcIm1hdHJpeC1qcy1zZGsvc3JjL3V0aWxzXCI7XG5cbmltcG9ydCBkaXMgZnJvbSBcIi4vZGlzcGF0Y2hlci9kaXNwYXRjaGVyXCI7XG5pbXBvcnQgeyBfdCB9IGZyb20gXCIuL2xhbmd1YWdlSGFuZGxlclwiO1xuaW1wb3J0IE1vZGFsIGZyb20gXCIuL01vZGFsXCI7XG5pbXBvcnQgU3Bpbm5lciBmcm9tIFwiLi9jb21wb25lbnRzL3ZpZXdzL2VsZW1lbnRzL1NwaW5uZXJcIjtcbmltcG9ydCB7IEFjdGlvbiB9IGZyb20gXCIuL2Rpc3BhdGNoZXIvYWN0aW9uc1wiO1xuaW1wb3J0IHtcbiAgICBVcGxvYWRDYW5jZWxlZFBheWxvYWQsXG4gICAgVXBsb2FkRXJyb3JQYXlsb2FkLFxuICAgIFVwbG9hZEZpbmlzaGVkUGF5bG9hZCxcbiAgICBVcGxvYWRQcm9ncmVzc1BheWxvYWQsXG4gICAgVXBsb2FkU3RhcnRlZFBheWxvYWQsXG59IGZyb20gXCIuL2Rpc3BhdGNoZXIvcGF5bG9hZHMvVXBsb2FkUGF5bG9hZFwiO1xuaW1wb3J0IHsgUm9vbVVwbG9hZCB9IGZyb20gXCIuL21vZGVscy9Sb29tVXBsb2FkXCI7XG5pbXBvcnQgU2V0dGluZ3NTdG9yZSBmcm9tIFwiLi9zZXR0aW5ncy9TZXR0aW5nc1N0b3JlXCI7XG5pbXBvcnQgeyBkZWNvcmF0ZVN0YXJ0U2VuZGluZ1RpbWUsIHNlbmRSb3VuZFRyaXBNZXRyaWMgfSBmcm9tIFwiLi9zZW5kVGltZVBlcmZvcm1hbmNlTWV0cmljc1wiO1xuaW1wb3J0IHsgVGltZWxpbmVSZW5kZXJpbmdUeXBlIH0gZnJvbSBcIi4vY29udGV4dHMvUm9vbUNvbnRleHRcIjtcbmltcG9ydCB7IGFkZFJlcGx5VG9NZXNzYWdlQ29udGVudCB9IGZyb20gXCIuL3V0aWxzL1JlcGx5XCI7XG5pbXBvcnQgRXJyb3JEaWFsb2cgZnJvbSBcIi4vY29tcG9uZW50cy92aWV3cy9kaWFsb2dzL0Vycm9yRGlhbG9nXCI7XG5pbXBvcnQgVXBsb2FkRmFpbHVyZURpYWxvZyBmcm9tIFwiLi9jb21wb25lbnRzL3ZpZXdzL2RpYWxvZ3MvVXBsb2FkRmFpbHVyZURpYWxvZ1wiO1xuaW1wb3J0IFVwbG9hZENvbmZpcm1EaWFsb2cgZnJvbSBcIi4vY29tcG9uZW50cy92aWV3cy9kaWFsb2dzL1VwbG9hZENvbmZpcm1EaWFsb2dcIjtcbmltcG9ydCB7IGNyZWF0ZVRodW1ibmFpbCB9IGZyb20gXCIuL3V0aWxzL2ltYWdlLW1lZGlhXCI7XG5pbXBvcnQgeyBhdHRhY2hNZW50aW9ucywgYXR0YWNoUmVsYXRpb24gfSBmcm9tIFwiLi9jb21wb25lbnRzL3ZpZXdzL3Jvb21zL1NlbmRNZXNzYWdlQ29tcG9zZXJcIjtcbmltcG9ydCB7IGRvTWF5YmVMb2NhbFJvb21BY3Rpb24gfSBmcm9tIFwiLi91dGlscy9sb2NhbC1yb29tXCI7XG5pbXBvcnQgeyBTZGtDb250ZXh0Q2xhc3MgfSBmcm9tIFwiLi9jb250ZXh0cy9TREtDb250ZXh0XCI7XG5cbi8vIHNjcmFwZWQgb3V0IG9mIGEgbWFjT1MgaGlkcGkgKDU2NjBwcG0pIHNjcmVlbnNob3QgcG5nXG4vLyAgICAgICAgICAgICAgICAgIDU2NjkgcHggKHgtYXhpcykgICAgICAsIDU2NjkgcHggKHktYXhpcykgICAgICAsIHBlciBtZXRyZVxuY29uc3QgUEhZU19ISURQSSA9IFsweDAwLCAweDAwLCAweDE2LCAweDI1LCAweDAwLCAweDAwLCAweDE2LCAweDI1LCAweDAxXTtcblxuZXhwb3J0IGNsYXNzIFVwbG9hZENhbmNlbGVkRXJyb3IgZXh0ZW5kcyBFcnJvciB7fVxuXG5pbnRlcmZhY2UgSU1lZGlhQ29uZmlnIHtcbiAgICBcIm0udXBsb2FkLnNpemVcIj86IG51bWJlcjtcbn1cblxuLyoqXG4gKiBMb2FkIGEgZmlsZSBpbnRvIGEgbmV3bHkgY3JlYXRlZCBpbWFnZSBlbGVtZW50LlxuICpcbiAqIEBwYXJhbSB7RmlsZX0gaW1hZ2VGaWxlIFRoZSBmaWxlIHRvIGxvYWQgaW4gYW4gaW1hZ2UgZWxlbWVudC5cbiAqIEByZXR1cm4ge1Byb21pc2V9IEEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdpdGggdGhlIGh0bWwgaW1hZ2UgZWxlbWVudC5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gbG9hZEltYWdlRWxlbWVudChpbWFnZUZpbGU6IEZpbGUpOiBQcm9taXNlPHtcbiAgICB3aWR0aDogbnVtYmVyO1xuICAgIGhlaWdodDogbnVtYmVyO1xuICAgIGltZzogSFRNTEltYWdlRWxlbWVudDtcbn0+IHtcbiAgICAvLyBMb2FkIHRoZSBmaWxlIGludG8gYW4gaHRtbCBlbGVtZW50XG4gICAgY29uc3QgaW1nID0gbmV3IEltYWdlKCk7XG4gICAgY29uc3Qgb2JqZWN0VXJsID0gVVJMLmNyZWF0ZU9iamVjdFVSTChpbWFnZUZpbGUpO1xuICAgIGNvbnN0IGltZ1Byb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIGltZy5vbmxvYWQgPSBmdW5jdGlvbiAoKTogdm9pZCB7XG4gICAgICAgICAgICBVUkwucmV2b2tlT2JqZWN0VVJMKG9iamVjdFVybCk7XG4gICAgICAgICAgICByZXNvbHZlKGltZyk7XG4gICAgICAgIH07XG4gICAgICAgIGltZy5vbmVycm9yID0gZnVuY3Rpb24gKGUpOiB2b2lkIHtcbiAgICAgICAgICAgIHJlamVjdChlKTtcbiAgICAgICAgfTtcbiAgICB9KTtcbiAgICBpbWcuc3JjID0gb2JqZWN0VXJsO1xuXG4gICAgLy8gY2hlY2sgZm9yIGhpLWRwaSBQTkdzIGFuZCBmdWRnZSBkaXNwbGF5IHJlc29sdXRpb24gYXMgbmVlZGVkLlxuICAgIC8vIHRoaXMgaXMgbWFpbmx5IG5lZWRlZCBmb3IgbWFjT1Mgc2NyZWVuY2Fwc1xuICAgIGxldCBwYXJzZVByb21pc2UgPSBQcm9taXNlLnJlc29sdmUoZmFsc2UpO1xuICAgIGlmIChpbWFnZUZpbGUudHlwZSA9PT0gXCJpbWFnZS9wbmdcIikge1xuICAgICAgICAvLyBpbiBwcmFjdGljZSBtYWNPUyBoYXBwZW5zIHRvIG9yZGVyIHRoZSBjaHVua3Mgc28gdGhleSBmYWxsIGluXG4gICAgICAgIC8vIHRoZSBmaXJzdCAweDEwMDAgYnl0ZXMgKHRoYW5rcyB0byBhIG1hc3NpdmUgSUNDIGhlYWRlcikuXG4gICAgICAgIC8vIFRodXMgd2UgY291bGQgc2xpY2UgdGhlIGZpbGUgZG93biB0byBvbmx5IHNuaWZmIHRoZSBmaXJzdCAweDEwMDBcbiAgICAgICAgLy8gYnl0ZXMgKGJ1dCB0aGlzIG1ha2VzIGV4dHJhY3RQbmdDaHVua3MgY2hva2Ugb24gdGhlIGNvcnJ1cHQgZmlsZSlcbiAgICAgICAgY29uc3QgaGVhZGVycyA9IGltYWdlRmlsZTsgLy8uc2xpY2UoMCwgMHgxMDAwKTtcbiAgICAgICAgcGFyc2VQcm9taXNlID0gcmVhZEZpbGVBc0FycmF5QnVmZmVyKGhlYWRlcnMpXG4gICAgICAgICAgICAudGhlbigoYXJyYXlCdWZmZXIpID0+IHtcbiAgICAgICAgICAgICAgICBjb25zdCBidWZmZXIgPSBuZXcgVWludDhBcnJheShhcnJheUJ1ZmZlcik7XG4gICAgICAgICAgICAgICAgY29uc3QgY2h1bmtzID0gZXh0cmFjdFBuZ0NodW5rcyhidWZmZXIpO1xuICAgICAgICAgICAgICAgIGZvciAoY29uc3QgY2h1bmsgb2YgY2h1bmtzKSB7XG4gICAgICAgICAgICAgICAgICAgIGlmIChjaHVuay5uYW1lID09PSBcInBIWXNcIikge1xuICAgICAgICAgICAgICAgICAgICAgICAgaWYgKGNodW5rLmRhdGEuYnl0ZUxlbmd0aCAhPT0gUEhZU19ISURQSS5sZW5ndGgpIHJldHVybiBmYWxzZTtcbiAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybiBjaHVuay5kYXRhLmV2ZXJ5KCh2YWwsIGkpID0+IHZhbCA9PT0gUEhZU19ISURQSVtpXSk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICAgICAgfSlcbiAgICAgICAgICAgIC5jYXRjaCgoZSkgPT4ge1xuICAgICAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoXCJGYWlsZWQgdG8gcGFyc2UgUE5HXCIsIGUpO1xuICAgICAgICAgICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgICAgICAgIH0pO1xuICAgIH1cblxuICAgIGNvbnN0IFtoaWRwaV0gPSBhd2FpdCBQcm9taXNlLmFsbChbcGFyc2VQcm9taXNlLCBpbWdQcm9taXNlXSk7XG4gICAgY29uc3Qgd2lkdGggPSBoaWRwaSA/IGltZy53aWR0aCA+PiAxIDogaW1nLndpZHRoO1xuICAgIGNvbnN0IGhlaWdodCA9IGhpZHBpID8gaW1nLmhlaWdodCA+PiAxIDogaW1nLmhlaWdodDtcbiAgICByZXR1cm4geyB3aWR0aCwgaGVpZ2h0LCBpbWcgfTtcbn1cblxuLy8gTWluaW11bSBzaXplIGZvciBpbWFnZSBmaWxlcyBiZWZvcmUgd2UgZ2VuZXJhdGUgYSB0aHVtYm5haWwgZm9yIHRoZW0uXG5jb25zdCBJTUFHRV9TSVpFX1RIUkVTSE9MRF9USFVNQk5BSUwgPSAxIDw8IDE1OyAvLyAzMktCXG4vLyBNaW5pbXVtIHNpemUgaW1wcm92ZW1lbnQgZm9yIGltYWdlIHRodW1ibmFpbHMsIGlmIGJvdGggYXJlIG5vdCBtZXQgdGhlbiBkb24ndCBib3RoZXIgdXBsb2FkaW5nIHRodW1ibmFpbC5cbmNvbnN0IElNQUdFX1RIVU1CTkFJTF9NSU5fUkVEVUNUSU9OX1NJWkUgPSAxIDw8IDE2OyAvLyAxTUJcbmNvbnN0IElNQUdFX1RIVU1CTkFJTF9NSU5fUkVEVUNUSU9OX1BFUkNFTlQgPSAwLjE7IC8vIDEwJVxuLy8gV2UgZG9uJ3QgYXBwbHkgdGhlc2UgdGhyZXNob2xkcyB0byB2aWRlbyB0aHVtYm5haWxzIGFzIGEgcG9zdGVyIGltYWdlIGlzIGFsd2F5cyB1c2VmdWxcbi8vIGFuZCB2aWRlb3MgdGVuZCB0byBiZSBtdWNoIGxhcmdlci5cblxuLy8gSW1hZ2UgbWltZSB0eXBlcyBmb3Igd2hpY2ggdG8gYWx3YXlzIGluY2x1ZGUgYSB0aHVtYm5haWwgZm9yIGV2ZW4gaWYgaXQgaXMgbGFyZ2VyIHRoYW4gdGhlIGlucHV0IGZvciB3aWRlciBzdXBwb3J0LlxuY29uc3QgQUxXQVlTX0lOQ0xVREVfVEhVTUJOQUlMID0gW1wiaW1hZ2UvYXZpZlwiLCBcImltYWdlL3dlYnBcIl07XG5cbi8qKlxuICogUmVhZCB0aGUgbWV0YWRhdGEgZm9yIGFuIGltYWdlIGZpbGUgYW5kIGNyZWF0ZSBhbmQgdXBsb2FkIGEgdGh1bWJuYWlsIG9mIHRoZSBpbWFnZS5cbiAqXG4gKiBAcGFyYW0ge01hdHJpeENsaWVudH0gbWF0cml4Q2xpZW50IEEgbWF0cml4Q2xpZW50IHRvIHVwbG9hZCB0aGUgdGh1bWJuYWlsIHdpdGguXG4gKiBAcGFyYW0ge1N0cmluZ30gcm9vbUlkIFRoZSBJRCBvZiB0aGUgcm9vbSB0aGUgaW1hZ2Ugd2lsbCBiZSB1cGxvYWRlZCBpbi5cbiAqIEBwYXJhbSB7RmlsZX0gaW1hZ2VGaWxlIFRoZSBpbWFnZSB0byByZWFkIGFuZCB0aHVtYm5haWwuXG4gKiBAcmV0dXJuIHtQcm9taXNlfSBBIHByb21pc2UgdGhhdCByZXNvbHZlcyB3aXRoIHRoZSBhdHRhY2htZW50IGluZm8uXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIGluZm9Gb3JJbWFnZUZpbGUobWF0cml4Q2xpZW50OiBNYXRyaXhDbGllbnQsIHJvb21JZDogc3RyaW5nLCBpbWFnZUZpbGU6IEZpbGUpOiBQcm9taXNlPEltYWdlSW5mbz4ge1xuICAgIGxldCB0aHVtYm5haWxUeXBlID0gXCJpbWFnZS9wbmdcIjtcbiAgICBpZiAoaW1hZ2VGaWxlLnR5cGUgPT09IFwiaW1hZ2UvanBlZ1wiKSB7XG4gICAgICAgIHRodW1ibmFpbFR5cGUgPSBcImltYWdlL2pwZWdcIjtcbiAgICB9XG5cbiAgICBjb25zdCBpbWFnZUVsZW1lbnQgPSBhd2FpdCBsb2FkSW1hZ2VFbGVtZW50KGltYWdlRmlsZSk7XG5cbiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBjcmVhdGVUaHVtYm5haWwoaW1hZ2VFbGVtZW50LmltZywgaW1hZ2VFbGVtZW50LndpZHRoLCBpbWFnZUVsZW1lbnQuaGVpZ2h0LCB0aHVtYm5haWxUeXBlKTtcbiAgICBjb25zdCBpbWFnZUluZm8gPSByZXN1bHQuaW5mbztcblxuICAgIC8vIEZvciBsZXNzZXIgc3VwcG9ydGVkIGltYWdlIHR5cGVzLCBhbHdheXMgaW5jbHVkZSB0aGUgdGh1bWJuYWlsIGV2ZW4gaWYgaXQgaXMgbGFyZ2VyXG4gICAgaWYgKCFBTFdBWVNfSU5DTFVERV9USFVNQk5BSUwuaW5jbHVkZXMoaW1hZ2VGaWxlLnR5cGUpKSB7XG4gICAgICAgIC8vIHdlIGRvIGFsbCBzaXppbmcgY2hlY2tzIGhlcmUgYmVjYXVzZSB3ZSBzdGlsbCByZWx5IG9uIHRodW1ibmFpbCBnZW5lcmF0aW9uIGZvciBtYWtpbmcgYSBibHVyaGFzaCBmcm9tLlxuICAgICAgICBjb25zdCBzaXplRGlmZmVyZW5jZSA9IGltYWdlRmlsZS5zaXplIC0gaW1hZ2VJbmZvLnRodW1ibmFpbF9pbmZvIS5zaXplO1xuICAgICAgICBpZiAoXG4gICAgICAgICAgICAvLyBpbWFnZSBpcyBzbWFsbCBlbm91Z2ggYWxyZWFkeVxuICAgICAgICAgICAgaW1hZ2VGaWxlLnNpemUgPD0gSU1BR0VfU0laRV9USFJFU0hPTERfVEhVTUJOQUlMIHx8XG4gICAgICAgICAgICAvLyB0aHVtYm5haWwgaXMgbm90IHN1ZmZpY2llbnRseSBzbWFsbGVyIHRoYW4gb3JpZ2luYWxcbiAgICAgICAgICAgIChzaXplRGlmZmVyZW5jZSA8PSBJTUFHRV9USFVNQk5BSUxfTUlOX1JFRFVDVElPTl9TSVpFICYmXG4gICAgICAgICAgICAgICAgc2l6ZURpZmZlcmVuY2UgPD0gaW1hZ2VGaWxlLnNpemUgKiBJTUFHRV9USFVNQk5BSUxfTUlOX1JFRFVDVElPTl9QRVJDRU5UKVxuICAgICAgICApIHtcbiAgICAgICAgICAgIGRlbGV0ZSBpbWFnZUluZm9bXCJ0aHVtYm5haWxfaW5mb1wiXTtcbiAgICAgICAgICAgIHJldHVybiBpbWFnZUluZm87XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBjb25zdCB1cGxvYWRSZXN1bHQgPSBhd2FpdCB1cGxvYWRGaWxlKG1hdHJpeENsaWVudCwgcm9vbUlkLCByZXN1bHQudGh1bWJuYWlsKTtcblxuICAgIGltYWdlSW5mb1tcInRodW1ibmFpbF91cmxcIl0gPSB1cGxvYWRSZXN1bHQudXJsO1xuICAgIGltYWdlSW5mb1tcInRodW1ibmFpbF9maWxlXCJdID0gdXBsb2FkUmVzdWx0LmZpbGU7XG4gICAgcmV0dXJuIGltYWdlSW5mbztcbn1cblxuLyoqXG4gKiBMb2FkIGEgZmlsZSBpbnRvIGEgbmV3bHkgY3JlYXRlZCBhdWRpbyBlbGVtZW50IGFuZCBsb2FkIHRoZSBtZXRhZGF0YVxuICpcbiAqIEBwYXJhbSB7RmlsZX0gYXVkaW9GaWxlIFRoZSBmaWxlIHRvIGxvYWQgaW4gYW4gYXVkaW8gZWxlbWVudC5cbiAqIEByZXR1cm4ge1Byb21pc2V9IEEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdpdGggdGhlIGF1ZGlvIGVsZW1lbnQuXG4gKi9cbmZ1bmN0aW9uIGxvYWRBdWRpb0VsZW1lbnQoYXVkaW9GaWxlOiBGaWxlKTogUHJvbWlzZTxIVE1MQXVkaW9FbGVtZW50PiB7XG4gICAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgLy8gTG9hZCB0aGUgZmlsZSBpbnRvIGEgaHRtbCBlbGVtZW50XG4gICAgICAgIGNvbnN0IGF1ZGlvID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImF1ZGlvXCIpO1xuICAgICAgICBhdWRpby5wcmVsb2FkID0gXCJtZXRhZGF0YVwiO1xuICAgICAgICBhdWRpby5tdXRlZCA9IHRydWU7XG5cbiAgICAgICAgY29uc3QgcmVhZGVyID0gbmV3IEZpbGVSZWFkZXIoKTtcblxuICAgICAgICByZWFkZXIub25sb2FkID0gZnVuY3Rpb24gKGV2KTogdm9pZCB7XG4gICAgICAgICAgICBhdWRpby5vbmxvYWRlZG1ldGFkYXRhID0gYXN5bmMgZnVuY3Rpb24gKCk6IFByb21pc2U8dm9pZD4ge1xuICAgICAgICAgICAgICAgIHJlc29sdmUoYXVkaW8pO1xuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIGF1ZGlvLm9uZXJyb3IgPSBmdW5jdGlvbiAoZSk6IHZvaWQge1xuICAgICAgICAgICAgICAgIHJlamVjdChlKTtcbiAgICAgICAgICAgIH07XG5cbiAgICAgICAgICAgIGF1ZGlvLnNyYyA9IGV2LnRhcmdldD8ucmVzdWx0IGFzIHN0cmluZztcbiAgICAgICAgfTtcbiAgICAgICAgcmVhZGVyLm9uZXJyb3IgPSBmdW5jdGlvbiAoZSk6IHZvaWQge1xuICAgICAgICAgICAgcmVqZWN0KGUpO1xuICAgICAgICB9O1xuICAgICAgICByZWFkZXIucmVhZEFzRGF0YVVSTChhdWRpb0ZpbGUpO1xuICAgIH0pO1xufVxuXG4vKipcbiAqIFJlYWQgdGhlIG1ldGFkYXRhIGZvciBhbiBhdWRpbyBmaWxlLlxuICpcbiAqIEBwYXJhbSB7RmlsZX0gYXVkaW9GaWxlIFRoZSBhdWRpbyB0byByZWFkLlxuICogQHJldHVybiB7UHJvbWlzZX0gQSBwcm9taXNlIHRoYXQgcmVzb2x2ZXMgd2l0aCB0aGUgYXR0YWNobWVudCBpbmZvLlxuICovXG5hc3luYyBmdW5jdGlvbiBpbmZvRm9yQXVkaW9GaWxlKGF1ZGlvRmlsZTogRmlsZSk6IFByb21pc2U8QXVkaW9JbmZvPiB7XG4gICAgY29uc3QgYXVkaW8gPSBhd2FpdCBsb2FkQXVkaW9FbGVtZW50KGF1ZGlvRmlsZSk7XG4gICAgcmV0dXJuIHsgZHVyYXRpb246IE1hdGguY2VpbChhdWRpby5kdXJhdGlvbiAqIDEwMDApIH07XG59XG5cbi8qKlxuICogTG9hZCBhIGZpbGUgaW50byBhIG5ld2x5IGNyZWF0ZWQgdmlkZW8gZWxlbWVudCBhbmQgcHVsbCBzb21lIHN0cmluZ3NcbiAqIGluIGFuIGF0dGVtcHQgdG8gZ3VhcmFudGVlIHRoZSBmaXJzdCBmcmFtZSB3aWxsIGJlIHNob3dpbmcuXG4gKlxuICogQHBhcmFtIHtGaWxlfSB2aWRlb0ZpbGUgVGhlIGZpbGUgdG8gbG9hZCBpbiBhIHZpZGVvIGVsZW1lbnQuXG4gKiBAcmV0dXJuIHtQcm9taXNlfSBBIHByb21pc2UgdGhhdCByZXNvbHZlcyB3aXRoIHRoZSB2aWRlbyBlbGVtZW50LlxuICovXG5mdW5jdGlvbiBsb2FkVmlkZW9FbGVtZW50KHZpZGVvRmlsZTogRmlsZSk6IFByb21pc2U8SFRNTFZpZGVvRWxlbWVudD4ge1xuICAgIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIC8vIExvYWQgdGhlIGZpbGUgaW50byBhIGh0bWwgZWxlbWVudFxuICAgICAgICBjb25zdCB2aWRlbyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJ2aWRlb1wiKTtcbiAgICAgICAgdmlkZW8ucHJlbG9hZCA9IFwibWV0YWRhdGFcIjtcbiAgICAgICAgdmlkZW8ucGxheXNJbmxpbmUgPSB0cnVlO1xuICAgICAgICB2aWRlby5tdXRlZCA9IHRydWU7XG5cbiAgICAgICAgY29uc3QgcmVhZGVyID0gbmV3IEZpbGVSZWFkZXIoKTtcblxuICAgICAgICByZWFkZXIub25sb2FkID0gZnVuY3Rpb24gKGV2KTogdm9pZCB7XG4gICAgICAgICAgICAvLyBXYWl0IHVudGlsIHdlIGhhdmUgZW5vdWdoIGRhdGEgdG8gdGh1bWJuYWlsIHRoZSBmaXJzdCBmcmFtZS5cbiAgICAgICAgICAgIHZpZGVvLm9ubG9hZGVkZGF0YSA9IGFzeW5jIGZ1bmN0aW9uICgpOiBQcm9taXNlPHZvaWQ+IHtcbiAgICAgICAgICAgICAgICByZXNvbHZlKHZpZGVvKTtcbiAgICAgICAgICAgICAgICB2aWRlby5wYXVzZSgpO1xuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIHZpZGVvLm9uZXJyb3IgPSBmdW5jdGlvbiAoZSk6IHZvaWQge1xuICAgICAgICAgICAgICAgIHJlamVjdChlKTtcbiAgICAgICAgICAgIH07XG5cbiAgICAgICAgICAgIGxldCBkYXRhVXJsID0gZXYudGFyZ2V0Py5yZXN1bHQgYXMgc3RyaW5nO1xuICAgICAgICAgICAgLy8gQ2hyb21lIGNob2tlcyBvbiBxdWlja3RpbWUgYnV0IGxpa2VzIG1wNCwgYW5kIGBmaWxlLnR5cGVgIGlzXG4gICAgICAgICAgICAvLyByZWFkIG9ubHksIHNvIGRvIHRoaXMgaG9ycmlibGUgaGFjayB0byB1bmJyZWFrIHF1aWNrdGltZVxuICAgICAgICAgICAgaWYgKGRhdGFVcmw/LnN0YXJ0c1dpdGgoXCJkYXRhOnZpZGVvL3F1aWNrdGltZTtcIikpIHtcbiAgICAgICAgICAgICAgICBkYXRhVXJsID0gZGF0YVVybC5yZXBsYWNlKFwiZGF0YTp2aWRlby9xdWlja3RpbWU7XCIsIFwiZGF0YTp2aWRlby9tcDQ7XCIpO1xuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICB2aWRlby5zcmMgPSBkYXRhVXJsO1xuICAgICAgICAgICAgdmlkZW8ubG9hZCgpO1xuICAgICAgICAgICAgdmlkZW8ucGxheSgpO1xuICAgICAgICB9O1xuICAgICAgICByZWFkZXIub25lcnJvciA9IGZ1bmN0aW9uIChlKTogdm9pZCB7XG4gICAgICAgICAgICByZWplY3QoZSk7XG4gICAgICAgIH07XG4gICAgICAgIHJlYWRlci5yZWFkQXNEYXRhVVJMKHZpZGVvRmlsZSk7XG4gICAgfSk7XG59XG5cbi8qKlxuICogUmVhZCB0aGUgbWV0YWRhdGEgZm9yIGEgdmlkZW8gZmlsZSBhbmQgY3JlYXRlIGFuZCB1cGxvYWQgYSB0aHVtYm5haWwgb2YgdGhlIHZpZGVvLlxuICpcbiAqIEBwYXJhbSB7TWF0cml4Q2xpZW50fSBtYXRyaXhDbGllbnQgQSBtYXRyaXhDbGllbnQgdG8gdXBsb2FkIHRoZSB0aHVtYm5haWwgd2l0aC5cbiAqIEBwYXJhbSB7U3RyaW5nfSByb29tSWQgVGhlIElEIG9mIHRoZSByb29tIHRoZSB2aWRlbyB3aWxsIGJlIHVwbG9hZGVkIHRvLlxuICogQHBhcmFtIHtGaWxlfSB2aWRlb0ZpbGUgVGhlIHZpZGVvIHRvIHJlYWQgYW5kIHRodW1ibmFpbC5cbiAqIEByZXR1cm4ge1Byb21pc2V9IEEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdpdGggdGhlIGF0dGFjaG1lbnQgaW5mby5cbiAqL1xuZnVuY3Rpb24gaW5mb0ZvclZpZGVvRmlsZShtYXRyaXhDbGllbnQ6IE1hdHJpeENsaWVudCwgcm9vbUlkOiBzdHJpbmcsIHZpZGVvRmlsZTogRmlsZSk6IFByb21pc2U8VmlkZW9JbmZvPiB7XG4gICAgY29uc3QgdGh1bWJuYWlsVHlwZSA9IFwiaW1hZ2UvanBlZ1wiO1xuXG4gICAgY29uc3QgdmlkZW9JbmZvOiBWaWRlb0luZm8gPSB7fTtcbiAgICByZXR1cm4gbG9hZFZpZGVvRWxlbWVudCh2aWRlb0ZpbGUpXG4gICAgICAgIC50aGVuKCh2aWRlbykgPT4ge1xuICAgICAgICAgICAgdmlkZW9JbmZvLmR1cmF0aW9uID0gTWF0aC5jZWlsKHZpZGVvLmR1cmF0aW9uICogMTAwMCk7XG4gICAgICAgICAgICByZXR1cm4gY3JlYXRlVGh1bWJuYWlsKHZpZGVvLCB2aWRlby52aWRlb1dpZHRoLCB2aWRlby52aWRlb0hlaWdodCwgdGh1bWJuYWlsVHlwZSk7XG4gICAgICAgIH0pXG4gICAgICAgIC50aGVuKChyZXN1bHQpID0+IHtcbiAgICAgICAgICAgIE9iamVjdC5hc3NpZ24odmlkZW9JbmZvLCByZXN1bHQuaW5mbyk7XG4gICAgICAgICAgICByZXR1cm4gdXBsb2FkRmlsZShtYXRyaXhDbGllbnQsIHJvb21JZCwgcmVzdWx0LnRodW1ibmFpbCk7XG4gICAgICAgIH0pXG4gICAgICAgIC50aGVuKChyZXN1bHQpID0+IHtcbiAgICAgICAgICAgIHZpZGVvSW5mby50aHVtYm5haWxfdXJsID0gcmVzdWx0LnVybDtcbiAgICAgICAgICAgIHZpZGVvSW5mby50aHVtYm5haWxfZmlsZSA9IHJlc3VsdC5maWxlO1xuICAgICAgICAgICAgcmV0dXJuIHZpZGVvSW5mbztcbiAgICAgICAgfSk7XG59XG5cbi8qKlxuICogUmVhZCB0aGUgZmlsZSBhcyBhbiBBcnJheUJ1ZmZlci5cbiAqIEBwYXJhbSB7RmlsZX0gZmlsZSBUaGUgZmlsZSB0byByZWFkXG4gKiBAcmV0dXJuIHtQcm9taXNlfSBBIHByb21pc2UgdGhhdCByZXNvbHZlcyB3aXRoIGFuIEFycmF5QnVmZmVyIHdoZW4gdGhlIGZpbGVcbiAqICAgaXMgcmVhZC5cbiAqL1xuZnVuY3Rpb24gcmVhZEZpbGVBc0FycmF5QnVmZmVyKGZpbGU6IEZpbGUgfCBCbG9iKTogUHJvbWlzZTxBcnJheUJ1ZmZlcj4ge1xuICAgIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIGNvbnN0IHJlYWRlciA9IG5ldyBGaWxlUmVhZGVyKCk7XG4gICAgICAgIHJlYWRlci5vbmxvYWQgPSBmdW5jdGlvbiAoZSk6IHZvaWQge1xuICAgICAgICAgICAgcmVzb2x2ZShlLnRhcmdldD8ucmVzdWx0IGFzIEFycmF5QnVmZmVyKTtcbiAgICAgICAgfTtcbiAgICAgICAgcmVhZGVyLm9uZXJyb3IgPSBmdW5jdGlvbiAoZSk6IHZvaWQge1xuICAgICAgICAgICAgcmVqZWN0KGUpO1xuICAgICAgICB9O1xuICAgICAgICByZWFkZXIucmVhZEFzQXJyYXlCdWZmZXIoZmlsZSk7XG4gICAgfSk7XG59XG5cbi8qKlxuICogVXBsb2FkIHRoZSBmaWxlIHRvIHRoZSBjb250ZW50IHJlcG9zaXRvcnkuXG4gKiBJZiB0aGUgcm9vbSBpcyBlbmNyeXB0ZWQgdGhlbiBlbmNyeXB0IHRoZSBmaWxlIGJlZm9yZSB1cGxvYWRpbmcuXG4gKlxuICogQHBhcmFtIHtNYXRyaXhDbGllbnR9IG1hdHJpeENsaWVudCBUaGUgbWF0cml4IGNsaWVudCB0byB1cGxvYWQgdGhlIGZpbGUgd2l0aC5cbiAqIEBwYXJhbSB7U3RyaW5nfSByb29tSWQgVGhlIElEIG9mIHRoZSByb29tIGJlaW5nIHVwbG9hZGVkIHRvLlxuICogQHBhcmFtIHtGaWxlfSBmaWxlIFRoZSBmaWxlIHRvIHVwbG9hZC5cbiAqIEBwYXJhbSB7RnVuY3Rpb24/fSBwcm9ncmVzc0hhbmRsZXIgb3B0aW9uYWwgY2FsbGJhY2sgdG8gYmUgY2FsbGVkIHdoZW4gYSBjaHVuayBvZlxuICogICAgZGF0YSBpcyB1cGxvYWRlZC5cbiAqIEBwYXJhbSB7QWJvcnRDb250cm9sbGVyP30gY29udHJvbGxlciBvcHRpb25hbCBhYm9ydENvbnRyb2xsZXIgdG8gdXNlIGZvciB0aGlzIHVwbG9hZC5cbiAqIEByZXR1cm4ge1Byb21pc2V9IEEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdpdGggYW4gb2JqZWN0LlxuICogIElmIHRoZSBmaWxlIGlzIHVuZW5jcnlwdGVkIHRoZW4gdGhlIG9iamVjdCB3aWxsIGhhdmUgYSBcInVybFwiIGtleS5cbiAqICBJZiB0aGUgZmlsZSBpcyBlbmNyeXB0ZWQgdGhlbiB0aGUgb2JqZWN0IHdpbGwgaGF2ZSBhIFwiZmlsZVwiIGtleS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHVwbG9hZEZpbGUoXG4gICAgbWF0cml4Q2xpZW50OiBNYXRyaXhDbGllbnQsXG4gICAgcm9vbUlkOiBzdHJpbmcsXG4gICAgZmlsZTogRmlsZSB8IEJsb2IsXG4gICAgcHJvZ3Jlc3NIYW5kbGVyPzogVXBsb2FkT3B0c1tcInByb2dyZXNzSGFuZGxlclwiXSxcbiAgICBjb250cm9sbGVyPzogQWJvcnRDb250cm9sbGVyLFxuKTogUHJvbWlzZTx7IHVybD86IHN0cmluZzsgZmlsZT86IEVuY3J5cHRlZEZpbGUgfT4ge1xuICAgIGNvbnN0IGFib3J0Q29udHJvbGxlciA9IGNvbnRyb2xsZXIgPz8gbmV3IEFib3J0Q29udHJvbGxlcigpO1xuXG4gICAgLy8gSWYgdGhlIHJvb20gaXMgZW5jcnlwdGVkIHRoZW4gZW5jcnlwdCB0aGUgZmlsZSBiZWZvcmUgdXBsb2FkaW5nIGl0LlxuICAgIGlmIChtYXRyaXhDbGllbnQuaXNSb29tRW5jcnlwdGVkKHJvb21JZCkpIHtcbiAgICAgICAgLy8gRmlyc3QgcmVhZCB0aGUgZmlsZSBpbnRvIG1lbW9yeS5cbiAgICAgICAgY29uc3QgZGF0YSA9IGF3YWl0IHJlYWRGaWxlQXNBcnJheUJ1ZmZlcihmaWxlKTtcbiAgICAgICAgaWYgKGFib3J0Q29udHJvbGxlci5zaWduYWwuYWJvcnRlZCkgdGhyb3cgbmV3IFVwbG9hZENhbmNlbGVkRXJyb3IoKTtcblxuICAgICAgICAvLyBUaGVuIGVuY3J5cHQgdGhlIGZpbGUuXG4gICAgICAgIGNvbnN0IGVuY3J5cHRSZXN1bHQgPSBhd2FpdCBlbmNyeXB0LmVuY3J5cHRBdHRhY2htZW50KGRhdGEpO1xuICAgICAgICBpZiAoYWJvcnRDb250cm9sbGVyLnNpZ25hbC5hYm9ydGVkKSB0aHJvdyBuZXcgVXBsb2FkQ2FuY2VsZWRFcnJvcigpO1xuXG4gICAgICAgIC8vIFBhc3MgdGhlIGVuY3J5cHRlZCBkYXRhIGFzIGEgQmxvYiB0byB0aGUgdXBsb2FkZXIuXG4gICAgICAgIGNvbnN0IGJsb2IgPSBuZXcgQmxvYihbZW5jcnlwdFJlc3VsdC5kYXRhXSk7XG5cbiAgICAgICAgY29uc3QgeyBjb250ZW50X3VyaTogdXJsIH0gPSBhd2FpdCBtYXRyaXhDbGllbnQudXBsb2FkQ29udGVudChibG9iLCB7XG4gICAgICAgICAgICBwcm9ncmVzc0hhbmRsZXIsXG4gICAgICAgICAgICBhYm9ydENvbnRyb2xsZXIsXG4gICAgICAgICAgICBpbmNsdWRlRmlsZW5hbWU6IGZhbHNlLFxuICAgICAgICAgICAgdHlwZTogXCJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW1cIixcbiAgICAgICAgfSk7XG4gICAgICAgIGlmIChhYm9ydENvbnRyb2xsZXIuc2lnbmFsLmFib3J0ZWQpIHRocm93IG5ldyBVcGxvYWRDYW5jZWxlZEVycm9yKCk7XG5cbiAgICAgICAgLy8gSWYgdGhlIGF0dGFjaG1lbnQgaXMgZW5jcnlwdGVkIHRoZW4gYnVuZGxlIHRoZSBVUkwgYWxvbmcgd2l0aCB0aGUgaW5mb3JtYXRpb25cbiAgICAgICAgLy8gbmVlZGVkIHRvIGRlY3J5cHQgdGhlIGF0dGFjaG1lbnQgYW5kIGFkZCBpdCB1bmRlciBhIGZpbGUga2V5LlxuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgZmlsZToge1xuICAgICAgICAgICAgICAgIC4uLmVuY3J5cHRSZXN1bHQuaW5mbyxcbiAgICAgICAgICAgICAgICB1cmwsXG4gICAgICAgICAgICB9IGFzIEVuY3J5cHRlZEZpbGUsXG4gICAgICAgIH07XG4gICAgfSBlbHNlIHtcbiAgICAgICAgY29uc3QgeyBjb250ZW50X3VyaTogdXJsIH0gPSBhd2FpdCBtYXRyaXhDbGllbnQudXBsb2FkQ29udGVudChmaWxlLCB7IHByb2dyZXNzSGFuZGxlciwgYWJvcnRDb250cm9sbGVyIH0pO1xuICAgICAgICBpZiAoYWJvcnRDb250cm9sbGVyLnNpZ25hbC5hYm9ydGVkKSB0aHJvdyBuZXcgVXBsb2FkQ2FuY2VsZWRFcnJvcigpO1xuICAgICAgICAvLyBJZiB0aGUgYXR0YWNobWVudCBpc24ndCBlbmNyeXB0ZWQgdGhlbiBpbmNsdWRlIHRoZSBVUkwgZGlyZWN0bHkuXG4gICAgICAgIHJldHVybiB7IHVybCB9O1xuICAgIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgQ29udGVudE1lc3NhZ2VzIHtcbiAgICBwcml2YXRlIGlucHJvZ3Jlc3M6IFJvb21VcGxvYWRbXSA9IFtdO1xuICAgIHByaXZhdGUgbWVkaWFDb25maWc6IElNZWRpYUNvbmZpZyB8IG51bGwgPSBudWxsO1xuXG4gICAgcHVibGljIHNlbmRTdGlja2VyQ29udGVudFRvUm9vbShcbiAgICAgICAgdXJsOiBzdHJpbmcsXG4gICAgICAgIHJvb21JZDogc3RyaW5nLFxuICAgICAgICB0aHJlYWRJZDogc3RyaW5nIHwgbnVsbCxcbiAgICAgICAgaW5mbzogSW1hZ2VJbmZvLFxuICAgICAgICB0ZXh0OiBzdHJpbmcsXG4gICAgICAgIG1hdHJpeENsaWVudDogTWF0cml4Q2xpZW50LFxuICAgICk6IFByb21pc2U8SVNlbmRFdmVudFJlc3BvbnNlPiB7XG4gICAgICAgIHJldHVybiBkb01heWJlTG9jYWxSb29tQWN0aW9uKFxuICAgICAgICAgICAgcm9vbUlkLFxuICAgICAgICAgICAgKGFjdHVhbFJvb21JZDogc3RyaW5nKSA9PiBtYXRyaXhDbGllbnQuc2VuZFN0aWNrZXJNZXNzYWdlKGFjdHVhbFJvb21JZCwgdGhyZWFkSWQsIHVybCwgaW5mbywgdGV4dCksXG4gICAgICAgICAgICBtYXRyaXhDbGllbnQsXG4gICAgICAgICkuY2F0Y2goKGUpID0+IHtcbiAgICAgICAgICAgIGxvZ2dlci53YXJuKGBGYWlsZWQgdG8gc2VuZCBjb250ZW50IHdpdGggVVJMICR7dXJsfSB0byByb29tICR7cm9vbUlkfWAsIGUpO1xuICAgICAgICAgICAgdGhyb3cgZTtcbiAgICAgICAgfSk7XG4gICAgfVxuXG4gICAgcHVibGljIGdldFVwbG9hZExpbWl0KCk6IG51bWJlciB8IG51bGwge1xuICAgICAgICBpZiAodGhpcy5tZWRpYUNvbmZpZyAhPT0gbnVsbCAmJiB0aGlzLm1lZGlhQ29uZmlnW1wibS51cGxvYWQuc2l6ZVwiXSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm4gdGhpcy5tZWRpYUNvbmZpZ1tcIm0udXBsb2FkLnNpemVcIl07XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICByZXR1cm4gbnVsbDtcbiAgICAgICAgfVxuICAgIH1cblxuICAgIHB1YmxpYyBhc3luYyBzZW5kQ29udGVudExpc3RUb1Jvb20oXG4gICAgICAgIGZpbGVzOiBGaWxlW10sXG4gICAgICAgIHJvb21JZDogc3RyaW5nLFxuICAgICAgI