resedit-cli
Version:
Command-line tool for editing Windows Resource data in executable binaries
201 lines (200 loc) • 7.67 kB
JavaScript
import { validateIntegerValue, validateStringValue } from './utils.js';
const standardVersionStringKeys = {
comments: 'Comments',
companyname: 'CompanyName',
filedescription: 'FileDescription',
fileversion: 'FileVersion',
internalname: 'InternalName',
legalcopyright: 'LegalCopyright',
legaltrademarks: 'LegalTrademarks',
originalfilename: 'OriginalFilename',
privatebuild: 'PrivateBuild',
productname: 'ProductName',
productversion: 'ProductVersion',
specialbuild: 'SpecialBuild',
};
function getPreferredPropNamesForVersion(data, pickExtraValuesFirst) {
const isEqualNameWithFirstCharCaseIgnored = (a, b) => {
return (a.replace(/^([A-Z])/, (_, c) => c.toLowerCase()) ===
b.replace(/^([A-Z])/, (_, c) => c.toLowerCase()));
};
return Object.keys(data)
.sort((a, b) => {
if (a === b) {
return 0;
}
if (pickExtraValuesFirst) {
if (a === 'extraValues') {
return -1;
}
else if (b === 'extraValues') {
return 1;
}
}
if (isEqualNameWithFirstCharCaseIgnored(a, b)) {
return /^a-z/.test(a) ? -1 : 1;
}
return a.localeCompare(b);
})
.reduce((prev, cur) => {
if (prev.length === 0 ||
!isEqualNameWithFirstCharCaseIgnored(prev[prev.length - 1], cur)) {
prev.push(cur);
}
return prev;
}, []);
}
function parseVersionBase(data, propName, outUnknownPropNames) {
if (typeof data !== 'object' || !data) {
throw new Error(`Invalid data: '${propName}' is not an object`);
}
const ret = {};
getPreferredPropNamesForVersion(data, true).forEach((key) => {
const lowerCaseName = key.toLowerCase();
if (Object.prototype.hasOwnProperty.call(standardVersionStringKeys, lowerCaseName)) {
const actualName = standardVersionStringKeys[lowerCaseName];
const value = data[key];
validateStringValue(value, `${propName}.${key}`);
ret[actualName] = value;
}
else if (key === 'extraValues') {
const value = data[key];
if (typeof value !== 'object' || !value) {
throw new Error(`Invalid data: '${propName}.extraValues' is not an object`);
}
getPreferredPropNamesForVersion(value, false).forEach((k) => {
const v = value[k];
validateStringValue(v, `${propName}.extraValues,${k}`);
ret[k] = v;
});
}
else {
outUnknownPropNames.push(key);
}
});
return ret;
}
export function parseVersionTranslation(data, propName) {
const props = [];
const versionStringData = parseVersionBase(data, propName, props);
let lang = 0;
if (!props.includes('lang')) {
throw new Error(`Invalid data: '${propName}.lang' is missing`);
}
props.forEach((prop) => {
if (prop === 'lang') {
const v = data[prop];
validateIntegerValue(v, `${propName}.lang`);
lang = v;
}
else {
throw new Error(`Invalid data: unknown property '${prop}' on '${propName}`);
}
});
return {
lang,
values: versionStringData,
};
}
export default function parseVersion(data) {
if (typeof data !== 'object' || !data) {
throw new Error("Invalid data: 'version' is not an object");
}
const ret = {
fixedInfo: {},
strings: [],
};
const props = [];
const thisVersionStrings = {
values: parseVersionBase(data, 'version', props),
};
const translations = [];
ret.strings.push(thisVersionStrings);
props.forEach((key) => {
const adjustedKey = key.replace(/^([A-Z])/, (_, c) => c.toLowerCase());
const value = data[key];
switch (adjustedKey) {
case 'fileVersionMS':
case 'fileVersionLS':
case 'productVersionMS':
case 'productVersionLS':
case 'fileFlagsMask':
case 'fileFlags':
case 'fileOS':
case 'fileType':
case 'fileSubtype':
case 'fileDateMS':
case 'fileDateLS':
validateIntegerValue(value, `version.${key}`);
ret.fixedInfo[adjustedKey] = value;
break;
default:
switch (key) {
case 'lang':
validateIntegerValue(value, `version.lang`);
thisVersionStrings.lang = value;
break;
case 'translations':
if (!Array.isArray(value)) {
throw new Error(`Invalid data: 'version.translations' is not an array`);
}
if (value.length === 0) {
throw new Error(`Invalid data: 'version.translations' is empty`);
}
value.forEach((item, i) => {
const t = parseVersionTranslation(item, `version.translations[${i}]`);
let found = false;
translations.forEach((item, i) => {
if (found) {
return;
}
if (item.lang === t.lang) {
translations[i] = t;
found = true;
}
});
if (!found) {
translations.push(t);
}
});
break;
default:
throw new Error(`Invalid data: unknown property '${key}' on 'version`);
}
break;
}
});
translations.forEach((t) => {
if (t.lang === thisVersionStrings.lang) {
thisVersionStrings.values = Object.assign({}, thisVersionStrings.values, t.values);
}
else {
ret.strings.push(t);
}
});
if (!('fileVersionLS' in ret.fixedInfo) &&
!('fileVersionMS' in ret.fixedInfo) &&
'FileVersion' in thisVersionStrings.values) {
const val = thisVersionStrings.values.FileVersion;
const ra = /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(val);
if (ra) {
ret.fixedInfo.fileVersionMS =
((Number(ra[1]) & 0xffff) << 16) | (Number(ra[2]) & 0xffff);
ret.fixedInfo.fileVersionLS =
((Number(ra[3]) & 0xffff) << 16) | (Number(ra[4]) & 0xffff);
}
}
if (!('productVersionLS' in ret.fixedInfo) &&
!('productVersionMS' in ret.fixedInfo) &&
'ProductVersion' in thisVersionStrings.values) {
const val = thisVersionStrings.values.ProductVersion;
const ra = /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(val);
if (ra) {
ret.fixedInfo.productVersionMS =
((Number(ra[1]) & 0xffff) << 16) | (Number(ra[2]) & 0xffff);
ret.fixedInfo.productVersionLS =
((Number(ra[3]) & 0xffff) << 16) | (Number(ra[4]) & 0xffff);
}
}
return ret;
}