Files
WebDrop/packages/core/src/bytes.ts
T
2026-10-02 00:59:22 +08:00

176 lines
5.6 KiB
TypeScript

/**
* 字节工具。只用标准 JS,不依赖 Node 或 DOM 之外的任何东西。
*
* WebCrypto 的类型(TS 5.7+)要求 BufferSource 由 ArrayBuffer 支撑,
* 所以库内部统一使用 Bytes = Uint8Array<ArrayBuffer>。
*/
export type Bytes = Uint8Array<ArrayBuffer>;
/** 确保由 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);
}