UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,158 lines (1,156 loc) 46.6 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. Object.defineProperty(exports, "__esModule", { value: true }); exports.EncodingType = exports.AllowedExtensions = exports.MaxShareableContentStringLength = void 0; const DifferenceSet_1 = require("./DifferenceSet"); const IFolderDifference_1 = require("./IFolderDifference"); const IFileDifference_1 = require("./IFileDifference"); const Utilities_1 = require("../core/Utilities"); const ZipStorage_1 = require("./ZipStorage"); const Log_1 = require("../core/Log"); const Storage_1 = require("./Storage"); const BasicValidators_1 = require("./BasicValidators"); exports.MaxShareableContentStringLength = 65536; // part of security/reliability and defense in depth is to only allow our file functions to work with files from an allow list // this list is also replicated in /public/preload.js exports.AllowedExtensions = [ "js", "ts", "json", "md", "png", "css", "woff", "ttf", "woff2", "jpg", "gitignore", "jpeg", "gif", "lang", "fsb", "map", "yml", "ico", "ogg", "nojekyll", "mjs", "env", "wav", "tga", "old", "mc", "", "zip", "mcstructure", "mcworld", "mcproject", "material", "vertex", "geometry", "fragment", "map", "js.map", "mctemplate", "mcfunction", "mcaddon", "mcpack", "html", "dat", "dat_old", "txt", "ldb", "log", ]; const _minecraftProjectFolderNames = [ "behavior_packs", "resource_packs", "worlds", "world", "world_template", "skin_pack", "scripts", "content", "marketing art", "store art", "db", "texts", "animation_controllers", "blocks", "structures", "entities", "functions", "items", "dialogue", "animations", "entity", "materials", "models", "textures", "fogs", "materials", "particles", "ui", ]; var EncodingType; (function (EncodingType) { EncodingType[EncodingType["ByteBuffer"] = 0] = "ByteBuffer"; EncodingType[EncodingType["Utf8String"] = 1] = "Utf8String"; })(EncodingType = exports.EncodingType || (exports.EncodingType = {})); class StorageUtilities { static isUsableFile(path) { const extension = StorageUtilities.getTypeFromName(path); return exports.AllowedExtensions.includes(extension); } static getEncodingByFileName(name) { const fileType = this.getTypeFromName(name); const nameLow = name.toLowerCase(); switch (fileType) { case "mcstructure": case "zip": case "dat": case "dat_old": case "ldb": case "ico": case "tga": case "ogg": case "wav": case "gif": case "jpeg": case "jpg": case "png": case "mp3": case "fsb": case "woff": case "woff2": case "ttf": case "pdb": case "exe": case "nbt": case "mcworld": case "mcproject": case "mctemplate": case "mcpack": case "mcaddon": return EncodingType.ByteBuffer; } if (fileType === "" && nameLow.startsWith("manifest-")) { return EncodingType.ByteBuffer; } if (nameLow.startsWith("0") && fileType === "log") { return EncodingType.ByteBuffer; } return EncodingType.Utf8String; } static absolutize(path) { if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) { path = StorageUtilities.standardFolderDelimiter + path; } return path; } static getUniqueChildFolderName(name, folder) { let num = 1; let nameCand = name; let isUnique = false; while (!isUnique) { isUnique = true; for (const childFolderName in folder.folders) { if (StorageUtilities.canonicalizeName(childFolderName) === StorageUtilities.canonicalizeName(nameCand)) { isUnique = false; } } if (!isUnique) { nameCand = name + " " + num; num++; } } return nameCand; } static ensureEndsDelimited(path) { if (!path.endsWith(StorageUtilities.standardFolderDelimiter)) { path = path + StorageUtilities.standardFolderDelimiter; } if (path.startsWith("." + StorageUtilities.standardFolderDelimiter)) { path = path.substring(1); } else if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) { path = StorageUtilities.standardFolderDelimiter + path; } return path; } static ensureEndsWithDelimiter(path) { if (!path.endsWith(StorageUtilities.standardFolderDelimiter)) { path = path + StorageUtilities.standardFolderDelimiter; } return path; } static ensureStartsWithDelimiter(path) { if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) { path = StorageUtilities.standardFolderDelimiter + path; } return path; } static ensureNotStartsWithDelimiter(path) { if (path.startsWith(StorageUtilities.standardFolderDelimiter)) { path = path.substring(1); } return path; } static joinPath(pathA, pathB) { let fullPath = pathA; if (!fullPath.endsWith(StorageUtilities.standardFolderDelimiter)) { fullPath += StorageUtilities.standardFolderDelimiter; } if (pathB.startsWith("/")) { fullPath += pathB.substring(1, pathB.length); } else { fullPath += pathB; } return fullPath; } static getMimeType(file) { switch (StorageUtilities.getTypeFromName(file.name)) { case "js": return "application/javascript"; case "ts": return "application/typescript"; case "json": return "application/json"; case "mcworld": case "mctemplate": case "mcproject": case "mcaddon": case "mcpack": case "zip": return "application/zip"; case "mcstucture": return "application/octet-stream"; case "mcfunction": case "material": case "env": case "lang": return "text/plain"; case "wav": return "audio/wav"; case "mp3": return "audio/mp3"; case "ogg": return "audio/ogg"; case "jpg": case "jpeg": return "image/jpg"; case "png": return "image/png"; case "tiff": case "tga": return "image/tiff"; default: return "application/octet-stream"; } } static getContentAsString(file) { if (typeof file.content === "string") { return file.content; } else if (file.content instanceof Uint8Array) { return "data:" + StorageUtilities.getMimeType(file) + ";base64," + Utilities_1.default.uint8ArrayToBase64(file.content); } return undefined; } static async getFileStorageFolder(file) { let zipStorage = file.fileContainerStorage; if (!zipStorage) { await file.loadContent(); if (!file.content || !(file.content instanceof Uint8Array)) { return undefined; } zipStorage = new ZipStorage_1.default(); await zipStorage.loadFromUint8Array(file.content, file.name); file.fileContainerStorage = zipStorage; file.fileContainerStorage.storagePath = file.storageRelativePath + "#"; } return zipStorage.rootFolder; } static getContaineredFileLeafPath(path) { if (!path) { return; } const lastHash = path.lastIndexOf("#"); if (lastHash >= 0) { path = path.substring(lastHash + 1); } return path; } static isMinecraftInternalFolder(folder) { const nameCanon = folder.name.toLowerCase(); return _minecraftProjectFolderNames.includes(nameCanon); } static isContainerFile(path) { const extension = StorageUtilities.getTypeFromName(path); if (extension === "zip" || extension === "mcworld" || extension === "mcproject" || extension === "mctemplate" || extension === "mcpack" || extension === "mcaddon") { return true; } return false; } static isFileStorageItem(file) { return this.isContainerFile(file.name); } static canonicalizeName(name) { name = name.trim(); //.toLowerCase(); if (name.startsWith("/") || name.startsWith("\\")) { name = name.substring(1, name.length); } if (name.endsWith("/") || name.endsWith("\\")) { name = name.substring(0, name.length - 1); } // constructor is a keyword that will cause array existence checks to fail in interesting ways if (name === "constructor") { name = "__constructor"; } name = name.replace(/%20/g, " "); name = name.replace(/%28/g, "("); name = name.replace(/%29/g, ")"); return name; } static isPathEqual(pathA, pathB) { return StorageUtilities.canonicalizePath(pathA) === StorageUtilities.canonicalizePath(pathB); } static canonicalizePath(path) { path = path.trim(); // .toLowerCase(); path = path.replace(/\\/g, "/"); path = path.replace(/%20/g, " "); path = path.replace(/%28/g, "("); path = path.replace(/%29/g, ")"); return path; } static canonicalizePathAsFileName(path) { let result = path.trim().toLowerCase(); path = path.replace(/%20/g, " "); path = path.replace(/%28/g, "("); path = path.replace(/%29/g, ")"); result = result.replace(/:/g, "_"); result = result.replace(/\//g, "-"); result = result.replace(/\\/g, "-"); result = result.replace(/%/g, "-"); result = result.replace(/--/g, "-"); result = result.replace(/--/g, "-"); result = result.replace(/\?/g, "-"); result = result.replace(/\|/g, "-"); return result; } static ensureFileNameIsSafe(path) { let result = path.trim().toLowerCase(); path = path.replace(/%20/g, " "); path = path.replace(/%28/g, "("); path = path.replace(/%29/g, ")"); result = result.replace(/:/g, "_"); result = result.replace(/\//g, "-"); result = result.replace(/\\/g, "-"); result = result.replace(/%/g, "-"); result = result.replace(/--/g, "-"); result = result.replace(/`/g, "-"); result = result.replace(/'/g, "-"); result = result.replace(/–/g, "-"); return result; } static hasPathSeparator(path) { if (!path) { return false; } let lastSlash = path.lastIndexOf("/", path.length - 1); if (lastSlash >= 0) { return true; } lastSlash = path.lastIndexOf("\\", path.length - 1); if (lastSlash >= 0) { return true; } return false; } static getLeafName(path) { let name = path; if (name.endsWith("/")) { name = name.substring(0, name.length - 1); } if (name.endsWith("\\")) { name = name.substring(0, name.length - 1); } let lastSlash = name.lastIndexOf("/", path.length - 1); if (lastSlash >= 0) { name = name.substring(lastSlash + 1, name.length); } lastSlash = name.lastIndexOf("\\", name.length - 1); if (lastSlash >= 0) { name = name.substring(lastSlash + 1, name.length); } return name; } static getFolderPath(path) { let lastSlash = path.lastIndexOf("/", path.length - 1); if (lastSlash >= 0 && lastSlash < path.length - 1) { path = path.substring(0, lastSlash + 1); } else { lastSlash = path.lastIndexOf("\\", path.length - 1); if (lastSlash >= 0 && lastSlash < path.length - 1) { path = path.substring(0, lastSlash + 1); } } return path; } static getParentFolderNameFromPath(path) { let lastSlash = path.lastIndexOf("/", path.length - 1); if (lastSlash >= 0 && lastSlash < path.length - 1) { const nextLastSlash = path.lastIndexOf("/", lastSlash - 1); return path.substring(nextLastSlash + 1, lastSlash); } else { lastSlash = path.lastIndexOf("\\", path.length - 1); if (lastSlash >= 0 && lastSlash < path.length - 1) { const nextLastSlash = path.lastIndexOf("/", lastSlash - 1); return path.substring(nextLastSlash + 1, lastSlash); } } return undefined; } static removeContainerExtension(name) { let nameW = name.trim(); if (nameW.endsWith(".zip")) { nameW = nameW.substring(0, nameW.length - 4); } return nameW; } static getBaseFromName(name) { const nameW = name.trim(); const lastPeriod = nameW.lastIndexOf("."); if (lastPeriod < 0) { return name; } return nameW.substring(0, lastPeriod); } static getTypeFromName(name) { const nameW = name.trim().toLowerCase(); const lastPeriod = nameW.lastIndexOf("."); if (lastPeriod < 0) { return ""; } return nameW.substring(lastPeriod + 1, nameW.length); } static async folderContentsEqual(folderA, folderB, excludingFiles, whitespaceAgnostic, ignoreLinesContaining) { if (folderA === undefined && folderB === undefined) { return { result: true, reason: "Both folders are undefined." }; } if (folderA === undefined) { return { result: false, reason: "First folder is undefined." }; } if (folderB === undefined) { return { result: false, reason: "Second folder is undefined." }; } await folderA.load(false); await folderB.load(false); if (folderA.fileCount !== folderB.fileCount) { return { result: false, reason: "Folder '" + folderA.fullPath + "' has " + folderA.fileCount + " files; folder '" + folderB.fullPath + "' has " + folderB.fileCount + " files.", }; } for (const fileName in folderA.files) { const fileA = folderA.files[fileName]; const fileB = folderB.files[fileName]; if (fileA === undefined) { return { result: false, reason: "Unexpected file '" + fileName + "' undefined." }; } if (fileB === undefined) { return { result: false, reason: "File '" + fileName + "' does not exist in '" + folderB.fullPath + "'" }; } if (!excludingFiles || !excludingFiles.includes(fileA.name)) { const result = await StorageUtilities.fileContentsEqual(fileA, fileB, whitespaceAgnostic, ignoreLinesContaining); if (!result) { return { result: false, reason: "File '" + fileA.fullPath + "' (size: " + fileA.content?.length + (fileA.isBinary ? "B" : "C") + ") contents does not match '" + fileB.fullPath + "' (size: " + fileB.content?.length + (fileB.isBinary ? "B" : "C") + ")", }; } } } for (const folderName in folderA.folders) { const childFolderA = folderA.folders[folderName]; const childFolderB = folderB.folders[folderName]; if (childFolderA === undefined) { return { result: false, reason: "Unexpected folder undefined. " }; } if (childFolderB === undefined) { return { result: false, reason: "Folder '" + folderName + "' does not exist in '" + folderB.fullPath + "'" }; } const result = await StorageUtilities.folderContentsEqual(childFolderA, childFolderB, excludingFiles, whitespaceAgnostic, ignoreLinesContaining); if (!result.result) { return result; } } return { result: true, reason: "Folders are equal" }; } static async fileContentsEqual(fileA, fileB, whitespaceAgnostic, ignoreLinesContaining) { if (fileA === undefined && fileB === undefined) { return true; } if (fileA === undefined) { return false; } if (fileB === undefined) { return false; } const fileAExists = await fileA.exists(); if (!fileAExists) { return false; } const fileBExists = await fileB.exists(); if (fileAExists && !fileBExists) { return false; } await fileA.loadContent(false); await fileB.loadContent(false); if (fileA.content === undefined && fileB.content === undefined) { return true; } const extA = StorageUtilities.getTypeFromName(fileA.name); const extB = StorageUtilities.getTypeFromName(fileB.name); let contentA = fileA.content; let contentB = fileB.content; if (contentA === null && contentB === null) { return true; } if (contentA === null || contentB === null) { return false; } if (ignoreLinesContaining && typeof contentA === "string" && typeof contentB === "string") { for (const ignoreLine of ignoreLinesContaining) { contentA = Utilities_1.default.stripLinesContaining(contentA, ignoreLine); contentB = Utilities_1.default.stripLinesContaining(contentB, ignoreLine); } } if (extA === "json" && extB === "json" && typeof contentA === "string" && typeof contentB === "string") { return this.jsonContentsAreEqualIgnoreWhitespace(contentA, contentB); } else if (whitespaceAgnostic) { return StorageUtilities.contentsAreEqualIgnoreWhitespace(contentA, contentB); } return StorageUtilities.contentsAreEqual(contentA, contentB); } static jsonContentsAreEqualIgnoreWhitespace(contentA, contentB) { contentA = this.stripWhitespace(contentA); contentB = this.stripWhitespace(contentB); return contentA === contentB; } static stripWhitespace(content) { content = content.trim(); content = content.replace(/ /gi, ""); content = content.replace(/\r/gi, ""); content = content.replace(/\n/gi, ""); content = content.replace(/\t/gi, ""); return content; } static contentsAreEqual(contentA, contentB) { if (contentA === null && contentB === null) { return true; } if (typeof contentA === "string" && typeof contentB === "string") { return contentA === contentB; } if (contentA instanceof Uint8Array && contentB instanceof Uint8Array) { return Utilities_1.default.uint8ArraysAreEqual(contentA, contentB); } return false; } static contentsAreEqualIgnoreWhitespace(contentA, contentB) { if (contentA === null && contentB === null) { return true; } if (typeof contentA === "string" && typeof contentB === "string") { return this.stripWhitespace(contentA) === this.stripWhitespace(contentB); } if (contentA instanceof Uint8Array && contentB instanceof Uint8Array) { return Utilities_1.default.uint8ArraysAreEqual(contentA, contentB); } return false; } static async getDifferences(original, updated, includeDeletions, matchSingleChildFolders) { // matchSingleChildFolders: for project template starters, where they have a structure like: // resource_packs/template_name // that gets renamed to // resource_packs/mikesfooproject // then -- if there is one folder in the original and one folder in the updated, // we want to match them up irrespective of the name const data = new DifferenceSet_1.default(); await this.addDifferences(data, original, updated, includeDeletions, matchSingleChildFolders); return data; } static getFirstFile(folder) { for (const fileName in folder.files) { const file = folder.files[fileName]; if (file) { return file; } } for (const folderName in folder.folders) { const subFolder = folder.folders[folderName]; if (subFolder) { const file = this.getFirstFile(subFolder); if (file) { return file; } } } return undefined; } static async addDifferences(differences, original, updated, includeDeletions, matchSingleFolders) { await original.load(false); await updated.load(false); let result = IFolderDifference_1.FolderDifferenceType.none; // get a list of existing files and folders in the target const updatedFilesToConsider = {}; const updatedFoldersToConsider = {}; for (const updatedFileName in updated.files) { if (BasicValidators_1.BasicValidators.isFileNameOKForSharing(updatedFileName)) { updatedFilesToConsider[updatedFileName] = true; } } for (const updatedFolderName in updated.folders) { if (BasicValidators_1.BasicValidators.isFolderNameOKForSharing(updatedFolderName)) { updatedFoldersToConsider[updatedFolderName] = true; } } for (const originalFileName in original.files) { if (BasicValidators_1.BasicValidators.isFileNameOKForSharing(originalFileName)) { const originalFile = original.files[originalFileName]; if (originalFile !== undefined) { updatedFilesToConsider[originalFileName] = false; if (updated.fileExists(originalFileName)) { const updatedFile = updated.files[originalFileName]; const areEqual = await StorageUtilities.fileContentsEqual(originalFile, updatedFile); if (!areEqual) { if ((result & IFolderDifference_1.FolderDifferenceType.fileContentsDifferent) === 0) { result += IFolderDifference_1.FolderDifferenceType.fileContentsDifferent; } differences.fileDifferences.push({ type: IFileDifference_1.FileDifferenceType.contentsDifferent, path: originalFile.storageRelativePath, original: originalFile, updated: updatedFile, }); } } else if (includeDeletions) { if ((result & IFolderDifference_1.FolderDifferenceType.fileListDifferent) === 0) { result += IFolderDifference_1.FolderDifferenceType.fileListDifferent; } differences.fileDifferences.push({ type: IFileDifference_1.FileDifferenceType.fileDeleted, path: originalFile.storageRelativePath, original: originalFile, }); } } } } for (const originalFolderName in original.folders) { if (BasicValidators_1.BasicValidators.isFolderNameOKForSharing(originalFolderName)) { const originalChildFolder = original.folders[originalFolderName]; if (originalChildFolder !== undefined) { updatedFoldersToConsider[originalFolderName] = false; if (updated.folderExists(originalFolderName) || (matchSingleFolders && updated.folderCount === 1 && original.folderCount === 1)) { let updatedChildFolder = updated.folders[originalFolderName]; if (matchSingleFolders && updated.folderCount && original.folderCount && !updatedChildFolder) { updatedChildFolder = updated.getFolderByIndex(0); } if (updatedChildFolder !== undefined) { updatedFoldersToConsider[updatedChildFolder.name] = false; const childResult = await StorageUtilities.addDifferences(differences, originalChildFolder, updatedChildFolder, includeDeletions, matchSingleFolders); if (childResult !== IFolderDifference_1.FolderDifferenceType.none) { result = result | childResult; } } } else if (includeDeletions) { if ((result & IFolderDifference_1.FolderDifferenceType.folderDeleted) === 0) { result += IFolderDifference_1.FolderDifferenceType.folderDeleted; } this.addDifferencesAsFolderDelete(differences, originalChildFolder); } } } } for (const updatedFileName in updatedFilesToConsider) { if (updatedFilesToConsider[updatedFileName] === true) { const updatedFile = updated.files[updatedFileName]; if (updatedFile !== undefined) { if ((result & IFolderDifference_1.FolderDifferenceType.fileListDifferent) === 0) { result += IFolderDifference_1.FolderDifferenceType.fileListDifferent; } if (updatedFile.content !== undefined && updatedFile.content !== null) { differences.fileDifferences.push({ type: IFileDifference_1.FileDifferenceType.fileAdded, path: StorageUtilities.relativizePathToOriginal(original, updated, updatedFile.storageRelativePath), updated: updatedFile, }); } } } } for (const updatedFolderName in updatedFoldersToConsider) { if (updatedFoldersToConsider[updatedFolderName] === true) { const updatedFolder = updated.folders[updatedFolderName]; if (updatedFolder !== undefined) { if ((result & IFolderDifference_1.FolderDifferenceType.folderAdded) === 0) { result += IFolderDifference_1.FolderDifferenceType.folderAdded; } this.addDifferencesAsFolderAdd(differences, updatedFolder, original, updated); } } } if (result !== IFolderDifference_1.FolderDifferenceType.none) { differences.folderDifferences.push({ type: result, path: original.storageRelativePath, original: original, updated: updated, }); } return result; } static relativizePathToOriginal(original, updated, path) { const folders = original.storageRelativePath.split(StorageUtilities.standardFolderDelimiter); for (let i = folders.length - 1; i >= 0; i--) { const lastIndexOfInPath = path.lastIndexOf("/" + folders[i] + "/"); const lastIndexOfInSource = original.storageRelativePath.lastIndexOf("/" + folders[i] + "/"); if (lastIndexOfInPath >= 0 && lastIndexOfInSource >= 0) { return original.storageRelativePath.substring(0, lastIndexOfInSource) + path.substring(lastIndexOfInPath); } } const originalPathLength = original.storageRelativePath.length; const updatedPathLength = updated.storageRelativePath.length; if (updatedPathLength > originalPathLength) { path = path.substring(updatedPathLength - originalPathLength, path.length); } return path; } static async addDifferencesAsFolderAdd(differences, childUpdated, original, updated) { await childUpdated.load(false); differences.folderDifferences.push({ type: IFolderDifference_1.FolderDifferenceType.folderAdded, path: StorageUtilities.relativizePathToOriginal(original, updated, childUpdated.storageRelativePath), updated: childUpdated, }); for (const updatedFileName in childUpdated.files) { const updatedFile = childUpdated.files[updatedFileName]; if (updatedFile !== undefined) { differences.fileDifferences.push({ type: IFileDifference_1.FileDifferenceType.fileAdded, path: StorageUtilities.relativizePathToOriginal(original, updated, updatedFile.storageRelativePath), updated: updatedFile, }); } } for (const updatedFolderName in childUpdated.folders) { const updatedFolder = childUpdated.folders[updatedFolderName]; if (updatedFolder !== undefined) { this.addDifferencesAsFolderAdd(differences, updatedFolder, original, updated); } } } static async addDifferencesAsFolderDelete(differences, original) { await original.load(false); differences.folderDifferences.push({ type: IFolderDifference_1.FolderDifferenceType.folderDeleted, path: original.storageRelativePath, updated: original, }); for (const originalFileName in original.files) { const originalFile = original.files[originalFileName]; if (originalFile !== undefined) { differences.fileDifferences.push({ type: IFileDifference_1.FileDifferenceType.fileDeleted, path: originalFile.storageRelativePath, original: originalFile, }); } } for (const originalFolderName in original.folders) { const originalFolder = original.folders[originalFolderName]; if (originalFolder !== undefined) { this.addDifferencesAsFolderDelete(differences, originalFolder); } } } static isPathRiskyForDelete(path) { path = path.toLowerCase().trim(); if (path.indexOf("system32") >= 0 || path.indexOf("program files") >= 0 || path.indexOf("programdata") >= 0) { return true; } // a very crude way to ensure this code never removes c:\ or c:\my documents or whatever or something elemental. return Utilities_1.default.countChar(path, "/") + Utilities_1.default.countChar(path, "\\") < 4; } static getParentOfParentFolderNamed(folderName, folder) { let ancestorFolder = undefined; while (folder.name !== folderName && folder.parentFolder) { folder = folder.parentFolder; } if (folder.parentFolder) { ancestorFolder = folder.parentFolder; } return ancestorFolder; } static getJsonObject(file) { if (!file.content) { return undefined; } if (!(typeof file.content === "string")) { return undefined; } let jsonObject = undefined; let didFailToParse = false; let contents = file.content; contents = Utilities_1.default.fixJsonContent(contents); try { jsonObject = JSON.parse(contents); } catch (e) { file.isInErrorState = true; file.errorStateMessage = e.message; didFailToParse = true; Log_1.default.fail("Could not parse JSON from '" + file.fullPath + "': " + e.message); } if (file.isInErrorState && !didFailToParse && contents.length > 0) { file.isInErrorState = false; file.errorStateMessage = undefined; } return jsonObject; } static isIgnorableFolder(folderName) { return folderName.startsWith(".") && !folderName.toLowerCase().startsWith(".vscode"); } static async getUniqueFileName(baseName, extension, folder) { let candFileName = baseName + "." + extension; let exists = folder.fileExists(candFileName); let increment = 0; while (exists && increment < 99) { increment++; candFileName = baseName + " " + String(increment) + "." + extension; exists = folder.fileExists(candFileName); } return candFileName; } static async ensureFilesFromJson(storage, json) { if (typeof json === "string") { try { json = JSON.parse(json); } catch (e) { return e.toString(); } } if (typeof json === "object") { for (let path in json) { let data = json[path]; if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) { path = StorageUtilities.standardFolderDelimiter + path; } const file = await storage.rootFolder.ensureFileFromRelativePath(path); if (!file) { return "Could not create file '" + path + "'."; } if (file.isBinary) { return "Could not create file '" + path + "'; it is a binary file."; } if (typeof data === "object") { try { data = JSON.stringify(data, null, 2); } catch (e) { return e.toString(); } } file.setContent(data); } } return; } static async createStorageFromString(content) { let storage = undefined; try { content = content.trim(); if (content.length < 1) { return "No content provided."; } if (content.startsWith("{") || content.startsWith("[")) { storage = new Storage_1.default(); const result = await StorageUtilities.ensureFilesFromJson(storage, content); if (result) { return result; } } else { const contentUnbase64 = Utilities_1.default.base64ToUint8Array(content); if (contentUnbase64.length < 2) { return "Invalid base64 content provided."; } if (contentUnbase64[0] === "{".charCodeAt(0)) { const jsonStr = new TextDecoder("utf-8").decode(contentUnbase64); storage = new Storage_1.default(); const result = await StorageUtilities.ensureFilesFromJson(storage, jsonStr); if (!result && typeof result === "string") { return result; } } else { storage = new ZipStorage_1.default(); await storage.loadFromBase64(content); } } if (storage.errorStatus) { return "Error processing content." + (storage.errorMessage ? "Details: " + storage.errorMessage : ""); } } catch (e) { return "Unexpected error processing content."; } return storage; } static async createStorageFromUntrustedString(untrustedContent) { if (untrustedContent.length > exports.MaxShareableContentStringLength) { return ("Shared content are too large to include in the URL (" + untrustedContent.length + " > " + exports.MaxShareableContentStringLength + ")."); } const result = await StorageUtilities.createStorageFromString(untrustedContent); if (!result || typeof result === "string") { return result; } const valResult = await BasicValidators_1.BasicValidators.isFolderSharingValid(result.rootFolder); if (valResult !== undefined) { return valResult; } return result; } static async syncFolderTo(source, target, forceFolders, forceFileUpdates, removeOnTarget, exclude, include, messageUpdater, dontOverwriteExistingFiles, skipFilesAtRoot) { let modifiedFileCount = 0; // Log.debug("Syncing folder '" + source.storageRelativePath + "' to '" + target.storageRelativePath + "'"); if (StorageUtilities.isIgnorableFolder(source.name)) { return 0; } /* if (messageUpdater) { await messageUpdater( "Syncing folder from '" + source.storageRelativePath + "' to '" + target.storageRelativePath + "'." ); }*/ await source.load(forceFolders); await target.load(forceFolders); // get a list of existing files and folders in the target let targetFiles = {}; let targetFolders = {}; for (const targetFileName in target.files) { if (target.files[targetFileName] !== undefined) { targetFiles[targetFileName] = true; } } if (!skipFilesAtRoot) { for (const targetFolderName in target.folders) { if (target.folders[targetFolderName] !== undefined && !StorageUtilities.isIgnorableFolder(targetFolderName)) { targetFolders[targetFolderName] = true; } } for (const sourceFileName in source.files) { const sourceFile = source.files[sourceFileName]; let process = true; if (exclude !== undefined && StorageUtilities.matchesList(sourceFileName, exclude)) { process = false; } if (include !== undefined && !StorageUtilities.matchesList(sourceFileName, include)) { process = false; } if (sourceFile !== undefined) { if (process) { targetFiles[sourceFileName] = false; const targetFile = target.ensureFile(sourceFile.name); let updateFile = true; if (dontOverwriteExistingFiles) { if (await targetFile.exists()) { updateFile = false; if (messageUpdater) { messageUpdater("Not updating '" + targetFile.fullPath + "' as it already exists."); } } } if (updateFile) { const wasUpdated = await this.syncFileTo(sourceFile, targetFile, forceFileUpdates, messageUpdater); if (wasUpdated) { modifiedFileCount++; } } } } } } for (const sourceFolderName in source.folders) { if (!StorageUtilities.isIgnorableFolder(sourceFolderName)) { const sourceChildFolder = source.folders[sourceFolderName]; let process = true; if (exclude !== undefined && StorageUtilities.matchesList("/" + sourceFolderName, exclude)) { process = false; } if (sourceChildFolder !== undefined) { if (process) { targetFolders[sourceFolderName] = false; const targetChildFolder = target.ensureFolder(sourceChildFolder.name); await targetChildFolder.ensureExists(); const subfolderFilesUpdated = await this.syncFolderTo(sourceChildFolder, targetChildFolder, forceFolders, forceFileUpdates, removeOnTarget, exclude, include, messageUpdater, dontOverwriteExistingFiles); modifiedFileCount += subfolderFilesUpdated; } } } } if (removeOnTarget) { for (const targetFileName in targetFiles) { let process = true; /* If a file matches the exclude list, ignore it, don't remove it. But commenting this out so that excluded files get removed on target. if (exclude !== undefined && StorageUtilities.matchesList(targetFileName, exclude)) { process = false; }*/ if (process && targetFiles[targetFileName] === true) { if (messageUpdater) { await messageUpdater("Removing file '" + target.fullPath + "' (" + targetFileName + ")"); } await target.deleteFile(targetFileName); modifiedFileCount++; } } } return modifiedFileCount; } static matchesList(name, list) { name = StorageUtilities.canonicalizeName(name); let nameMinusSlash = name; if (nameMinusSlash.startsWith("/")) { nameMinusSlash = nameMinusSlash.substring(1, nameMinusSlash.length); } for (let i = 0; i < list.length; i++) { const listC = StorageUtilities.canonicalizeName(list[i]); if (name === listC) { return true; } if (!listC.startsWith("/")) { if (nameMinusSlash === listC) { return true; } } if (listC.length > 2 && listC.startsWith("*") && listC.endsWith("*")) { if (name.indexOf(listC.substring(1, listC.length - 1)) >= 0) { return true; } } else if (listC.length > 2 && listC.startsWith("*") && !listC.endsWith("*")) { if (name.endsWith(listC.substring(1))) { return true; } } else if (listC.length > 2 && !listC.startsWith("*") && listC.endsWith("*")) { if (name.startsWith(listC.substring(0, listC.length - 1))) { return true; } } } return false; } static sanitizePathBasic(path) { path = path.replace(/</gi, "_"); path = path.replace(/>/gi, "_"); path = path.replace(/ /gi, "_"); path = path.replace(/"/gi, "_"); path = path.replace(/'/gi, "_"); path = path.replace(/::/gi, "_"); path = path.replace(/,/gi, "_"); path = path.replace(/:/gi, "_"); path = path.replace(/\r/gi, "_"); path = path.replace(/\n/gi, "_"); path = path.replace(/__/gi, "_"); path = path.replace(/__/gi, "_"); while (path.length > 1 && path.startsWith("_")) { path = path.substring(1, path.length); } while (path.length > 1 && path.endsWith("_")) { path = path.substring(0, path.length - 1); } path = path.trim(); return path; } static sanitizePath(path) { if (Utilities_1.default.isAlphaNumeric(path)) { return path; } let utf8Encode = new TextEncoder(); const base64 = Utilities_1.default.arrayBufferToBase64(utf8Encode.encode(path)) .replace(/\//gi, " ") .replace(/=/gi, "_"); return base64; } static async syncFileTo(source, target, force, messageUpdater) { // Log.debug("Syncing file content '" + source.fullPath + "'"); await source.loadContent(true); if (source.content == null) { Log_1.default.debug("No content for file " + source.storageRelativePath); return; } if (!force) { if (await target.exists()) { await target.loadContent(false); if (StorageUtilities.contentsAreEqual(source.content, target.content)) { return; } } } if (messageUpdater) { let targetPath = target.fullPath; targetPath = targetPath.replace("fs.mctprojects/root/", ""); let mess = "Updating file: " + targetPath; if (source.content) { mess += " (size: " + source.content.length + ")"; } await messageUpdater(mess); } // Log.debug("Copying file " + source.storageRelativePath + " to " + target.storageRelativePath); target.setContent(source.content); return true; } } exports.default = StorageUtilities; StorageUtilities.standardFolderDelimiter = "/"; //# sourceMappingURL=../maps/storage/StorageUtilities.js.map