UNPKG

matrix-react-sdk

Version:
598 lines (578 loc) 88.1 kB
"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