import app for details and icon

This commit is contained in:
KingKDot
2026-04-04 21:09:51 -05:00
parent 3e198f76a2
commit 2070e640bb
2 changed files with 305 additions and 0 deletions
+290
View File
@@ -363,6 +363,296 @@ if (iconClear) {
});
}
function extractPEMetadata(buffer) {
const dv = new DataView(buffer);
const bytes = new Uint8Array(buffer);
const u8 = (o) => dv.getUint8(o);
const u16 = (o) => dv.getUint16(o, true);
const u32 = (o) => dv.getUint32(o, true);
const alignUp = (n) => (n + 3) & ~3;
if (buffer.byteLength < 0x40) throw new Error("File too small");
if (u16(0) !== 0x5A4D) throw new Error("Not a PE file (missing MZ header)");
const peOff = u32(0x3C);
if (peOff + 24 > buffer.byteLength) throw new Error("Invalid PE offset");
if (u32(peOff) !== 0x4550) throw new Error("Not a PE file (missing PE signature)");
const numSections = u16(peOff + 6);
const optHdrSize = u16(peOff + 20);
const optMagic = u16(peOff + 24);
const is64 = optMagic === 0x20B; // PE32+ vs PE32
const dataDirsOff = peOff + 24 + (is64 ? 112 : 96);
const rsrcRVA = u32(dataDirsOff + 2 * 8); // index 2 = IMAGE_DIRECTORY_ENTRY_RESOURCE
if (rsrcRVA === 0) throw new Error("No resource section RVA");
const secTableOff = peOff + 24 + optHdrSize;
let rsrcRaw = 0;
for (let i = 0; i < numSections; i++) {
const s = secTableOff + i * 40;
const va = u32(s + 12);
const vsz = Math.max(u32(s + 8), u32(s + 16));
const raw = u32(s + 20);
if (rsrcRVA >= va && rsrcRVA < va + vsz) {
rsrcRaw = raw + (rsrcRVA - va);
break;
}
}
if (!rsrcRaw) throw new Error("Could not locate resource section raw data");
const rvaToOff = (rva) => rsrcRaw + (rva - rsrcRVA);
function rsrcDir(dirOff) {
if (dirOff + 16 > buffer.byteLength) return [];
const numNamed = u16(dirOff + 12);
const numId = u16(dirOff + 14);
const total = numNamed + numId;
const entries = [];
for (let i = 0; i < total; i++) {
const e = dirOff + 16 + i * 8;
if (e + 8 > buffer.byteLength) break;
const nameOrId = u32(e);
const dataOrDir = u32(e + 4);
entries.push({
id: (nameOrId & 0x80000000) ? null : nameOrId,
isSubDir: (dataOrDir & 0x80000000) !== 0,
off: dataOrDir & 0x7FFFFFFF,
});
}
return entries;
}
const WANT = new Set([3, 14, 16]);
const resources = {};
for (const te of rsrcDir(rsrcRaw)) {
if (te.id === null || !WANT.has(te.id) || !te.isSubDir) continue;
resources[te.id] = {};
for (const ne of rsrcDir(rsrcRaw + te.off)) {
if (!ne.isSubDir) continue;
const nameId = ne.id ?? 1;
resources[te.id][nameId] = [];
for (const le of rsrcDir(rsrcRaw + ne.off)) {
if (le.isSubDir) continue;
const deOff = rsrcRaw + le.off;
if (deOff + 8 > buffer.byteLength) continue;
const dataRVA = u32(deOff);
const dataSize = u32(deOff + 4);
const dataOff = rvaToOff(dataRVA);
if (dataOff + dataSize <= buffer.byteLength)
resources[te.id][nameId].push({ dataOff, dataSize });
}
}
}
function parseVersionStrings(absOff, size) {
const strings = {};
const end = absOff + size;
let pos = absOff;
if (pos + 6 > end) return strings;
const viLen = u16(pos);
const viValLen = u16(pos + 2);
pos += 6;
while (pos + 1 < end && (u8(pos) | u8(pos + 1))) pos += 2;
pos = alignUp(pos + 2);
pos += viValLen;
pos = alignUp(pos);
const viEnd = Math.min(absOff + viLen, end);
while (pos + 6 < viEnd) {
const childLen = u16(pos);
if (childLen < 6) break;
const childEnd = Math.min(pos + childLen, viEnd);
let kp = pos + 6;
let key = "";
while (kp + 1 < childEnd && (u8(kp) | u8(kp + 1))) {
key += String.fromCharCode(u8(kp) | (u8(kp + 1) << 8));
kp += 2;
}
kp = alignUp(kp + 2);
if (key === "StringFileInfo") {
let sp = kp;
while (sp + 6 < childEnd) {
const stLen = u16(sp);
if (stLen < 6) break;
const stEnd = Math.min(sp + stLen, childEnd);
let tp = sp + 6;
while (tp + 1 < stEnd && (u8(tp) | u8(tp + 1))) tp += 2;
tp = alignUp(tp + 2);
while (tp + 6 < stEnd) {
const sLen = u16(tp);
if (sLen < 6) break;
const sEnd = Math.min(tp + sLen, stEnd);
const sValLen = u16(tp + 2);
let np = tp + 6;
let name = "";
while (np + 1 < sEnd && (u8(np) | u8(np + 1))) {
name += String.fromCharCode(u8(np) | (u8(np + 1) << 8));
np += 2;
}
np = alignUp(np + 2);
let val = "";
const valEnd = Math.min(np + sValLen * 2, sEnd);
while (np + 1 < valEnd && (u8(np) | u8(np + 1))) {
val += String.fromCharCode(u8(np) | (u8(np + 1) << 8));
np += 2;
}
if (name) strings[name] = val;
tp = alignUp(sEnd);
}
sp = alignUp(stEnd);
}
}
pos = alignUp(childEnd);
}
return strings;
}
function buildIco(groupOff, iconRes) {
if (groupOff + 6 > buffer.byteLength) return null;
const count = u16(groupOff + 4);
if (count === 0 || groupOff + 6 + count * 14 > buffer.byteLength) return null;
const grpEntries = [];
for (let i = 0; i < count; i++) {
const e = groupOff + 6 + i * 14;
grpEntries.push({
w: u8(e), h: u8(e + 1), cc: u8(e + 2),
planes: u16(e + 4), bits: u16(e + 6),
size: u32(e + 8), id: u16(e + 12),
});
}
const iconData = [];
for (const en of grpEntries) {
const rd = iconRes[en.id];
if (!rd || rd.length === 0) continue;
iconData.push({ en, dataOff: rd[0].dataOff, dataSize: rd[0].dataSize });
}
if (iconData.length === 0) return null;
let totalSize = 6 + iconData.length * 16;
for (const id of iconData) totalSize += id.dataSize;
const ico = new Uint8Array(totalSize);
const idv = new DataView(ico.buffer);
let p = 0;
idv.setUint16(p, 0, true); p += 2; // reserved
idv.setUint16(p, 1, true); p += 2; // type = ICO
idv.setUint16(p, iconData.length, true); p += 2;
let dataOffset = 6 + iconData.length * 16;
const entryStart = p;
let ep = entryStart;
p += iconData.length * 16;
for (const { en, dataOff, dataSize } of iconData) {
ico[ep] = en.w;
ico[ep+1] = en.h;
ico[ep+2] = en.cc;
ico[ep+3] = 0;
idv.setUint16(ep + 4, en.planes, true);
idv.setUint16(ep + 6, en.bits, true);
idv.setUint32(ep + 8, dataSize, true);
idv.setUint32(ep + 12, dataOffset, true);
ep += 16;
ico.set(bytes.subarray(dataOff, dataOff + dataSize), dataOffset);
dataOffset += dataSize;
}
let bin = "";
for (let i = 0; i < ico.length; i++) bin += String.fromCharCode(ico[i]);
return btoa(bin);
}
const result = {};
if (resources[16]) {
const vd = Object.values(resources[16]).flat()[0];
if (vd) {
try { result.strings = parseVersionStrings(vd.dataOff, vd.dataSize); } catch (_) {}
}
}
if (resources[14] && resources[3]) {
const gd = Object.values(resources[14]).flat()[0];
if (gd) {
try { result.iconBase64 = buildIco(gd.dataOff, resources[3]); } catch (_) {}
}
}
return result;
}
const cloneExeUpload = document.getElementById("clone-exe-upload");
const cloneExeLabel = document.getElementById("clone-exe-label");
const cloneExeStatus = document.getElementById("clone-exe-status");
function setCloneStatus(msg, isError) {
if (!cloneExeStatus) return;
cloneExeStatus.textContent = msg;
cloneExeStatus.className = "text-xs " + (isError ? "text-red-400" : "text-emerald-400");
cloneExeStatus.classList.remove("hidden");
}
if (cloneExeUpload) {
cloneExeUpload.addEventListener("change", () => {
const file = cloneExeUpload.files[0];
if (!file) return;
if (cloneExeLabel) cloneExeLabel.textContent = file.name;
setCloneStatus("Parsing...", false);
const reader = new FileReader();
reader.onload = () => {
try {
const meta = extractPEMetadata(reader.result);
const s = meta.strings || {};
const fields = [
["assembly-title", s["FileDescription"] ?? s["InternalName"] ?? ""],
["assembly-product", s["ProductName"] ?? ""],
["assembly-company", s["CompanyName"] ?? ""],
["assembly-version", s["FileVersion"]?.replace(/,\s*/g, ".") ?? s["ProductVersion"]?.replace(/,\s*/g, ".") ?? ""],
["assembly-copyright", s["LegalCopyright"] ?? s["LegalTrademarks"] ?? ""],
];
let filled = 0;
for (const [id, val] of fields) {
const el = document.getElementById(id);
if (el && val) { el.value = val; filled++; }
}
if (meta.iconBase64) {
const decodedBytes = Math.floor(meta.iconBase64.length * 3 / 4);
if (decodedBytes <= 1024 * 1024) {
pendingIconBase64 = meta.iconBase64;
if (iconLabel) iconLabel.textContent = file.name + " (cloned icon)";
if (iconClear) iconClear.classList.remove("hidden");
setCloneStatus(`Cloned ${filled} metadata field(s) + icon`, false);
} else {
setCloneStatus(`Cloned ${filled} metadata field(s) (icon too large, skipped)`, false);
}
} else {
setCloneStatus(filled > 0 ? `Cloned ${filled} metadata field(s), no icon found` : "No metadata found in file", !filled);
}
} catch (err) {
setCloneStatus("Error: " + err.message, true);
}
cloneExeUpload.value = "";
};
reader.readAsArrayBuffer(file);
});
}
const MAX_BIND_FILES = 5;
const MAX_BIND_FILE_BYTES = 10 * 1024 * 1024; // 10 MB