@shockpkg/ria-packager
Version:
Package for creating Adobe AIR packages
719 lines (649 loc) • 20.6 kB
JavaScript
"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