var unpackage = (function() { 'use strict'; /** * @returns {Promise} */ const unzipOrNull = async (binaryData) => { try { return await JSZip.loadAsync(binaryData); } catch (e) { return null; } }; /** * @param {Blob} blob * @returns {Promise} */ const readAsText = (blob) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error('Could not read blob as text')); reader.readAsText(blob); }); /** * @param {string} string * @param {RegExp} regex * @returns {string[][]} */ const matchAll = (string, regex) => { const result = []; let match = null; while ((match = regex.exec(string)) !== null) { result.push(match); } return result; }; const getContainingFolder = (name) => { const parts = name.split('/'); parts.pop(); return parts.join('/'); }; const identifyProjectJSONType = (data) => { if ('targets' in data) { return 'sb3'; } else if ('objName' in data) { return 'sb2'; } throw new Error('Can not determine project.json type'); }; const decodeBase85 = (str) => { const decode_1 = (str) => { // The initial version of base85 // https://github.com/TurboWarp/packager/blob/9234d057585132d2514a831476abbcf2a7b9b151/src/packager/lib/base85-encode.js // "0x29 - 0x7d of ASCII with 0x5c (\) replaced with 0x7e (~)" const getValue = (code) => { if (code === 0x7e) { return 0x5c - 0x29; } return code - 0x29; }; const toMultipleOfFour = (n) => { if (n % 4 === 0) { return n; } return n + (4 - n % 4); }; const stringToBytes = (str) => new TextEncoder().encode(str); const lengthEndsAt = str.indexOf(','); const byteLength = +str.substring(0, lengthEndsAt); const resultBuffer = new ArrayBuffer(toMultipleOfFour(byteLength)); const resultView = new Uint32Array(resultBuffer); const stringBytes = stringToBytes(str); for (let i = lengthEndsAt + 1, j = 0; i < str.length; i += 5, j++) { resultView[j] = ( getValue(stringBytes[i + 4]) * 85 * 85 * 85 * 85 + getValue(stringBytes[i + 3]) * 85 * 85 * 85 + getValue(stringBytes[i + 2]) * 85 * 85 + getValue(stringBytes[i + 1]) * 85 + getValue(stringBytes[i]) ); } return new Uint8Array(resultBuffer, 0, byteLength); }; const decode_2 = (str) => { // Second version, modified to be HTML safe // https://github.com/TurboWarp/packager/blob/44638a3f6daf03290c4020c5fd0d022edc1d0229/src/packager/lib/base85-encode.js // "The character set used is 0x2a - 0x7e of ASCII" // "0x3c (<) is replaced with 0x28 (opening parenthesis) and 0x3e (>) is replaced with 0x29 (closing parenthesis)" const getValue = (code) => { if (code === 0x28) code = 0x3c; if (code === 0x29) code = 0x3e; return code - 0x2a; }; const toMultipleOfFour = (n) => { if (n % 4 === 0) { return n; } return n + (4 - n % 4); }; const stringToBytes = (str) => new TextEncoder().encode(str); const lengthEndsAt = str.indexOf(','); const byteLength = +str.substring(0, lengthEndsAt); const resultBuffer = new ArrayBuffer(toMultipleOfFour(byteLength)); const resultView = new Uint32Array(resultBuffer); const stringBytes = stringToBytes(str); for (let i = lengthEndsAt + 1, j = 0; i < str.length; i += 5, j++) { resultView[j] = ( getValue(stringBytes[i + 4]) * 85 * 85 * 85 * 85 + getValue(stringBytes[i + 3]) * 85 * 85 * 85 + getValue(stringBytes[i + 2]) * 85 * 85 + getValue(stringBytes[i + 1]) * 85 + getValue(stringBytes[i]) ); } return new Uint8Array(resultBuffer, 0, byteLength); }; const decode_3 = (str) => { // Third version, length header was is now encoded so people don't misinterpret it // https://github.com/TurboWarp/packager/blob/61b6905853320332dd44b08f9f7ab03c4b3542b9/src/packager/base85.js const getValue = (code) => { if (code === 0x28) code = 0x3c; if (code === 0x29) code = 0x3e; return code - 0x2a; }; const toMultipleOfFour = (n) => { if (n % 4 === 0) { return n; } return n + (4 - n % 4); }; const lengthEndsAt = str.indexOf(','); const byteLength = +str .substring(0, lengthEndsAt) .split('') .map(i => String.fromCharCode(i.charCodeAt(0) - 49)) .join(''); const resultBuffer = new ArrayBuffer(toMultipleOfFour(byteLength)); const resultView = new Uint32Array(resultBuffer); for (let i = lengthEndsAt + 1, j = 0; i < str.length; i += 5, j++) { resultView[j] = ( getValue(str.charCodeAt(i + 4)) * 85 * 85 * 85 * 85 + getValue(str.charCodeAt(i + 3)) * 85 * 85 * 85 + getValue(str.charCodeAt(i + 2)) * 85 * 85 + getValue(str.charCodeAt(i + 1)) * 85 + getValue(str.charCodeAt(i)) ); } return new Uint8Array(resultBuffer, 0, byteLength); }; const header = str.substring(0, str.indexOf(',')); // Version 1 and 2 use numbers for the length header while version 3 encodes it if (/^\d+$/.test(header)) { // Version 2 uses \, version 1 does not // This is accurate enough for now. Technically someone could encode something with // version 2 that doesn't include \, but projects are effectively random bytes to // zip compression so the likelihood of that happening randomly is pretty low. if (str.includes('\\')) { return decode_2(str); } return decode_1(str); } return decode_3(str); }; /** * @param {string} str * @returns {Uint8Array} */ const decodeBase64 = (str) => { const decoded = atob(str); const result = new Uint8Array(decoded.length); for (let i = 0; i < decoded.length; i++) { result[i] = decoded.charCodeAt(i); } return result; }; /** * @param {string} uri */ const decodeDataURI = (uri) => { const parts = uri.split(';base64,'); if (parts.length < 2) { throw new Error('Data URI is not base64'); } const base64 = parts[1]; return decodeBase64(base64); }; /** * Find a file in a JSZip using its name regardless of the folder it's in. * @param {JSZip} zip * @param {string} path * @returns {JSZip.File|null} */ const findFileInZip = (zip, path) => { const f = zip.file(path); if (f) { return f; } for (const filename of Object.keys(zip.files)) { if (filename.endsWith(`/${path}`)) { return zip.file(filename); } } return null; }; const unpackageBinaryBlob = async (data) => { const projectZip = await unzipOrNull(data); if (projectZip) { // The project is a compressed sb2 or sb3 project. const projectJSON = findFileInZip(projectZip, 'project.json'); const projectJSONData = JSON.parse(await projectJSON.async('text')); const type = identifyProjectJSONType(projectJSONData); return { type, data }; } // The project is a Scratch 1 project. return { type: 'sb', data }; }; const unpackage = async (blob) => { const packagedZip = await unzipOrNull(blob); if (packagedZip) { // Zip files generated by the TurboWarp Packager can have a project.json alongside the assets const projectJSON = findFileInZip(packagedZip, 'project.json'); if (projectJSON) { // The project is an sb3 project. const innerFolderPath = getContainingFolder(projectJSON.name); const innerZip = packagedZip.folder(innerFolderPath); // Remove extra files that aren't part of the project but are in the same folder // This matters for HTMLifier zips of Scratch 3 projects for (const path of Object.keys(innerZip.files)) { const isPartOfProject = ( path === 'project.json' || /^[a-f0-9]{32}\.[a-z0-9]{3}$/i.test(path) ); if (!isPartOfProject) { innerZip.remove(path); } } return { type: 'sb3', data: await innerZip.generateAsync({ type: 'arraybuffer', compression: 'DEFLATE' }) }; } const projectBinary = ( // Zip files generated by the TurboWarp Packager, the legacy TurboWarp Packager, or the forkphorus packager // can have a "project.zip" file findFileInZip(packagedZip, 'project.zip') || // Zip files generated by HTMLifier for Scratch 1 projects have a "project" file findFileInZip(packagedZip, 'project') ); if (projectBinary) { const projectData = await projectBinary.async('arraybuffer'); return unpackageBinaryBlob(projectData); } throw new Error('Input was a zip but we could not find a project.') } const text = await readAsText(blob); // HTML files generated by the TurboWarp Packager use base85 in several inline script tags const base85Matches = matchAll(text, /