Files
usenet-indexer/lib/content/sniff.ts
T
Daan MeijerandClaude Opus 5.5 99b72ddd8f Sniff Windows executables (MZ with its PE header)
For the junk judge: a "movie" whose first bytes are a program is fake.
Two bytes alone match random data too often, so the PE signature where the
DOS header points must be there too. No PROBE_VERSION bump: new probes only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:42:29 +02:00

89 lines
3.6 KiB
TypeScript

/**
* Recognises a file type from its first bytes, independent of AdonisJS.
* `nzbe` is an encrypted-NZB container seen on Usenet (LOG.md 2026-09-28 (11)):
* "nzbe", 16 random bytes (salt/IV), then ciphertext.
* `yencbin` is the encrypted `.bin` container of `yEncBin@Poster.com`
* (LOG.md 2026-09-30 (3)): a fixed header, then data with 8 bits/byte of entropy.
*/
export type ContentType =
/** Not sniffed: the server no longer has the file's first article (inspection records it, no retries). */
| 'missing'
| 'nzbe'
| 'par2'
| 'rar4'
| 'rar5'
| '7z'
| 'zip'
| 'zstd'
| 'gzip'
| 'xz'
| 'matroska'
| 'mp4'
| 'rclone'
| 'yencbin'
| 'iso'
| 'udf'
| 'pdf'
| 'nzb'
| 'jpeg'
| 'png'
| 'mp3'
| 'flac'
| 'avi'
| 'wav'
| 'ogg'
| 'bzip2'
/** A Windows executable (MZ with its PE header): a "movie" that is a program is fake (app/services/release_judge.ts). */
| 'exe'
/** Magic bytes at an offset; null matches any byte. */
const SIGNATURES: [ContentType, number, (number | null)[]][] = [
// Two header variants seen (54f399d117c49269 6e4dc4ef5b60c11d…, 54f399e917c49269 6a4e9fb05b60c31d…):
// the bytes they share.
['yencbin', 0, [0x54, 0xf3, 0x99, null, 0x17, 0xc4, 0x92, 0x69, null, null, null, null, 0x5b, 0x60, null, 0x1d, 0x7f, 0x83, 0xe9, 0x2d]],
['nzbe', 0, [0x6e, 0x7a, 0x62, 0x65]],
['par2', 0, [0x50, 0x41, 0x52, 0x32, 0x00, 0x50, 0x4b, 0x54]],
['rar5', 0, [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00]],
['rar4', 0, [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00]],
['7z', 0, [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]],
['zip', 0, [0x50, 0x4b, 0x03, 0x04]],
['zstd', 0, [0x28, 0xb5, 0x2f, 0xfd]],
['gzip', 0, [0x1f, 0x8b]],
['xz', 0, [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00]],
['matroska', 0, [0x1a, 0x45, 0xdf, 0xa3]],
['mp4', 4, [0x66, 0x74, 0x79, 0x70]], // "ftyp" box
['rclone', 0, [0x52, 0x43, 0x4c, 0x4f, 0x4e, 0x45]], // rclone crypt
['pdf', 0, [0x25, 0x50, 0x44, 0x46, 0x2d]],
['jpeg', 0, [0xff, 0xd8, 0xff]],
['png', 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]],
['mp3', 0, [0x49, 0x44, 0x33]], // "ID3" tag
['flac', 0, [0x66, 0x4c, 0x61, 0x43]], // "fLaC"
['ogg', 0, [0x4f, 0x67, 0x67, 0x53]], // "OggS"
['bzip2', 0, [0x42, 0x5a, 0x68]], // "BZh"
]
export function sniffContentType(data: Buffer): ContentType | null {
for (const [type, offset, bytes] of SIGNATURES) {
if (data.length >= offset + bytes.length && bytes.every((b, i) => b === null || data[offset + i] === b)) return type
}
// Disc images, visible when the first part is big enough (sector 16 at 32 KiB):
// ISO 9660 has "CD001"; UDF (DVD/Blu-ray) has BEA01 followed by NSR02/NSR03.
if (data.length >= 0x8006 && data.subarray(0x8001, 0x8006).toString('latin1') === 'CD001') return 'iso'
if (data.length >= 0x8806 && data.subarray(0x8001, 0x8006).toString('latin1') === 'BEA01' &&
/^NSR0[23]$/.test(data.subarray(0x8801, 0x8806).toString('latin1'))) return 'udf'
// RIFF container: AVI or WAV by form type.
if (data.length >= 12 && data.subarray(0, 4).toString('latin1') === 'RIFF') {
const form = data.subarray(8, 12).toString('latin1')
if (form === 'AVI ') return 'avi'
if (form === 'WAVE') return 'wav'
}
// MZ, and "PE\0\0" where the DOS header points (e_lfanew at 0x3c): two bytes alone match random data too often.
if (data.length >= 0x40 && data[0] === 0x4d && data[1] === 0x5a) {
const pe = data.readUInt32LE(0x3c)
if (pe + 4 <= data.length && data.readUInt32BE(pe) === 0x50450000) return 'exe'
}
const head = data.subarray(0, 512).toString('latin1')
if (/^\s*(<\?xml[^>]*>\s*)?(<!DOCTYPE nzb|<nzb[\s>])/.test(head)) return 'nzb'
return null
}