@@ -849,44 +849,116 @@ where
849849 }
850850}
851851
852- /// Read the next entry package from `buf`, pulling more chunks from
853- /// the underlying stream until the package parses or the stream ends.
854- /// See [`ParseOutcome`] for the four cases.
852+ /// Pull chunks until at least `n` bytes are buffered. Returns `Ok(true)`
853+ /// once `n` bytes are available, or `Ok(false)` if the stream ended first.
854+ /// Nothing is consumed; the caller parses the now-buffered bytes.
855+ async fn fill_at_least < S > ( buf : & mut StreamBuffer < S > , n : usize ) -> ApiResult < bool >
856+ where
857+ S : futures_util:: Stream < Item = std:: result:: Result < Vec < u8 > , BodyError > > + Unpin ,
858+ {
859+ while buf. len ( ) < n {
860+ if !buf. pull_one ( ) . await ? {
861+ return Ok ( false ) ;
862+ }
863+ }
864+ Ok ( true )
865+ }
866+
867+ /// Read one length-prefixed entry (`u16 len || len bytes`) from the front
868+ /// of `buf`, consuming exactly the bytes read. Returns `Ok(None)` if the
869+ /// stream ends before a complete entry is buffered (truncation). Unlike a
870+ /// whole-package reparse, each call consumes what it reads, so pulling more
871+ /// bytes for a later entry never re-copies the entries already taken.
872+ async fn read_one_entry < S > ( buf : & mut StreamBuffer < S > ) -> ApiResult < Option < Vec < u8 > > >
873+ where
874+ S : futures_util:: Stream < Item = std:: result:: Result < Vec < u8 > , BodyError > > + Unpin ,
875+ {
876+ if !fill_at_least ( buf, 2 ) . await ? {
877+ return Ok ( None ) ;
878+ }
879+ let bytes = buf. buffered ( ) ;
880+ let len = usize:: from ( u16:: from_be_bytes ( [ bytes[ 0 ] , bytes[ 1 ] ] ) ) ;
881+ let total = 2 + len;
882+ if !fill_at_least ( buf, total) . await ? {
883+ return Ok ( None ) ;
884+ }
885+ let entry = buf. buffered ( ) [ 2 ..total] . to_vec ( ) ;
886+ buf. consume ( total) ;
887+ Ok ( Some ( entry) )
888+ }
889+
890+ /// Read the next entry package from `buf`, pulling more chunks from the
891+ /// underlying stream until the package parses or the stream ends. See
892+ /// [`ParseOutcome`] for the four cases.
893+ ///
894+ /// The package is parsed incrementally, consuming each entry and the proof
895+ /// from `buf` as soon as it is fully buffered. A short read pulls one more
896+ /// chunk and resumes from the next unread unit, so a large package
897+ /// arriving in small chunks is parsed and copied once, not re-parsed from
898+ /// byte zero on every chunk.
855899async fn parse_next_package < S > (
856900 buf : & mut StreamBuffer < S > ,
857901 num_entries : u64 ,
858902) -> ApiResult < ParseOutcome >
859903where
860904 S : futures_util:: Stream < Item = std:: result:: Result < Vec < u8 > , BodyError > > + Unpin ,
861905{
906+ // Reject oversized counts before allocating, matching
907+ // EntryPackage::read_from so the two parse paths agree on limits.
908+ if num_entries > PACKAGE_ALIGNMENT {
909+ return Ok ( ParseOutcome :: Err ( ParseError :: TooManyEntries ( num_entries) ) ) ;
910+ }
862911 // EOF with an empty buffer: clean truncation between packages.
863912 if buf. is_eof ( ) && buf. len ( ) == 0 {
864913 return Ok ( ParseOutcome :: CleanEof ) ;
865914 }
866- loop {
867- let mut cursor = Cursor :: new ( buf. buffered ( ) ) ;
868- match EntryPackage :: read_from ( & mut cursor, num_entries) {
869- Ok ( pkg) => {
870- let consumed = usize:: try_from ( cursor. position ( ) ) . unwrap_or ( usize:: MAX ) ;
871- buf. consume ( consumed) ;
872- return Ok ( ParseOutcome :: Ok ( pkg) ) ;
873- }
874- Err ( ParseError :: Io ( ref e) ) if e. kind ( ) == ErrorKind :: UnexpectedEof => {
875- if !buf. pull_one ( ) . await ? {
876- // Stream ended mid-parse. An empty buffer means the
877- // previous package consumed exactly all buffered bytes
878- // and this call started a fresh (never-arriving)
879- // package: a clean between-package truncation. A
880- // non-empty buffer holds a partial package.
881- if buf. len ( ) == 0 {
882- return Ok ( ParseOutcome :: CleanEof ) ;
883- }
884- return Ok ( ParseOutcome :: MidPackageEof ) ;
885- }
915+
916+ let num_entries = usize:: try_from ( num_entries) . unwrap_or ( usize:: MAX ) ;
917+ let mut entries = Vec :: with_capacity ( num_entries) ;
918+ for _ in 0 ..num_entries {
919+ match read_one_entry ( buf) . await ? {
920+ Some ( entry) => entries. push ( entry) ,
921+ None => {
922+ // Stream ended before this entry completed. Nothing buffered
923+ // and no partial bytes means a clean between-package
924+ // truncation; otherwise a partial package was left behind.
925+ return Ok ( package_eof ( entries. is_empty ( ) , buf. len ( ) > 0 ) ) ;
886926 }
887- Err ( e) => return Ok ( ParseOutcome :: Err ( e) ) ,
888927 }
889928 }
929+
930+ // Proof: `u8 num_hashes || num_hashes * HASH_SIZE bytes`.
931+ if !fill_at_least ( buf, 1 ) . await ? {
932+ return Ok ( package_eof ( entries. is_empty ( ) , buf. len ( ) > 0 ) ) ;
933+ }
934+ let num_hashes = buf. buffered ( ) [ 0 ] ;
935+ if num_hashes > tlog_mirror:: MAX_HASHES_PER_PROOF {
936+ return Ok ( ParseOutcome :: Err ( ParseError :: TooManyHashes ( num_hashes) ) ) ;
937+ }
938+ let proof_bytes = 1 + usize:: from ( num_hashes) * tlog_core:: HASH_SIZE ;
939+ if !fill_at_least ( buf, proof_bytes) . await ? {
940+ return Ok ( package_eof ( entries. is_empty ( ) , buf. len ( ) > 0 ) ) ;
941+ }
942+ let mut proof = Vec :: with_capacity ( usize:: from ( num_hashes) ) ;
943+ for i in 0 ..usize:: from ( num_hashes) {
944+ let off = 1 + i * tlog_core:: HASH_SIZE ;
945+ let mut hash = [ 0u8 ; tlog_core:: HASH_SIZE ] ;
946+ hash. copy_from_slice ( & buf. buffered ( ) [ off..off + tlog_core:: HASH_SIZE ] ) ;
947+ proof. push ( Hash ( hash) ) ;
948+ }
949+ buf. consume ( proof_bytes) ;
950+ Ok ( ParseOutcome :: Ok ( EntryPackage { entries, proof } ) )
951+ }
952+
953+ /// Classify a stream end reached partway through [`parse_next_package`]:
954+ /// a clean between-package truncation when nothing of this package had
955+ /// been read, otherwise a mid-package truncation.
956+ fn package_eof ( no_entries_yet : bool , saw_partial : bool ) -> ParseOutcome {
957+ if no_entries_yet && !saw_partial {
958+ ParseOutcome :: CleanEof
959+ } else {
960+ ParseOutcome :: MidPackageEof
961+ }
890962}
891963
892964/// Read the per-origin DO state snapshot. A non-200 status or RPC failure
@@ -1202,8 +1274,8 @@ impl HashReader for MapReader<'_> {
12021274#[ cfg( test) ]
12031275mod tests {
12041276 use super :: {
1205- CONTENT_TYPE , MapReader , content_type_is_octet_stream , excess_entries , parse_header ,
1206- verify_package,
1277+ CONTENT_TYPE , MapReader , ParseOutcome , content_type_is_octet_stream , excess_entries ,
1278+ parse_header , parse_next_package , verify_package,
12071279 } ;
12081280 use crate :: body:: BodyError ;
12091281 use crate :: mirror_state_do:: PendingCheckpoint ;
@@ -1458,4 +1530,120 @@ mod tests {
14581530 Err ( super :: AppError :: BadRequest ( _) )
14591531 ) ) ;
14601532 }
1533+
1534+ fn sample_package ( ) -> EntryPackage {
1535+ EntryPackage {
1536+ entries : vec ! [
1537+ b"first-entry" . to_vec( ) ,
1538+ Vec :: new( ) ,
1539+ b"a-much-longer-third-entry-with-more-bytes" . to_vec( ) ,
1540+ vec![ 0xab ; 300 ] ,
1541+ ] ,
1542+ proof : vec ! [
1543+ Hash ( [ 0x11 ; tlog_core:: HASH_SIZE ] ) ,
1544+ Hash ( [ 0x22 ; tlog_core:: HASH_SIZE ] ) ,
1545+ ] ,
1546+ }
1547+ }
1548+
1549+ fn package_bytes ( pkg : & EntryPackage ) -> Vec < u8 > {
1550+ let mut buf = Vec :: new ( ) ;
1551+ pkg. write_to ( & mut buf) . unwrap ( ) ;
1552+ buf
1553+ }
1554+
1555+ // parse_next_package returns ApiResult, whose Err (AppError) is not
1556+ // Debug, so unwrap the transport layer by hand for the tests.
1557+ fn ok_outcome ( res : super :: ApiResult < ParseOutcome > ) -> ParseOutcome {
1558+ let Ok ( outcome) = res else {
1559+ panic ! ( "unexpected transport error from parse_next_package" ) ;
1560+ } ;
1561+ outcome
1562+ }
1563+
1564+ // The incremental parser must reconstruct a package identical to a
1565+ // one-shot read regardless of how the wire bytes are chunked, including
1566+ // single-byte chunks that split every length prefix, entry, and proof
1567+ // hash. This is the regression for re-parsing from byte zero.
1568+ #[ tokio:: test( flavor = "current_thread" ) ]
1569+ async fn parse_next_package_reassembles_across_chunk_sizes ( ) {
1570+ let pkg = sample_package ( ) ;
1571+ let bytes = package_bytes ( & pkg) ;
1572+ let num_entries = pkg. entries . len ( ) as u64 ;
1573+ for size in [ 1usize , 2 , 3 , 7 , 33 , bytes. len ( ) ] {
1574+ let chunks: Vec < Vec < u8 > > = bytes. chunks ( size) . map ( <[ u8 ] >:: to_vec) . collect ( ) ;
1575+ let mut buf = stream_buffer ( chunks) ;
1576+ let ParseOutcome :: Ok ( parsed) =
1577+ ok_outcome ( parse_next_package ( & mut buf, num_entries) . await )
1578+ else {
1579+ panic ! ( "chunk size {size} should parse Ok" ) ;
1580+ } ;
1581+ assert_eq ! ( parsed. entries, pkg. entries, "chunk size {size} entries" ) ;
1582+ assert_eq ! ( parsed. proof, pkg. proof, "chunk size {size} proof" ) ;
1583+ }
1584+ }
1585+
1586+ // Two packages back to back: the first parse must consume exactly its
1587+ // bytes, leaving the second intact for the next call.
1588+ #[ tokio:: test( flavor = "current_thread" ) ]
1589+ async fn parse_next_package_leaves_following_package_intact ( ) {
1590+ let pkg = sample_package ( ) ;
1591+ let mut bytes = package_bytes ( & pkg) ;
1592+ bytes. extend ( package_bytes ( & pkg) ) ;
1593+ let num_entries = pkg. entries . len ( ) as u64 ;
1594+ // 5-byte chunks so package boundaries fall mid-chunk.
1595+ let chunks: Vec < Vec < u8 > > = bytes. chunks ( 5 ) . map ( <[ u8 ] >:: to_vec) . collect ( ) ;
1596+ let mut buf = stream_buffer ( chunks) ;
1597+ for which in [ "first" , "second" ] {
1598+ let ParseOutcome :: Ok ( parsed) =
1599+ ok_outcome ( parse_next_package ( & mut buf, num_entries) . await )
1600+ else {
1601+ panic ! ( "{which} package should parse Ok" ) ;
1602+ } ;
1603+ assert_eq ! ( parsed. entries, pkg. entries, "{which} entries" ) ;
1604+ assert_eq ! ( parsed. proof, pkg. proof, "{which} proof" ) ;
1605+ }
1606+ }
1607+
1608+ // Stream ending exactly on a package boundary is a clean truncation.
1609+ #[ tokio:: test( flavor = "current_thread" ) ]
1610+ async fn parse_next_package_clean_eof_between_packages ( ) {
1611+ let pkg = sample_package ( ) ;
1612+ let mut buf = stream_buffer ( vec ! [ package_bytes( & pkg) ] ) ;
1613+ let num_entries = pkg. entries . len ( ) as u64 ;
1614+ assert ! ( matches!(
1615+ ok_outcome( parse_next_package( & mut buf, num_entries) . await ) ,
1616+ ParseOutcome :: Ok ( _)
1617+ ) ) ;
1618+ // Buffer now empty and stream ended: next call is a clean EOF.
1619+ assert ! ( matches!(
1620+ ok_outcome( parse_next_package( & mut buf, num_entries) . await ) ,
1621+ ParseOutcome :: CleanEof
1622+ ) ) ;
1623+ }
1624+
1625+ // Stream ending partway through a package is a mid-package truncation.
1626+ #[ tokio:: test( flavor = "current_thread" ) ]
1627+ async fn parse_next_package_mid_package_eof ( ) {
1628+ let pkg = sample_package ( ) ;
1629+ let bytes = package_bytes ( & pkg) ;
1630+ let mut buf = stream_buffer ( vec ! [ bytes[ ..10 ] . to_vec( ) ] ) ;
1631+ let num_entries = pkg. entries . len ( ) as u64 ;
1632+ assert ! ( matches!(
1633+ ok_outcome( parse_next_package( & mut buf, num_entries) . await ) ,
1634+ ParseOutcome :: MidPackageEof
1635+ ) ) ;
1636+ }
1637+
1638+ // An oversized num_hashes must be an Err, matching read_from's limit.
1639+ #[ tokio:: test( flavor = "current_thread" ) ]
1640+ async fn parse_next_package_rejects_too_many_hashes ( ) {
1641+ // One zero-length entry, then num_hashes = 64 (> spec max 63).
1642+ let bytes = vec ! [ 0x00 , 0x00 , 64 ] ;
1643+ let mut buf = stream_buffer ( vec ! [ bytes] ) ;
1644+ assert ! ( matches!(
1645+ ok_outcome( parse_next_package( & mut buf, 1 ) . await ) ,
1646+ ParseOutcome :: Err ( super :: ParseError :: TooManyHashes ( 64 ) )
1647+ ) ) ;
1648+ }
14611649}
0 commit comments