UNPKG

@shockpkg/ria-packager

Version:

Package for creating Adobe AIR packages

719 lines (649 loc) 20.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PackagerBundleWindows = void 0; var _promises = require("node:fs/promises"); var _nodePath = require("node:path"); var _portableExecutableSignature = require("portable-executable-signature"); var _archiveFiles = require("@shockpkg/archive-files"); var _iconEncoder = require("@shockpkg/icon-encoder"); var _resedit = require("@shockpkg/resedit"); var _util = require("../../util.js"); var _bundle = require("../bundle.js"); // IMAGE_DATA_DIRECTORY indexes. const IDD_RESOURCE = 2; const IDD_BASE_RELOCATION = 5; // IMAGE_SECTION_HEADER characteristics. const IMAGE_SCN_CNT_CODE = 0x00000020; const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040; const IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080; /** * Assert the given section is last section. * * @param exe NtExecutable instance. * @param index ImageDirectory index. * @param name Friendly name for messages. */ function exeAssertLastSection(exe, index, name) { const section = exe.getSectionByEntry(index); if (!section) { throw new Error(`Missing section: ${index}:${name}`); } const allSections = exe.getAllSections(); let last = allSections[0].info; for (const { info } of allSections) { if (info.pointerToRawData > last.pointerToRawData) { last = info; } } const { info } = section; if (info.pointerToRawData < last.pointerToRawData) { throw new Error(`Not the last section: ${index}:${name}`); } } /** * Removes the reloc section if exists, fails if not the last section. * * @param exe NtExecutable instance. * @returns Restore function. */ function exeRemoveReloc(exe) { const section = exe.getSectionByEntry(IDD_BASE_RELOCATION); if (!section) { return () => {}; } const { size } = exe.newHeader.optionalHeaderDataDirectory.get(IDD_BASE_RELOCATION); exeAssertLastSection(exe, IDD_BASE_RELOCATION, '.reloc'); exe.setSectionByEntry(IDD_BASE_RELOCATION, null); return () => { exe.setSectionByEntry(IDD_BASE_RELOCATION, section); const { virtualAddress } = exe.newHeader.optionalHeaderDataDirectory.get(IDD_BASE_RELOCATION); exe.newHeader.optionalHeaderDataDirectory.set(IDD_BASE_RELOCATION, { virtualAddress, size }); }; } /** * Update the sizes in EXE headers. * * @param exe NtExecutable instance. */ function exeUpdateSizes(exe) { const { optionalHeader } = exe.newHeader; const { fileAlignment } = optionalHeader; let sizeOfCode = 0; let sizeOfInitializedData = 0; let sizeOfUninitializedData = 0; for (const { info: { characteristics, sizeOfRawData, virtualSize } } of exe.getAllSections()) { // eslint-disable-next-line no-bitwise if (characteristics & IMAGE_SCN_CNT_CODE) { sizeOfCode += sizeOfRawData; } // eslint-disable-next-line no-bitwise if (characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) { sizeOfInitializedData += Math.max(sizeOfRawData, (0, _util.align)(virtualSize, fileAlignment)); } // eslint-disable-next-line no-bitwise if (characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) { sizeOfUninitializedData += (0, _util.align)(virtualSize, fileAlignment); } } optionalHeader.sizeOfCode = sizeOfCode; optionalHeader.sizeOfInitializedData = sizeOfInitializedData; optionalHeader.sizeOfUninitializedData = sizeOfUninitializedData; } /** * PackagerBundleWindows object. */ class PackagerBundleWindows extends _bundle.PackagerBundle { /** * Create modern application icon resource. * Enables higher resolution 256x256 icon with PNG compression. * Higher resolutions resized with lanczos from 512x512 or 1024x1024. * Default false uses the legacy formats of the official packager. */ applicationIconModern = false; /** * Create modern document type icon resource. * Enables higher resolution 256x256 icon with PNG compression. * Higher resolutions resized with lanczos from 512x512 or 1024x1024. * Default false uses the legacy formats of the official packager. */ fileTypeIconModern = false; /** * Remove unnecessary helper files from framework. * The official packages will include these. */ frameworkCleanHelpers = false; /** * Optionally preserve resource mtime. * The official packager does not preserve resource mtimes. */ preserveResourceMtime = false; /** * Optionally use specific architecture. */ architecture = null; /** * Version strings. * * @default null */ fileVersion = null; /** * Product version. * * @default null */ productVersion = null; /** * Version strings. * * @default null */ versionStrings = null; /** * PackagerBundleWindows constructor. * * @param path Output path. */ constructor(path) { super(path); } /** * Get app binary path. * * @returns Binary path. */ getAppBinaryPath() { return `${this._getFilename()}.exe`; } /** * Get app framework path. * * @returns Framework path. */ getAppFrameworkPath() { return 'Adobe AIR'; } /** * Get SDK binary path. * * @returns Binary path. */ getSdkBinaryPath() { const framework = this.getSdkFrameworkPath(); return `${framework}/Versions/1.0/Resources/CaptiveAppEntry.exe`; } /** * Get SDK framework path. * * @returns Framework path. */ getSdkFrameworkPath() { const win = this._getArchitecture() === 'x64' ? 'win64' : 'win'; return `runtimes/air-captive/${win}/Adobe AIR`; } /** * Get all version strings, if any. * * @returns Verion strings. */ getVersionStrings() { const { fileVersion, productVersion, versionStrings } = this; if (fileVersion === null && productVersion === null && versionStrings === null) { return null; } const values = { ...versionStrings }; if (fileVersion !== null) { values.FileVersion = fileVersion; } if (productVersion !== null) { values.ProductVersion = productVersion; } return values; } /** * Get file mode value. * * @param executable Is the entry executable. * @returns File mode. */ _getFileMode(executable) { return executable ? 0b111100100 : 0b110100100; } /** * Open implementation. */ async _open() { const { sdkPath } = this; if (!sdkPath) { throw new Error('SDK path not set'); } const { frameworkCleanHelpers } = this; const appBinaryPath = this.getAppBinaryPath(); const appFrameworkPath = this.getAppFrameworkPath(); const sdkBinaryPath = this.getSdkBinaryPath(); const sdkFrameworkPath = this.getSdkFrameworkPath(); const appBinaryPathFull = (0, _nodePath.join)(this.path, appBinaryPath); const appFrameworkPathFull = (0, _nodePath.join)(this.path, appFrameworkPath); let extractedBinary = false; let extractedFramework = false; let binaryInFrameworkPath = ''; // Extract everything needed from the SDK. const sdk = await (0, _archiveFiles.createArchiveByFileStatOrThrow)(sdkPath, { nobrowse: this.nobrowse }); await sdk.read(async entry => { // Ignore any resource forks. if (entry.type === _archiveFiles.PathType.RESOURCE_FORK) { return true; } const path = entry.volumePath; const sdkBinaryPathRel = (0, _util.pathRelativeBase)(path, sdkBinaryPath, true); const frameworkPathRel = (0, _util.pathRelativeBase)(path, sdkFrameworkPath, true); // Extract if the framework. if (frameworkPathRel !== null) { const dest = (0, _nodePath.join)(appFrameworkPathFull, frameworkPathRel); extractedFramework = true; // If also the binary, remember it for later. if (sdkBinaryPathRel === null) { await entry.extract(dest); return true; } // If not removing framework binary, copy into framework. // Remember where to copy again after. // Otherwise let it be extracted to destination below. if (!frameworkCleanHelpers) { // Remember the shortest path, not empty. binaryInFrameworkPath = binaryInFrameworkPath || dest; if (dest.length < binaryInFrameworkPath.length) { binaryInFrameworkPath = dest; } await entry.extract(dest); return true; } } // Copy binary from framework if there. if (sdkBinaryPathRel !== null) { const dest = (0, _nodePath.join)(appBinaryPathFull, sdkBinaryPathRel); await entry.extract(dest); extractedBinary = true; return true; } // Optimization to avoid walking unrelated directories if possible. return (0, _util.pathRelativeBaseMatch)(sdkFrameworkPath, path, true) || (0, _util.pathRelativeBaseMatch)(sdkBinaryPath, path, true) ? true : null; }); // If the binary is in framework, copy it. if (binaryInFrameworkPath) { const st = await (0, _promises.stat)(binaryInFrameworkPath); await (0, _promises.copyFile)(binaryInFrameworkPath, appBinaryPathFull); await (0, _promises.utimes)(binaryInFrameworkPath, st.atime, st.mtime); extractedBinary = true; } // Check that required components were extracted. if (!extractedBinary) { throw new Error(`Failed to locate binary in SDK: ${sdkBinaryPath}`); } if (!extractedFramework) { throw new Error(`Failed to locate framework in SDK: ${sdkFrameworkPath}`); } } /** * Close implementation. */ async _close() { const appBinaryModifier = await this._getAppBinaryModifier(); if (appBinaryModifier) { const appBinaryPath = this.getAppBinaryPath(); const appBinaryPathFull = (0, _nodePath.join)(this.path, appBinaryPath); await (0, _promises.writeFile)(appBinaryPathFull, await appBinaryModifier(await (0, _promises.readFile)(appBinaryPathFull))); } } /** * Write resource with data implementation. * * @param destination Packaged file relative destination. * @param data Resource data. * @param options Resource options. */ async _writeResource(destination, data, options) { // Write resource to file. const mode = this._getFileMode(options.executable || false); const dest = this._getResourcePath(destination); await (0, _promises.mkdir)((0, _nodePath.dirname)(dest), { recursive: true }); await (0, _promises.writeFile)(dest, data, { mode }); // Optionally preserve mtime information. if (this.preserveResourceMtime) { const { mtime } = options; if (mtime) { await (0, _promises.utimes)(dest, mtime, mtime); } } } /** * Get path to a resource file. * * @param parts Path parts. * @returns Full path. */ _getResourcePath(...parts) { return (0, _nodePath.join)(this.path, ...parts); } /** * Get the configured architecture. * Prefers the architecture option, descriptor file, then default of x86. * * @returns Architecture string. */ _getArchitecture() { return this.architecture || (this._applicationInfoArchitecture === '64' ? 'x64' : 'x86'); } /** * Get the main app binary data modifier function if any. * * @returns Modifier function or null. */ async _getAppBinaryModifier() { // Get any version strings. const versionStrings = this.getVersionStrings(); // Assemble all of the icons into a list. const icons = await Promise.all([this._encodeApplicationIcon().then(d => d ? [d] : []), this._encodeFileTypeIcons().then(a => a || [])]).then(([a, b]) => [...a, ...b]); // Skip if nothing to be changed. if (!versionStrings && !icons.length) { return null; } return async data => { // Parse EXE. const exe = _resedit.NtExecutable.from((0, _portableExecutableSignature.signatureSet)(data, null, true, true)); // Remove reloc so rsrc can safely be resized. const relocRestore = exeRemoveReloc(exe); // Remove rsrc to modify. exeAssertLastSection(exe, IDD_RESOURCE, '.rsrc'); const rsrc = _resedit.NtExecutableResource.from(exe); exe.setSectionByEntry(IDD_RESOURCE, null); // Check that icons and version info not present. if (_resedit.Resource.IconGroupEntry.fromEntries(rsrc.entries).length) { throw new Error('Executable resources contains unexpected icons'); } if (_resedit.Resource.VersionInfo.fromEntries(rsrc.entries).length) { throw new Error('Executable resources contains unexpected version info'); } // The lang and codepage resource values. const lang = 1033; const codepage = 1252; // Add icons, resource ID 100 plus. let resIdsNext = 100; for (const iconData of icons) { // Parse ico. const ico = _resedit.Data.IconFile.from(iconData); // Get the next icon group ID. const iconGroupId = resIdsNext++; // Add this group to the list. _resedit.Resource.IconGroupEntry.replaceIconsForResource(rsrc.entries, iconGroupId, 0, ico.icons.map(icon => icon.data)); // List all the resources now in the list. const entriesById = new Map(rsrc.entries.map((resource, index) => [resource.id, { index, resource }])); // Get icon group info. const entryInfo = entriesById.get(iconGroupId); if (!entryInfo) { throw new Error('Internal error'); } // Read icon group entry. const [iconGroup] = _resedit.Resource.IconGroupEntry.fromEntries([entryInfo.resource]); // Change individual icon resource id values. for (const icon of iconGroup.icons) { const iconInfo = entriesById.get(icon.iconID); if (!iconInfo) { throw new Error('Internal error'); } icon.iconID = iconInfo.resource.id = resIdsNext++; } // Update the group entry. rsrc.entries[entryInfo.index] = iconGroup.generateEntry(); } // Add the version info if any. if (versionStrings) { const versionInfo = _resedit.Resource.VersionInfo.createEmpty(); versionInfo.setStringValues({ lang, codepage }, versionStrings); // Update integer values from parsed strings if possible. const { FileVersion, ProductVersion } = versionStrings; if (FileVersion) { const uints = this._peVersionInts(FileVersion); if (uints) { const [ms, ls] = uints; versionInfo.fixedInfo.fileVersionMS = ms; versionInfo.fixedInfo.fileVersionLS = ls; } } if (ProductVersion) { const uints = this._peVersionInts(ProductVersion); if (uints) { const [ms, ls] = uints; versionInfo.fixedInfo.productVersionMS = ms; versionInfo.fixedInfo.productVersionLS = ls; } } versionInfo.outputToResourceEntries(rsrc.entries); } // Update the codepage on all resources. // Matches the behavior of official packager. for (const entry of rsrc.entries) { entry.codepage = codepage; } // Update resources. rsrc.outputResource(exe, false, true); // Add reloc back. relocRestore(); // Update sizes. exeUpdateSizes(exe); // Encode new EXE file. return new Uint8Array(exe.generate()); }; } /** * Parse PE version string to integers (MS then LS bits) or null. * * @param version Version string. * @returns Version integers ([MS, LS]) or null. */ _peVersionInts(version) { const parts = version.split(/[,.]/); const numbers = []; for (const part of parts) { const n = /^\d+$/.test(part) ? +part : -1; if (n < 0 || n > 0xffff) { return null; } numbers.push(n); } return numbers.length ? [ // eslint-disable-next-line no-bitwise ((numbers[0] || 0) << 16 | (numbers[1] || 0)) >>> 0, // eslint-disable-next-line no-bitwise ((numbers[2] || 0) << 16 | (numbers[3] || 0)) >>> 0] : null; } /** * Calculate UID for icon, or null if none of required icons set. * * @param icon Icon info. * @returns UID string or null. */ _uidIcon(icon) { const paths = [icon.image16x16, icon.image32x32, icon.image48x48, icon.image128x128]; // If none set, skip. let has = false; for (const p of paths) { if (p) { has = true; break; } } // Compute a unique identifier for the used icon set paths. return has ? paths.map(s => `${s ? s.length : 0}:${s || ''}`).join('|') : null; } /** * Encode the application icon if specified. * * @returns Encoded icon. */ async _encodeApplicationIcon() { const icon = this._getIcon(); if (!icon || !this._uidIcon(icon)) { return null; } // Encode either a modern or a reference icon. return this.applicationIconModern ? this._encodeIconModern(icon) : this._encodeIconReference(icon); } /** * Encode file type icons. * Avoids writting duplicate icons where the file/data is the same. * * @returns Encoded icons. */ async _encodeFileTypeIcons() { const fileIcons = this._getFileTypes(); if (!fileIcons) { return null; } // Encode either a modern or a reference icon. const encode = this.fileTypeIconModern ? async icon => this._encodeIconModern(icon) : async icon => this._encodeIconReference(icon); const r = []; const did = new Set(); for (const [, { icon }] of fileIcons) { if (!icon) { continue; } // Compute a unique identifier for the used icon set paths. const uid = this._uidIcon(icon); if (!uid) { continue; } // Check if file was already generated for this icon set. if (did.has(uid)) { continue; } did.add(uid); r.push(encode(icon)); } return Promise.all(r); } /** * Encode icon matching official format. * * @param icon Icon info. * @returns Encoded icon. */ async _encodeIconReference(icon) { // Add icons in the same order official packager would use. const ico = new _iconEncoder.IconIco(); const readers = []; for (const path of [icon.image16x16, icon.image48x48, icon.image128x128, icon.image32x32]) { if (path) { readers.push(async () => (0, _promises.readFile)(this._getResourcePath(path))); } } const datas = await Promise.all(readers.map(async f => f())); for (const data of datas) { // eslint-disable-next-line no-await-in-loop await ico.addFromPng(data, false); } return ico.encode(); } /** * Encode icon using modern format. * * @param icon Icon info. * @returns Encoded icon. */ async _encodeIconModern(icon) { // Add icons in the same order official packager would use, plus extra. const ico = new _iconEncoder.IconIco(); const readers = []; for (const path of [icon.image16x16, icon.image48x48, icon.image128x128, icon.image32x32]) { if (path) { readers.push(async () => (0, _promises.readFile)(this._getResourcePath(path)).then(d => [d, false])); } } readers.push(async () => this._getIcon256x256Data(icon).then(d => [d, true])); const datas = await Promise.all(readers.map(async f => f())); for (const [data, png] of datas) { if (data) { // eslint-disable-next-line no-await-in-loop await ico.addFromPng(data, png); } } return ico.encode(); } /** * Get 256x256 icon data from icon set. * Unfortuantely the icon set does not support this icon size. * This functions will resize a larger icon instead. * Uses the lanczos algorithm to resize icon down. * * @param icon Icon info. * @returns Encoded icon or null. */ async _getIcon256x256Data(icon) { const { image512x512, image1024x1024 } = icon; // Resize 512x512 or 1024x1024 icon down if available. let image = image512x512; let x = 1; if (!image) { image = image1024x1024; x = 2; } return image ? (0, _util.pngHalfSize)(await (0, _promises.readFile)(this._getResourcePath(image)), x) : null; } } exports.PackagerBundleWindows = PackagerBundleWindows; //# sourceMappingURL=windows.js.map