implemented a lot of features
This commit is contained in:
1 parent
62f1767df3
commit
bcb9be5da9
141 files changed
+10705
-385
No files matched your search
@@ -0,0 +1,86 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user