@@ -833,44 +833,116 @@ where
833833 }
834834}
835835
836- /// Read the next entry package from `buf`, pulling more chunks from
837- /// the underlying stream until the package parses or the stream ends.
838- /// See [`ParseOutcome`] for the four cases.
836+ /// Pull chunks until at least `n` bytes are buffered. Returns `Ok(true)`
837+ /// once `n` bytes are available, or `Ok(false)` if the stream ended first.
838+ /// Nothing is consumed; the caller parses the now-buffered bytes.
839+ async fn fill_at_least < S > ( buf : & mut StreamBuffer < S > , n : usize ) -> ApiResult < bool >
840+ where
841+ S : futures_util:: Stream < Item = std:: result:: Result < Vec < u8 > , BodyError > > + Unpin ,
842+ {
843+ while buf. len ( ) < n {
844+ if !buf. pull_one ( ) . await ? {
845+ return Ok ( false ) ;
846+ }
847+ }
848+ Ok ( true )
849+ }
850+
851+ /// Read one length-prefixed entry (`u16 len || len bytes`) from the front
852+ /// of `buf`, consuming exactly the bytes read. Returns `Ok(None)` if the
853+ /// stream ends before a complete entry is buffered (truncation). Unlike a
854+ /// whole-package reparse, each call consumes what it reads, so pulling more
855+ /// bytes for a later entry never re-copies the entries already taken.
856+ async fn read_one_entry < S > ( buf : & mut StreamBuffer < S > ) -> ApiResult < Option < Vec < u8 > > >
857+ where
858+ S : futures_util:: Stream < Item = std:: result:: Result < Vec < u8 > , BodyError > > + Unpin ,
859+ {
860+ if !fill_at_least ( buf, 2 ) . await ? {
861+ return Ok ( None ) ;
862+ }
863+ let bytes = buf. buffered ( ) ;
864+ let len = usize:: from ( u16:: from_be_bytes ( [ bytes[ 0 ] , bytes[ 1 ] ] ) ) ;
865+ let total = 2 + len;
866+ if !fill_at_least ( buf, total) . await ? {
867+ return Ok ( None ) ;
868+ }
869+ let entry = buf. buffered ( ) [ 2 ..total] . to_vec ( ) ;
870+ buf. consume ( total) ;
871+ Ok ( Some ( entry) )
872+ }
873+
874+ /// Read the next entry package from `buf`, pulling more chunks from the
875+ /// underlying stream until the package parses or the stream ends. See
876+ /// [`ParseOutcome`] for the four cases.
877+ ///
878+ /// The package is parsed incrementally, consuming each entry and the proof
879+ /// from `buf` as soon as it is fully buffered. A short read pulls one more
880+ /// chunk and resumes from the next unread unit, so a large package
881+ /// arriving in small chunks is parsed and copied once, not re-parsed from
882+ /// byte zero on every chunk.
839883async fn parse_next_package < S > (
840884 buf : & mut StreamBuffer < S > ,
841885 num_entries : u64 ,
842886) -> ApiResult < ParseOutcome >
843887where
844888 S : futures_util:: Stream < Item = std:: result:: Result < Vec < u8 > , BodyError > > + Unpin ,
845889{
890+ // Reject oversized counts before allocating, matching
891+ // EntryPackage::read_from so the two parse paths agree on limits.
892+ if num_entries > PACKAGE_ALIGNMENT {
893+ return Ok ( ParseOutcome :: Err ( ParseError :: TooManyEntries ( num_entries) ) ) ;
894+ }
846895 // EOF with an empty buffer: clean truncation between packages.
847896 if buf. is_eof ( ) && buf. len ( ) == 0 {
848897 return Ok ( ParseOutcome :: CleanEof ) ;
849898 }
850- loop {
851- let mut cursor = Cursor :: new ( buf. buffered ( ) ) ;
852- match EntryPackage :: read_from ( & mut cursor, num_entries) {
853- Ok ( pkg) => {
854- let consumed = usize:: try_from ( cursor. position ( ) ) . unwrap_or ( usize:: MAX ) ;
855- buf. consume ( consumed) ;
856- return Ok ( ParseOutcome :: Ok ( pkg) ) ;
857- }
858- Err ( ParseError :: Io ( ref e) ) if e. kind ( ) == ErrorKind :: UnexpectedEof => {
859- if !buf. pull_one ( ) . await ? {
860- // Stream ended mid-parse. An empty buffer means the
861- // previous package consumed exactly all buffered bytes
862- // and this call started a fresh (never-arriving)
863- // package: a clean between-package truncation. A
864- // non-empty buffer holds a partial package.
865- if buf. len ( ) == 0 {
866- return Ok ( ParseOutcome :: CleanEof ) ;
867- }
868- return Ok ( ParseOutcome :: MidPackageEof ) ;
869- }
899+
900+ let num_entries = usize:: try_from ( num_entries) . unwrap_or ( usize:: MAX ) ;
901+ let mut entries = Vec :: with_capacity ( num_entries) ;
902+ for _ in 0 ..num_entries {
903+ match read_one_entry ( buf) . await ? {
904+ Some ( entry) => entries. push ( entry) ,
905+ None => {
906+ // Stream ended before this entry completed. Nothing buffered
907+ // and no partial bytes means a clean between-package
908+ // truncation; otherwise a partial package was left behind.
909+ return Ok ( package_eof ( entries. is_empty ( ) , buf. len ( ) > 0 ) ) ;
870910 }
871- Err ( e) => return Ok ( ParseOutcome :: Err ( e) ) ,
872911 }
873912 }
913+
914+ // Proof: `u8 num_hashes || num_hashes * HASH_SIZE bytes`.
915+ if !fill_at_least ( buf, 1 ) . await ? {
916+ return Ok ( package_eof ( entries. is_empty ( ) , buf. len ( ) > 0 ) ) ;
917+ }
918+ let num_hashes = buf. buffered ( ) [ 0 ] ;
919+ if num_hashes > tlog_mirror:: MAX_HASHES_PER_PROOF {
920+ return Ok ( ParseOutcome :: Err ( ParseError :: TooManyHashes ( num_hashes) ) ) ;
921+ }
922+ let proof_bytes = 1 + usize:: from ( num_hashes) * tlog_core:: HASH_SIZE ;
923+ if !fill_at_least ( buf, proof_bytes) . await ? {
924+ return Ok ( package_eof ( entries. is_empty ( ) , buf. len ( ) > 0 ) ) ;
925+ }
926+ let mut proof = Vec :: with_capacity ( usize:: from ( num_hashes) ) ;
927+ for i in 0 ..usize:: from ( num_hashes) {
928+ let off = 1 + i * tlog_core:: HASH_SIZE ;
929+ let mut hash = [ 0u8 ; tlog_core:: HASH_SIZE ] ;
930+ hash. copy_from_slice ( & buf. buffered ( ) [ off..off + tlog_core:: HASH_SIZE ] ) ;
931+ proof. push ( Hash ( hash) ) ;
932+ }
933+ buf. consume ( proof_bytes) ;
934+ Ok ( ParseOutcome :: Ok ( EntryPackage { entries, proof } ) )
935+ }
936+
937+ /// Classify a stream end reached partway through [`parse_next_package`]:
938+ /// a clean between-package truncation when nothing of this package had
939+ /// been read, otherwise a mid-package truncation.
940+ fn package_eof ( no_entries_yet : bool , saw_partial : bool ) -> ParseOutcome {
941+ if no_entries_yet && !saw_partial {
942+ ParseOutcome :: CleanEof
943+ } else {
944+ ParseOutcome :: MidPackageEof
945+ }
874946}
875947
876948/// Read the per-origin DO state snapshot. A non-200 status or RPC failure
@@ -1186,8 +1258,8 @@ impl HashReader for MapReader<'_> {
11861258#[ cfg( test) ]
11871259mod tests {
11881260 use super :: {
1189- CONTENT_TYPE , MapReader , content_type_is_octet_stream , excess_entries , parse_header ,
1190- verify_package,
1261+ CONTENT_TYPE , MapReader , ParseOutcome , content_type_is_octet_stream , excess_entries ,
1262+ parse_header , parse_next_package , verify_package,
11911263 } ;
11921264 use crate :: body:: BodyError ;
11931265 use crate :: mirror_state_do:: PendingCheckpoint ;
@@ -1442,4 +1514,120 @@ mod tests {
14421514 Err ( super :: AppError :: BadRequest ( _) )
14431515 ) ) ;
14441516 }
1517+
1518+ fn sample_package ( ) -> EntryPackage {
1519+ EntryPackage {
1520+ entries : vec ! [
1521+ b"first-entry" . to_vec( ) ,
1522+ Vec :: new( ) ,
1523+ b"a-much-longer-third-entry-with-more-bytes" . to_vec( ) ,
1524+ vec![ 0xab ; 300 ] ,
1525+ ] ,
1526+ proof : vec ! [
1527+ Hash ( [ 0x11 ; tlog_core:: HASH_SIZE ] ) ,
1528+ Hash ( [ 0x22 ; tlog_core:: HASH_SIZE ] ) ,
1529+ ] ,
1530+ }
1531+ }
1532+
1533+ fn package_bytes ( pkg : & EntryPackage ) -> Vec < u8 > {
1534+ let mut buf = Vec :: new ( ) ;
1535+ pkg. write_to ( & mut buf) . unwrap ( ) ;
1536+ buf
1537+ }
1538+
1539+ // parse_next_package returns ApiResult, whose Err (AppError) is not
1540+ // Debug, so unwrap the transport layer by hand for the tests.
1541+ fn ok_outcome ( res : super :: ApiResult < ParseOutcome > ) -> ParseOutcome {
1542+ let Ok ( outcome) = res else {
1543+ panic ! ( "unexpected transport error from parse_next_package" ) ;
1544+ } ;
1545+ outcome
1546+ }
1547+
1548+ // The incremental parser must reconstruct a package identical to a
1549+ // one-shot read regardless of how the wire bytes are chunked, including
1550+ // single-byte chunks that split every length prefix, entry, and proof
1551+ // hash. This is the regression for re-parsing from byte zero.
1552+ #[ tokio:: test( flavor = "current_thread" ) ]
1553+ async fn parse_next_package_reassembles_across_chunk_sizes ( ) {
1554+ let pkg = sample_package ( ) ;
1555+ let bytes = package_bytes ( & pkg) ;
1556+ let num_entries = pkg. entries . len ( ) as u64 ;
1557+ for size in [ 1usize , 2 , 3 , 7 , 33 , bytes. len ( ) ] {
1558+ let chunks: Vec < Vec < u8 > > = bytes. chunks ( size) . map ( <[ u8 ] >:: to_vec) . collect ( ) ;
1559+ let mut buf = stream_buffer ( chunks) ;
1560+ let ParseOutcome :: Ok ( parsed) =
1561+ ok_outcome ( parse_next_package ( & mut buf, num_entries) . await )
1562+ else {
1563+ panic ! ( "chunk size {size} should parse Ok" ) ;
1564+ } ;
1565+ assert_eq ! ( parsed. entries, pkg. entries, "chunk size {size} entries" ) ;
1566+ assert_eq ! ( parsed. proof, pkg. proof, "chunk size {size} proof" ) ;
1567+ }
1568+ }
1569+
1570+ // Two packages back to back: the first parse must consume exactly its
1571+ // bytes, leaving the second intact for the next call.
1572+ #[ tokio:: test( flavor = "current_thread" ) ]
1573+ async fn parse_next_package_leaves_following_package_intact ( ) {
1574+ let pkg = sample_package ( ) ;
1575+ let mut bytes = package_bytes ( & pkg) ;
1576+ bytes. extend ( package_bytes ( & pkg) ) ;
1577+ let num_entries = pkg. entries . len ( ) as u64 ;
1578+ // 5-byte chunks so package boundaries fall mid-chunk.
1579+ let chunks: Vec < Vec < u8 > > = bytes. chunks ( 5 ) . map ( <[ u8 ] >:: to_vec) . collect ( ) ;
1580+ let mut buf = stream_buffer ( chunks) ;
1581+ for which in [ "first" , "second" ] {
1582+ let ParseOutcome :: Ok ( parsed) =
1583+ ok_outcome ( parse_next_package ( & mut buf, num_entries) . await )
1584+ else {
1585+ panic ! ( "{which} package should parse Ok" ) ;
1586+ } ;
1587+ assert_eq ! ( parsed. entries, pkg. entries, "{which} entries" ) ;
1588+ assert_eq ! ( parsed. proof, pkg. proof, "{which} proof" ) ;
1589+ }
1590+ }
1591+
1592+ // Stream ending exactly on a package boundary is a clean truncation.
1593+ #[ tokio:: test( flavor = "current_thread" ) ]
1594+ async fn parse_next_package_clean_eof_between_packages ( ) {
1595+ let pkg = sample_package ( ) ;
1596+ let mut buf = stream_buffer ( vec ! [ package_bytes( & pkg) ] ) ;
1597+ let num_entries = pkg. entries . len ( ) as u64 ;
1598+ assert ! ( matches!(
1599+ ok_outcome( parse_next_package( & mut buf, num_entries) . await ) ,
1600+ ParseOutcome :: Ok ( _)
1601+ ) ) ;
1602+ // Buffer now empty and stream ended: next call is a clean EOF.
1603+ assert ! ( matches!(
1604+ ok_outcome( parse_next_package( & mut buf, num_entries) . await ) ,
1605+ ParseOutcome :: CleanEof
1606+ ) ) ;
1607+ }
1608+
1609+ // Stream ending partway through a package is a mid-package truncation.
1610+ #[ tokio:: test( flavor = "current_thread" ) ]
1611+ async fn parse_next_package_mid_package_eof ( ) {
1612+ let pkg = sample_package ( ) ;
1613+ let bytes = package_bytes ( & pkg) ;
1614+ let mut buf = stream_buffer ( vec ! [ bytes[ ..10 ] . to_vec( ) ] ) ;
1615+ let num_entries = pkg. entries . len ( ) as u64 ;
1616+ assert ! ( matches!(
1617+ ok_outcome( parse_next_package( & mut buf, num_entries) . await ) ,
1618+ ParseOutcome :: MidPackageEof
1619+ ) ) ;
1620+ }
1621+
1622+ // An oversized num_hashes must be an Err, matching read_from's limit.
1623+ #[ tokio:: test( flavor = "current_thread" ) ]
1624+ async fn parse_next_package_rejects_too_many_hashes ( ) {
1625+ // One zero-length entry, then num_hashes = 64 (> spec max 63).
1626+ let bytes = vec ! [ 0x00 , 0x00 , 64 ] ;
1627+ let mut buf = stream_buffer ( vec ! [ bytes] ) ;
1628+ assert ! ( matches!(
1629+ ok_outcome( parse_next_package( & mut buf, 1 ) . await ) ,
1630+ ParseOutcome :: Err ( super :: ParseError :: TooManyHashes ( 64 ) )
1631+ ) ) ;
1632+ }
14451633}
0 commit comments