Files
usenet-indexer/tests/unit/probe/inspect.spec.ts
T
Daan MeijerandClaude Opus 5.5 4ed7f89951
Test and deploy / test (push) Successful in 43s
Test and deploy / deploy (push) Successful in 1m18s
Prober: no packaging or release-name-plus-extension as inner name
Lone candidates are often par2 volumes of random-named sets
("42507096….vol007+08.par2"): their yEnc name was taken as the inner
name. Packaging names are skipped for lone files too, and a name that
starts with the release's own name says nothing new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:44:08 +02:00

131 lines
6.4 KiB
TypeScript

import { test } from '@japa/runner'
import { createHash } from 'node:crypto'
import { crc32 } from 'node:zlib'
import yencode from 'yencode'
import { crc32Hex } from '#lib/yenc/index'
import { inspect, innerName, MAX_NAMES } from '#lib/probe/index'
/** A single-part yEnc article of `data`. */
function article(data: Buffer, name: string) {
const lines = [`=ybegin line=128 size=${data.length} name=${name}`, yencode.encode(data).toString('latin1'), `=yend size=${data.length} crc32=${crc32Hex(data)}`]
return Buffer.from(lines.join('\r\n'), 'latin1')
}
const md5 = (data: Buffer) => createHash('md5').update(data).digest()
/** Builds one PAR2 packet with a valid checksum. */
function packet(setId: Buffer, type: string, body: Buffer) {
const padded = Buffer.concat([body, Buffer.alloc((4 - (body.length % 4)) % 4)])
const length = Buffer.alloc(8)
length.writeBigUInt64LE(BigInt(64 + padded.length))
const typeBuf = Buffer.alloc(16)
typeBuf.write(type, 'latin1')
const hashed = Buffer.concat([setId, typeBuf, padded])
return Buffer.concat([Buffer.from('PAR2\0PKT', 'latin1'), length, md5(hashed), hashed])
}
function fileDesc(setId: Buffer, name: string, size: number) {
const len = Buffer.alloc(8)
len.writeBigUInt64LE(BigInt(size))
const md5_16k = md5(Buffer.from(`${name}-16k`))
const fileId = md5(Buffer.concat([md5_16k, len, Buffer.from(name)]))
const body = Buffer.concat([fileId, md5(Buffer.from(name)), md5_16k, len, Buffer.from(name, 'utf8')])
return packet(setId, 'PAR 2.0\0FileDesc', body)
}
const RAR5 = Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00])
function vint(value: number) {
const bytes: number[] = []
do {
let byte = value % 128
value = Math.floor(value / 128)
if (value > 0) byte |= 0x80
bytes.push(byte)
} while (value > 0)
return Buffer.from(bytes)
}
function rar5Block(type: number, headerFlags: number, fields: Buffer, opts: { extra?: Buffer; data?: number } = {}) {
let flags = headerFlags
const parts = [vint(type)]
const sizes: Buffer[] = []
if (opts.extra) {
flags |= 0x0001
sizes.push(vint(opts.extra.length))
}
if (opts.data !== undefined) {
flags |= 0x0002
sizes.push(vint(opts.data))
}
const header = Buffer.concat([...parts, vint(flags), ...sizes, fields, opts.extra ?? Buffer.alloc(0)])
const sized = Buffer.concat([vint(header.length), header])
const crc = Buffer.alloc(4)
crc.writeUInt32LE(crc32(sized) >>> 0)
return Buffer.concat([crc, sized, Buffer.alloc(opts.data ?? 0)])
}
function rar5File(name: string, size: number, opts: { packed: number; headerFlags?: number; encrypted?: boolean; dir?: boolean }) {
const nameBuf = Buffer.from(name, 'utf8')
const fields = Buffer.concat([
vint(opts.dir ? 0x0001 : 0), // file flags
vint(size),
vint(0x20), // attributes
vint(0), // compression info
vint(0), // host OS
vint(nameBuf.length),
nameBuf,
])
// Encryption record: size, type 1, then its data (dummy here).
const extra = opts.encrypted ? Buffer.concat([vint(3), vint(0x01), Buffer.from([0, 0])]) : undefined
return rar5Block(2, opts.headerFlags ?? 0, fields, { extra, data: opts.packed })
}
test.group('probe / inspect', () => {
test('a par2 index: its files, largest first', ({ assert }) => {
const setId = Buffer.alloc(16, 3)
const par2 = Buffer.concat([fileDesc(setId, 'Movie.2020.part02.rar', 100), fileDesc(setId, 'Movie.2020.part01.rar', 200)])
const result = inspect(article(par2, 'a8f3k2.par2'))
assert.deepEqual(result, { yencName: 'a8f3k2.par2', size: par2.length, type: 'par2', entries: 2, names: ['Movie.2020.part01.rar', 'Movie.2020.part02.rar'] })
})
test('a RAR volume: its entries, and whether they are encrypted', ({ assert }) => {
const rar = Buffer.concat([RAR5, rar5Block(1, 0, vint(0)), rar5File('Movie.2020.1080p/movie.mkv', 5_000_000, { packed: 10 }), rar5File('Movie.2020.1080p/sample.mkv', 1000, { packed: 10 })])
const open = inspect(article(rar, 'x7q.part01.rar'))
assert.include(open, { type: 'rar5', entries: 2, passworded: false })
assert.deepEqual(open.names, ['Movie.2020.1080p/movie.mkv', 'Movie.2020.1080p/sample.mkv'])
const locked = inspect(article(Buffer.concat([RAR5, rar5Block(1, 0, vint(0)), rar5File('x.mkv', 1, { packed: 1, encrypted: true })]), 'y.rar'))
assert.isTrue(locked.passworded)
const hidden = inspect(article(Buffer.concat([RAR5, rar5Block(4, 0, Buffer.alloc(3))]), 'z.rar'))
assert.include(hidden, { passworded: true, entries: 0 })
})
test('other data: the yEnc name and what the bytes are', ({ assert }) => {
const mkv = Buffer.concat([Buffer.from([0x1a, 0x45, 0xdf, 0xa3]), Buffer.alloc(100)])
assert.deepEqual(inspect(article(mkv, 'Real.Name.mkv')), { yencName: 'Real.Name.mkv', size: 104, type: 'matroska' })
})
test('not yEnc: throws', ({ assert }) => {
assert.throws(() => inspect(Buffer.from('just some text\r\n')))
})
test('at most MAX_NAMES names', ({ assert }) => {
const setId = Buffer.alloc(16, 4)
const par2 = Buffer.concat(Array.from({ length: MAX_NAMES + 5 }, (_, i) => fileDesc(setId, `f${i}.rar`, i)))
const result = inspect(article(par2, 'p.par2'))
assert.equal(result.entries, MAX_NAMES + 5)
assert.lengthOf(result.names!, MAX_NAMES)
})
})
test.group('probe / innerName', () => {
test("the archive's largest entry, else par2's largest non-packaging file, else a lone file's yEnc name", ({ assert }) => {
const archive = { yencName: 'a.rar', size: 1, type: 'rar5' as const, names: ['Show.S01E01/show.s01e01.mkv'] }
const par2 = { yencName: 'a.par2', size: 1, type: 'par2' as const, names: ['abc.part01.rar', 'Show.S01E01.1080p.mkv'] }
assert.equal(innerName([{ kind: 'par2', result: par2 }, { kind: 'archive', result: archive }]), 'show.s01e01.mkv')
assert.equal(innerName([{ kind: 'par2', result: par2 }]), 'Show.S01E01.1080p.mkv')
assert.equal(innerName([{ kind: 'par2', result: { ...par2, names: ['abc.part01.rar', 'abc.par2'] } }]), null)
const lone = { kind: 'lone', result: { yencName: 'Real.Name.mkv', size: 1, type: 'matroska' as const } }
assert.equal(innerName([lone], 'k3j4h5'), 'Real.Name.mkv')
// A random-named file whose yEnc name is its random name says nothing new.
assert.isNull(innerName([lone], 'real.name.mkv'))
assert.isNull(innerName([lone], 'Real.Name'))
// Nor does packaging: a lone par2 volume.
assert.isNull(innerName([{ kind: 'lone', result: { yencName: 'a1b2.vol007+08.par2', size: 1, type: 'par2' } }], 'x9y8'))
})
})