Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 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.
- 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
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rotu/structview",
"version": "0.16.2",
"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"
Expand Down
43 changes: 35 additions & 8 deletions fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,34 +228,61 @@ export function f64(fieldOffset: number): StructPropertyDescriptor<number> {
}

/**
* 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<string> {
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<Uint8Array>
& ReadOnlyAccessorDescriptor<Uint8Array> {
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
Expand Down
62 changes: 62 additions & 0 deletions mod_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
bigintle,
biguintle,
bool,
bytes,
f16,
f32,
f64,
Expand Down Expand Up @@ -488,6 +489,67 @@ 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")
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rotu/structview",
"version": "0.16.2",
"version": "0.17.0-pre",
"description": "Read and write structured binary data with typesafe views",
"license": "MIT",
"type": "module",
Expand Down