4545//! the output. Control data (a plan, a context, an options object, query
4646//! statistics) carries no secret and is read from its frame with serde.
4747
48- use stack_encrypt:: dynamic:: { self , FfiValue , Scope , ValueFrame } ;
48+ use stack_encrypt:: dynamic:: { self , Scope , ValueFrame } ;
4949use stack_encrypt:: sem:: MatchOptions ;
5050use stack_encrypt:: target:: IndexSpec ;
5151use stack_encrypt:: ErrorPayload ;
52- use stack_encrypt:: { CipherText , Encrypt , KeysetCipher , Label , Node , SealedValue , StackCipherText } ;
52+ use stack_encrypt:: { CipherText , KeysetCipher , Label , Node , SealedValue , StackCipherText } ;
5353use stack_kms:: DataKeySource ;
54- use vitaminc_aead_frame:: { decode_ciphertext, encode_ciphertext, Frame , FrameWriter } ;
55- use vitaminc_protected:: Controlled ;
54+ use vitaminc_aead_frame:: { decode_ciphertext, encode_ciphertext, Frame } ;
5655use zeroize:: Zeroizing ;
5756
5857use stack_guest_abi:: last_error;
@@ -232,7 +231,8 @@ where
232231/// Derive the query term for one plan field, an operator and its input
233232/// (ADR-0009; ADR-0007, amended 2026-10-16): a codec-encoded input, the
234233/// codec-encoded plan, the field's name and the operator's name (both UTF-8)
235- /// in, the term's codec-encoded object out ([`dynamic::QueryTerm::to_value`]):
234+ /// in, the term's codec-encoded object out (the `Serialize` form of
235+ /// [`dynamic::QueryTerm`]):
236236///
237237/// ```text
238238/// { "operator": "matches", "fidelity": "probabilistic", "index": "match",
@@ -269,7 +269,7 @@ where
269269 . map_err ( |e| fail_dynamic ( & e) ) ?
270270 . await
271271 . map_err ( |e| fail_error ( & e) ) ?;
272- encode_value ( term. to_value ( ) )
272+ encode_answer ( & term)
273273}
274274
275275/// Verify a page of decrypted values against a probabilistic query: the
@@ -303,9 +303,7 @@ pub fn verify(
303303 & resolver ( ) ,
304304 )
305305 . map_err ( |e| fail_dynamic ( & e) ) ?;
306- encode_value ( FfiValue :: Array (
307- verified. into_iter ( ) . map ( FfiValue :: Bool ) . collect ( ) ,
308- ) )
306+ encode_answer ( & verified)
309307}
310308
311309/// How many rows a probabilistic query should fetch for `want` of them to
@@ -314,7 +312,8 @@ pub fn verify(
314312/// ([`Stats::from_frame`](stack_encrypt::op::Stats::from_frame)), the plan,
315313/// the field's name and the operator's name in; the codec-encoded limit out,
316314/// an unsigned 64-bit integer or null for no finite fetch
317- /// ([`dynamic::limit_to_value`]). Needs no cipher and no key.
315+ /// ([`Limit::rows`](stack_encrypt::op::Limit::rows)). Needs no cipher and
316+ /// no key.
318317pub fn target_limit (
319318 term : & [ u8 ] ,
320319 stats : & [ u8 ] ,
@@ -330,7 +329,7 @@ pub fn target_limit(
330329 . map_err ( |e| fail_dynamic ( & e. in_field ( field) ) ) ?;
331330 let limit = dynamic:: target_limit ( & plan, field, operator, term, want, & stats, & resolver ( ) )
332331 . map_err ( |e| fail_dynamic ( & e) ) ?;
333- encode_value ( dynamic :: limit_to_value ( limit) )
332+ encode_answer ( & limit. rows ( ) )
334333}
335334
336335/// The plan, field name and operator a query names: the plan parsed against
@@ -383,8 +382,8 @@ pub fn plan_check(plan: &[u8]) -> Result<(), u32> {
383382
384383/// The EQL types this build of the engine knows, as a codec-encoded
385384/// `{"targets": [...]}`: one entry per type in
386- /// [`TargetDescriptor::to_value `](stack_encrypt::dynamic::TargetDescriptor::to_value )'s
387- /// wire form, producible or not, with the reason when not. The build
385+ /// [`TargetDescriptor`](stack_encrypt::dynamic::TargetDescriptor)'s
386+ /// `Serialize` form, producible or not, with the reason when not. The build
388387/// without EQL types lists none, which a generator reads as "no
389388/// `encrypt_into` type is available" and refuses the tag; the `eql` build
390389/// lists the catalog, and a generator writes `encrypt_into` only for an
@@ -413,22 +412,19 @@ pub fn plan_check(plan: &[u8]) -> Result<(), u32> {
413412/// type answers and how far each answer can be trusted.
414413pub fn targets ( ) -> Result < Vec < u8 > , u32 > {
415414 use stack_encrypt:: dynamic:: TargetResolver as _;
416- encode_value ( FfiValue :: Object ( vec ! [ (
417- "targets" . to_string( ) ,
418- FfiValue :: Array (
419- resolver( )
420- . targets( )
421- . iter( )
422- . map( |target| target. to_value( ) )
423- . collect( ) ,
424- ) ,
425- ) ] ) )
415+ #[ derive( serde:: Serialize ) ]
416+ struct Targets {
417+ targets : Vec < dynamic:: TargetDescriptor > ,
418+ }
419+ encode_answer ( & Targets {
420+ targets : resolver ( ) . targets ( ) ,
421+ } )
426422}
427423
428424/// The operators one field of a codec-encoded plan answers, as a
429425/// codec-encoded list: one object per operator, in the order the engine
430- /// lists them, each written by
431- /// [`dynamic::operator_to_value `]:
426+ /// lists them, each the `Serialize` form of a
427+ /// [`dynamic::OperatorEntry `]:
432428///
433429/// ```text
434430/// [ { "operator": "eq", "fidelity": "exact", "arity": "one", "index": "eq" }, ... ]
@@ -447,12 +443,12 @@ pub fn operators(plan: &[u8], field: &[u8]) -> Result<Vec<u8>, u32> {
447443 . map_err ( |_| last_error:: malformed ( "the field name is not UTF-8" ) ) ?;
448444 let operators =
449445 dynamic:: field_operators ( & plan, field, & resolver ( ) ) . map_err ( |e| fail_dynamic ( & e) ) ?;
450- encode_value ( FfiValue :: Array (
451- operators
446+ encode_answer (
447+ & operators
452448 . iter ( )
453- . map ( |( operator, index) | dynamic:: operator_to_value ( * operator, index) )
454- . collect ( ) ,
455- ) )
449+ . map ( |( operator, index) | dynamic:: OperatorEntry :: new ( * operator, index) )
450+ . collect :: < Vec < _ > > ( ) ,
451+ )
456452}
457453
458454// =============================================================================
@@ -538,7 +534,7 @@ fn checked_plaintext(bytes: &[u8]) -> Result<Frame<'_>, u32> {
538534}
539535
540536/// An opened value as the output buffer: its frame, copied into a buffer
541- /// whose capacity is exactly its length (see [`encode_value `]); the frame
537+ /// whose capacity is exactly its length (see [`encode_tree `]); the frame
542538/// is wiped when it drops.
543539fn encode_frame ( frame : ValueFrame ) -> Result < Vec < u8 > , u32 > {
544540 let mut out = exact_buffer ( Some ( frame. risky_ref ( ) . len ( ) ) ) ?;
@@ -556,23 +552,17 @@ fn not_a_frame() -> u32 {
556552 last_error:: malformed ( "an input is not a value in the transport frame" )
557553}
558554
559- /// Encode a value tree into a buffer whose capacity is exactly its length.
560- ///
561- /// This buffer is plaintext on the decrypt path, and the registry wipes only
562- /// the buffer it holds: `register`'s `into_boxed_slice` reallocates a `Vec`
563- /// whose capacity exceeds its length, leaving the old allocation unwiped.
564- /// The frame writer reserves an upper bound per leaf (a six-byte string
565- /// reserves 14 for 11), so writing straight into the exactly sized buffer
566- /// can still grow it. It writes into a `Zeroizing` staging buffer instead
567- /// (the writer wipes any allocation it replaces), which is copied into an
568- /// exactly sized output and then wiped.
569- fn encode_value ( value : FfiValue ) -> Result < Vec < u8 > , u32 > {
570- let mut staging = Zeroizing :: new ( exact_buffer ( value_encoded_len ( & value) ) ?) ;
571- ( & value)
572- . visit ( FrameWriter :: new ( & mut staging) )
573- . map_err ( |_| STATUS_ENCODING ) ?;
574- let mut out = exact_buffer ( Some ( staging. len ( ) ) ) ?;
575- out. extend_from_slice ( & staging) ;
555+ /// One of the engine's answers (a query term, a page of verdicts, a limit, a
556+ /// catalog), written to its frame by serde, in a buffer whose capacity is
557+ /// exactly its length. An answer is not plaintext, but the registry's
558+ /// `into_boxed_slice` would reallocate a buffer with spare capacity, so it
559+ /// is sized as every other output is. A query term's search tokens are
560+ /// derived from plaintext, so the staging buffer is wiped once copied.
561+ fn encode_answer < T : serde:: Serialize > ( answer : & T ) -> Result < Vec < u8 > , u32 > {
562+ let frame =
563+ Zeroizing :: new ( vitaminc_aead_frame:: serde:: to_vec ( answer) . map_err ( |_| STATUS_ENCODING ) ?) ;
564+ let mut out = exact_buffer ( Some ( frame. len ( ) ) ) ?;
565+ out. extend_from_slice ( & frame) ;
576566 Ok ( out)
577567}
578568
@@ -589,8 +579,9 @@ fn decode_tree(bytes: &[u8]) -> Result<StackCipherText, u32> {
589579}
590580
591581/// The encode twin of [`decode_tree`]. Passthrough nodes can carry caller
592- /// plaintext, so this is sized up front for the same reason
593- /// [`encode_value`] is.
582+ /// plaintext, so this is sized up front: the registry wipes only the buffer
583+ /// it holds, and `register`'s `into_boxed_slice` reallocates a `Vec` whose
584+ /// capacity exceeds its length, leaving the old allocation unwiped.
594585fn encode_tree ( tree : StackCipherText ) -> Result < Vec < u8 > , u32 > {
595586 let tree = map_leaves ( tree, & mut |l : SealedValue | Ok :: < _ , u32 > ( l. to_bytes ( ) ) ) ?;
596587 let mut out = exact_buffer ( tree_encoded_len ( & tree) ) ?;
@@ -640,29 +631,6 @@ fn exact_buffer(len: Option<usize>) -> Result<Vec<u8>, u32> {
640631 Ok ( out)
641632}
642633
643- /// Exact byte length of the frame of `value`. `None` on overflow or on a
644- /// length the frame's `u32` lengths cannot express — the encode would fail
645- /// on those anyway, so the caller reports an encoding error.
646- fn value_encoded_len ( value : & FfiValue ) -> Option < usize > {
647- // tag byte + fixed payload, or tag + u32 length prefix + payload.
648- let framed = |len : usize | u32:: try_from ( len) . ok ( ) . and_then ( |_| len. checked_add ( 5 ) ) ;
649- match value {
650- FfiValue :: Null | FfiValue :: Undefined | FfiValue :: Bool ( _) => Some ( 1 ) ,
651- FfiValue :: Int32 ( _) | FfiValue :: UInt32 ( _) | FfiValue :: Float32 ( _) => Some ( 5 ) ,
652- FfiValue :: Int64 ( _) | FfiValue :: UInt64 ( _) | FfiValue :: Float64 ( _) => Some ( 9 ) ,
653- FfiValue :: String ( s) => framed ( s. risky_ref ( ) . len ( ) ) ,
654- FfiValue :: Bytes ( b) => framed ( b. risky_ref ( ) . len ( ) ) ,
655- FfiValue :: Array ( items) => items. iter ( ) . try_fold ( 5usize , |acc, item| {
656- acc. checked_add ( value_encoded_len ( item) ?)
657- } ) ,
658- FfiValue :: Object ( entries) => entries. iter ( ) . try_fold ( 5usize , |acc, ( key, value) | {
659- acc. checked_add ( framed ( key. len ( ) ) ?. checked_sub ( 1 ) ?) ?
660- . checked_add ( value_encoded_len ( value) ?)
661- } ) ,
662- FfiValue :: Passthrough ( inner) => value_encoded_len ( inner) ?. checked_add ( 1 ) ,
663- }
664- }
665-
666634/// Exact byte length of the codec's encoding of a ciphertext tree. A
667635/// passthrough is its node byte and its frame, written as it is.
668636fn tree_encoded_len ( tree : & BytesTree ) -> Option < usize > {
@@ -685,65 +653,32 @@ fn tree_encoded_len(tree: &BytesTree) -> Option<usize> {
685653#[ cfg( test) ]
686654mod tests {
687655 use super :: * ;
688- use vitaminc_protected:: Protected ;
689-
690- // `value_encoded_len` / `tree_encoded_len` re-derive the frame's and the
691- // codec's arithmetic; neither exports an `encoded_len` of its own, so these
692- // pins are the only thing that fails if the two drift. Drift is not a
693- // cosmetic bug: an undersized reservation makes `encode_value`
694- // reallocate mid-encode, leaving unwiped partial plaintext in the
695- // abandoned allocation — silently.
696-
697- fn every_value_shape ( ) -> Vec < FfiValue > {
698- vec ! [
699- FfiValue :: Null ,
700- FfiValue :: Undefined ,
701- FfiValue :: Bool ( true ) ,
702- FfiValue :: Int32 ( -5 ) ,
703- FfiValue :: UInt32 ( 5 ) ,
704- FfiValue :: Float32 ( 1.5 ) ,
705- FfiValue :: Int64 ( -9 ) ,
706- FfiValue :: UInt64 ( 9 ) ,
707- FfiValue :: Float64 ( 2.5 ) ,
708- FfiValue :: String ( "" . into( ) ) ,
709- FfiValue :: String ( "héllo" . into( ) ) ,
710- FfiValue :: Bytes ( Protected :: new( Vec :: new( ) ) ) ,
711- FfiValue :: Bytes ( Protected :: new( vec![ 0u8 ; 300 ] ) ) ,
712- FfiValue :: Array ( Vec :: new( ) ) ,
713- FfiValue :: Array ( vec![ FfiValue :: Bool ( false ) , FfiValue :: String ( "x" . into( ) ) ] ) ,
714- FfiValue :: Object ( Vec :: new( ) ) ,
715- FfiValue :: Object ( vec![
716- ( "a" . to_string( ) , FfiValue :: Int32 ( 1 ) ) ,
717- (
718- "nested" . to_string( ) ,
719- FfiValue :: Object ( vec![ ( "b" . to_string( ) , FfiValue :: Null ) ] ) ,
720- ) ,
721- ] ) ,
722- FfiValue :: Passthrough ( Box :: new( FfiValue :: Int64 ( 7 ) ) ) ,
723- FfiValue :: Passthrough ( Box :: new( FfiValue :: Array ( vec![ FfiValue :: String (
724- "deep" . into( ) ,
725- ) ] ) ) ) ,
726- ]
727- }
656+ use stack_encrypt:: dynamic:: FfiValue ;
657+ use vitaminc_protected:: { Controlled , Protected } ;
728658
729- /// The registry wipes the buffer it is handed, and `into_boxed_slice`
730- /// reallocates one whose capacity exceeds its length, leaving the old
731- /// allocation unwiped. So every encoded output's capacity is its length,
732- /// not only its length the frame's.
733- #[ test]
734- fn an_encoded_value_has_no_spare_capacity ( ) {
735- for ( i, value) in every_value_shape ( ) . into_iter ( ) . enumerate ( ) {
736- let out = encode_value ( value) . expect ( "encodes" ) ;
737- assert_eq ! ( out. capacity( ) , out. len( ) , "shape {i}" ) ;
738- }
739- }
659+ // `tree_encoded_len` re-derives the codec's arithmetic; the codec exports
660+ // no `encoded_len` of its own, so this pin is the only thing that fails if
661+ // the two drift. Drift is not a cosmetic bug: an undersized reservation
662+ // makes `encode_tree` reallocate mid-encode, leaving unwiped partial
663+ // plaintext in the abandoned allocation — silently.
740664
665+ /// The registry wipes the buffer it is handed, and `into_boxed_slice`
666+ /// reallocates one whose capacity exceeds its length. So every answer's
667+ /// capacity is its length.
741668 #[ test]
742- fn value_encoded_len_matches_the_codec_exactly ( ) {
743- for ( i, value) in every_value_shape ( ) . into_iter ( ) . enumerate ( ) {
744- let expected = value_encoded_len ( & value) . expect ( "encodable shape" ) ;
745- let out = value. to_frame ( ) . expect ( "frame encode" ) ;
746- assert_eq ! ( out. risky_ref( ) . len( ) , expected, "shape {i}" ) ;
669+ fn an_encoded_answer_has_no_spare_capacity ( ) {
670+ let entries = [
671+ dynamic:: OperatorEntry :: new ( dynamic:: OpSpec :: Eq , & IndexSpec :: Equality ) ,
672+ dynamic:: OperatorEntry :: new ( dynamic:: OpSpec :: Between , & IndexSpec :: Ope ) ,
673+ ] ;
674+ for out in [
675+ encode_answer ( & vec ! [ true , false , true ] ) ,
676+ encode_answer ( & Some ( 11u64 ) ) ,
677+ encode_answer ( & None :: < u64 > ) ,
678+ encode_answer ( & entries) ,
679+ ] {
680+ let out = out. expect ( "encodes" ) ;
681+ assert_eq ! ( out. capacity( ) , out. len( ) ) ;
747682 }
748683 }
749684
0 commit comments