From 930c178f24205f0791488d325f1372151062f790 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:10:41 +0000 Subject: [PATCH 1/6] Initial plan From f62aaae182277214493dc35e20c2a1a3c40bf9f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:12:16 +0000 Subject: [PATCH 2/6] Add variable-length string and bytes field type with tests Agent-Logs-Url: https://github.com/rotu/structview/sessions/a76e014c-40c4-4051-80ca-4fb84eee5b03 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- fields.ts | 41 +++++++++++++++++++++++++++------- mod_test.ts | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/fields.ts b/fields.ts index eaf0a40..2e054f1 100644 --- a/fields.ts +++ b/fields.ts @@ -228,34 +228,59 @@ export function f64(fieldOffset: number): StructPropertyDescriptor { } /** - * Field for a UTF-8 fixed-length string + * Field for a UTF-8 string. When `byteLength` is provided, covers exactly that + * many bytes starting at `fieldOffset`. When omitted, extends from `fieldOffset` + * to the end of the struct's buffer (variable-length). */ export function string( fieldOffset: number, - byteLength: number, + byteLength?: number, ): StructPropertyDescriptor { const TEXT_DECODER = new TextDecoder() const TEXT_ENCODER = new TextEncoder() return { get() { + const end = byteLength !== undefined + ? fieldOffset + byteLength + : undefined const str = TEXT_DECODER.decode( - structBytes(this, fieldOffset, fieldOffset + byteLength), + structBytes(this, fieldOffset, end), ) // trim all trailing null characters return str.replace(/\0+$/, "") }, set(value) { - const bytes = structBytes( - this, - fieldOffset, - fieldOffset + byteLength, - ) + const end = byteLength !== undefined + ? fieldOffset + byteLength + : undefined + const bytes = structBytes(this, fieldOffset, end) bytes.fill(0) TEXT_ENCODER.encodeInto(value, bytes) }, } } +/** + * Field for a live `Uint8Array` view into the struct's buffer. When + * `byteLength` is provided, covers exactly that many bytes starting at + * `fieldOffset`. When omitted, extends from `fieldOffset` to the end of the + * struct's buffer (variable-length). The field is read-only; mutations happen + * through the returned `Uint8Array` directly. + */ +export function bytes( + fieldOffset: number, + byteLength?: number, +): StructPropertyDescriptor & ReadOnlyAccessorDescriptor { + return { + get() { + const end = byteLength !== undefined + ? fieldOffset + byteLength + : undefined + return structBytes(this, fieldOffset, end) + }, + } +} + /** * Field for a boolean stored in a byte (0 = false, nonzero = true) * True will be stored as 1 diff --git a/mod_test.ts b/mod_test.ts index 44458a4..a833d36 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -2,6 +2,7 @@ import { bigintle, biguintle, bool, + bytes, f16, f32, f64, @@ -488,6 +489,68 @@ test("fromDataView with setter is writable and enumerable", () => { assert(keys.includes("val")) }) +test("string variable-length (no byteLength)", () => { + // 4 bytes prefix + 6 bytes for variable-length string + const Cls = defineStruct({ + prefix: u32(0), + name: string(4), + }) + const c = new Cls(new Uint8Array(10)) + deepStrictEqual(c.name, "") + c.name = "hello!" + deepStrictEqual(c.name, "hello!") + // trailing nulls are trimmed + c.name = "hi" + deepStrictEqual(c.name, "hi") + // prefix field is unaffected + c.prefix = 0xdeadbeef + deepStrictEqual(c.prefix, 0xdeadbeef) + deepStrictEqual(c.name, "hi") +}) + +test("bytes fixed-length", () => { + const buf = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]) + const Cls = defineStruct({ + data: bytes(2, 4), + }) + const c = new Cls(buf) + expect(c.data).toBeInstanceOf(Uint8Array) + deepStrictEqual(c.data.length, 4) + // is a live view of the same underlying buffer + strictEqual(c.data.buffer, buf.buffer) + deepStrictEqual(c.data.byteOffset, 2) + // mutations through the Uint8Array are reflected in buf + c.data[0] = 0xff + deepStrictEqual(buf[2], 0xff) + // is read-only (no setter) + throws(() => { + // @ts-expect-error assigning to readonly property + c.data = new Uint8Array(4) + }) +}) + +test("bytes variable-length (no byteLength)", () => { + const buf = new Uint8Array([10, 20, 30, 40, 50]) + const Cls = defineStruct({ + data: bytes(2), + }) + const c = new Cls(buf) + expect(c.data).toBeInstanceOf(Uint8Array) + // extends from offset 2 to end of struct + deepStrictEqual(c.data.length, 3) + strictEqual(c.data.buffer, buf.buffer) + deepStrictEqual(c.data.byteOffset, 2) + // mutations through the Uint8Array are reflected in buf + c.data[1] = 0xab + deepStrictEqual(buf[3], 0xab) + // is read-only (no setter) + throws(() => { + // @ts-expect-error assigning to readonly property + c.data = new Uint8Array(3) + }) +}) + + function hexToUint8Array(hex: string): Uint8Array { if (hex.length % 2 !== 0) { throw new TypeError("Hex input must have an even length") From db9b848cba9d42edb0629c8df7c23c92f7714aa2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:13:11 +0000 Subject: [PATCH 3/6] Remove extra blank line in test file Agent-Logs-Url: https://github.com/rotu/structview/sessions/a76e014c-40c4-4051-80ca-4fb84eee5b03 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- mod_test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/mod_test.ts b/mod_test.ts index a833d36..7a04b69 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -550,7 +550,6 @@ test("bytes variable-length (no byteLength)", () => { }) }) - function hexToUint8Array(hex: string): Uint8Array { if (hex.length % 2 !== 0) { throw new TypeError("Hex input must have an even length") From f4d23db4abdf89c7749b4e85dff0d7a14b7c126b Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 12 Apr 2026 13:55:19 -0500 Subject: [PATCH 4/6] fix fmt --- fields.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fields.ts b/fields.ts index 2e054f1..d4cdb57 100644 --- a/fields.ts +++ b/fields.ts @@ -270,7 +270,9 @@ export function string( export function bytes( fieldOffset: number, byteLength?: number, -): StructPropertyDescriptor & ReadOnlyAccessorDescriptor { +): + & StructPropertyDescriptor + & ReadOnlyAccessorDescriptor { return { get() { const end = byteLength !== undefined From f8b59f1a6d4c0216af905dcd4de66946887a48ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:00:04 +0000 Subject: [PATCH 5/6] Bump version to 0.17.0 and add changelog entry Agent-Logs-Url: https://github.com/rotu/structview/sessions/bfcb4acd-f01b-4495-9580-419915c00f9e Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ deno.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce1784..150efb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.17.0 — 2026-04-12 + +- Add variable-length `string` field: `byteLength` is now optional and defaults + to the end of the struct's buffer. +- Add `bytes` field: returns a live `Uint8Array` view into the struct's buffer, + with optional fixed or variable length. + ## 0.16.2 — 2026-04-05 - Fix npm package imports under Node by shipping JavaScript entrypoints instead diff --git a/deno.json b/deno.json index 4cef62b..76d145d 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@rotu/structview", - "version": "0.16.2", + "version": "0.17.0", "license": "MIT", "tasks": { "validate": "deno run -A scripts/sync-package-metadata.ts --check && deno fmt --check && deno lint && deno x vitest run && deno publish --dry-run --allow-dirty && npm pack --dry-run && npm run smoke:npm" diff --git a/package.json b/package.json index 9626b77..85bdf19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rotu/structview", - "version": "0.16.2", + "version": "0.17.0", "description": "Read and write structured binary data with typesafe views", "license": "MIT", "type": "module", From 7549a42e23e6d327469f817f2521e8c05c50b15a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:05:21 +0000 Subject: [PATCH 6/6] Use prerelease version 0.17.0-pre Agent-Logs-Url: https://github.com/rotu/structview/sessions/a2fba9b2-26b1-4194-a770-c799e30f326d Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- CHANGELOG.md | 2 +- deno.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 150efb8..762fac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.17.0 — 2026-04-12 +## 0.17.0-pre — 2026-04-12 - Add variable-length `string` field: `byteLength` is now optional and defaults to the end of the struct's buffer. diff --git a/deno.json b/deno.json index 76d145d..19e1b10 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@rotu/structview", - "version": "0.17.0", + "version": "0.17.0-pre", "license": "MIT", "tasks": { "validate": "deno run -A scripts/sync-package-metadata.ts --check && deno fmt --check && deno lint && deno x vitest run && deno publish --dry-run --allow-dirty && npm pack --dry-run && npm run smoke:npm" diff --git a/package.json b/package.json index 85bdf19..95ff53b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rotu/structview", - "version": "0.17.0", + "version": "0.17.0-pre", "description": "Read and write structured binary data with typesafe views", "license": "MIT", "type": "module",