resedit-cli
Version:
Command-line tool for editing Windows Resource data in executable binaries
355 lines (354 loc) • 16.2 kB
JavaScript
import { cosmiconfig } from 'cosmiconfig';
import * as PE from 'pe-library';
import * as ResEdit from 'resedit';
import { readFile, writeFile } from './fs.js';
import * as log from './log.js';
import { CertificateSelectMode, certificateSelectModeValues, } from './definitions/DefinitionData.js';
import parseDefinitionData from './definitions/parser/index.js';
import { isValidDigestAlgorithm, } from './definitions/parser/sign.js';
import emitIcons from './emit/icons.js';
import emitRawResources from './emit/raw.js';
import emitVersion from './emit/version.js';
import { doSign, prepare } from './signing/index.js';
function convertOptionsToDefinitionData(outDefinition, options) {
var _a, _b;
const lang = options.lang;
if (typeof lang !== 'undefined') {
outDefinition.lang = lang;
const strings = (_a = outDefinition.version) === null || _a === void 0 ? void 0 : _a.strings;
if (strings) {
const sameLangData = strings.filter((s) => s.lang === lang).shift();
const undefLangData = strings
.filter((s) => typeof s.lang === 'undefined')
.shift();
if (undefLangData) {
if (sameLangData) {
log.info(`Merging version string values for default language into '${lang}' language.`);
Object.keys(sameLangData.values).forEach((k) => {
undefLangData.values[k] = sameLangData.values[k];
});
outDefinition.version.strings = strings.filter((s) => s.lang !== lang);
}
else {
log.debug(`Update lang value to '${lang}' for version string values without language.`);
}
undefLangData.lang = lang;
}
}
}
if (options.icon && options.icon.length > 0) {
if (outDefinition.icons && outDefinition.icons.length > 0) {
log.info(`Replace icon definitions with ones from option. (count = ${options.icon.length})`);
}
else {
log.debug(`Add icon definitions from option. (count = ${options.icon.length})`);
}
outDefinition.icons = options.icon.map((value) => {
const ra = /^([^,]+),(.*)$/.exec(value);
if (ra) {
const iconId = convertIntegerOrStringValue(ra[1]);
return {
id: iconId,
sourceFile: ra[2].trim(),
};
}
else {
return {
sourceFile: value,
};
}
});
}
if (typeof options['product-name'] !== 'undefined') {
const o = getVersionStringData(getVersionObject()).values;
log.debug(`Set 'ProductName' to '${options['product-name']}'.`);
o.ProductName = options['product-name'];
}
if (typeof options['product-version'] !== 'undefined') {
const s = options['product-version'];
const v = validateAndConvertVersionString(s, 'product-version');
const base = getVersionObject();
const fixed = base.fixedInfo;
const stringValues = getVersionStringData(base).values;
log.debug(`Set 'ProductVersion' to '${s}', as well as 'ProductVersionXX' to '${v[0]}.${v[1]}.${v[2]}.${v[3]}'.`);
stringValues.ProductVersion = s;
fixed.productVersionMS = ((v[0] & 0xffff) << 16) | (v[1] & 0xffff);
fixed.productVersionLS = ((v[2] & 0xffff) << 16) | (v[3] & 0xffff);
}
if (typeof options['file-description'] !== 'undefined') {
const o = getVersionStringData(getVersionObject()).values;
log.debug(`Set 'FileDescription' to '${options['file-description']}'.`);
o.FileDescription = options['file-description'];
}
if (typeof options['file-version'] !== 'undefined') {
const s = options['file-version'];
const v = validateAndConvertVersionString(s, 'file-version');
const base = getVersionObject();
const fixed = base.fixedInfo;
const stringValues = getVersionStringData(base).values;
log.debug(`Set 'FileVersion' to '${s}', as well as 'FileVersionXX' to '${v[0]}.${v[1]}.${v[2]}.${v[3]}'.`);
stringValues.FileVersion = s;
fixed.fileVersionMS = ((v[0] & 0xffff) << 16) | (v[1] & 0xffff);
fixed.fileVersionLS = ((v[2] & 0xffff) << 16) | (v[3] & 0xffff);
}
if (typeof options['company-name'] !== 'undefined') {
const o = getVersionStringData(getVersionObject()).values;
log.debug(`Set 'CompanyName' to '${options['company-name']}'.`);
o.CompanyName = options['company-name'];
}
if (typeof options['original-filename'] !== 'undefined') {
const o = getVersionStringData(getVersionObject()).values;
log.debug(`Set 'OriginalFilename' to '${options['original-filename']}'.`);
o.OriginalFilename = options['original-filename'];
}
if (typeof options['internal-name'] !== 'undefined') {
const o = getVersionStringData(getVersionObject()).values;
log.debug(`Set 'InternalName' to '${options['internal-name']}'.`);
o.InternalName = options['internal-name'];
}
if (options.raw && options.raw.length > 0) {
if (outDefinition.raw && outDefinition.raw.length > 0) {
log.debug(`Append raw resource definitions with ones from option. (count = ${options.raw.length})`);
}
else {
log.debug(`Add raw resource definitions from option. (count = ${options.raw.length})`);
}
outDefinition.raw = ((_b = outDefinition.raw) !== null && _b !== void 0 ? _b : []).concat(options.raw.map((value, i) => {
const ra = /^([^,]+?),([^,]+?),(.+)$/.exec(value);
if (!ra) {
throw new Error(`Invalid '--raw' option value (index: ${i}, expected: <type>,<ID>,<data>, actual: ${value})`);
}
const type = convertIntegerOrStringValue(ra[1]);
const id = convertIntegerOrStringValue(ra[2]);
if (ra[3][0] === '@') {
return {
type,
id,
file: ra[3].substring(1),
};
}
else {
return {
type,
id,
value: ra[3],
};
}
}));
}
if ((options.sign !== undefined && options.sign) || outDefinition.sign) {
log.debug('Make executable with signing.');
if (!outDefinition.sign &&
typeof options.p12 === 'undefined' &&
typeof options.certificate === 'undefined' &&
typeof options['private-key'] === 'undefined') {
throw new Error("'--p12' or ('--certificate' and '--private-key') is missing.");
}
else if (typeof options.p12 !== 'undefined') {
if (typeof options.certificate !== 'undefined' ||
typeof options['private-key'] !== 'undefined') {
throw new Error("Only either '--p12' or ('--certificate' and '--private-key') can be specified.");
}
if (outDefinition.sign) {
log.info(`Overwrite signing info in definition data.`);
}
storeSignObjectWithP12(options.p12);
log.debug(`Set signing info with p12 file '${options.p12}'.`);
}
else if (typeof options.certificate !== 'undefined' ||
typeof options['private-key'] !== 'undefined') {
if (typeof options.certificate === 'undefined') {
throw new Error("'--certificate' is missing.");
}
else if (typeof options['private-key'] === 'undefined') {
throw new Error("'--private-key' is missing.");
}
if (outDefinition.sign) {
log.info(`Overwrite signing info in definition data.`);
}
storeSignObjectWithPKey(options.certificate, options['private-key']);
log.debug(`Set signing info with certificate file '${options.certificate}' and private key file '${options['private-key']}'.`);
}
if (options.select !== undefined &&
certificateSelectModeValues.includes(options.select) &&
outDefinition.sign) {
outDefinition.sign.certSelect = options.select;
log.debug(`Set certificate selection mode to '${options.select}'.`);
}
if (typeof options.password !== 'undefined' && outDefinition.sign) {
outDefinition.sign.password = options.password;
log.debug(`Set password for private key.`);
}
if (isValidDigestAlgorithm(options.digest) && outDefinition.sign) {
outDefinition.sign.digestAlgorithm = options.digest;
log.debug(`Set digest algorithm to '${options.digest}'.`);
}
if (typeof options.timestamp !== 'undefined' && outDefinition.sign) {
outDefinition.sign.timestampServer = options.timestamp;
log.debug(`Use timestamp server '${options.timestamp}'.`);
}
}
else {
throwIfSigningOptionSpecified(options, 'p12');
throwIfSigningOptionSpecified(options, 'private-key');
throwIfSigningOptionSpecified(options, 'certificate');
throwIfSigningOptionSpecified(options, 'password');
throwIfSigningOptionSpecified(options, 'digest');
throwIfSigningOptionSpecified(options, 'timestamp');
}
function convertIntegerOrStringValue(value) {
if (/^(?:0|[1-9][0-9]*)$/.test(value) && !isNaN(Number(value))) {
return Number(value);
}
else {
return value;
}
}
function validateAndConvertVersionString(ver, propName) {
const ra = /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(ver);
if (!ra) {
throw new Error(`Invalid version string format: ${ver} (parsing option '--${propName}')`);
}
return [
Number(ra[1]) & 0xffff,
Number(ra[2]) & 0xffff,
Number(ra[3]) & 0xffff,
Number(ra[4]) & 0xffff,
];
}
function getVersionObject() {
var _a;
return ((_a = outDefinition.version) !== null && _a !== void 0 ? _a : (outDefinition.version = {
fixedInfo: {},
strings: [],
}));
}
function getVersionStringData(o) {
let s = o.strings.filter((s) => s.lang === lang).shift();
if (!s) {
s = { lang, values: {} };
o.strings.push(s);
}
return s;
}
function storeSignObjectWithP12(p12File) {
if (outDefinition.sign) {
outDefinition.sign.p12File =
p12File;
delete outDefinition.sign.certificateFile;
delete outDefinition.sign.privateKeyFile;
}
else {
outDefinition.sign = {
p12File,
certSelect: CertificateSelectMode.Leaf,
password: undefined,
digestAlgorithm: 'sha256',
timestampServer: undefined,
};
}
return outDefinition.sign;
}
function storeSignObjectWithPKey(certificateFile, privateKeyFile) {
if (outDefinition.sign) {
outDefinition.sign.certificateFile = certificateFile;
outDefinition.sign.privateKeyFile = privateKeyFile;
delete outDefinition.sign.p12File;
}
else {
outDefinition.sign = {
certificateFile,
privateKeyFile,
certSelect: CertificateSelectMode.Leaf,
password: undefined,
digestAlgorithm: 'sha256',
timestampServer: undefined,
};
}
return outDefinition.sign;
}
function throwIfSigningOptionSpecified(options, field) {
if (typeof options[field] !== 'undefined') {
throw new Error(`'--sign' option must be true when '--${field}' is specified.`);
}
}
}
async function emitResources(isExe, res, defData) {
const lang = typeof defData.lang !== 'undefined' ? defData.lang : 1033;
let modified = false;
modified = (await emitIcons(res, lang, defData.icons)) || modified;
modified =
(await emitVersion(res, lang, isExe, defData.version)) || modified;
modified = (await emitRawResources(res, lang, defData.raw)) || modified;
return modified;
}
export default async function run(options) {
var _a;
let convertedDefData = {};
log.debug('noGrow: ', !!options.noGrow);
log.debug('allowShrink: ', !!options.allowShrink);
if (options.definition !== undefined) {
if (typeof options.definition === 'string') {
const explorer = cosmiconfig('resedit');
log.info(`Load definition data structure from '${options.definition}'.`);
const ret = await explorer.load(options.definition);
if (!ret || ret.isEmpty) {
throw new Error();
}
log.info(`Check definition data structure.`);
convertedDefData = parseDefinitionData(ret.config);
}
else {
log.info(`Check definition data structure in the 'options' object.`);
convertedDefData = parseDefinitionData(options.definition);
}
}
log.debug('Merge definition data with options (if specified).');
convertOptionsToDefinitionData(convertedDefData, options);
let executable;
if (options.in === undefined) {
log.info(`Create an empty executable binary (32-bit: ${options.as32bit ? 'true' : 'false'}, as EXE: ${options.asExeFile ? 'true' : 'false'}).`);
executable = PE.NtExecutable.createEmpty(options.as32bit, options.asExeFile !== undefined ? !options.asExeFile : true);
}
else {
log.info(`Load the executable file from '${options.in}'.`);
const inFile = await readFile(options.in);
log.debug(`Parse the executable file '${options.in}' (ignore-signed: ${((_a = options['ignore-signed']) !== null && _a !== void 0 ? _a : false) ? 'true' : 'false'}).`);
executable = PE.NtExecutable.from(inFile, {
ignoreCert: options['ignore-signed'],
});
}
const res = PE.NtExecutableResource.from(executable);
const hasResOnBase = res.entries.length > 0;
log.debug(`The input executable file has ${hasResOnBase ? '' : 'no '}resource(s).`);
const isExe = options.in !== undefined
? /\.(?:exe|com)$/i.test(options.in)
: options.asExeFile !== undefined
? options.asExeFile
: false;
const modified = await emitResources(isExe, res, convertedDefData);
if (modified) {
log.info('Resources has been created. Apply resources to the new executable.');
if (res.entries.length > 0 || (!options.allowShrink && hasResOnBase)) {
res.outputResource(executable, options.noGrow, options.allowShrink);
}
else if (hasResOnBase) {
executable.setSectionByEntry(ResEdit.Format.ImageDirectoryEntry.Resource, null);
}
}
let newBin;
if (convertedDefData.sign) {
const signData = convertedDefData.sign;
log.debug(`Prepare signing information.`);
const certAndKeyData = await prepare(signData);
log.info('Sign the executable.');
newBin = await doSign(executable, certAndKeyData, signData.digestAlgorithm, signData.timestampServer);
}
else {
log.debug(`Generate executable binary.`);
newBin = executable.generate();
}
log.info(`Write executable binary to '${options.out}'.`);
await writeFile(options.out, Buffer.from(newBin));
log.info('Done.');
}