diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index a2d17b2..64fa674 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -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 diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index fb47dd0..4ab7115 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -430,6 +430,21 @@ +
+ +
+ + +
+
+