Files
usenet-indexer/lib/yenc/yenc_file.ts
T

87 lines
2.7 KiB
TypeScript

import { crc32Hex, type DecodedSegment } from './decode.js'
import { YencCrcError, YencError } from './errors.js'
import { inferTotalParts } from './meta.js'
/**
* Assembles the decoded segments of one file into a single buffer. Segments
* may be added in any order.
*/
export class YencFile {
readonly name: string
readonly size: number
readonly totalParts: number
private readonly buffer: Buffer
private readonly received = new Set<number>()
private bytesWritten = 0
private fileCrc?: string
constructor(name: string, size: number, totalParts = 1) {
this.name = name
this.size = size
this.totalParts = totalParts
this.buffer = Buffer.alloc(size)
}
/** Creates a file sized from the metadata of any of its segments. */
static fromSegment(segment: DecodedSegment): YencFile {
const { begin } = segment.meta
// Many posters leave total= out; it follows from the part size.
return new YencFile(begin.name, begin.size, inferTotalParts(segment.meta))
}
add(segment: DecodedSegment): void {
const { meta, data, offset } = segment
// Only the size is compared: obfuscating posters can give every segment
// of one file a different random name.
if (meta.begin.size !== this.size) {
throw new YencError(
`Segment of "${meta.begin.name}" (${meta.begin.size} bytes) does not belong to "${this.name}" (${this.size} bytes)`
)
}
if (offset + data.length > this.size) {
throw new YencError(`Segment at offset ${offset} overruns file size ${this.size}`)
}
const partNumber = meta.begin.part ?? 1
if (this.received.has(partNumber)) return
data.copy(this.buffer, offset)
this.received.add(partNumber)
this.bytesWritten += data.length
if (meta.end.crc32) this.fileCrc = meta.end.crc32
}
get partsReceived(): number {
return this.received.size
}
get isComplete(): boolean {
return this.received.size === this.totalParts && this.bytesWritten === this.size
}
/** Part numbers (1-based) that have not been added yet. */
missingParts(): number[] {
const missing: number[] = []
for (let part = 1; part <= this.totalParts; part++) {
if (!this.received.has(part)) missing.push(part)
}
return missing
}
/**
* Returns the assembled file. Throws if parts are missing or, when the poster
* included a whole-file crc32, if it does not match.
*/
getBuffer(): Buffer {
if (!this.isComplete) {
throw new YencError(`File "${this.name}" is incomplete, missing parts: ${this.missingParts().join(', ')}`)
}
if (this.fileCrc) {
const actual = crc32Hex(this.buffer)
if (actual !== this.fileCrc) throw new YencCrcError(this.fileCrc, actual)
}
return this.buffer
}
}