securityos/node_modules/resedit/dist/resource/VersionInfo.js

715 lines
29 KiB
JavaScript
Raw Permalink Normal View History

2024-09-06 15:32:35 +00:00
import { allocatePartialBinary, cloneObject, copyBuffer, readUint32WithLastOffset, roundUp, } from '../util/functions.js';
function readStringToNullChar(view, offset, last) {
var r = '';
while (offset + 2 <= last) {
var c = view.getUint16(offset, true);
if (!c) {
break;
}
r += String.fromCharCode(c);
offset += 2;
}
return r;
}
function writeStringWithNullChar(view, offset, value) {
for (var i = 0; i < value.length; ++i) {
view.setUint16(offset, value.charCodeAt(i), true);
offset += 2;
}
view.setUint16(offset, 0, true);
return offset + 2;
}
function createFixedInfo() {
return {
fileVersionMS: 0,
fileVersionLS: 0,
productVersionMS: 0,
productVersionLS: 0,
fileFlagsMask: 0,
fileFlags: 0,
fileOS: 0,
fileType: 0,
fileSubtype: 0,
fileDateMS: 0,
fileDateLS: 0,
};
}
////////////////////////////////////////////////////////////////////////////////
// parsings
// returns offset and structure
function parseStringTable(view, offset, last) {
var tableLen = view.getUint16(offset, true);
var valueLen = view.getUint16(offset + 2, true);
if (offset + tableLen < last) {
last = offset + tableLen;
}
// value type check is not needed; because no value is needed
var tableName = readStringToNullChar(view, offset + 6, last);
offset += roundUp(6 + 2 * (tableName.length + 1), 4);
var langAndCp = parseInt(tableName, 16);
if (isNaN(langAndCp)) {
throw new Error('Invalid StringTable data format');
}
// this should be zero
offset += roundUp(valueLen, 4);
var r = {
lang: Math.floor(langAndCp / 0x10000),
codepage: langAndCp & 0xffff,
values: {},
};
while (offset < last) {
// String structure
var childDataLen = view.getUint16(offset, true);
var childValueLen = view.getUint16(offset + 2, true);
// value type must be string; if not, skip it
if (view.getUint16(offset + 4, true) !== 1) {
offset += childDataLen;
continue;
}
var childDataLast = offset + childDataLen;
if (childDataLast > last) {
childDataLast = last;
}
var name_1 = readStringToNullChar(view, offset + 6, childDataLast);
offset = roundUp(offset + 6 + 2 * (name_1.length + 1), 4);
var childValueLast = offset + childValueLen * 2;
if (childValueLast > childDataLast) {
childValueLast = childDataLast;
}
var value = readStringToNullChar(view, offset, childValueLast);
offset = roundUp(childValueLast, 4);
r.values[name_1] = value;
}
// return 'last' instead of 'offset'
return [last, r];
}
function parseStringFileInfo(view, offset, last) {
var valueLen = view.getUint16(offset + 2, true);
// value type check is not needed; because no value is needed
offset += 36; // roundUp(6 + ByteLenWithNull(L'StringFileInfo'), 4)
// this should be zero
offset += roundUp(valueLen, 4);
var r = [];
var _loop_1 = function () {
// StringTable structure
var childData = parseStringTable(view, offset, last);
var table = childData[1];
var a = r.filter(function (e) { return e.lang === table.lang && e.codepage === table.codepage; });
if (a.length === 0) {
r.push(table);
}
else {
// merge values
for (var key in table.values) {
a[0].values[key] = table.values[key];
}
}
offset = roundUp(childData[0], 4);
};
while (offset < last) {
_loop_1();
}
return r;
}
function parseVarFileInfo(view, offset, last) {
var valueLen = view.getUint16(offset + 2, true);
// value type check is not needed; because no value is needed
offset += 32; // roundUp(6 + ByteLenWithNull(L'VarFileInfo'), 4)
// this should be zero
offset += roundUp(valueLen, 4);
var r = [];
while (offset < last) {
// Var structure
var childDataLen = view.getUint16(offset, true);
var childValueLen = view.getUint16(offset + 2, true);
// value type must be binary; if not, skip it
if (view.getUint16(offset + 4, true) !== 0) {
offset += roundUp(childDataLen, 4);
continue;
}
var childDataLast = offset + childDataLen;
if (childDataLast > last) {
childDataLast = last;
}
var name_2 = readStringToNullChar(view, offset + 6, childDataLast);
offset = roundUp(offset + 6 + 2 * (name_2.length + 1), 4);
if (name_2 !== 'Translation' || childValueLen % 4 !== 0) {
// unknown entry
offset = roundUp(childDataLast, 4);
continue;
}
var _loop_2 = function (child) {
if (offset + 4 > childDataLast) {
return "break";
}
var lang = view.getUint16(offset, true);
var codepage = view.getUint16(offset + 2, true);
offset += 4;
if (r.filter(function (e) { return e.lang === lang && e.codepage === codepage; })
.length === 0) {
r.push({ lang: lang, codepage: codepage });
}
};
for (var child = 0; child < childValueLen; child += 4) {
var state_1 = _loop_2(child);
if (state_1 === "break")
break;
}
offset = roundUp(childDataLast, 4);
}
return r;
}
function parseVersionEntry(view, entry) {
var totalLen = view.getUint16(0, true);
var dataLen = view.getUint16(2, true);
// value type must be binary
if (view.getUint16(4, true) !== 0) {
throw new Error('Invalid version data format');
}
// 40 === roundUp(6 + ByteLenWithNull(L'VS_VERSION_INFO'), 4)
if (totalLen < dataLen + 40) {
throw new Error('Invalid version data format');
}
if (readStringToNullChar(view, 6, totalLen) !== 'VS_VERSION_INFO') {
throw new Error('Invalid version data format');
}
var d = {
lang: entry.lang,
fixedInfo: createFixedInfo(),
strings: [],
translations: [],
unknowns: [],
};
var offset = 38; // without padding
if (dataLen) {
dataLen += 40; // with padding
var sig = readUint32WithLastOffset(view, 40, dataLen);
var sVer = readUint32WithLastOffset(view, 44, dataLen);
// check signature
if (sig === 0xfeef04bd && sVer <= 0x10000) {
d.fixedInfo = {
fileVersionMS: readUint32WithLastOffset(view, 48, dataLen),
fileVersionLS: readUint32WithLastOffset(view, 52, dataLen),
productVersionMS: readUint32WithLastOffset(view, 56, dataLen),
productVersionLS: readUint32WithLastOffset(view, 60, dataLen),
fileFlagsMask: readUint32WithLastOffset(view, 64, dataLen),
fileFlags: readUint32WithLastOffset(view, 68, dataLen),
fileOS: readUint32WithLastOffset(view, 72, dataLen),
fileType: readUint32WithLastOffset(view, 76, dataLen),
fileSubtype: readUint32WithLastOffset(view, 80, dataLen),
fileDateMS: readUint32WithLastOffset(view, 84, dataLen),
fileDateLS: readUint32WithLastOffset(view, 88, dataLen),
};
}
offset = dataLen;
}
offset = roundUp(offset, 4);
// parse children
while (offset < totalLen) {
var childLen = view.getUint16(offset, true);
var childLast = offset + childLen;
// failsafe
if (childLast > totalLen) {
childLast = totalLen;
}
var name_3 = readStringToNullChar(view, offset + 6, childLast);
switch (name_3) {
case 'StringFileInfo':
d.strings = d.strings.concat(parseStringFileInfo(view, offset, childLast));
break;
case 'VarFileInfo':
d.translations = d.translations.concat(parseVarFileInfo(view, offset, childLast));
break;
default:
// unknown or unsupported type
d.unknowns.push({
name: name_3,
entireBin: allocatePartialBinary(view, offset, childLen),
});
break;
}
offset += roundUp(childLen, 4);
}
return d;
}
////////////////////////////////////////////////////////////////////////////////
// serializings
function generateStringTable(table) {
// estimate size
var size = 24; // roundUp(6 + ByteLenWithNull(L'xxxxxxxx'), 4)
var keys = Object.keys(table.values);
size = keys.reduce(function (prev, key) {
var value = table.values[key];
var childHeaderSize = roundUp(6 + 2 * (key.length + 1), 4);
var newSize = roundUp(prev + childHeaderSize + 2 * (value.length + 1), 4);
// limit to 65532 because the table size is restricted to 16-bit value
return newSize > 65532 ? prev : newSize;
}, size);
// generate binary
var bin = new ArrayBuffer(size);
var view = new DataView(bin);
view.setUint16(0, size, true);
view.setUint16(2, 0, true); // no value length
view.setUint16(4, 1, true);
var langAndCp = ((table.lang & 0xffff) * 0x10000 +
(table.codepage & 0xffff))
.toString(16)
.toLowerCase();
// fixed length
if (langAndCp.length < 8) {
var l = 8 - langAndCp.length;
langAndCp = '00000000'.substr(0, l) + langAndCp;
}
var offset = roundUp(writeStringWithNullChar(view, 6, langAndCp), 4);
keys.forEach(function (key) {
var value = table.values[key];
var childHeaderSize = roundUp(6 + 2 * (key.length + 1), 4);
var newSize = roundUp(childHeaderSize + 2 * (value.length + 1), 4);
if (offset + newSize <= 65532) {
view.setUint16(offset, newSize, true);
view.setUint16(offset + 2, value.length + 1, true); // value length is in character count
view.setUint16(offset + 4, 1, true);
offset = roundUp(writeStringWithNullChar(view, offset + 6, key), 4);
offset = roundUp(writeStringWithNullChar(view, offset, value), 4);
}
});
return bin;
}
function generateStringTableInfo(tables) {
// estimate size
var size = 36; // roundUp(6 + ByteLenWithNull(L'StringFileInfo'), 4)
var tableBins = tables.map(function (table) { return generateStringTable(table); });
// (all table sizes are rounded up)
size += tableBins.reduce(function (p, c) { return p + c.byteLength; }, 0);
var bin = new ArrayBuffer(size);
var view = new DataView(bin);
view.setUint16(0, size, true);
view.setUint16(2, 0, true); // no value length
view.setUint16(4, 1, true);
var offset = roundUp(writeStringWithNullChar(view, 6, 'StringFileInfo'), 4);
tableBins.forEach(function (table) {
var len = table.byteLength;
copyBuffer(bin, offset, table, 0, len);
offset += len;
});
return bin;
}
function generateVarFileInfo(translations) {
// estimate size
var size = 32; // roundUp(6 + ByteLenWithNull(L'VarFileInfo'), 4)
// (translation data is fixed length)
var translationsValueSize = translations.length * 4;
size += 32 + translationsValueSize;
var bin = new ArrayBuffer(size);
var view = new DataView(bin);
view.setUint16(0, size, true);
view.setUint16(2, 0, true); // no value length
view.setUint16(4, 1, true);
var offset = roundUp(writeStringWithNullChar(view, 6, 'VarFileInfo'), 4);
view.setUint16(offset, 32 + translationsValueSize, true);
view.setUint16(offset + 2, translationsValueSize, true);
view.setUint16(offset + 4, 0, true);
offset = roundUp(writeStringWithNullChar(view, offset + 6, 'Translation'), 4);
translations.forEach(function (translation) {
view.setUint16(offset, translation.lang, true);
view.setUint16(offset + 2, translation.codepage, true);
offset += 4;
});
return bin;
}
function generateVersionEntryBinary(entry) {
var size = 92; // roundUp(6 + ByteLenWithNull(L'VS_VERSION_INFO'), 4) + 52 (sizeof VS_FIXEDFILEINFO)
var stringTableInfoBin = generateStringTableInfo(entry.strings);
var stringTableInfoLen = stringTableInfoBin.byteLength;
size += stringTableInfoLen;
var varFileInfoBin = generateVarFileInfo(entry.translations);
var varFileInfoLen = varFileInfoBin.byteLength;
size += varFileInfoLen;
size = entry.unknowns.reduce(function (p, data) { return p + roundUp(data.entireBin.byteLength, 4); }, size);
var bin = new ArrayBuffer(size);
var view = new DataView(bin);
view.setUint16(0, size, true);
view.setUint16(2, 52, true);
view.setUint16(4, 0, true); // value is binary
var offset = roundUp(writeStringWithNullChar(view, 6, 'VS_VERSION_INFO'), 4);
view.setUint32(offset, 0xfeef04bd, true); // signature
view.setUint32(offset + 4, 0x10000, true); // structure version
view.setUint32(offset + 8, entry.fixedInfo.fileVersionMS, true);
view.setUint32(offset + 12, entry.fixedInfo.fileVersionLS, true);
view.setUint32(offset + 16, entry.fixedInfo.productVersionMS, true);
view.setUint32(offset + 20, entry.fixedInfo.productVersionLS, true);
view.setUint32(offset + 24, entry.fixedInfo.fileFlagsMask, true);
view.setUint32(offset + 28, entry.fixedInfo.fileFlags, true);
view.setUint32(offset + 32, entry.fixedInfo.fileOS, true);
view.setUint32(offset + 36, entry.fixedInfo.fileType, true);
view.setUint32(offset + 40, entry.fixedInfo.fileSubtype, true);
view.setUint32(offset + 44, entry.fixedInfo.fileDateMS, true);
view.setUint32(offset + 48, entry.fixedInfo.fileDateLS, true);
offset += 52;
copyBuffer(bin, offset, stringTableInfoBin, 0, stringTableInfoLen);
offset += stringTableInfoLen;
copyBuffer(bin, offset, varFileInfoBin, 0, varFileInfoLen);
offset += varFileInfoLen;
entry.unknowns.forEach(function (e) {
var len = e.entireBin.byteLength;
copyBuffer(bin, offset, e.entireBin, 0, len);
offset += roundUp(len, 4);
});
return bin;
}
////////////////////////////////////////////////////////////////////////////////
function clampInt(val, min, max) {
if (isNaN(val) || val < min) {
return min;
}
else if (val >= max) {
return max;
}
return Math.floor(val);
}
function parseVersionArguments(arg1, arg2, arg3, arg4, arg5) {
var _a;
var major;
var minor;
var micro;
var revision;
var lang;
if (typeof arg1 === 'string' &&
(typeof arg2 === 'undefined' || typeof arg2 === 'number') &&
typeof arg3 === 'undefined') {
_a = arg1
.split('.')
.map(function (token) { return clampInt(Number(token), 0, 65535); })
// add zeros for missing fields
.concat(0, 0, 0), major = _a[0], minor = _a[1], micro = _a[2], revision = _a[3];
lang = arg2;
}
else {
major = clampInt(Number(arg1), 0, 65535);
minor = clampInt(Number(arg2), 0, 65535);
micro = clampInt(typeof arg3 === 'undefined' ? 0 : Number(arg3), 0, 65535);
revision = clampInt(typeof arg4 === 'undefined' ? 0 : Number(arg4), 0, 65535);
lang = arg5;
}
return [major, minor, micro, revision, lang];
}
////////////////////////////////////////////////////////////////////////////////
/**
* Treats 'Version information' (`VS_VERSIONINFO`) resource data.
*/
var VersionInfo = /** @class */ (function () {
function VersionInfo(entry) {
if (!entry) {
this.data = {
lang: 0,
fixedInfo: createFixedInfo(),
strings: [],
translations: [],
unknowns: [],
};
}
else {
var view = new DataView(entry.bin);
this.data = parseVersionEntry(view, entry);
}
}
/** Returns new `VersionInfo` instance with empty data. */
VersionInfo.createEmpty = function () {
return new VersionInfo();
};
VersionInfo.create = function (arg1, fixedInfo, strings) {
var lang;
if (typeof arg1 === 'object') {
lang = arg1.lang;
fixedInfo = arg1.fixedInfo;
strings = arg1.strings;
}
else {
lang = arg1;
}
var vi = new VersionInfo();
vi.data.lang = lang;
// copy all specified values
// (if unspecified, use default value set by `createFixedInfo`)
for (var fixedInfoKey in fixedInfo) {
if (fixedInfoKey in fixedInfo) {
vi.data.fixedInfo[fixedInfoKey] = fixedInfo[fixedInfoKey];
}
}
vi.data.strings = strings.map(function (_a) {
var lang = _a.lang, codepage = _a.codepage, values = _a.values;
return ({
lang: lang,
codepage: codepage,
values: cloneObject(values),
});
});
vi.data.translations = strings.map(function (_a) {
var lang = _a.lang, codepage = _a.codepage;
return ({ lang: lang, codepage: codepage });
});
return vi;
};
/** Pick up all version-info entries */
VersionInfo.fromEntries = function (entries) {
return entries
.filter(function (e) { return e.type === 16; })
.map(function (e) { return new VersionInfo(e); });
};
Object.defineProperty(VersionInfo.prototype, "lang", {
/** A language value for this resource entry. */
get: function () {
return this.data.lang;
},
set: function (value) {
this.data.lang = value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(VersionInfo.prototype, "fixedInfo", {
/**
* The property of fixed version info, containing file version, product version, etc.
* (data: `VS_FIXEDFILEINFO`)
*
* Although this property is read-only, you can rewrite
* each child fields directly to apply data.
*/
get: function () {
return this.data.fixedInfo;
},
enumerable: false,
configurable: true
});
/**
* Returns all languages that the executable supports. (data: `VarFileInfo`)
*
* Usually the returned array is equal to the one returned by `getAllLanguagesForStringValues`,
* but some resource-generating tools doesn't generate same values.
*/
VersionInfo.prototype.getAvailableLanguages = function () {
return this.data.translations.slice(0);
};
/**
* Replaces all languages that the executable supports.
*/
VersionInfo.prototype.replaceAvailableLanguages = function (languages) {
this.data.translations = languages.slice(0);
};
/**
* Returns all string values for the specified language. (data: values in lang-charset block of `StringFileInfo`)
*/
VersionInfo.prototype.getStringValues = function (language) {
var a = this.data.strings
.filter(function (e) {
return e.lang === language.lang && e.codepage === language.codepage;
})
.map(function (e) { return e.values; });
return a.length > 0 ? a[0] : {};
};
/**
* Returns all languages used by string values. (data: lang-charset name of `StringFileInfo`)
*
* Usually the returned array is equal to the one returned by `getAvailableLanguages`,
* but some resource-generating tools doesn't generate same values.
*/
VersionInfo.prototype.getAllLanguagesForStringValues = function () {
return this.data.strings.map(function (_a) {
var codepage = _a.codepage, lang = _a.lang;
return ({ codepage: codepage, lang: lang });
});
};
/**
* Add or replace the string values.
* @param language language info
* @param values string values (key-value pairs)
* @param addToAvailableLanguage set `true` to add `language` into available languages
* if not existing in `getAvailableLanguages()` (default: `true`)
*/
VersionInfo.prototype.setStringValues = function (language, values, addToAvailableLanguage) {
if (addToAvailableLanguage === void 0) { addToAvailableLanguage = true; }
var a = this.data.strings.filter(function (e) { return e.lang === language.lang && e.codepage === language.codepage; });
var table;
if (a.length === 0) {
table = {
lang: language.lang,
codepage: language.codepage,
values: {},
};
this.data.strings.push(table);
}
else {
table = a[0];
}
for (var key in values) {
table.values[key] = values[key];
}
if (addToAvailableLanguage) {
// if no translation is available, then add it
var t = this.data.translations.filter(function (e) {
return e.lang === language.lang && e.codepage === language.codepage;
});
if (t.length === 0) {
this.data.translations.push({
lang: language.lang,
codepage: language.codepage,
});
}
}
};
/**
* Add or replace the string value.
* @param language language info
* @param key the key name of string value
* @param value the string value
* @param addToAvailableLanguage set `true` to add `language` into available languages
* if not existing in `getAvailableLanguages()` (default: `true`)
*/
VersionInfo.prototype.setStringValue = function (language, key, value, addToAvailableLanguage) {
var _a;
if (addToAvailableLanguage === void 0) { addToAvailableLanguage = true; }
this.setStringValues(language, (_a = {}, _a[key] = value, _a), addToAvailableLanguage);
};
/**
* Remove all string values for specified language.
* @param language language info
* @param removeFromAvailableLanguage set `true` to remove `language` from available languages
* if existing in `getAvailableLanguages()` (default: `true`)
*/
VersionInfo.prototype.removeAllStringValues = function (language, removeFromAvailableLanguage) {
if (removeFromAvailableLanguage === void 0) { removeFromAvailableLanguage = true; }
var strings = this.data.strings;
var len = strings.length;
for (var i = 0; i < len; ++i) {
var e = strings[i];
if (e.lang === language.lang && e.codepage === language.codepage) {
strings.splice(i, 1);
if (removeFromAvailableLanguage) {
var translations = this.data.translations;
for (var j = 0; j < translations.length; j++) {
var t = translations[j];
if (t.lang === language.lang &&
t.codepage === language.codepage) {
translations.splice(j, 1);
break;
}
}
}
break;
}
}
};
/**
* Remove specified string value for specified language.
* @param language language info
* @param key the key name of string value to be removed
* @param removeFromAvailableLanguage set `true` to remove `language` from available languages
* if no more string values exist for `language` (default: `true`)
*/
VersionInfo.prototype.removeStringValue = function (language, key, removeFromAvailableLanguage) {
if (removeFromAvailableLanguage === void 0) { removeFromAvailableLanguage = true; }
var strings = this.data.strings;
var len = strings.length;
for (var i = 0; i < len; ++i) {
var e = strings[i];
if (e.lang === language.lang && e.codepage === language.codepage) {
try {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete e.values[key];
}
catch (_ex) { }
if (removeFromAvailableLanguage &&
Object.keys(e.values).length === 0) {
// if no entries are left, remove table and translations
strings.splice(i, 1);
var translations = this.data.translations;
for (var j = 0; j < translations.length; j++) {
var t = translations[j];
if (t.lang === language.lang &&
t.codepage === language.codepage) {
translations.splice(j, 1);
break;
}
}
}
break;
}
}
};
/**
* Creates `Type.ResourceEntry` object for this instance.
* Usually `outputToResourceEntries` is suitable for generating resource data
* into executables, but you can use this method if necessary.
*/
VersionInfo.prototype.generateResource = function () {
var bin = generateVersionEntryBinary(this.data);
return {
type: 16,
id: 1,
lang: this.lang,
codepage: 1200,
bin: bin,
};
};
/**
* Generates version info resource data (using `generateResource()`) and emits into `entries` array.
* If version info resource already exists in `entries`, this method replaces it with the new one.
* @param entries resource entry array for output
*/
VersionInfo.prototype.outputToResourceEntries = function (entries) {
var res = this.generateResource();
var len = entries.length;
for (var i = 0; i < len; ++i) {
var e = entries[i];
if (e.type === 16 && e.id === res.id && e.lang === res.lang) {
entries[i] = res;
return;
}
}
entries.push(res);
};
// utility methods
VersionInfo.prototype.getDefaultVersionLang = function (propName) {
// first, use `this.lang` if it is a numeric value
var num = Number(this.lang);
if (this.lang !== '' && !isNaN(num)) {
return num;
}
// second, use lang value for propName if there is only one language
var a = this.data.strings
.filter(function (e) { return propName in e.values; })
.map(function (e) { return e.lang; });
if (a.length === 1) {
return a[0];
}
// use English language
return 1033;
};
VersionInfo.prototype.setFileVersion = function (arg1, arg2, arg3, arg4, arg5) {
this.setFileVersionImpl.apply(this, parseVersionArguments(arg1, arg2, arg3, arg4, arg5));
};
VersionInfo.prototype.setFileVersionImpl = function (major, minor, micro, revision, lang) {
lang =
typeof lang !== 'undefined'
? lang
: this.getDefaultVersionLang('FileVersion');
this.fixedInfo.fileVersionMS = (major << 16) | minor;
this.fixedInfo.fileVersionLS = (micro << 16) | revision;
this.setStringValue({ lang: lang, codepage: 1200 }, 'FileVersion', "".concat(major, ".").concat(minor, ".").concat(micro, ".").concat(revision), true);
};
VersionInfo.prototype.setProductVersion = function (arg1, arg2, arg3, arg4, arg5) {
this.setProductVersionImpl.apply(this, parseVersionArguments(arg1, arg2, arg3, arg4, arg5));
};
VersionInfo.prototype.setProductVersionImpl = function (major, minor, micro, revision, lang) {
lang =
typeof lang !== 'undefined'
? lang
: this.getDefaultVersionLang('ProductVersion');
this.fixedInfo.productVersionMS = (major << 16) | minor;
this.fixedInfo.productVersionLS = (micro << 16) | revision;
this.setStringValue({ lang: lang, codepage: 1200 }, 'ProductVersion', "".concat(major, ".").concat(minor, ".").concat(micro, ".").concat(revision), true);
};
return VersionInfo;
}());
export default VersionInfo;