diff --git a/src/Reader.ts b/src/Reader.ts index ec93156..697a524 100644 --- a/src/Reader.ts +++ b/src/Reader.ts @@ -2,9 +2,12 @@ import {decodeUtf8} from './utf8/decodeUtf8'; import type {IReader, IReaderResettable} from './types'; export class Reader implements IReader, IReaderResettable { - public uint8: Uint8Array = new Uint8Array([]); - public view: DataView = new DataView(this.uint8.buffer); - public x = 0; + constructor( + public uint8: Uint8Array = new Uint8Array([]), + public view: DataView = new DataView(uint8.buffer as ArrayBuffer, uint8.byteOffset, uint8.length), + public x: number = 0, + public end: number = uint8.length, + ) {} public reset(uint8: Uint8Array): void { this.x = 0; @@ -12,6 +15,10 @@ export class Reader implements IReader, IReaderResettable { this.view = new DataView(uint8.buffer as ArrayBuffer, uint8.byteOffset, uint8.length); } + public size(): number { + return this.end - this.x; + } + /** * Get current byte value without advancing the cursor. */ @@ -30,13 +37,46 @@ export class Reader implements IReader, IReaderResettable { this.x += length; } - public buf(size: number): Uint8Array { - const end = this.x + size; - const bin = this.uint8.subarray(this.x, end); + public buf(size: number = this.size()): Uint8Array { + const x = this.x; + const end = x + size; + const bin = this.uint8.subarray(x, end); this.x = end; return bin; } + /** + * Creates a new {@link Reader} that references the same underlying memory + * buffer. But with independent cursor and end. + * + * Preferred over {@link buf} since it also provides a DataView and is much + * faster to allocate a new {@link Slice} than a new {@link Uint8Array}. + * + * @param start Start offset relative to the current cursor position. + * @param end End offset relative to the current cursor position. + * @returns A new {@link Reader} instance. + */ + public slice(start: number = 0, end?: number): Reader { + const x = this.x; + const actualStart = x + start; + const actualEnd = typeof end === 'number' ? x + end : this.end; + return new Reader(this.uint8, this.view, actualStart, actualEnd); + } + + /** + * Similar to {@link slice} but also advances the cursor. Returns a new + * {@link Reader} that references the same underlying memory buffer, starting + * from the current cursor position. + * + * @param size Number of bytes to cut from the current position. + * @returns A new {@link Reader} instance. + */ + public cut(size: number = this.size()): Reader { + const slice = this.slice(0, size); + this.skip(size); + return slice; + } + public u8(): number { return this.uint8[this.x++]; // return this.view.getUint8(this.x++); diff --git a/src/Slice.ts b/src/Slice.ts index c0287b8..d174e63 100644 --- a/src/Slice.ts +++ b/src/Slice.ts @@ -1,3 +1,6 @@ +/** + * @deprecated Use {@link Reader} instead. + */ export class Slice { constructor( public readonly uint8: Uint8Array, diff --git a/src/StreamingOctetReader.ts b/src/StreamingOctetReader.ts index 5370e7b..3ebe073 100644 --- a/src/StreamingOctetReader.ts +++ b/src/StreamingOctetReader.ts @@ -1,5 +1,11 @@ const fromCharCode = String.fromCharCode; +/** + * A streaming reader which internally manages multiple chunks of + * Uint8Array instances. For performance it does not merge the chunks into + * a single Uint8Array instance. Instead it keeps track of the chunks and + * reads across chunk boundaries as needed. + */ export class StreamingOctetReader { protected readonly chunks: Uint8Array[] = []; diff --git a/src/StreamingReader.ts b/src/StreamingReader.ts index 34caac1..28292e3 100644 --- a/src/StreamingReader.ts +++ b/src/StreamingReader.ts @@ -1,6 +1,7 @@ import {Writer} from './Writer'; import {decodeUtf8} from './utf8/decodeUtf8'; import type {IReader, IReaderResettable} from './types'; +import {Reader} from './Reader'; export class StreamingReader implements IReader, IReaderResettable { protected readonly writer: Writer; @@ -86,7 +87,7 @@ export class StreamingReader implements IReader, IReaderResettable { this.x += length; } - public buf(size: number): Uint8Array { + public buf(size: number = this.size()): Uint8Array { this.assertSize(size); const end = this.x + size; const bin = this.uint8.subarray(this.x, end); @@ -94,6 +95,35 @@ export class StreamingReader implements IReader, IReaderResettable { return bin; } + /** + * Creates a new {@link Reader} that references the same underlying memory + * buffer. But with independent cursor and end. + * + * @param start Start offset relative to the current cursor position. + * @param end End offset relative to the current cursor position. + * @returns A new {@link Reader} instance. + */ + public slice(start: number = 0, end?: number): Reader { + const x = this.x; + const actualStart = x + start; + const actualEnd = typeof end === 'number' ? x + end : this.size() + x - start; + return new Reader(this.uint8, this.view, actualStart, actualEnd); + } + + /** + * Similar to {@link slice} but also advances the cursor. Returns a new + * {@link Reader} that references the same underlying memory buffer, starting + * from the current cursor position. + * + * @param size Number of bytes to cut from the current position. + * @returns A new {@link Reader} instance. + */ + public cut(size: number = this.size()): Reader { + const slice = this.slice(0, size); + this.skip(size); + return slice; + } + public u8(): number { this.assertSize(1); return this.view.getUint8(this.x++); diff --git a/src/Writer.ts b/src/Writer.ts index 8b96de2..956abec 100644 --- a/src/Writer.ts +++ b/src/Writer.ts @@ -12,7 +12,7 @@ const from = hasBuffer ? Buffer.from : null; const textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null; /** - * Encoder class provides an efficient way to encode binary data. It grows the + * Writer class provides an efficient way to encode binary data. It grows the * internal memory buffer automatically as more space is required. It is useful * in cases when it is not known in advance the size of memory needed. */ diff --git a/src/__tests__/Reader.slice-cut.spec.ts b/src/__tests__/Reader.slice-cut.spec.ts new file mode 100644 index 0000000..f6268b4 --- /dev/null +++ b/src/__tests__/Reader.slice-cut.spec.ts @@ -0,0 +1,298 @@ +import {Reader} from '../Reader'; + +describe('Reader.slice() and Reader.cut() methods', () => { + describe('slice()', () => { + test('creates a new Reader with independent cursor', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + const slice = reader.slice(); + // slice starts at the same absolute position as reader + expect(slice.x).toBe(0); + expect(slice.u8()).toBe(1); + expect(slice.x).toBe(1); + // original reader's cursor should not be affected + expect(reader.x).toBe(0); + expect(reader.u8()).toBe(1); + expect(reader.x).toBe(1); + }); + + test('slice with no arguments returns full remaining buffer', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + reader.skip(2); // advance to position 2 + const slice = reader.slice(); + // slice starts at reader's current position and goes to end + expect(slice.x).toBe(2); + expect(slice.end).toBe(5); + expect(slice.size()).toBe(3); + expect(slice.buf()).toEqual(new Uint8Array([3, 4, 5])); + }); + + test('slice with start offset', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); + const slice = reader.slice(2); + expect(slice.x).toBe(2); + expect(slice.buf()).toEqual(new Uint8Array([3, 4, 5, 6, 7, 8])); + }); + + test('slice with start and end offsets', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); + const slice = reader.slice(2, 5); + expect(slice.x).toBe(2); + expect(slice.size()).toBe(3); + expect(slice.buf()).toEqual(new Uint8Array([3, 4, 5])); + }); + + test('slice after advancing cursor', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); + reader.skip(2); // position is now at index 2 (value 3) + const slice = reader.slice(1, 3); + // slice should start at reader.x + 1 = 3, end at reader.x + 3 = 5 + expect(slice.x).toBe(3); + expect(slice.size()).toBe(2); + expect(slice.buf()).toEqual(new Uint8Array([4, 5])); + }); + + test('slice does not advance original reader cursor', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + const slice = reader.slice(1, 3); + slice.u8(); + expect(reader.x).toBe(0); + }); + + test('slice shares underlying buffer', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + const slice = reader.slice(); + // Both should reference the same underlying Uint8Array + expect(slice.uint8).toBe(reader.uint8); + expect(slice.view).toBe(reader.view); + }); + + test('multiple slices are independent', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); + const slice1 = reader.slice(0, 4); + const slice2 = reader.slice(4, 8); + expect(slice1.u8()).toBe(1); + expect(slice2.u8()).toBe(5); + expect(slice1.u8()).toBe(2); + expect(slice2.u8()).toBe(6); + }); + + test('slice with size() for bounds', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + reader.skip(1); + const slice = reader.slice(); + expect(slice.x).toBe(1); + expect(slice.size()).toBe(4); + expect(slice.u8()).toBe(2); + }); + }); + + describe('cut()', () => { + test('creates slice and advances cursor', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + const cut = reader.cut(3); + // cut should contain first 3 bytes, starting at position 0 + expect(cut.x).toBe(0); + expect(cut.end).toBe(3); + expect(cut.size()).toBe(3); + expect(cut.u8()).toBe(1); + expect(cut.u8()).toBe(2); + expect(cut.u8()).toBe(3); + // original reader cursor should advance by 3 + expect(reader.x).toBe(3); + expect(reader.u8()).toBe(4); + }); + + test('cut with no arguments cuts entire remaining buffer', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + reader.skip(2); + const cut = reader.cut(); + expect(cut.x).toBe(2); + expect(cut.size()).toBe(3); + expect(cut.u8()).toBe(3); + expect(cut.u8()).toBe(4); + expect(cut.u8()).toBe(5); + // original reader should be at the end + expect(reader.x).toBe(5); + expect(reader.size()).toBe(0); + }); + + test('multiple cuts partition the buffer', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); + const cut1 = reader.cut(2); + const cut2 = reader.cut(3); + const cut3 = reader.cut(3); + + expect(cut1.x).toBe(0); + expect(cut1.u8()).toBe(1); + expect(cut1.u8()).toBe(2); + + expect(cut2.x).toBe(2); + expect(cut2.u8()).toBe(3); + expect(cut2.u8()).toBe(4); + expect(cut2.u8()).toBe(5); + + expect(cut3.x).toBe(5); + expect(cut3.u8()).toBe(6); + expect(cut3.u8()).toBe(7); + expect(cut3.u8()).toBe(8); + + expect(reader.x).toBe(8); + expect(reader.size()).toBe(0); + }); + + test('cut returns independent Reader instance', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + const cut = reader.cut(2); + cut.u8(); // advance cut cursor + expect(reader.x).toBe(2); + expect(cut.x).toBe(1); + expect(reader.buf()).toEqual(new Uint8Array([3, 4, 5])); + }); + + test('cut shares underlying buffer', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5])); + const cut = reader.cut(3); + // Both should reference the same underlying Uint8Array + expect(cut.uint8).toBe(reader.uint8); + expect(cut.view).toBe(reader.view); + }); + + test('cut can be used to parse structured data', () => { + const reader = new Reader(new Uint8Array([3, 1, 2, 3, 2, 4, 5])); + // Simulate a binary format with length-prefixed chunks + // [length: 3][data: 1,2,3][length: 2][data: 4,5] + + const len1 = reader.u8(); + const chunk1 = reader.cut(len1); + + const len2 = reader.u8(); + const chunk2 = reader.cut(len2); + + expect(chunk1.x).toBe(1); + expect(chunk1.size()).toBe(3); + expect(chunk1.u8()).toBe(1); + expect(chunk1.u8()).toBe(2); + expect(chunk1.u8()).toBe(3); + + expect(chunk2.x).toBe(5); + expect(chunk2.size()).toBe(2); + expect(chunk2.u8()).toBe(4); + expect(chunk2.u8()).toBe(5); + + expect(reader.x).toBe(7); + expect(reader.size()).toBe(0); + }); + + test('cut with size zero', () => { + const reader = new Reader(new Uint8Array([1, 2, 3])); + const cut = reader.cut(0); + expect(cut.size()).toBe(0); + expect(reader.x).toBe(0); + expect(reader.u8()).toBe(1); + }); + }); + + describe('slice() and cut() integration', () => { + test('slice then cut', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); + const slice = reader.slice(2, 6); // [3, 4, 5, 6] + const cut = slice.cut(2); // [3, 4] + + expect(cut.u8()).toBe(3); + expect(cut.u8()).toBe(4); + expect(slice.u8()).toBe(5); + expect(slice.u8()).toBe(6); + + // original reader should be unaffected + expect(reader.x).toBe(0); + }); + + test('cut then slice', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); + const cut = reader.cut(4); + const slice = cut.slice(1, 3); + + expect(slice.u8()).toBe(2); + expect(slice.u8()).toBe(3); + + expect(reader.x).toBe(4); + expect(reader.u8()).toBe(5); + }); + + test('nested cuts', () => { + const reader = new Reader(new Uint8Array([1, 2, 3, 4, 5, 6])); + const outer = reader.cut(6); + const inner1 = outer.cut(2); + const inner2 = outer.cut(2); + const inner3 = outer.cut(2); + + expect(inner1.u8()).toBe(1); + expect(inner1.u8()).toBe(2); + + expect(inner2.u8()).toBe(3); + expect(inner2.u8()).toBe(4); + + expect(inner3.u8()).toBe(5); + expect(inner3.u8()).toBe(6); + }); + }); + + describe('DataView access in slices and cuts', () => { + test('slice can use DataView methods', () => { + const reader = new Reader(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06])); + const slice = reader.slice(0, 4); + expect(slice.u16()).toBe(0x0102); + expect(slice.u16()).toBe(0x0304); + }); + + test('cut can use DataView methods', () => { + const reader = new Reader(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06])); + const cut = reader.cut(4); + expect(cut.u16()).toBe(0x0102); + expect(cut.u16()).toBe(0x0304); + expect(reader.u16()).toBe(0x0506); + }); + + test('slice with u32 reads', () => { + const buffer = new Uint8Array(8); + const view = new DataView(buffer.buffer); + view.setUint32(0, 0x12345678); + view.setUint32(4, 0x9abcdef0); + const reader = new Reader(buffer); + const slice = reader.slice(0, 8); + expect(slice.u32()).toBe(0x12345678); + expect(slice.u32()).toBe(0x9abcdef0); + }); + }); + + describe('edge cases', () => { + test('slice at the end of buffer', () => { + const reader = new Reader(new Uint8Array([1, 2, 3])); + reader.skip(3); + const slice = reader.slice(); + expect(slice.x).toBe(3); + expect(slice.size()).toBe(0); + }); + + test('cut at the end of buffer', () => { + const reader = new Reader(new Uint8Array([1, 2, 3])); + reader.skip(3); + const cut = reader.cut(); + expect(cut.x).toBe(3); + expect(cut.size()).toBe(0); + expect(reader.size()).toBe(0); + }); + + test('empty reader slice', () => { + const reader = new Reader(new Uint8Array([])); + const slice = reader.slice(); + expect(slice.size()).toBe(0); + }); + + test('empty reader cut', () => { + const reader = new Reader(new Uint8Array([])); + const cut = reader.cut(); + expect(cut.size()).toBe(0); + }); + }); +}); diff --git a/src/types.ts b/src/types.ts index 2345713..d891134 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import {Reader} from './Reader'; import type {Slice} from './Slice'; export interface IWriter { @@ -70,6 +71,26 @@ export interface IWriterGrowable { } export interface IReaderBase { + /** + * Creates a new {@link IReaderBase} that references the same underlying memory + * buffer. But with independent cursor and end. + * + * @param start Start offset relative to the current cursor position. + * @param end End offset relative to the current cursor position. + * @returns A new {@link IReaderBase} instance. + */ + slice(start?: number, end?: number): IReaderBase; + + /** + * Similar to {@link slice} but also advances the cursor. Returns a new + * {@link IReaderBase} that references the same underlying memory buffer, starting + * from the current cursor position. + * + * @param size Number of bytes to cut from the current position. + * @returns A new {@link IReaderBase} instance. + */ + cut(size?: number): IReaderBase; + /** Get current byte value without advancing the cursor. */ peek(): number; @@ -85,8 +106,12 @@ export interface IReaderBase { /** * Create a new Uint8Array view of provided length starting at * the current cursor position. + * + * If size is not provided, it will return a view of all remaining bytes. + * + * @param size Length of the returned Uint8Array. */ - buf(size: number): Uint8Array; + buf(size?: number): Uint8Array; u8(): number; i8(): number;