@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
text/typescript
/**
* 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");
});
});
});