Skip to content

Commit bc8b39e

Browse files
authored
Add variable-length string and new bytes field type (#10)
1 parent fc53be1 commit bc8b39e

5 files changed

Lines changed: 106 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 0.17.0-pre — 2026-04-12
4+
5+
- Add variable-length `string` field: `byteLength` is now optional and defaults
6+
to the end of the struct's buffer.
7+
- Add `bytes` field: returns a live `Uint8Array` view into the struct's buffer,
8+
with optional fixed or variable length.
9+
310
## 0.16.2 — 2026-04-05
411

512
- Fix npm package imports under Node by shipping JavaScript entrypoints instead

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rotu/structview",
3-
"version": "0.16.2",
3+
"version": "0.17.0-pre",
44
"license": "MIT",
55
"tasks": {
66
"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"

fields.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,34 +228,61 @@ export function f64(fieldOffset: number): StructPropertyDescriptor<number> {
228228
}
229229

230230
/**
231-
* Field for a UTF-8 fixed-length string
231+
* Field for a UTF-8 string. When `byteLength` is provided, covers exactly that
232+
* many bytes starting at `fieldOffset`. When omitted, extends from `fieldOffset`
233+
* to the end of the struct's buffer (variable-length).
232234
*/
233235
export function string(
234236
fieldOffset: number,
235-
byteLength: number,
237+
byteLength?: number,
236238
): StructPropertyDescriptor<string> {
237239
const TEXT_DECODER = new TextDecoder()
238240
const TEXT_ENCODER = new TextEncoder()
239241
return {
240242
get() {
243+
const end = byteLength !== undefined
244+
? fieldOffset + byteLength
245+
: undefined
241246
const str = TEXT_DECODER.decode(
242-
structBytes(this, fieldOffset, fieldOffset + byteLength),
247+
structBytes(this, fieldOffset, end),
243248
)
244249
// trim all trailing null characters
245250
return str.replace(/\0+$/, "")
246251
},
247252
set(value) {
248-
const bytes = structBytes(
249-
this,
250-
fieldOffset,
251-
fieldOffset + byteLength,
252-
)
253+
const end = byteLength !== undefined
254+
? fieldOffset + byteLength
255+
: undefined
256+
const bytes = structBytes(this, fieldOffset, end)
253257
bytes.fill(0)
254258
TEXT_ENCODER.encodeInto(value, bytes)
255259
},
256260
}
257261
}
258262

263+
/**
264+
* Field for a live `Uint8Array` view into the struct's buffer. When
265+
* `byteLength` is provided, covers exactly that many bytes starting at
266+
* `fieldOffset`. When omitted, extends from `fieldOffset` to the end of the
267+
* struct's buffer (variable-length). The field is read-only; mutations happen
268+
* through the returned `Uint8Array` directly.
269+
*/
270+
export function bytes(
271+
fieldOffset: number,
272+
byteLength?: number,
273+
):
274+
& StructPropertyDescriptor<Uint8Array>
275+
& ReadOnlyAccessorDescriptor<Uint8Array> {
276+
return {
277+
get() {
278+
const end = byteLength !== undefined
279+
? fieldOffset + byteLength
280+
: undefined
281+
return structBytes(this, fieldOffset, end)
282+
},
283+
}
284+
}
285+
259286
/**
260287
* Field for a boolean stored in a byte (0 = false, nonzero = true)
261288
* True will be stored as 1

mod_test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
bigintle,
33
biguintle,
44
bool,
5+
bytes,
56
f16,
67
f32,
78
f64,
@@ -488,6 +489,67 @@ test("fromDataView with setter is writable and enumerable", () => {
488489
assert(keys.includes("val"))
489490
})
490491

492+
test("string variable-length (no byteLength)", () => {
493+
// 4 bytes prefix + 6 bytes for variable-length string
494+
const Cls = defineStruct({
495+
prefix: u32(0),
496+
name: string(4),
497+
})
498+
const c = new Cls(new Uint8Array(10))
499+
deepStrictEqual(c.name, "")
500+
c.name = "hello!"
501+
deepStrictEqual(c.name, "hello!")
502+
// trailing nulls are trimmed
503+
c.name = "hi"
504+
deepStrictEqual(c.name, "hi")
505+
// prefix field is unaffected
506+
c.prefix = 0xdeadbeef
507+
deepStrictEqual(c.prefix, 0xdeadbeef)
508+
deepStrictEqual(c.name, "hi")
509+
})
510+
511+
test("bytes fixed-length", () => {
512+
const buf = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7])
513+
const Cls = defineStruct({
514+
data: bytes(2, 4),
515+
})
516+
const c = new Cls(buf)
517+
expect(c.data).toBeInstanceOf(Uint8Array)
518+
deepStrictEqual(c.data.length, 4)
519+
// is a live view of the same underlying buffer
520+
strictEqual(c.data.buffer, buf.buffer)
521+
deepStrictEqual(c.data.byteOffset, 2)
522+
// mutations through the Uint8Array are reflected in buf
523+
c.data[0] = 0xff
524+
deepStrictEqual(buf[2], 0xff)
525+
// is read-only (no setter)
526+
throws(() => {
527+
// @ts-expect-error assigning to readonly property
528+
c.data = new Uint8Array(4)
529+
})
530+
})
531+
532+
test("bytes variable-length (no byteLength)", () => {
533+
const buf = new Uint8Array([10, 20, 30, 40, 50])
534+
const Cls = defineStruct({
535+
data: bytes(2),
536+
})
537+
const c = new Cls(buf)
538+
expect(c.data).toBeInstanceOf(Uint8Array)
539+
// extends from offset 2 to end of struct
540+
deepStrictEqual(c.data.length, 3)
541+
strictEqual(c.data.buffer, buf.buffer)
542+
deepStrictEqual(c.data.byteOffset, 2)
543+
// mutations through the Uint8Array are reflected in buf
544+
c.data[1] = 0xab
545+
deepStrictEqual(buf[3], 0xab)
546+
// is read-only (no setter)
547+
throws(() => {
548+
// @ts-expect-error assigning to readonly property
549+
c.data = new Uint8Array(3)
550+
})
551+
})
552+
491553
function hexToUint8Array(hex: string): Uint8Array {
492554
if (hex.length % 2 !== 0) {
493555
throw new TypeError("Hex input must have an even length")

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rotu/structview",
3-
"version": "0.16.2",
3+
"version": "0.17.0-pre",
44
"description": "Read and write structured binary data with typesafe views",
55
"license": "MIT",
66
"type": "module",

0 commit comments

Comments
 (0)