UNPKG

resedit-cli

Version:

Command-line tool for editing Windows Resource data in executable binaries

201 lines (200 loc) 7.67 kB
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; }