import { HEADER_SIZE, ID_SIZE } from './constants.js'; import { readU32, readU64, toBase64Url, type Bytes } from './bytes.js'; import { IntegrityError } from './errors.js'; /** 墓碑原因(protocol §8)。 */ export type RemovedReason = 'deleted' | 'expired' | 'evicted' | 'download_limit' | 'superseded'; const REASONS: readonly RemovedReason[] = ['deleted', 'expired', 'evicted', 'download_limit', 'superseded']; export type LogRecord = | { kind: 'frame'; seq: number; envelope: Bytes } | { kind: 'tombstone'; seq: number; itemId: string; reason: RemovedReason } | { kind: 'announce'; seq: number; itemId: string; flags: number; createdAt: number }; const KIND_FRAME = 1; const KIND_TOMBSTONE = 2; const KIND_ANNOUNCE = 3; /** body 的字节数,不够返回 null。 */ function bodyLength(kind: number, buf: Uint8Array, bodyStart: number): number | null { switch (kind) { case KIND_FRAME: { // 封装自带 payload_len:body 长度 = 26 + payload_len if (buf.length < bodyStart + HEADER_SIZE) return null; return HEADER_SIZE + readU32(buf, bodyStart + 22); } case KIND_TOMBSTONE: return ID_SIZE + 1; case KIND_ANNOUNCE: return ID_SIZE + 1 + 8; default: throw new IntegrityError('bad_envelope'); } } function parseOne(buf: Uint8Array, offset: number): { record: LogRecord; next: number } { if (buf.length < offset + 9) throw new IntegrityError('bad_envelope'); const seq = readU64(buf, offset); const kind = buf[offset + 8] as number; const bodyStart = offset + 9; const len = bodyLength(kind, buf, bodyStart); if (len === null || buf.length < bodyStart + len) throw new IntegrityError('bad_envelope'); const body = buf.subarray(bodyStart, bodyStart + len); const next = bodyStart + len; switch (kind) { case KIND_FRAME: return { record: { kind: 'frame', seq, envelope: body.slice() }, next }; case KIND_TOMBSTONE: { const reason = REASONS[body[ID_SIZE] as number]; if (!reason) throw new IntegrityError('bad_envelope'); return { record: { kind: 'tombstone', seq, itemId: toBase64Url(body.subarray(0, ID_SIZE)), reason }, next }; } default: return { record: { kind: 'announce', seq, itemId: toBase64Url(body.subarray(0, ID_SIZE)), flags: body[ID_SIZE] as number, createdAt: readU64(body, ID_SIZE + 1), }, next, }; } } /** 解析 log 接口返回的记录拼接(长轮询的响应体)。 */ export function parseLogRecords(data: Uint8Array): LogRecord[] { const out: LogRecord[] = []; let offset = 0; while (offset < data.length) { const { record, next } = parseOne(data, offset); out.push(record); offset = next; } return out; } /** 解析恰好一条记录(WebSocket 的一条二进制消息)。多余或缺少字节都视为非法。 */ export function parseLogRecord(data: Uint8Array): LogRecord { const { record, next } = parseOne(data, 0); if (next !== data.length) throw new IntegrityError('bad_envelope'); return record; }