73 lines
2.5 KiB
TypeScript
73 lines
2.5 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.
|
|
*/
|
|
export type ContentType =
|
|
| 'nzbe'
|
|
| 'par2'
|
|
| 'rar4'
|
|
| 'rar5'
|
|
| '7z'
|
|
| 'zip'
|
|
| 'zstd'
|
|
| 'gzip'
|
|
| 'xz'
|
|
| 'matroska'
|
|
| 'mp4'
|
|
| 'rclone'
|
|
| 'iso'
|
|
| 'udf'
|
|
| 'pdf'
|
|
| 'nzb'
|
|
| 'jpeg'
|
|
| 'png'
|
|
| 'mp3'
|
|
| 'flac'
|
|
| 'avi'
|
|
| 'wav'
|
|
| 'ogg'
|
|
| 'bzip2'
|
|
|
|
const SIGNATURES: [ContentType, number, number[]][] = [
|
|
['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) => 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'
|
|
}
|
|
const head = data.subarray(0, 512).toString('latin1')
|
|
if (/^\s*(<\?xml[^>]*>\s*)?(<!DOCTYPE nzb|<nzb[\s>])/.test(head)) return 'nzb'
|
|
return null
|
|
}
|