77 lines
2.6 KiB
TypeScript
77 lines
2.6 KiB
TypeScript
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 }
|
|
}
|