55 * LICENSE file in the root directory of this source tree.
66 */
77
8+ use futures:: Stream ;
89use futures:: StreamExt ;
10+ use futures:: future;
911use tokio:: io:: AsyncRead ;
1012use tokio_util:: codec:: FramedRead ;
1113
@@ -15,45 +17,83 @@ mod nombytes;
1517mod tlv;
1618pub use nombytes:: NomBytes ;
1719
18- #[ derive( Debug ) ]
19- pub enum ParserControl {
20- KeepGoing ,
21- Enough ,
22- }
23-
2420/// Parse an async source of bytes, expecting to find it to contain one or more sendstreams.
2521/// Because the parsed commands reference data owned by the source, we do not collect the commands.
2622/// Instead, we allow the caller to process them via `f`, which can instruct the processing to
2723/// continue or shut down gracefully via the returned `ParserControl`.
2824///
2925/// Each sendstream is expected to (1) start with a header, followed by (2) either a Subvol or
3026/// Snapshot command, followed by (3) 0 or more additional commands, terminated by (4) an End
31- /// command. Note that we don't validate #2 here, but we do expect #1 and #4.
32- ///
33- /// Returns number of commands parsed.
27+ /// command. Note that only (1) is actually enforced, afterward every command
28+ /// will be emitted into the stream as long as it could be read and parsed.
3429///
3530/// See https://btrfs.readthedocs.io/en/latest/dev/dev-send-stream.html for reference.
36- pub async fn parse < R , F > ( reader : R , mut f : F ) -> crate :: Result < u128 >
31+ pub fn parse < R > ( reader : R ) -> impl Stream < Item = crate :: Result < crate :: Command > >
3732where
38- R : AsyncRead + Unpin + Send ,
39- F : FnMut ( & crate :: Command ) -> ParserControl + Send ,
33+ R : AsyncRead ,
4034{
41- let mut reader = FramedRead :: new ( reader, framed:: SendstreamDecoder :: new ( ) ) ;
42- let mut command_count = 0 ;
43- while let Some ( item_res) = reader. next ( ) . await {
44- match item_res {
45- Ok ( framed:: Item :: Command ( command) ) => {
46- command_count += 1 ;
47- if let ParserControl :: Enough = f ( & command) {
48- // caller got what they needed, no need to continue parsing
49- break ;
35+ let reader = FramedRead :: new ( reader, framed:: SendstreamDecoder :: new ( ) ) ;
36+ reader. filter_map ( |item_res| {
37+ future:: ready ( match item_res {
38+ Ok ( framed:: Item :: Command ( command) ) => Some ( Ok ( command) ) ,
39+ Ok ( framed:: Item :: SendstreamStart ( _) ) => None ,
40+ Err ( e) => Some ( Err ( e) ) ,
41+ } )
42+ } )
43+ }
44+
45+ #[ cfg( test) ]
46+ #[ allow( clippy:: expect_used) ]
47+ mod tests {
48+ use std:: io:: Cursor ;
49+ use std:: time:: Duration ;
50+
51+ use futures:: StreamExt ;
52+ use tokio:: io:: AsyncWriteExt ;
53+ use tokio:: time:: sleep;
54+
55+ use super :: * ;
56+
57+ /// Historically, we couldn't stream commands as they were parsed very well,
58+ /// so the early exit was implemented with a not-very-Rusty callback
59+ /// interface. This test proves that the parser stops operating as soon as
60+ /// the caller stops asking for commands (via a reader that will only
61+ /// provide enough bytes for a few commands before then stalling forever)
62+ #[ tokio:: test]
63+ async fn early_exit ( ) {
64+ let make_parser = |truncate : Option < usize > | async move {
65+ let src = include_bytes ! ( "../../testdata/demo.sendstream" ) ;
66+ // only use simplex stream if we're going to truncate it
67+ // prematurely, otherwise use a Cursor on top of the static byte
68+ // slice so that the framed codec actually sees the EOF
69+ let reader: Box < dyn AsyncRead + Unpin > = match truncate {
70+ Some ( size) => {
71+ let ( receiver, mut sender) = tokio:: io:: simplex ( size) ;
72+ sender
73+ . write_all ( & src[ ..size] )
74+ . await
75+ . expect ( "failed to write input data" ) ;
76+ Box :: new ( receiver)
5077 }
51- }
52- Ok ( framed:: Item :: SendstreamStart ( _) ) => { }
53- Err ( e) => {
54- return Err ( e) ;
55- }
56- }
78+ None => Box :: new ( Cursor :: new ( src) ) ,
79+ } ;
80+ parse ( reader)
81+ } ;
82+
83+ // empirically determined that 46 commands fit in the first 4k
84+ // first, prove that we can read the expected number of commands, before
85+ // dropping the stream which while then stop consuming its upstream input
86+ let parser = make_parser ( Some ( 4 * 1024 ) ) . await ;
87+ let count = parser
88+ . take_until ( sleep ( Duration :: from_millis ( 100 ) ) )
89+ . count ( )
90+ . await ;
91+ assert_eq ! ( count, 46 ) ;
92+
93+ // the entire stream should be consumable (but obviously only when given
94+ // the entire input)
95+ let parser = make_parser ( None ) . await ;
96+ let count = parser. count ( ) . await ;
97+ assert_eq ! ( count, 106 ) ;
5798 }
58- Ok ( command_count)
5999}
0 commit comments