/** * 字节工具。只用标准 JS,不依赖 Node 或 DOM 之外的任何东西。 * * WebCrypto 的类型(TS 5.7+)要求 BufferSource 由 ArrayBuffer 支撑, * 所以库内部统一使用 Bytes = Uint8Array。 */ export type Bytes = Uint8Array; /** 确保由 ArrayBuffer 支撑(SharedArrayBuffer 或其他情况下复制一份)。 */ export function toBytes(u: Uint8Array): Bytes { return u.buffer instanceof ArrayBuffer ? (u as Bytes) : new Uint8Array(u); } export function concat(...parts: Uint8Array[]): Bytes { let total = 0; for (const p of parts) total += p.length; const out = new Uint8Array(total); let offset = 0; for (const p of parts) { out.set(p, offset); offset += p.length; } return out; } export function equalBytes(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= (a[i] as number) ^ (b[i] as number); return diff === 0; } export function readU16(b: Uint8Array, offset: number): number { return ((b[offset] as number) << 8) | (b[offset + 1] as number); } export function readU32(b: Uint8Array, offset: number): number { return ( (((b[offset] as number) << 24) | ((b[offset + 1] as number) << 16) | ((b[offset + 2] as number) << 8) | (b[offset + 3] as number)) >>> 0 ); } /** 读 u64,超过 2^53 - 1 时抛错(seq 和时间戳实际远小于这个值)。 */ export function readU64(b: Uint8Array, offset: number): number { const hi = readU32(b, offset); const lo = readU32(b, offset + 4); if (hi > 0x1fffff) throw new RangeError('u64 超出 JS 安全整数范围'); return hi * 0x1_0000_0000 + lo; } export function writeU16(value: number): Bytes { if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new RangeError('u16 超出范围'); return new Uint8Array([value >>> 8, value & 0xff]); } export function writeU32(value: number): Bytes { if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) throw new RangeError('u32 超出范围'); return new Uint8Array([value >>> 24, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff]); } export function writeU64(value: number): Bytes { if (!Number.isSafeInteger(value) || value < 0) throw new RangeError('u64 超出范围'); const out = new Uint8Array(8); const hi = Math.floor(value / 0x1_0000_0000); const lo = value >>> 0; out.set(writeU32(hi), 0); out.set(writeU32(lo), 4); return out; } // ───────── hex ───────── export function toHex(b: Uint8Array): string { let s = ''; for (const x of b) s += x.toString(16).padStart(2, '0'); return s; } export function fromHex(s: string): Bytes { if (s.length % 2 !== 0 || /[^0-9a-fA-F]/.test(s)) throw new RangeError('非法的十六进制字符串'); const out = new Uint8Array(s.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16); return out; } // ───────── base64url(无填充,严格规范) ───────── const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; const LOOKUP = (() => { const t = new Int8Array(128).fill(-1); for (let i = 0; i < ALPHABET.length; i++) t[ALPHABET.charCodeAt(i)] = i; return t; })(); export function toBase64Url(b: Uint8Array): string { let out = ''; let i = 0; for (; i + 2 < b.length; i += 3) { const n = ((b[i] as number) << 16) | ((b[i + 1] as number) << 8) | (b[i + 2] as number); out += ALPHABET[(n >>> 18) & 63]! + ALPHABET[(n >>> 12) & 63]! + ALPHABET[(n >>> 6) & 63]! + ALPHABET[n & 63]!; } const rest = b.length - i; if (rest === 1) { const n = (b[i] as number) << 16; out += ALPHABET[(n >>> 18) & 63]! + ALPHABET[(n >>> 12) & 63]!; } else if (rest === 2) { const n = ((b[i] as number) << 16) | ((b[i + 1] as number) << 8); out += ALPHABET[(n >>> 18) & 63]! + ALPHABET[(n >>> 12) & 63]! + ALPHABET[(n >>> 6) & 63]!; } return out; } /** * 严格解码:只接受 url 字母表,拒绝填充和非规范编码(末尾多余的比特必须为 0), * 与服务端 Codec.TryDecodeExact 的行为一致。非法输入返回 null,绝不抛异常。 */ export function fromBase64Url(s: string, expectedLength?: number): Bytes | null { const len = s.length; if (len % 4 === 1) return null; const outLen = Math.floor((len * 3) / 4); if (expectedLength !== undefined && outLen !== expectedLength) return null; const out = new Uint8Array(outLen); let o = 0; let acc = 0; let bits = 0; for (let i = 0; i < len; i++) { const code = s.charCodeAt(i); const v = code < 128 ? (LOOKUP[code] as number) : -1; if (v < 0) return null; acc = (acc << 6) | v; bits += 6; if (bits >= 8) { bits -= 8; out[o++] = (acc >>> bits) & 0xff; acc &= (1 << bits) - 1; } } if (acc !== 0) return null; // 末尾多余比特必须为 0 return out; } // ───────── 文本 ───────── const encoder = new TextEncoder(); const strictDecoder = new TextDecoder('utf-8', { fatal: true }); export function utf8(s: string): Bytes { return encoder.encode(s) as Bytes; } /** 严格的 UTF-8 解码,非法序列返回 null。 */ export function tryDecodeUtf8(b: Uint8Array): string | null { try { return strictDecoder.decode(b); } catch { return null; } } export function randomBytes(n: number): Bytes { const out = new Uint8Array(n); globalThis.crypto.getRandomValues(out); return out; } /** 尽力清零。JS 无法保证内存被真正擦除(GC 可能已复制过),这只是缩小暴露窗口。 */ export function wipe(b: Uint8Array): void { b.fill(0); }