import { readFile } from 'node:fs/promises'
const MACH_CPU_ARCHES = new Map([
[0x01000007, 'x64'],
[0x0100000c, 'arm64'],
])
const ELF_MACHINE_ARCHES = new Map([
[62, 'x64'],
[183, 'arm64'],
])
const PE_MACHINE_ARCHES = new Map([
[0x8664, 'x64'],
[0xaa64, 'arm64'],
])
const PLATFORM_FORMATS = {
darwin: 'mach-o',
linux: 'elf',
win32: 'pe',
}
function readUInt32(bytes, offset, littleEndian) {
return littleEndian ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset)
}
function uniqueSorted(values) {
return [...new Set(values)].sort()
}
function detectMach(bytes) {
if (bytes.length < 8) return undefined
const magic = bytes.subarray(0, 4).toString('hex')
const thin =
magic === 'cffaedfe'
? { littleEndian: true }
: magic === 'feedfacf'
? { littleEndian: false }
: undefined
if (thin) {
const arch = MACH_CPU_ARCHES.get(readUInt32(bytes, 4, thin.littleEndian))
return arch ? { format: 'mach-o', arches: [arch] } : undefined
}
const fat =
magic === 'cafebabe'
? { littleEndian: false, entrySize: 20 }
: magic === 'bebafeca'
? { littleEndian: true, entrySize: 20 }
: magic === 'cafebabf'
? { littleEndian: false, entrySize: 32 }
: magic === 'bfbafeca'
? { littleEndian: true, entrySize: 32 }
: undefined
if (!fat) return undefined
const count = readUInt32(bytes, 4, fat.littleEndian)
if (count === 0 || count > 64 || bytes.length < 8 + count * fat.entrySize) {
return undefined
}
const arches = []
for (let index = 0; index < count; index += 1) {
const arch = MACH_CPU_ARCHES.get(
readUInt32(bytes, 8 + index * fat.entrySize, fat.littleEndian)
)
if (arch) arches.push(arch)
}
return arches.length > 0
? { format: 'mach-o', arches: uniqueSorted(arches) }
: undefined
}
function detectElf(bytes) {
if (
bytes.length < 20 ||
!bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) ||
bytes[4] !== 2
) {
return undefined
}
const machine =
bytes[5] === 1
? bytes.readUInt16LE(18)
: bytes[5] === 2
? bytes.readUInt16BE(18)
: undefined
const arch = ELF_MACHINE_ARCHES.get(machine)
return arch ? { format: 'elf', arches: [arch] } : undefined
}
function detectPe(bytes) {
if (bytes.length < 64 || !bytes.subarray(0, 2).equals(Buffer.from('MZ'))) {
return undefined
}
const offset = bytes.readUInt32LE(0x3c)
if (
offset > bytes.length - 6 ||
!bytes.subarray(offset, offset + 4).equals(Buffer.from([0x50, 0x45, 0, 0]))
) {
return undefined
}
const arch = PE_MACHINE_ARCHES.get(bytes.readUInt16LE(offset + 4))
return arch ? { format: 'pe', arches: [arch] } : undefined
}
export function detectNativeBinaryTarget(bytes) {
if (!Buffer.isBuffer(bytes)) {
throw new Error('native binary input must be a Buffer')
}
return detectMach(bytes) ?? detectElf(bytes) ?? detectPe(bytes)
}
export async function readNativeBinaryTarget(filePath) {
return detectNativeBinaryTarget(await readFile(filePath))
}
export async function assertNativeBinaryTarget(
filePath,
platform,
arch,
options = {}
) {
const expectedFormat = PLATFORM_FORMATS[platform]
if (!expectedFormat || !['arm64', 'x64'].includes(arch)) {
throw new Error(`unsupported native binary target ${platform}-${arch}`)
}
const detected = await readNativeBinaryTarget(filePath)
const label = options.label ?? 'native binary'
if (
!detected ||
detected.format !== expectedFormat ||
!detected.arches.includes(arch)
) {
throw new Error(
`${label} has an invalid header; expected ${platform}-${arch}`
)
}
if (
detected.format === 'mach-o' &&
detected.arches.length > 1 &&
options.allowUniversal !== true
) {
throw new Error(
`${label} is universal Mach-O; universal Mach-O is not allowed`
)
}
return detected
}