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 ;
56- use zeroize:: Zeroizing ;
54+ use vitaminc_aead_frame:: { decode_ciphertext, encode_ciphertext, Frame } ;
5755
5856use stack_guest_abi:: last_error;
5957
@@ -232,7 +230,8 @@ where
232230/// Derive the query term for one plan field, an operator and its input
233231/// (ADR-0009; ADR-0007, amended 2026-10-16): a codec-encoded input, the
234232/// 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`]):
233+ /// in, the term's codec-encoded object out (the `Serialize` form of
234+ /// [`dynamic::QueryTerm`]):
236235///
237236/// ```text
238237/// { "operator": "matches", "fidelity": "probabilistic", "index": "match",
@@ -269,7 +268,7 @@ where
269268 . map_err ( |e| fail_dynamic ( & e) ) ?
270269 . await
271270 . map_err ( |e| fail_error ( & e) ) ?;
272- encode_value ( term. to_value ( ) )
271+ encode_answer ( & term)
273272}
274273
275274/// Verify a page of decrypted values against a probabilistic query: the
@@ -303,9 +302,7 @@ pub fn verify(
303302 & resolver ( ) ,
304303 )
305304 . map_err ( |e| fail_dynamic ( & e) ) ?;
306- encode_value ( FfiValue :: Array (
307- verified. into_iter ( ) . map ( FfiValue :: Bool ) . collect ( ) ,
308- ) )
305+ encode_answer ( & verified)
309306}
310307
311308/// How many rows a probabilistic query should fetch for `want` of them to
@@ -314,7 +311,8 @@ pub fn verify(
314311/// ([`Stats::from_frame`](stack_encrypt::op::Stats::from_frame)), the plan,
315312/// the field's name and the operator's name in; the codec-encoded limit out,
316313/// an unsigned 64-bit integer or null for no finite fetch
317- /// ([`dynamic::limit_to_value`]). Needs no cipher and no key.
314+ /// ([`Limit::rows`](stack_encrypt::op::Limit::rows)). Needs no cipher and
315+ /// no key.
318316pub fn target_limit (
319317 term : & [ u8 ] ,
320318 stats : & [ u8 ] ,
@@ -330,7 +328,7 @@ pub fn target_limit(
330328 . map_err ( |e| fail_dynamic ( & e. in_field ( field) ) ) ?;
331329 let limit = dynamic:: target_limit ( & plan, field, operator, term, want, & stats, & resolver ( ) )
332330 . map_err ( |e| fail_dynamic ( & e) ) ?;
333- encode_value ( dynamic :: limit_to_value ( limit) )
331+ encode_answer ( & limit. rows ( ) )
334332}
335333
336334/// The plan, field name and operator a query names: the plan parsed against
@@ -383,8 +381,8 @@ pub fn plan_check(plan: &[u8]) -> Result<(), u32> {
383381
384382/// The EQL types this build of the engine knows, as a codec-encoded
385383/// `{"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
384+ /// [`TargetDescriptor`](stack_encrypt::dynamic::TargetDescriptor)'s
385+ /// `Serialize` form, producible or not, with the reason when not. The build
388386/// without EQL types lists none, which a generator reads as "no
389387/// `encrypt_into` type is available" and refuses the tag; the `eql` build
390388/// lists the catalog, and a generator writes `encrypt_into` only for an
@@ -413,22 +411,19 @@ pub fn plan_check(plan: &[u8]) -> Result<(), u32> {
413411/// type answers and how far each answer can be trusted.
414412pub fn targets ( ) -> Result < Vec < u8 > , u32 > {
415413 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- ) ] ) )
414+ #[ derive( serde:: Serialize ) ]
415+ struct Targets {
416+ targets : Vec < dynamic:: TargetDescriptor > ,
417+ }
418+ encode_answer ( & Targets {
419+ targets : resolver ( ) . targets ( ) ,
420+ } )
426421}
427422
428423/// The operators one field of a codec-encoded plan answers, as a
429424/// codec-encoded list: one object per operator, in the order the engine
430- /// lists them, each written by
431- /// [`dynamic::operator_to_value `]:
425+ /// lists them, each the `Serialize` form of a
426+ /// [`dynamic::OperatorEntry `]:
432427///
433428/// ```text
434429/// [ { "operator": "eq", "fidelity": "exact", "arity": "one", "index": "eq" }, ... ]
@@ -447,12 +442,12 @@ pub fn operators(plan: &[u8], field: &[u8]) -> Result<Vec<u8>, u32> {
447442 . map_err ( |_| last_error:: malformed ( "the field name is not UTF-8" ) ) ?;
448443 let operators =
449444 dynamic:: field_operators ( & plan, field, & resolver ( ) ) . map_err ( |e| fail_dynamic ( & e) ) ?;
450- encode_value ( FfiValue :: Array (
451- operators
445+ encode_answer (
446+ & operators
452447 . iter ( )
453- . map ( |( operator, index) | dynamic:: operator_to_value ( * operator, index) )
454- . collect ( ) ,
455- ) )
448+ . map ( |( operator, index) | dynamic:: OperatorEntry :: new ( * operator, index) )
449+ . collect :: < Vec < _ > > ( ) ,
450+ )
456451}
457452
458453// =============================================================================
@@ -538,7 +533,7 @@ fn checked_plaintext(bytes: &[u8]) -> Result<Frame<'_>, u32> {
538533}
539534
540535/// 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
536+ /// whose capacity is exactly its length (see [`encode_tree `]); the frame
542537/// is wiped when it drops.
543538fn encode_frame ( frame : ValueFrame ) -> Result < Vec < u8 > , u32 > {
544539 let mut out = exact_buffer ( Some ( frame. risky_ref ( ) . len ( ) ) ) ?;
@@ -556,23 +551,15 @@ fn not_a_frame() -> u32 {
556551 last_error:: malformed ( "an input is not a value in the transport frame" )
557552}
558553
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) ;
554+ /// One of the engine's answers (a query term, a page of verdicts, a limit, a
555+ /// catalog), written to its frame by serde, in a buffer whose capacity is
556+ /// exactly its length. An answer is not plaintext, but the registry's
557+ /// `into_boxed_slice` would reallocate a buffer with spare capacity, so it
558+ /// is sized as every other output is.
559+ fn encode_answer < T : serde:: Serialize > ( answer : & T ) -> Result < Vec < u8 > , u32 > {
560+ let frame = vitaminc_aead_frame:: serde:: to_vec ( answer) . map_err ( |_| STATUS_ENCODING ) ?;
561+ let mut out = exact_buffer ( Some ( frame. len ( ) ) ) ?;
562+ out. extend_from_slice ( & frame) ;
576563 Ok ( out)
577564}
578565
@@ -589,8 +576,9 @@ fn decode_tree(bytes: &[u8]) -> Result<StackCipherText, u32> {
589576}
590577
591578/// 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.
579+ /// plaintext, so this is sized up front: the registry wipes only the buffer
580+ /// it holds, and `register`'s `into_boxed_slice` reallocates a `Vec` whose
581+ /// capacity exceeds its length, leaving the old allocation unwiped.
594582fn encode_tree ( tree : StackCipherText ) -> Result < Vec < u8 > , u32 > {
595583 let tree = map_leaves ( tree, & mut |l : SealedValue | Ok :: < _ , u32 > ( l. to_bytes ( ) ) ) ?;
596584 let mut out = exact_buffer ( tree_encoded_len ( & tree) ) ?;
@@ -640,29 +628,6 @@ fn exact_buffer(len: Option<usize>) -> Result<Vec<u8>, u32> {
640628 Ok ( out)
641629}
642630
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-
666631/// Exact byte length of the codec's encoding of a ciphertext tree. A
667632/// passthrough is its node byte and its frame, written as it is.
668633fn tree_encoded_len ( tree : & BytesTree ) -> Option < usize > {
@@ -685,65 +650,32 @@ fn tree_encoded_len(tree: &BytesTree) -> Option<usize> {
685650#[ cfg( test) ]
686651mod tests {
687652 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- }
653+ use stack_encrypt:: dynamic:: FfiValue ;
654+ use vitaminc_protected:: { Controlled , Protected } ;
728655
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- }
656+ // `tree_encoded_len` re-derives the codec's arithmetic; the codec exports
657+ // no `encoded_len` of its own, so this pin is the only thing that fails if
658+ // the two drift. Drift is not a cosmetic bug: an undersized reservation
659+ // makes `encode_tree` reallocate mid-encode, leaving unwiped partial
660+ // plaintext in the abandoned allocation — silently.
740661
662+ /// The registry wipes the buffer it is handed, and `into_boxed_slice`
663+ /// reallocates one whose capacity exceeds its length. So every answer's
664+ /// capacity is its length.
741665 #[ 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}" ) ;
666+ fn an_encoded_answer_has_no_spare_capacity ( ) {
667+ let entries = [
668+ dynamic:: OperatorEntry :: new ( dynamic:: OpSpec :: Eq , & IndexSpec :: Equality ) ,
669+ dynamic:: OperatorEntry :: new ( dynamic:: OpSpec :: Between , & IndexSpec :: Ope ) ,
670+ ] ;
671+ for out in [
672+ encode_answer ( & vec ! [ true , false , true ] ) ,
673+ encode_answer ( & Some ( 11u64 ) ) ,
674+ encode_answer ( & None :: < u64 > ) ,
675+ encode_answer ( & entries) ,
676+ ] {
677+ let out = out. expect ( "encodes" ) ;
678+ assert_eq ! ( out. capacity( ) , out. len( ) ) ;
747679 }
748680 }
749681
0 commit comments