@@ -233,26 +233,61 @@ export function f64(fieldOffset: number): StructPropertyDescriptor<number> {
233233export function string (
234234 fieldOffset : number ,
235235 byteLength : number ,
236+ ) : StructPropertyDescriptor < string >
237+ /**
238+ * Field for a UTF-8 string whose byte length is determined at read time.
239+ *
240+ * @remarks The returned descriptor is read-only because writing a string of
241+ * variable size requires external coordination (e.g. also updating the length
242+ * field). Use `fromDataView` for a writable custom implementation.
243+ *
244+ * @param fieldOffset - Byte offset of the string within the struct.
245+ * @param options.length - A property name on the struct whose value gives the
246+ * byte length, or a function `(dv: DataView) => number` that computes it.
247+ */
248+ export function string (
249+ fieldOffset : number ,
250+ options : { readonly length : string | ( ( dv : DataView ) => number ) } ,
251+ ) : StructPropertyDescriptor < string > & ReadOnlyAccessorDescriptor < string >
252+ export function string (
253+ fieldOffset : number ,
254+ arg : number | { readonly length : string | ( ( dv : DataView ) => number ) } ,
236255) : StructPropertyDescriptor < string > {
237256 const TEXT_DECODER = new TextDecoder ( )
238257 const TEXT_ENCODER = new TextEncoder ( )
258+ if ( typeof arg === "number" ) {
259+ const byteLength = arg
260+ return {
261+ get ( ) {
262+ const str = TEXT_DECODER . decode (
263+ structBytes ( this , fieldOffset , fieldOffset + byteLength ) ,
264+ )
265+ // trim all trailing null characters
266+ return str . replace ( / \0 + $ / , "" )
267+ } ,
268+ set ( value ) {
269+ const bytes = structBytes (
270+ this ,
271+ fieldOffset ,
272+ fieldOffset + byteLength ,
273+ )
274+ bytes . fill ( 0 )
275+ TEXT_ENCODER . encodeInto ( value , bytes )
276+ } ,
277+ }
278+ }
279+ const { length } = arg
239280 return {
240281 get ( ) {
282+ const dv = structDataView ( this )
283+ const len : number = typeof length === "string"
284+ ? ( Reflect . get ( this , length ) as number )
285+ : length ( dv )
241286 const str = TEXT_DECODER . decode (
242- structBytes ( this , fieldOffset , fieldOffset + byteLength ) ,
287+ structBytes ( this , fieldOffset , fieldOffset + len ) ,
243288 )
244- // trim all trailing null characters
245289 return str . replace ( / \0 + $ / , "" )
246290 } ,
247- set ( value ) {
248- const bytes = structBytes (
249- this ,
250- fieldOffset ,
251- fieldOffset + byteLength ,
252- )
253- bytes . fill ( 0 )
254- TEXT_ENCODER . encodeInto ( value , bytes )
255- } ,
256291 }
257292}
258293
@@ -272,11 +307,80 @@ export function bool(fieldOffset: number): StructPropertyDescriptor<boolean> {
272307}
273308
274309/**
275- * Define a descriptor based on a dataview of the struct
276- * @param fieldGetter function which, given a dataview, returns the field value
277- * @param fieldSetter optional function which, given a dataview and a value, sets the field value
278- * @returns an enumerable property descriptor; readonly if no setter is provided
310+ * Wrap a field descriptor to make it optional, returning `null` when the field
311+ * is considered absent.
312+ *
313+ * The returned descriptor inherits the writability of the wrapped descriptor:
314+ * - If `descriptor` has a setter, the returned descriptor also has a setter.
315+ * - If `descriptor` has no setter (read-only), the returned descriptor is
316+ * read-only too.
317+ *
318+ * Two presence strategies are supported:
319+ *
320+ * **Sentinel value** — the field is absent when its binary value equals the
321+ * sentinel (compared via `Object.is`). Setting the property to `null` writes
322+ * the sentinel back into the buffer.
323+ *
324+ * ```ts
325+ * const Cls = defineStruct({
326+ * value: optional(u16(0), { sentinel: 0xffff }),
327+ * })
328+ * ```
329+ *
330+ * **Predicate function** — the field is absent when `(dv: DataView) => boolean`
331+ * returns `false`. Setting the field to a non-null value writes it normally;
332+ * setting to `null` is a no-op.
333+ *
334+ * ```ts
335+ * const Cls = defineStruct({
336+ * flags: u8(0),
337+ * value: optional(u32(4), (dv) => (dv.getUint8(0) & 0x01) !== 0),
338+ * })
339+ * ```
279340 */
341+ export function optional < T > (
342+ descriptor : StructPropertyDescriptor < T > & { set ( t : T ) : undefined } ,
343+ presence : ( ( dv : DataView ) => boolean ) | { readonly sentinel : T } ,
344+ ) : StructPropertyDescriptor < T | null >
345+ export function optional < T > (
346+ descriptor : StructPropertyDescriptor < T > ,
347+ presence : ( ( dv : DataView ) => boolean ) | { readonly sentinel : T } ,
348+ ) : StructPropertyDescriptor < T | null > & ReadOnlyAccessorDescriptor < T | null >
349+ export function optional < T > (
350+ descriptor : StructPropertyDescriptor < T > ,
351+ presence : ( ( dv : DataView ) => boolean ) | { readonly sentinel : T } ,
352+ ) : StructPropertyDescriptor < T | null > {
353+ if ( typeof presence === "function" ) {
354+ const result : StructPropertyDescriptor < T | null > = {
355+ get ( ) {
356+ if ( ! presence ( structDataView ( this ) ) ) return null
357+ return descriptor . get ! . call ( this ) as T
358+ } ,
359+ }
360+ if ( typeof descriptor . set === "function" ) {
361+ result . set = function ( value : T | null ) {
362+ if ( value !== null ) {
363+ descriptor . set ! . call ( this , value )
364+ }
365+ }
366+ }
367+ return result
368+ }
369+ const { sentinel } = presence
370+ const result : StructPropertyDescriptor < T | null > = {
371+ get ( ) {
372+ const value = descriptor . get ! . call ( this ) as T
373+ return Object . is ( value , sentinel ) ? null : value
374+ } ,
375+ }
376+ if ( typeof descriptor . set === "function" ) {
377+ result . set = function ( value : T | null ) {
378+ descriptor . set ! . call ( this , value === null ? sentinel : value )
379+ }
380+ }
381+ return result
382+ }
383+
280384export function fromDataView < T > (
281385 fieldGetter : ( dv : DataView ) => T ,
282386 fieldSetter : ( dv : DataView , value : T ) => void ,
@@ -351,8 +455,12 @@ export function substruct<
351455export function typedArray < T > (
352456 fieldOffset : number ,
353457 kwargs : {
354- /** length or property name for the length of the array */
355- readonly length : number | string | undefined
458+ /** length, property name, function, or undefined (fill remaining buffer) */
459+ readonly length :
460+ | number
461+ | string
462+ | ( ( dv : DataView ) => number )
463+ | undefined
356464 /** TypedArray constructor */
357465 readonly species : TypedArraySpecies < T >
358466 } ,
@@ -370,6 +478,8 @@ export function typedArray<T>(
370478 lengthValue = length
371479 } else if ( typeof length === "string" ) {
372480 lengthValue = Reflect . get ( this , length )
481+ } else {
482+ lengthValue = length ( dv )
373483 }
374484 return new species (
375485 dv . buffer ,
0 commit comments