Spotnet (CONCEPT D29): - Index free.pt fully and import spots with their descriptions, categories and NZBs; a spot renames its release, since Spotnet names are more trustworthy than names derived from subjects. - Find passwords in spot descriptions too, and tell "needs a password we have" from "needs a password we don't have". Content inspection: - List the contents of 7z (.7z.001) archives, SFV sets, nicovideo uploads and other archive-like files; mark yEncBin .bin containers as encrypted. - Take real names from yEnc headers for randomly named files, and merge uploads that were split across posts. Storage (D31, D32): - Store segment lists zstd-compressed behind the FileStore accessor. - group:backfill indexes older headers at low priority and pauses when the NVMe or HDD part of the database gets too large. Grouping (D35, D36): - Lone header segments wait in staged_segments until a second segment of their file arrives, which drops the per-article junk binaries. - Counter grouping joins random-subject uploads by poster, [i/M] counter, part count and day. Search and maintenance (D33, D34): - A trigram-indexed search_text column, smaller indexes, estimated stats. - Passes that clean up nonsense names; weekly index maintenance; GC budgets per kind. - Web UI: grey out stale results while a search runs and cancel superseded requests. Count only up to 1,000 matches for the page, which made filter clicks take seconds (Newznab keeps the 10,000 cap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
2.4 KiB
TypeScript
53 lines
2.4 KiB
TypeScript
import { test } from '@japa/runner'
|
|
import { delimitedReader } from '#lib/nntp/delimited_reader'
|
|
|
|
const CRLF = Buffer.from('\r\n')
|
|
const END = Buffer.from('\r\n.\r\n')
|
|
|
|
async function* chunks(...parts: string[]) {
|
|
for (const part of parts) yield Buffer.from(part, 'latin1')
|
|
}
|
|
|
|
test.group('nntp / delimitedReader', () => {
|
|
test('reads an empty multi-line reply (XOVER of a range without articles)', async ({ assert }) => {
|
|
const reader = delimitedReader(chunks('224 Overview follows\r\n.\r\n', '211 1 2 3 g\r\n'), CRLF)
|
|
assert.equal((await reader.next()).value!.toString(), '224 Overview follows')
|
|
assert.equal((await reader.next(END)).value!.toString(), '')
|
|
assert.equal((await reader.next(CRLF)).value!.toString(), '211 1 2 3 g')
|
|
})
|
|
|
|
test('reads multi-line data, also with the terminator split over chunks', async ({ assert }) => {
|
|
for (const split of [1, 2, 3, 4, 5, 6, 7]) {
|
|
const reply = '224 ok\r\nline 1\r\n..dotted\r\nline 3\r\n.\r\n200 next\r\n'
|
|
const at = reply.indexOf('.\r\n200') - 2 + split - 1
|
|
const reader = delimitedReader(chunks(reply.slice(0, at), reply.slice(at)), CRLF)
|
|
assert.equal((await reader.next()).value!.toString(), '224 ok')
|
|
assert.equal((await reader.next(END)).value!.toString(), 'line 1\r\n..dotted\r\nline 3', `split ${split}`)
|
|
assert.equal((await reader.next(CRLF)).value!.toString(), '200 next')
|
|
}
|
|
})
|
|
|
|
test('reads replies byte by byte', async ({ assert }) => {
|
|
const text = '200 welcome\r\n224 ok\r\na\r\n.\r\n224 ok\r\n.\r\n211 x\r\n'
|
|
const reader = delimitedReader(chunks(...text.split('')), CRLF)
|
|
assert.equal((await reader.next()).value!.toString(), '200 welcome')
|
|
assert.equal((await reader.next(CRLF)).value!.toString(), '224 ok')
|
|
assert.equal((await reader.next(END)).value!.toString(), 'a')
|
|
assert.equal((await reader.next(CRLF)).value!.toString(), '224 ok')
|
|
assert.equal((await reader.next(END)).value!.toString(), '')
|
|
assert.equal((await reader.next(CRLF)).value!.toString(), '211 x')
|
|
})
|
|
|
|
test('keeps binary data intact', async ({ assert }) => {
|
|
const body = Buffer.from([0, 13, 255, 10, 46, 13, 46, 10, 13])
|
|
const source = (async function* () {
|
|
yield Buffer.from('222 body\r\n')
|
|
yield body
|
|
yield Buffer.from('\r\n.\r\n')
|
|
})()
|
|
const reader = delimitedReader(source, CRLF)
|
|
await reader.next()
|
|
assert.deepEqual((await reader.next(END)).value, body)
|
|
})
|
|
})
|