UNPKG

@shockpkg/dir-projector

Version:

Package for creating Shockwave Director projectors

375 lines (300 loc) 10.6 kB
import { signatureGet, signatureSet } from "portable-executable-signature/module.mjs"; import * as resedit from 'resedit'; import fse from 'fs-extra'; import { bufferToArrayBuffer, launcher } from "../util.mjs"; const ResEditNtExecutable = resedit.NtExecutable || resedit.default.NtExecutable; const ResEditNtExecutableResource = resedit.NtExecutableResource || resedit.default.NtExecutableResource; const ResEditResource = resedit.Resource || resedit.default.Resource; const ResEditData = resedit.Data || resedit.default.Data; /** * Parse PE version string to integers (MS then LS bits) or null. * * @param version Version string. * @returns Version integers ([MS, LS]) or null. */ export function peVersionInts(version) { const parts = version.split(/[.,]/); const numbers = []; for (const part of parts) { const n = /^\d+$/.test(part) ? +part : NaN; 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; } /** * Replace resources in Windows PE file. * * @param path File path. * @param options Replacement options. */ export async function peResourceReplace(path, options) { const { iconData, versionStrings, removeSignature } = options; // Read EXE file and remove signature if present. const exeOriginal = await fse.readFile(path); const signedData = removeSignature ? null : signatureGet(exeOriginal); let exeData = signatureSet(exeOriginal, null, true, true); // Parse resources. const exe = ResEditNtExecutable.from(exeData); const res = ResEditNtExecutableResource.from(exe); // Replace all the icons in all icon groups. if (iconData) { const ico = ResEditData.IconFile.from(bufferToArrayBuffer(iconData)); for (const iconGroup of ResEditResource.IconGroupEntry.fromEntries(res.entries)) { ResEditResource.IconGroupEntry.replaceIconsForResource(res.entries, iconGroup.id, iconGroup.lang, ico.icons.map(icon => icon.data)); } } // Update strings if present for all the languages. if (versionStrings) { for (const versionInfo of ResEditResource.VersionInfo.fromEntries(res.entries)) { // Get all the languages, not just available languages. const languages = versionInfo.getAllLanguagesForStringValues(); for (const language of languages) { versionInfo.setStringValues(language, versionStrings); } // Update integer values from parsed strings if possible. const { FileVersion, ProductVersion } = versionStrings; if (FileVersion) { const uints = peVersionInts(FileVersion); if (uints) { const [ms, ls] = uints; versionInfo.fixedInfo.fileVersionMS = ms; versionInfo.fixedInfo.fileVersionLS = ls; } } if (ProductVersion) { const uints = peVersionInts(ProductVersion); if (uints) { const [ms, ls] = uints; versionInfo.fixedInfo.productVersionMS = ms; versionInfo.fixedInfo.productVersionLS = ls; } } versionInfo.outputToResourceEntries(res.entries); } } // Update resources. res.outputResource(exe); exeData = exe.generate(); // Add back signature if not removing. if (signedData) { exeData = signatureSet(exeData, signedData, true, true); } // Write updated EXE file. await fse.writeFile(path, Buffer.from(exeData)); } /** * Get Windows launcher for the specified type. * * @param type Executable type. * @param resources File to optionally copy resources from. * @returns Launcher data. */ export async function windowsLauncher(type, resources = null) { let data; switch (type) { case 'i686': { data = await launcher('windows-i686'); break; } case 'x86_64': { data = await launcher('windows-x86_64'); break; } default: { throw new Error(`Invalid type: ${type}`); } } // Check if copying resources. if (!resources) { return data; } // Remove signature if present. const signedData = signatureGet(data); let exeData = signatureSet(data, null, true, true); // Read resources from file. const res = ResEditNtExecutableResource.from(ResEditNtExecutable.from(await fse.readFile(resources), { ignoreCert: true })); // Find the first icon group for each language. const resIconGroups = new Map(); for (const iconGroup of ResEditResource.IconGroupEntry.fromEntries(res.entries)) { const known = resIconGroups.get(iconGroup.lang) || null; if (!known || iconGroup.id < known.id) { resIconGroups.set(iconGroup.lang, iconGroup); } } // List the groups and icons to be kept. const iconGroups = new Set(); const iconDatas = new Set(); for (const [, group] of resIconGroups) { iconGroups.add(group.id); for (const icon of group.icons) { iconDatas.add(icon.iconID); } } // Filter out the resources to keep. const typeVersionInfo = 16; const typeIcon = 3; const typeIconGroup = 14; res.entries = res.entries.filter(entry => entry.type === typeVersionInfo || entry.type === typeIcon && iconDatas.has(entry.id) || entry.type === typeIconGroup && iconGroups.has(entry.id)); // Apply resources to launcher. const exe = ResEditNtExecutable.from(exeData); res.outputResource(exe); exeData = exe.generate(); // Add back signature if one present. if (signedData) { exeData = signatureSet(exeData, signedData, true, true); } return Buffer.from(exeData); } /** * Converts a hex string into a series of byte values, with unknowns being null. * * @param str Hex string. * @returns Bytes and null values. */ function patchHexToBytes(str) { return (str.replace(/[\s\r\n]/g, '').match(/.{1,2}/g) || []).map(s => { if (s.length !== 2) { throw new Error('Internal error'); } return /[0-9A-F]{2}/i.test(s) ? parseInt(s, 16) : null; }); } /* eslint-disable no-multi-spaces */ /* eslint-disable line-comment-position */ /* eslint-disable no-inline-comments */ // A list of patch candidates, made to be partially position independant. // Basically these patches just increase the temporary buffer sizes. // Enough to provide amply room for anything that should be in the registry. // Sizes 0x10000 for ASCII, and 0x20000 for WCHAR. // Not enough room to calculate the correct size, and use it directly. const patchShockwave3dInstalledDisplayDriversSizePatches = [// director-8.5.0 - director-11.0.0-hotfix-1: { find: patchHexToBytes(['FF 15 -- -- -- --', // call DWORD PTR ds:-- -- -- -- 'BE 04 01 00 00', // mov esi, 0x104 '56', // push esi 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')), replace: patchHexToBytes(['FF 15 -- -- -- --', // call DWORD PTR ds:-- -- -- -- // Change: 'BE 00 00 01 00', // mov esi, 0x10000 '56', // push esi 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')) }, // director-11.0.0-hotfix-3 - director-11.5.0: { find: patchHexToBytes(['FF 15 -- -- -- --', // call DWORD PTR ds:-- -- -- -- 'BF 04 01 00 00', // mov edi, 0x104 '57', // push edi 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')), replace: patchHexToBytes(['FF 15 -- -- -- --', // call DWORD PTR ds:-- -- -- -- // Change: 'BF 00 00 01 00', // mov edi, 0x10000 '57', // push edi 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')) }, // director-11.5.8 - director-11.5.9: { find: patchHexToBytes(['68 -- -- -- --', // push -- -- -- -- '57', // push edi 'FF D6', // call esi '68 08 02 00 00', // push 0x208 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')), replace: patchHexToBytes(['68 -- -- -- --', // push -- -- -- -- '57', // push edi 'FF D6', // call esi // Change: '68 00 00 02 00', // push 0x20000 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')) }, // director-12.0.0: { find: patchHexToBytes(['68 -- -- -- --', // push -- -- -- -- '53', // push ebx 'FF D7', // call edi '68 08 02 00 00', // push 0x208 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')), replace: patchHexToBytes(['68 -- -- -- --', // push -- -- -- -- '53', // push ebx 'FF D7', // call edi // Change: '68 00 00 02 00', // push 0x20000 'E8 -- -- -- --' // call -- -- -- -- ].join(' ')) }]; /* eslint-enable no-multi-spaces */ /* eslint-enable line-comment-position */ /* eslint-enable no-inline-comments */ /** * Patch data buffer once. * * @param data Data buffer. * @param candidates Patch candidates. * @param name Patch name. */ function patchDataOnce(data, candidates, name) { // Search the buffer for patch candidates. let foundOffset = -1; let foundPatch = []; for (const patch of candidates) { const { find, replace } = patch; if (replace.length !== find.length) { throw new Error('Internal error'); } const end = data.length - find.length; for (let i = 0; i < end; i++) { let found = true; for (let j = 0; j < find.length; j++) { const b = find[j]; if (b !== null && data[i + j] !== b) { found = false; break; } } if (!found) { continue; } if (foundOffset !== -1) { throw new Error(`Multiple patch candidates found for: ${name}`); } // Remember patch to apply. foundOffset = i; foundPatch = replace; } } if (foundOffset === -1) { throw new Error(`No patch candidates found for: ${name}`); } // Apply the patch to the buffer, and write to file. for (let i = 0; i < foundPatch.length; i++) { const b = foundPatch[i]; if (b !== null) { data[foundOffset + i] = b; } } } /** * Patch a file once. * * @param file File path. * @param candidates Patch candidates. * @param name Patch name. */ async function patchFileOnce(file, candidates, name) { const data = await fse.readFile(file); patchDataOnce(data, candidates, name); await fse.writeFile(file, data); } /** * Patch Windows Shockwave 3D InstalledDisplayDrivers size. * * @param file File path. */ export async function windowsPatchShockwave3dInstalledDisplayDriversSize(file) { await patchFileOnce(file, patchShockwave3dInstalledDisplayDriversSizePatches, 'Windows Shockwave 3D InstalledDisplayDrivers Size'); } //# sourceMappingURL=windows.mjs.map