import yencode from 'yencode' import { YencCrcError, YencError } from './errors.js' import { parseYencMeta, type YencMeta } from './meta.js' export interface DecodedSegment { meta: YencMeta /** Decoded bytes of this segment. */ data: Buffer /** 0-based offset of `data` within the complete file. */ offset: number } export interface DecodeOptions { /** * Undo NNTP dot-stuffing (`..` at line start). nntp-js hands back raw * bodies, so this defaults to true. */ stripDots?: boolean /** Verify the part (or whole-file) CRC32 from the `=yend` line. Default true. */ verifyCrc?: boolean /** * Accept a decoded length that differs from the declared size (the CRC is * still checked). Some posting tools declare a size that leaves out a * header, e.g. the 20-byte header of `nzbe` containers. */ lenientSize?: boolean } export function crc32Hex(data: Buffer): string { return yencode.crc32(data).toString('hex') } /** * Whole-file CRC32 from the CRCs and decoded lengths of its parts, in order, * without the data itself. */ export function combineCrc32(parts: { crc: string; length: number }[]): string { let combined: Buffer | undefined for (const part of parts) { const crc = Buffer.from(part.crc.padStart(8, '0'), 'hex') combined = combined ? yencode.crc32_combine(combined, crc, part.length) : crc } return (combined ?? Buffer.alloc(4)).toString('hex') } /** * Decodes one yEnc article body and checks its size and CRC32 against the * `=yend` trailer. */ export function decodeSegment(body: Buffer, options: DecodeOptions = {}): DecodedSegment { const { stripDots = true, verifyCrc = true, lenientSize = false } = options const meta = parseYencMeta(body) const data = yencode.decode(body.subarray(meta.dataStart, meta.dataEnd), stripDots) if (lenientSize) { // Only the CRC below decides. } else if (data.length !== meta.end.size) { throw new YencError(`Decoded size ${data.length} does not match =yend size ${meta.end.size}`) } if (!lenientSize && meta.part && meta.part.end - meta.part.begin + 1 !== data.length) { throw new YencError( `Decoded size ${data.length} does not match =ypart range ${meta.part.begin}-${meta.part.end}` ) } if (verifyCrc) { // Multi-part posts carry pcrc32 per part; single-part posts only crc32. const expected = meta.end.pcrc32 ?? (meta.part ? undefined : meta.end.crc32) if (expected) { const actual = crc32Hex(data) if (actual !== expected) throw new YencCrcError(expected, actual) } } return { meta, data, offset: meta.part ? meta.part.begin - 1 : 0 } }