211 lines
7.5 KiB
JavaScript
211 lines
7.5 KiB
JavaScript
'use strict'
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
function rvaToOffset(sections, rva) {
|
|
for (const s of sections) {
|
|
if (rva >= s.va && rva < s.va + (s.vs || s.rawSize)) return rva - s.va + s.raw
|
|
}
|
|
return null
|
|
}
|
|
|
|
function dirEntries(buf, dirOff) {
|
|
const named = buf.readUInt16LE(dirOff + 12)
|
|
const ids = buf.readUInt16LE(dirOff + 14)
|
|
const out = []
|
|
for (let i = 0; i < named + ids; i++) {
|
|
const e = dirOff + 16 + i * 8
|
|
const nameOrId = buf.readUInt32LE(e)
|
|
const value = buf.readUInt32LE(e + 4)
|
|
out.push({ id: nameOrId & 0x7fffffff, isName: (nameOrId & 0x80000000) !== 0, value, isDir: (value & 0x80000000) !== 0 })
|
|
}
|
|
return out
|
|
}
|
|
|
|
function parseIcons(buf) {
|
|
try {
|
|
if (buf.length < 64 || buf.readUInt16LE(0) !== 0x5a4d) return null
|
|
const peOff = buf.readUInt32LE(0x3c)
|
|
if (peOff + 24 > buf.length || buf.readUInt32LE(peOff) !== 0x00004550) return null
|
|
const numSections = buf.readUInt16LE(peOff + 6)
|
|
const optSize = buf.readUInt16LE(peOff + 20)
|
|
const optOff = peOff + 24
|
|
const magic = buf.readUInt16LE(optOff)
|
|
const resRva = magic === 0x20b ? buf.readUInt32LE(optOff + 128) : buf.readUInt32LE(optOff + 112)
|
|
if (!resRva) return []
|
|
|
|
const secOff = optOff + optSize
|
|
const sections = []
|
|
for (let i = 0; i < numSections; i++) {
|
|
const s = secOff + i * 40
|
|
sections.push({
|
|
va: buf.readUInt32LE(s + 12),
|
|
vs: buf.readUInt32LE(s + 8),
|
|
rawSize: buf.readUInt32LE(s + 16),
|
|
raw: buf.readUInt32LE(s + 20)
|
|
})
|
|
}
|
|
|
|
const rootOff = rvaToOffset(sections, resRva)
|
|
if (rootOff == null) return []
|
|
|
|
const typeEntry = dirEntries(buf, rootOff).find(e => e.id === 3 && e.isDir)
|
|
if (!typeEntry) return []
|
|
const iconDirOff = rvaToOffset(sections, resRva + (typeEntry.value & 0x7fffffff))
|
|
if (iconDirOff == null) return []
|
|
|
|
const iconDatas = []
|
|
for (const nameEntry of dirEntries(buf, iconDirOff)) {
|
|
if (!nameEntry.isDir) continue
|
|
const langDirOff = rvaToOffset(sections, resRva + (nameEntry.value & 0x7fffffff))
|
|
if (langDirOff == null) continue
|
|
const langEntry = dirEntries(buf, langDirOff)[0]
|
|
if (!langEntry || langEntry.isDir) continue
|
|
const dataEntryOff = rvaToOffset(sections, resRva + langEntry.value)
|
|
if (dataEntryOff == null) continue
|
|
const dataRva = buf.readUInt32LE(dataEntryOff)
|
|
const dataSize = buf.readUInt32LE(dataEntryOff + 4)
|
|
const dataOff = rvaToOffset(sections, dataRva)
|
|
if (dataOff == null || !dataSize) continue
|
|
iconDatas.push({ id: nameEntry.id, buf: Buffer.from(buf.subarray(dataOff, dataOff + dataSize)) })
|
|
}
|
|
if (!iconDatas.length) return []
|
|
|
|
const groupEntry = dirEntries(buf, rootOff).find(e => e.id === 14 && e.isDir)
|
|
if (groupEntry) {
|
|
const grpDirOff = rvaToOffset(sections, resRva + (groupEntry.value & 0x7fffffff))
|
|
if (grpDirOff != null) {
|
|
for (const nameEntry of dirEntries(buf, grpDirOff)) {
|
|
if (!nameEntry.isDir) continue
|
|
const langDirOff = rvaToOffset(sections, resRva + (nameEntry.value & 0x7fffffff))
|
|
if (langDirOff == null) continue
|
|
const langEntry = dirEntries(buf, langDirOff)[0]
|
|
if (!langEntry || langEntry.isDir) continue
|
|
const dataEntryOff = rvaToOffset(sections, resRva + langEntry.value)
|
|
if (dataEntryOff == null) continue
|
|
const dataRva = buf.readUInt32LE(dataEntryOff)
|
|
const dataSize = buf.readUInt32LE(dataEntryOff + 4)
|
|
const dataOff = rvaToOffset(sections, dataRva)
|
|
if (dataOff == null || !dataSize) continue
|
|
const grp = buf.subarray(dataOff, dataOff + dataSize)
|
|
if (grp.length < 6) continue
|
|
const count = grp.readUInt16LE(4)
|
|
const best = { area: -1, buf: null }
|
|
for (let i = 0; i < count; i++) {
|
|
const e = 6 + i * 14
|
|
const w = grp.readUInt8(e) || 256
|
|
const h = grp.readUInt8(e + 1) || 256
|
|
const id = grp.readUInt16LE(e + 12)
|
|
const icon = iconDatas.find(ic => ic.id === id)
|
|
if (icon && w * h > best.area) {
|
|
best.area = w * h
|
|
best.buf = icon.buf
|
|
}
|
|
}
|
|
if (best.buf) return [best.buf]
|
|
}
|
|
}
|
|
}
|
|
return [iconDatas.reduce((a, b) => (b.buf.length > a.buf.length ? b : a)).buf]
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function pngSize(img) {
|
|
if (img.length > 24 && img.readUInt32BE(0) === 0x89504e47) {
|
|
return { w: img.readUInt32BE(16), h: img.readUInt32BE(20) }
|
|
}
|
|
return null
|
|
}
|
|
|
|
// RT_ICON payloads are BITMAPINFOHEADER bitmaps; read real dimensions from
|
|
// there (ICONDIRENTRY-style first bytes yield garbage).
|
|
function bmpDims(img) {
|
|
if (img.length < 12) return null
|
|
const biSize = img.readUInt32LE(0)
|
|
if (biSize === 12) {
|
|
// BITMAPCOREHEADER (old icons): WORD width/height/planes/bitcount
|
|
const w = img.readUInt16LE(4)
|
|
const h = Math.floor(img.readUInt16LE(6) / 2)
|
|
const planes = img.readUInt16LE(8) || 1
|
|
const bpp = img.readUInt16LE(10) || 32
|
|
const colors = bpp <= 8 ? 1 << bpp : 0
|
|
if (w > 0 && h > 0 && w <= 1024 && h <= 1024) return { w, h, planes, bpp, colors }
|
|
return null
|
|
}
|
|
if (biSize >= 40 && biSize <= 124 && img.length > 40) {
|
|
const w = img.readUInt32LE(4)
|
|
const h = Math.floor(img.readUInt32LE(8) / 2)
|
|
const planes = img.readUInt16LE(12) || 1
|
|
const bpp = img.readUInt16LE(14) || 32
|
|
const colors = img.readUInt32LE(32) || (bpp <= 8 ? 1 << bpp : 0)
|
|
if (w > 0 && h > 0 && w <= 1024 && h <= 1024) return { w, h, planes, bpp, colors }
|
|
}
|
|
return null
|
|
}
|
|
|
|
function buildIco(images) {
|
|
const header = Buffer.alloc(6)
|
|
header.writeUInt16LE(0, 0)
|
|
header.writeUInt16LE(1, 2)
|
|
header.writeUInt16LE(images.length, 4)
|
|
const metas = []
|
|
const datas = []
|
|
let offset = 6 + images.length * 16
|
|
for (const img of images) {
|
|
let w, h, colors = 0, planes = 1, bpp = 32
|
|
if (img.readUInt32BE(0) === 0x89504e47) {
|
|
const dim = pngSize(img) || { w: 256, h: 256 }
|
|
w = dim.w
|
|
h = dim.h
|
|
} else {
|
|
const dims = bmpDims(img)
|
|
if (dims) {
|
|
w = dims.w
|
|
h = dims.h
|
|
colors = dims.colors
|
|
planes = dims.planes
|
|
bpp = dims.bpp
|
|
} else {
|
|
// legacy fallback: ICONDIRENTRY-style first bytes (PNG-era heuristic)
|
|
w = img.readUInt8(0) || 256
|
|
h = img.readUInt8(1) || 256
|
|
colors = img.readUInt8(2)
|
|
planes = img.length > 4 ? img.readUInt16LE(4) || 1 : 1
|
|
bpp = img.length > 6 ? img.readUInt16LE(6) || 32 : 32
|
|
}
|
|
}
|
|
const meta = Buffer.alloc(16)
|
|
meta.writeUInt8(w >= 256 ? 0 : w, 0)
|
|
meta.writeUInt8(h >= 256 ? 0 : h, 1)
|
|
// 0 in the ICO dir entry means "256+ colors" (palette implied by bpp)
|
|
meta.writeUInt8(colors >= 256 ? 0 : colors, 2)
|
|
meta.writeUInt8(0, 3)
|
|
meta.writeUInt16LE(planes, 4)
|
|
meta.writeUInt16LE(bpp, 6)
|
|
meta.writeUInt32LE(img.length, 8)
|
|
meta.writeUInt32LE(offset, 12)
|
|
metas.push(meta)
|
|
datas.push(img)
|
|
offset += img.length
|
|
}
|
|
return Buffer.concat([header, ...metas, ...datas])
|
|
}
|
|
|
|
function extractIconFromExe(exePath, outPath) {
|
|
try {
|
|
const buf = fs.readFileSync(exePath)
|
|
const icon = parseIcons(buf)
|
|
if (!icon || !icon.length) return { ok: false, error: 'no-icon' }
|
|
const ico = buildIco(icon)
|
|
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
|
fs.writeFileSync(outPath, ico)
|
|
return { ok: true, path: outPath }
|
|
} catch (err) {
|
|
return { ok: false, error: String(err && err.message || err) }
|
|
}
|
|
}
|
|
|
|
module.exports = { extractIconFromExe, parseIcons, buildIco }
|