UNPKG

@webbuf/rw

Version:

Read and write buffers, optimized with Rust/WASM for the web, node.js, deno, and bun.

700 lines (607 loc) 22.9 kB
/** * Audit tests for @webbuf/rw * * These tests verify that BufWriter and BufReader correctly handle * serialization/deserialization round-trips, boundary conditions, * and variable-length integer encoding. */ import { describe, it, expect } from "vitest"; import { WebBuf } from "@webbuf/webbuf"; import { U8, U16BE, U32BE, U64BE, U128BE, U256BE, } from "@webbuf/numbers"; import { BufReader } from "../src/buf-reader.js"; import { BufWriter } from "../src/buf-writer.js"; describe("Audit: Round-trip tests", () => { describe("U8 round-trip", () => { it("should round-trip minimum value (0)", () => { const writer = new BufWriter(); writer.writeU8(U8.fromN(0)); const reader = new BufReader(writer.toBuf()); expect(reader.readU8().n).toBe(0); }); it("should round-trip maximum value (255)", () => { const writer = new BufWriter(); writer.writeU8(U8.fromN(255)); const reader = new BufReader(writer.toBuf()); expect(reader.readU8().n).toBe(255); }); it("should round-trip all byte values", () => { const writer = new BufWriter(); for (let i = 0; i <= 255; i++) { writer.writeU8(U8.fromN(i)); } const reader = new BufReader(writer.toBuf()); for (let i = 0; i <= 255; i++) { expect(reader.readU8().n).toBe(i); } expect(reader.eof()).toBe(true); }); }); describe("U16BE round-trip", () => { it("should round-trip minimum value (0)", () => { const writer = new BufWriter(); writer.writeU16BE(U16BE.fromN(0)); const reader = new BufReader(writer.toBuf()); expect(reader.readU16BE().n).toBe(0); }); it("should round-trip maximum value (65535)", () => { const writer = new BufWriter(); writer.writeU16BE(U16BE.fromN(65535)); const reader = new BufReader(writer.toBuf()); expect(reader.readU16BE().n).toBe(65535); }); it("should round-trip various values", () => { const testValues = [0, 1, 255, 256, 0x0102, 0x1234, 0xabcd, 65535]; const writer = new BufWriter(); for (const value of testValues) { writer.writeU16BE(U16BE.fromN(value)); } const reader = new BufReader(writer.toBuf()); for (const value of testValues) { expect(reader.readU16BE().n).toBe(value); } expect(reader.eof()).toBe(true); }); }); describe("U32BE round-trip", () => { it("should round-trip minimum value (0)", () => { const writer = new BufWriter(); writer.writeU32BE(U32BE.fromN(0)); const reader = new BufReader(writer.toBuf()); expect(reader.readU32BE().n).toBe(0); }); it("should round-trip maximum value (4294967295)", () => { const writer = new BufWriter(); writer.writeU32BE(U32BE.fromN(4294967295)); const reader = new BufReader(writer.toBuf()); expect(reader.readU32BE().n).toBe(4294967295); }); it("should round-trip various values", () => { const testValues = [ 0, 1, 255, 256, 65535, 65536, 0x01020304, 0x12345678, 0xdeadbeef, 4294967295, ]; const writer = new BufWriter(); for (const value of testValues) { writer.writeU32BE(U32BE.fromN(value)); } const reader = new BufReader(writer.toBuf()); for (const value of testValues) { expect(reader.readU32BE().n).toBe(value); } expect(reader.eof()).toBe(true); }); }); describe("U64BE round-trip", () => { it("should round-trip minimum value (0)", () => { const writer = new BufWriter(); writer.writeU64BE(U64BE.fromBn(0n)); const reader = new BufReader(writer.toBuf()); expect(reader.readU64BE().bn).toBe(0n); }); it("should round-trip maximum value (2^64 - 1)", () => { const max = 2n ** 64n - 1n; const writer = new BufWriter(); writer.writeU64BE(U64BE.fromBn(max)); const reader = new BufReader(writer.toBuf()); expect(reader.readU64BE().bn).toBe(max); }); it("should round-trip various values", () => { const testValues = [ 0n, 1n, 255n, 256n, 65535n, 65536n, 0x0102030405060708n, 0x123456789abcdef0n, 2n ** 64n - 1n, ]; const writer = new BufWriter(); for (const value of testValues) { writer.writeU64BE(U64BE.fromBn(value)); } const reader = new BufReader(writer.toBuf()); for (const value of testValues) { expect(reader.readU64BE().bn).toBe(value); } expect(reader.eof()).toBe(true); }); }); describe("U128BE round-trip", () => { it("should round-trip minimum value (0)", () => { const writer = new BufWriter(); writer.writeU128BE(U128BE.fromBn(0n)); const reader = new BufReader(writer.toBuf()); expect(reader.readU128BE().bn).toBe(0n); }); it("should round-trip maximum value (2^128 - 1)", () => { const max = 2n ** 128n - 1n; const writer = new BufWriter(); writer.writeU128BE(U128BE.fromBn(max)); const reader = new BufReader(writer.toBuf()); expect(reader.readU128BE().bn).toBe(max); }); it("should round-trip various values", () => { const testValues = [ 0n, 1n, 2n ** 64n, 2n ** 127n, 0x0123456789abcdef0123456789abcdefn, 2n ** 128n - 1n, ]; const writer = new BufWriter(); for (const value of testValues) { writer.writeU128BE(U128BE.fromBn(value)); } const reader = new BufReader(writer.toBuf()); for (const value of testValues) { expect(reader.readU128BE().bn).toBe(value); } expect(reader.eof()).toBe(true); }); }); describe("U256BE round-trip", () => { it("should round-trip minimum value (0)", () => { const writer = new BufWriter(); writer.writeU256BE(U256BE.fromBn(0n)); const reader = new BufReader(writer.toBuf()); expect(reader.readU256BE().bn).toBe(0n); }); it("should round-trip maximum value (2^256 - 1)", () => { const max = 2n ** 256n - 1n; const writer = new BufWriter(); writer.writeU256BE(U256BE.fromBn(max)); const reader = new BufReader(writer.toBuf()); expect(reader.readU256BE().bn).toBe(max); }); it("should round-trip various values", () => { const testValues = [ 0n, 1n, 2n ** 64n, 2n ** 128n, 2n ** 255n, 2n ** 256n - 1n, ]; const writer = new BufWriter(); for (const value of testValues) { writer.writeU256BE(U256BE.fromBn(value)); } const reader = new BufReader(writer.toBuf()); for (const value of testValues) { expect(reader.readU256BE().bn).toBe(value); } expect(reader.eof()).toBe(true); }); }); }); describe("Audit: Boundary conditions", () => { describe("reading past end of buffer", () => { it("should throw when reading U8 from empty buffer", () => { const reader = new BufReader(WebBuf.alloc(0)); expect(() => reader.readU8()).toThrow(); }); it("should throw when reading U16BE from 1-byte buffer", () => { const reader = new BufReader(WebBuf.alloc(1)); expect(() => reader.readU16BE()).toThrow(); }); it("should throw when reading U32BE from 3-byte buffer", () => { const reader = new BufReader(WebBuf.alloc(3)); expect(() => reader.readU32BE()).toThrow(); }); it("should throw when reading U64BE from 7-byte buffer", () => { const reader = new BufReader(WebBuf.alloc(7)); expect(() => reader.readU64BE()).toThrow(); }); it("should throw when reading U128BE from 15-byte buffer", () => { const reader = new BufReader(WebBuf.alloc(15)); expect(() => reader.readU128BE()).toThrow(); }); it("should throw when reading U256BE from 31-byte buffer", () => { const reader = new BufReader(WebBuf.alloc(31)); expect(() => reader.readU256BE()).toThrow(); }); it("should throw when read() exceeds buffer length", () => { const reader = new BufReader(WebBuf.alloc(5)); expect(() => reader.read(10)).toThrow("not enough bytes"); }); it("should throw on second read when buffer exhausted", () => { const reader = new BufReader(WebBuf.alloc(4)); reader.readU32BE(); // Consumes all 4 bytes expect(() => reader.readU8()).toThrow(); }); }); describe("eof() behavior", () => { it("should return true for empty buffer", () => { const reader = new BufReader(WebBuf.alloc(0)); expect(reader.eof()).toBe(true); }); it("should return false for non-empty buffer at start", () => { const reader = new BufReader(WebBuf.alloc(1)); expect(reader.eof()).toBe(false); }); it("should return true after reading all bytes", () => { const reader = new BufReader(WebBuf.alloc(4)); reader.readU32BE(); expect(reader.eof()).toBe(true); }); it("should return false with remaining bytes", () => { const reader = new BufReader(WebBuf.alloc(5)); reader.readU32BE(); expect(reader.eof()).toBe(false); }); }); describe("position tracking", () => { it("should start at position 0", () => { const reader = new BufReader(WebBuf.alloc(10)); expect(reader.pos).toBe(0); }); it("should advance position by 1 after readU8", () => { const reader = new BufReader(WebBuf.alloc(10)); reader.readU8(); expect(reader.pos).toBe(1); }); it("should advance position by 2 after readU16BE", () => { const reader = new BufReader(WebBuf.alloc(10)); reader.readU16BE(); expect(reader.pos).toBe(2); }); it("should advance position by 4 after readU32BE", () => { const reader = new BufReader(WebBuf.alloc(10)); reader.readU32BE(); expect(reader.pos).toBe(4); }); it("should advance position by 8 after readU64BE", () => { const reader = new BufReader(WebBuf.alloc(10)); reader.readU64BE(); expect(reader.pos).toBe(8); }); it("should advance position by requested length after read()", () => { const reader = new BufReader(WebBuf.alloc(10)); reader.read(7); expect(reader.pos).toBe(7); }); it("should track cumulative position across multiple reads", () => { const reader = new BufReader(WebBuf.alloc(20)); reader.readU8(); expect(reader.pos).toBe(1); reader.readU16BE(); expect(reader.pos).toBe(3); reader.readU32BE(); expect(reader.pos).toBe(7); reader.readU64BE(); expect(reader.pos).toBe(15); }); }); }); describe("Audit: VarInt encoding", () => { describe("VarInt write/read round-trip", () => { it("should encode single-byte values (0-252)", () => { const testValues = [0n, 1n, 127n, 252n]; for (const value of testValues) { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromBn(value)); const buf = writer.toBuf(); expect(buf.length).toBe(1); // Single byte encoding expect(buf[0]).toBe(Number(value)); const reader = new BufReader(buf); expect(reader.readVarIntU64BE().bn).toBe(value); } }); it("should encode 3-byte values (253-65535)", () => { const testValues = [253n, 254n, 255n, 256n, 0xfffdn, 0xfffen, 0xffffn]; for (const value of testValues) { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromBn(value)); const buf = writer.toBuf(); expect(buf.length).toBe(3); // 0xFD prefix + 2 bytes expect(buf[0]).toBe(0xfd); const reader = new BufReader(buf); expect(reader.readVarIntU64BE().bn).toBe(value); } }); it("should encode 5-byte values (65536-4294967295)", () => { const testValues = [0x10000n, 0x12345678n, 0xffffffffn]; for (const value of testValues) { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromBn(value)); const buf = writer.toBuf(); expect(buf.length).toBe(5); // 0xFE prefix + 4 bytes expect(buf[0]).toBe(0xfe); const reader = new BufReader(buf); expect(reader.readVarIntU64BE().bn).toBe(value); } }); it("should encode 9-byte values (4294967296 and above)", () => { const testValues = [0x100000000n, 0x123456789abcdef0n, 2n ** 64n - 1n]; for (const value of testValues) { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromBn(value)); const buf = writer.toBuf(); expect(buf.length).toBe(9); // 0xFF prefix + 8 bytes expect(buf[0]).toBe(0xff); const reader = new BufReader(buf); expect(reader.readVarIntU64BE().bn).toBe(value); } }); }); describe("VarInt minimal encoding enforcement", () => { it("should reject non-minimal 3-byte encoding", () => { // Value 252 encoded as 0xFD 0x00 0xFC is non-minimal const buf = WebBuf.from([0xfd, 0x00, 0xfc]); const reader = new BufReader(buf); expect(() => reader.readVarIntU64BE()).toThrow("non-minimal"); }); it("should reject non-minimal 5-byte encoding", () => { // Value 0xFFFF encoded as 0xFE 0x00 0x00 0xFF 0xFF is non-minimal const buf = WebBuf.from([0xfe, 0x00, 0x00, 0xff, 0xff]); const reader = new BufReader(buf); expect(() => reader.readVarIntU64BE()).toThrow("non-minimal"); }); it("should reject non-minimal 9-byte encoding", () => { // Value 0xFFFFFFFF encoded as 0xFF followed by 8 bytes is non-minimal const buf = WebBuf.from([0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]); const reader = new BufReader(buf); expect(() => reader.readVarIntU64BE()).toThrow("non-minimal"); }); }); describe("VarInt boundary values", () => { it("should correctly encode/decode boundary at 252/253", () => { // 252 = single byte const writer1 = new BufWriter(); writer1.writeVarIntU64BE(U64BE.fromBn(252n)); expect(writer1.toBuf().length).toBe(1); // 253 = 3 bytes (0xFD prefix) const writer2 = new BufWriter(); writer2.writeVarIntU64BE(U64BE.fromBn(253n)); expect(writer2.toBuf().length).toBe(3); }); it("should correctly encode/decode boundary at 65535/65536", () => { // 65535 = 3 bytes const writer1 = new BufWriter(); writer1.writeVarIntU64BE(U64BE.fromBn(65535n)); expect(writer1.toBuf().length).toBe(3); // 65536 = 5 bytes (0xFE prefix) const writer2 = new BufWriter(); writer2.writeVarIntU64BE(U64BE.fromBn(65536n)); expect(writer2.toBuf().length).toBe(5); }); it("should correctly encode/decode boundary at 4294967295/4294967296", () => { // 4294967295 = 5 bytes const writer1 = new BufWriter(); writer1.writeVarIntU64BE(U64BE.fromBn(4294967295n)); expect(writer1.toBuf().length).toBe(5); // 4294967296 = 9 bytes (0xFF prefix) const writer2 = new BufWriter(); writer2.writeVarIntU64BE(U64BE.fromBn(4294967296n)); expect(writer2.toBuf().length).toBe(9); }); }); }); describe("Audit: Mixed type serialization", () => { it("should correctly serialize and deserialize mixed types", () => { const writer = new BufWriter(); // Write various types writer.writeU8(U8.fromN(0x12)); writer.writeU16BE(U16BE.fromN(0x3456)); writer.writeU32BE(U32BE.fromN(0x789abcde)); writer.writeU64BE(U64BE.fromBn(0xf0123456789abcden)); writer.writeVarIntU64BE(U64BE.fromBn(42n)); writer.writeVarIntU64BE(U64BE.fromBn(1000n)); writer.writeU128BE(U128BE.fromBn(0x0123456789abcdef0123456789abcdefn)); writer.writeU256BE(U256BE.fromBn(2n ** 200n)); const reader = new BufReader(writer.toBuf()); // Read back in same order expect(reader.readU8().n).toBe(0x12); expect(reader.readU16BE().n).toBe(0x3456); expect(reader.readU32BE().n).toBe(0x789abcde); expect(reader.readU64BE().bn).toBe(0xf0123456789abcden); expect(reader.readVarIntU64BE().bn).toBe(42n); expect(reader.readVarIntU64BE().bn).toBe(1000n); expect(reader.readU128BE().bn).toBe(0x0123456789abcdef0123456789abcdefn); expect(reader.readU256BE().bn).toBe(2n ** 200n); expect(reader.eof()).toBe(true); }); it("should handle complex message structure", () => { // Simulate a message with: version (U32), count (VarInt), items (U64[]) const writer = new BufWriter(); const version = 1; const items = [100n, 200n, 300n]; writer.writeU32BE(U32BE.fromN(version)); writer.writeVarIntU64BE(U64BE.fromBn(BigInt(items.length))); for (const item of items) { writer.writeU64BE(U64BE.fromBn(item)); } const reader = new BufReader(writer.toBuf()); expect(reader.readU32BE().n).toBe(version); const count = Number(reader.readVarIntU64BE().bn); expect(count).toBe(items.length); for (let i = 0; i < count; i++) { expect(reader.readU64BE().bn).toBe(items[i]); } expect(reader.eof()).toBe(true); }); }); describe("Audit: readFixed and readRemainder", () => { describe("readFixed", () => { it("should read fixed-size buffer", () => { const data = WebBuf.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); const reader = new BufReader(data); const fixed = reader.readFixed(4); expect(fixed.buf.length).toBe(4); expect(fixed.toHex()).toBe("01020304"); expect(reader.pos).toBe(4); }); it("should throw if not enough bytes", () => { const data = WebBuf.from([0x01, 0x02]); const reader = new BufReader(data); expect(() => reader.readFixed(4)).toThrow(); }); }); describe("readRemainder", () => { it("should read all remaining bytes", () => { const data = WebBuf.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); const reader = new BufReader(data); reader.readU16BE(); // Read 2 bytes const remainder = reader.readRemainder(); expect(remainder.length).toBe(4); expect(remainder.toHex()).toBe("03040506"); expect(reader.eof()).toBe(true); }); it("should return empty buffer when at end", () => { const data = WebBuf.from([0x01, 0x02]); const reader = new BufReader(data); reader.readU16BE(); const remainder = reader.readRemainder(); expect(remainder.length).toBe(0); }); it("should return entire buffer when at start", () => { const data = WebBuf.from([0x01, 0x02, 0x03]); const reader = new BufReader(data); const remainder = reader.readRemainder(); expect(remainder.length).toBe(3); expect(remainder.toHex()).toBe("010203"); }); }); }); describe("Audit: BufWriter behavior", () => { describe("getLength", () => { it("should return 0 for empty writer", () => { const writer = new BufWriter(); expect(writer.getLength()).toBe(0); }); it("should return correct cumulative length", () => { const writer = new BufWriter(); writer.writeU8(U8.fromN(1)); expect(writer.getLength()).toBe(1); writer.writeU16BE(U16BE.fromN(1)); expect(writer.getLength()).toBe(3); writer.writeU32BE(U32BE.fromN(1)); expect(writer.getLength()).toBe(7); writer.writeU64BE(U64BE.fromBn(1n)); expect(writer.getLength()).toBe(15); }); }); describe("write raw buffer", () => { it("should write raw buffer data", () => { const writer = new BufWriter(); writer.write(WebBuf.from([0xde, 0xad, 0xbe, 0xef])); expect(writer.toBuf().toHex()).toBe("deadbeef"); }); it("should support chaining", () => { const writer = new BufWriter(); writer .writeU8(U8.fromN(1)) .writeU16BE(U16BE.fromN(2)) .writeU32BE(U32BE.fromN(3)); expect(writer.getLength()).toBe(1 + 2 + 4); }); }); describe("constructor with initial buffers", () => { it("should accept initial buffers array", () => { const initial = [ WebBuf.from([0x01, 0x02]), WebBuf.from([0x03, 0x04]), ]; const writer = new BufWriter(initial); expect(writer.getLength()).toBe(4); expect(writer.toBuf().toHex()).toBe("01020304"); }); }); }); describe("Audit: Data integrity", () => { it("should not modify original buffer on read", () => { const original = WebBuf.from([0x01, 0x02, 0x03, 0x04]); const originalHex = original.toHex(); const reader = new BufReader(original); reader.readU8(); reader.readU16BE(); expect(original.toHex()).toBe(originalHex); }); it("should return independent copies from read()", () => { const data = WebBuf.from([0x01, 0x02, 0x03, 0x04]); const reader = new BufReader(data); const chunk1 = reader.read(2); chunk1[0] = 0xff; // Modify the returned chunk // Original data should be unchanged expect(data[0]).toBe(0x01); // Reading again should get original values const reader2 = new BufReader(data); const chunk2 = reader2.read(2); expect(chunk2[0]).toBe(0x01); }); }); describe("Audit: Known test vectors", () => { describe("Bitcoin-style VarInt (big-endian)", () => { // Note: This is big-endian VarInt, not Bitcoin's little-endian it("should encode small values as single byte", () => { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromN(0)); expect(writer.toBuf().toHex()).toBe("00"); const writer2 = new BufWriter(); writer2.writeVarIntU64BE(U64BE.fromN(252)); expect(writer2.toBuf().toHex()).toBe("fc"); }); it("should encode 0xFD prefix for 253-65535", () => { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromN(253)); expect(writer.toBuf().toHex()).toBe("fd00fd"); const writer2 = new BufWriter(); writer2.writeVarIntU64BE(U64BE.fromN(0x1234)); expect(writer2.toBuf().toHex()).toBe("fd1234"); }); it("should encode 0xFE prefix for 65536-4294967295", () => { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromN(0x10000)); expect(writer.toBuf().toHex()).toBe("fe00010000"); const writer2 = new BufWriter(); writer2.writeVarIntU64BE(U64BE.fromN(0x12345678)); expect(writer2.toBuf().toHex()).toBe("fe12345678"); }); it("should encode 0xFF prefix for >= 4294967296", () => { const writer = new BufWriter(); writer.writeVarIntU64BE(U64BE.fromBn(0x100000000n)); expect(writer.toBuf().toHex()).toBe("ff0000000100000000"); const writer2 = new BufWriter(); writer2.writeVarIntU64BE(U64BE.fromBn(0x123456789abcdef0n)); expect(writer2.toBuf().toHex()).toBe("ff123456789abcdef0"); }); }); });