Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 — 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",
"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",
"description": "Read and write structured binary data with typesafe views",
"license": "MIT",
"type": "module",
Expand Down