@@ -107,7 +107,11 @@ export const CML_DEFAULT_OPTIONS: CodecOptions = {
107107} as const
108108
109109/**
110- * Default CBOR encoding option for Data
110+ * Default CBOR encoding options for PlutusData.
111+ *
112+ * Uses indefinite-length arrays and maps. The `bounded_bytes` constraint
113+ * (Conway CDDL: byte strings ≤ 64 bytes) is enforced at the data-type layer
114+ * via the `BoundedBytes` CBOR node, independent of these codec options.
111115 *
112116 * @since 1.0.0
113117 * @category constants
@@ -123,13 +127,16 @@ export const CML_DATA_DEFAULT_OPTIONS: CodecOptions = {
123127} as const
124128
125129/**
126- * Aiken-compatible CBOR encoding options
130+ * Aiken-compatible CBOR encoding options.
127131 *
128- * Matches the encoding used by Aiken's cbor.serialise():
129- * - Indefinite-length arrays (9f...ff)
132+ * Matches the encoding produced by ` cbor.serialise()` in Aiken :
133+ * - Indefinite-length arrays (` 9f...ff` )
130134 * - Maps encoded as arrays of pairs (not CBOR maps)
131- * - Strings as bytearrays (major type 2, not 3)
132- * - Constructor tags: 121-127 for indices 0-6, then 1280+ for 7+
135+ * - Strings as byte arrays (major type 2, not 3)
136+ * - Constructor tags: 121–127 for indices 0–6, then 1280+ for 7+
137+ *
138+ * PlutusData byte strings are chunked per the Conway `bounded_bytes` rule
139+ * via the `BoundedBytes` CBOR node, independent of these codec options.
133140 *
134141 * @since 2.0.0
135142 * @category constants
@@ -333,6 +340,7 @@ export type CBOR =
333340 | null // null value
334341 | undefined // undefined value
335342 | number // floating point numbers
343+ | { _tag : "BoundedBytes" ; bytes : Uint8Array } // PlutusData bounded byte strings (Conway CDDL bounded_bytes = bytes .size (0..64))
336344
337345/**
338346 * **Record vs Map Key Ordering**
@@ -484,6 +492,7 @@ export const match = <R>(
484492 null : ( ) => R
485493 undefined : ( ) => R
486494 float : ( value : number ) => R
495+ boundedBytes : ( value : Uint8Array ) => R
487496 }
488497) : R => {
489498 if ( typeof value === "bigint" ) {
@@ -504,6 +513,9 @@ export const match = <R>(
504513 if ( isTag ( value ) ) {
505514 return patterns . tag ( value . tag , value . value )
506515 }
516+ if ( BoundedBytes . is ( value ) ) {
517+ return patterns . boundedBytes ( value . bytes )
518+ }
507519 if (
508520 typeof value === "object" &&
509521 value !== null &&
@@ -736,6 +748,15 @@ export const internalEncodeSync = (value: CBOR, options: CodecOptions = CML_DEFA
736748 if ( Array . isArray ( value ) ) return encodeArraySync ( value , options )
737749 if ( value instanceof Map ) return encodeMapSync ( value , options )
738750 if ( isTag ( value ) ) return encodeTagSync ( value . tag , value . value , options )
751+ // BoundedBytes: PlutusData byte strings, encoded per Conway CDDL bounded_bytes = bytes .size (0..64)
752+ if (
753+ typeof value === "object" &&
754+ value !== null &&
755+ "_tag" in value &&
756+ ( value as { _tag : unknown } ) . _tag === "BoundedBytes"
757+ ) {
758+ return encodeBoundedBytesSync ( ( value as { _tag : "BoundedBytes" ; bytes : Uint8Array } ) . bytes )
759+ }
739760 if (
740761 typeof value === "object" &&
741762 value !== null &&
@@ -828,6 +849,79 @@ const encodeNintSync = (value: bigint, options: CodecOptions): Uint8Array => {
828849 }
829850}
830851
852+ /** Byte-string chunk header size for a given chunk length (CBOR major type 2). */
853+ const chunkHeaderSize = ( len : number ) : number => {
854+ if ( len < 24 ) return 1
855+ if ( len < 256 ) return 2
856+ if ( len < 65536 ) return 3
857+ return 5
858+ }
859+
860+ /** Write a definite-length byte-string header for a chunk into `buf` at `pos`. Returns new position. */
861+ const writeChunkHeader = ( buf : Uint8Array , pos : number , len : number ) : number => {
862+ if ( len < 24 ) {
863+ buf [ pos ++ ] = 0x40 + len
864+ } else if ( len < 256 ) {
865+ buf [ pos ++ ] = 0x58
866+ buf [ pos ++ ] = len
867+ } else if ( len < 65536 ) {
868+ buf [ pos ++ ] = 0x59
869+ buf [ pos ++ ] = ( len >> 8 ) & 0xff
870+ buf [ pos ++ ] = len & 0xff
871+ } else {
872+ buf [ pos ++ ] = 0x5a
873+ buf [ pos ++ ] = ( len >> 24 ) & 0xff
874+ buf [ pos ++ ] = ( len >> 16 ) & 0xff
875+ buf [ pos ++ ] = ( len >> 8 ) & 0xff
876+ buf [ pos ++ ] = len & 0xff
877+ }
878+ return pos
879+ }
880+
881+ /**
882+ * Encodes a byte string under the Conway `bounded_bytes` rule:
883+ * - ≤ 64 bytes → definite-length CBOR bytes
884+ * - > 64 bytes → indefinite-length byte string (`0x5f` + 64-byte chunks + `0xff`)
885+ *
886+ * Called unconditionally by the `BoundedBytes` CBOR node handler. Does not
887+ * depend on `CodecOptions` — the rule is always applied.
888+ */
889+ const BOUNDED_BYTES_CHUNK_SIZE = 64
890+ const encodeBoundedBytesSync = ( value : Uint8Array ) : Uint8Array => {
891+ const length = value . length
892+ if ( length === 0 ) return new Uint8Array ( [ 0x40 ] )
893+ if ( length <= BOUNDED_BYTES_CHUNK_SIZE ) {
894+ // Definite-length encoding
895+ let headerLen : number
896+ if ( length < 24 ) headerLen = 1
897+ else if ( length < 256 ) headerLen = 2
898+ else headerLen = 3
899+ const out = new Uint8Array ( headerLen + length )
900+ if ( length < 24 ) out [ 0 ] = 0x40 + length
901+ else if ( length < 256 ) { out [ 0 ] = 0x58 ; out [ 1 ] = length }
902+ else { out [ 0 ] = 0x59 ; out [ 1 ] = length >> 8 ; out [ 2 ] = length & 0xff }
903+ out . set ( value , headerLen )
904+ return out
905+ }
906+ // Indefinite-length chunked byte string for > 64 bytes
907+ let totalSize = 2 // 0x5f + 0xff
908+ for ( let offset = 0 ; offset < length ; offset += BOUNDED_BYTES_CHUNK_SIZE ) {
909+ const chunkLen = Math . min ( BOUNDED_BYTES_CHUNK_SIZE , length - offset )
910+ totalSize += chunkHeaderSize ( chunkLen ) + chunkLen
911+ }
912+ const result = new Uint8Array ( totalSize )
913+ let pos = 0
914+ result [ pos ++ ] = 0x5f
915+ for ( let offset = 0 ; offset < length ; offset += BOUNDED_BYTES_CHUNK_SIZE ) {
916+ const chunkLen = Math . min ( BOUNDED_BYTES_CHUNK_SIZE , length - offset )
917+ pos = writeChunkHeader ( result , pos , chunkLen )
918+ result . set ( value . subarray ( offset , offset + BOUNDED_BYTES_CHUNK_SIZE ) , pos )
919+ pos += chunkLen
920+ }
921+ result [ pos ] = 0xff
922+ return result
923+ }
924+
831925const encodeBytesSync = ( value : Uint8Array , options : CodecOptions ) : Uint8Array => {
832926 const length = value . length
833927 const useMinimal = options . mode === "canonical" || ( options . mode === "custom" && options . useMinimalEncoding )
@@ -837,7 +931,7 @@ const encodeBytesSync = (value: Uint8Array, options: CodecOptions): Uint8Array =
837931 return new Uint8Array ( [ 0x40 ] )
838932 }
839933
840- // Optimize header encoding with direct buffer creation
934+ // Standard definite-length encoding
841935 let headerBytes : Uint8Array
842936 if ( length < 24 ) {
843937 headerBytes = new Uint8Array ( [ 0x40 + length ] )
@@ -863,6 +957,30 @@ const encodeBytesSync = (value: Uint8Array, options: CodecOptions): Uint8Array =
863957 return result
864958}
865959
960+ /**
961+ * `BoundedBytes` CBOR node — represents a PlutusData byte string that must comply
962+ * with the Conway CDDL constraint `bounded_bytes = bytes .size (0..64)`.
963+ *
964+ * The encoding rule is unconditional and options-independent:
965+ * - ≤ 64 bytes → definite-length CBOR bytes
966+ * - > 64 bytes → indefinite-length 64-byte chunked byte string (`0x5f` + chunks + `0xff`)
967+ *
968+ * Use `BoundedBytes.make` to construct the node; the encoder handles the rest.
969+ *
970+ * @since 2.0.0
971+ * @category model
972+ */
973+ export const BoundedBytes = {
974+ /** Construct a BoundedBytes CBOR node from a raw byte array. */
975+ make : ( bytes : Uint8Array ) : CBOR => ( { _tag : "BoundedBytes" as const , bytes } ) ,
976+ /** Type guard for BoundedBytes CBOR nodes. */
977+ is : ( value : CBOR ) : value is { _tag : "BoundedBytes" ; bytes : Uint8Array } =>
978+ typeof value === "object" &&
979+ value !== null &&
980+ "_tag" in value &&
981+ ( value as { _tag : unknown } ) . _tag === "BoundedBytes"
982+ } as const
983+
866984const encodeTextSync = ( value : string , options : CodecOptions ) : Uint8Array => {
867985 // Fast path for empty strings
868986 if ( value . length === 0 ) {
0 commit comments