Skip to main content

btrfs_stream/
stream.rs

1//! Binary send stream parser for the btrfs send/receive protocol.
2//!
3//! Parses the TLV-encoded command stream produced by `btrfs send` (or the
4//! kernel's `BTRFS_IOC_SEND` ioctl). The stream consists of a header followed
5//! by a sequence of commands, each with a CRC32C checksum and a set of
6//! typed TLV attributes.
7
8use std::io::Read;
9use uuid::Uuid;
10
11/// Errors that can occur while parsing a btrfs send stream.
12#[derive(Debug, thiserror::Error)]
13pub enum StreamError {
14    /// The stream header does not start with the btrfs send magic.
15    #[error("invalid send stream: bad magic")]
16    BadMagic,
17    /// The stream version is not supported (must be 1-3).
18    #[error("unsupported send stream version {0} (supported: 1-3)")]
19    UnsupportedVersion(u32),
20    /// CRC32C checksum mismatch on a command.
21    #[error(
22        "CRC mismatch for command {cmd}: expected {expected:#010x}, got {computed:#010x}"
23    )]
24    CrcMismatch {
25        cmd: u16,
26        expected: u32,
27        computed: u32,
28    },
29    /// The stream header could not be read completely.
30    #[error("truncated send stream header: {0}")]
31    TruncatedHeader(std::io::Error),
32    /// A command payload could not be read completely.
33    #[error("truncated send stream payload: {0}")]
34    TruncatedPayload(std::io::Error),
35    /// A TLV attribute structure is incomplete.
36    #[error("truncated TLV: {detail}")]
37    TruncatedTlv { detail: String },
38    /// A TLV attribute type is out of range.
39    #[error("invalid TLV attribute type {0}")]
40    InvalidTlvType(u16),
41    /// A required TLV attribute is missing from a command.
42    #[error("missing required attribute: {0}")]
43    MissingAttribute(&'static str),
44    /// A TLV attribute value is malformed (wrong size, bad encoding, etc.).
45    #[error("attribute {name}: {detail}")]
46    InvalidAttribute { name: &'static str, detail: String },
47    /// An unknown command type was encountered.
48    #[error("unknown send stream command type {0}")]
49    UnknownCommand(u16),
50    /// An underlying I/O error.
51    #[error(transparent)]
52    Io(#[from] std::io::Error),
53}
54
55type Result<T> = std::result::Result<T, StreamError>;
56
57const SEND_STREAM_MAGIC: &[u8] = b"btrfs-stream\0";
58const SEND_STREAM_MAGIC_LEN: usize = 13;
59const STREAM_HEADER_LEN: usize = SEND_STREAM_MAGIC_LEN + 4; // magic + version u32
60
61const CMD_HEADER_LEN: usize = 10; // len(u32) + cmd(u16) + crc(u32)
62// TLV header: 2 bytes type + 2 bytes length = 4 bytes.
63
64// Maximum number of TLV attribute types we track.
65const MAX_ATTRS: usize = 36;
66
67// Command types.
68const BTRFS_SEND_C_SUBVOL: u16 = 1;
69const BTRFS_SEND_C_SNAPSHOT: u16 = 2;
70const BTRFS_SEND_C_MKFILE: u16 = 3;
71const BTRFS_SEND_C_MKDIR: u16 = 4;
72const BTRFS_SEND_C_MKNOD: u16 = 5;
73const BTRFS_SEND_C_MKFIFO: u16 = 6;
74const BTRFS_SEND_C_MKSOCK: u16 = 7;
75const BTRFS_SEND_C_SYMLINK: u16 = 8;
76const BTRFS_SEND_C_RENAME: u16 = 9;
77const BTRFS_SEND_C_LINK: u16 = 10;
78const BTRFS_SEND_C_UNLINK: u16 = 11;
79const BTRFS_SEND_C_RMDIR: u16 = 12;
80const BTRFS_SEND_C_SET_XATTR: u16 = 13;
81const BTRFS_SEND_C_REMOVE_XATTR: u16 = 14;
82const BTRFS_SEND_C_WRITE: u16 = 15;
83const BTRFS_SEND_C_CLONE: u16 = 16;
84const BTRFS_SEND_C_TRUNCATE: u16 = 17;
85const BTRFS_SEND_C_CHMOD: u16 = 18;
86const BTRFS_SEND_C_CHOWN: u16 = 19;
87const BTRFS_SEND_C_UTIMES: u16 = 20;
88const BTRFS_SEND_C_END: u16 = 21;
89const BTRFS_SEND_C_UPDATE_EXTENT: u16 = 22;
90// v2 commands.
91const BTRFS_SEND_C_FALLOCATE: u16 = 23;
92const BTRFS_SEND_C_FILEATTR: u16 = 24;
93const BTRFS_SEND_C_ENCODED_WRITE: u16 = 25;
94// v3 commands.
95const BTRFS_SEND_C_ENABLE_VERITY: u16 = 26;
96
97// Attribute types.
98const BTRFS_SEND_A_UUID: u16 = 1;
99const BTRFS_SEND_A_CTRANSID: u16 = 2;
100#[allow(dead_code)]
101const BTRFS_SEND_A_INO: u16 = 3;
102const BTRFS_SEND_A_SIZE: u16 = 4;
103const BTRFS_SEND_A_MODE: u16 = 5;
104const BTRFS_SEND_A_UID: u16 = 6;
105const BTRFS_SEND_A_GID: u16 = 7;
106const BTRFS_SEND_A_RDEV: u16 = 8;
107const BTRFS_SEND_A_CTIME: u16 = 9;
108const BTRFS_SEND_A_MTIME: u16 = 10;
109const BTRFS_SEND_A_ATIME: u16 = 11;
110#[allow(dead_code)]
111const BTRFS_SEND_A_OTIME: u16 = 12;
112const BTRFS_SEND_A_XATTR_NAME: u16 = 13;
113const BTRFS_SEND_A_XATTR_DATA: u16 = 14;
114const BTRFS_SEND_A_PATH: u16 = 15;
115const BTRFS_SEND_A_PATH_TO: u16 = 16;
116const BTRFS_SEND_A_PATH_LINK: u16 = 17;
117const BTRFS_SEND_A_FILE_OFFSET: u16 = 18;
118const BTRFS_SEND_A_DATA: u16 = 19;
119const BTRFS_SEND_A_CLONE_UUID: u16 = 20;
120const BTRFS_SEND_A_CLONE_CTRANSID: u16 = 21;
121const BTRFS_SEND_A_CLONE_PATH: u16 = 22;
122const BTRFS_SEND_A_CLONE_OFFSET: u16 = 23;
123const BTRFS_SEND_A_CLONE_LEN: u16 = 24;
124// v2 attribute types.
125const BTRFS_SEND_A_FALLOCATE_MODE: u16 = 25;
126const BTRFS_SEND_A_FILEATTR: u16 = 26;
127const BTRFS_SEND_A_UNENCODED_FILE_LEN: u16 = 27;
128const BTRFS_SEND_A_UNENCODED_LEN: u16 = 28;
129const BTRFS_SEND_A_UNENCODED_OFFSET: u16 = 29;
130const BTRFS_SEND_A_COMPRESSION: u16 = 30;
131const BTRFS_SEND_A_ENCRYPTION: u16 = 31;
132// v3 attribute types.
133const BTRFS_SEND_A_VERITY_ALGORITHM: u16 = 32;
134const BTRFS_SEND_A_VERITY_BLOCK_SIZE: u16 = 33;
135const BTRFS_SEND_A_VERITY_SALT_DATA: u16 = 34;
136const BTRFS_SEND_A_VERITY_SIG_DATA: u16 = 35;
137
138/// A timestamp from the send stream (sec + nsec).
139#[derive(Debug, Clone, Copy)]
140pub struct Timespec {
141    pub sec: u64,
142    pub nsec: u32,
143}
144
145/// A parsed command from the send stream.
146#[derive(Debug)]
147pub enum StreamCommand {
148    Subvol {
149        path: String,
150        uuid: Uuid,
151        ctransid: u64,
152    },
153    Snapshot {
154        path: String,
155        uuid: Uuid,
156        ctransid: u64,
157        clone_uuid: Uuid,
158        clone_ctransid: u64,
159    },
160    Mkfile {
161        path: String,
162    },
163    Mkdir {
164        path: String,
165    },
166    Mknod {
167        path: String,
168        mode: u64,
169        rdev: u64,
170    },
171    Mkfifo {
172        path: String,
173    },
174    Mksock {
175        path: String,
176    },
177    Symlink {
178        path: String,
179        target: String,
180    },
181    Rename {
182        from: String,
183        to: String,
184    },
185    Link {
186        path: String,
187        target: String,
188    },
189    Unlink {
190        path: String,
191    },
192    Rmdir {
193        path: String,
194    },
195    Write {
196        path: String,
197        offset: u64,
198        data: Vec<u8>,
199    },
200    Clone {
201        path: String,
202        offset: u64,
203        len: u64,
204        clone_uuid: Uuid,
205        clone_ctransid: u64,
206        clone_path: String,
207        clone_offset: u64,
208    },
209    SetXattr {
210        path: String,
211        name: String,
212        data: Vec<u8>,
213    },
214    RemoveXattr {
215        path: String,
216        name: String,
217    },
218    Truncate {
219        path: String,
220        size: u64,
221    },
222    Chmod {
223        path: String,
224        mode: u64,
225    },
226    Chown {
227        path: String,
228        uid: u64,
229        gid: u64,
230    },
231    Utimes {
232        path: String,
233        atime: Timespec,
234        mtime: Timespec,
235        ctime: Timespec,
236    },
237    UpdateExtent {
238        path: String,
239        offset: u64,
240        len: u64,
241    },
242    /// v2: write pre-compressed data that can be passed through to the
243    /// filesystem without decompression via `BTRFS_IOC_ENCODED_WRITE`.
244    EncodedWrite {
245        path: String,
246        offset: u64,
247        /// Unencoded (decompressed) file length to write.
248        unencoded_file_len: u64,
249        /// Total unencoded length (may be larger due to sector alignment).
250        unencoded_len: u64,
251        /// Offset within the unencoded data where the file data starts.
252        unencoded_offset: u64,
253        /// Compression algorithm (0=none, 1=zlib, 2=zstd, 3-7=lzo with varying sector sizes).
254        compression: u32,
255        /// Encryption algorithm (currently always 0).
256        encryption: u32,
257        data: Vec<u8>,
258    },
259    /// v2: preallocate space or punch a hole.
260    Fallocate {
261        path: String,
262        /// `FALLOC_FL_*` flags (0=allocate, 1=KEEP_SIZE, 3=PUNCH_HOLE|KEEP_SIZE).
263        mode: u32,
264        offset: u64,
265        len: u64,
266    },
267    /// v2: set inode file attributes (chattr flags).
268    Fileattr {
269        path: String,
270        attr: u64,
271    },
272    /// v3: enable fs-verity on a file.
273    EnableVerity {
274        path: String,
275        algorithm: u8,
276        block_size: u32,
277        salt: Vec<u8>,
278        sig: Vec<u8>,
279    },
280    End,
281}
282
283/// Reads and parses a btrfs send stream.
284#[derive(Debug)]
285pub struct StreamReader<R: Read> {
286    reader: R,
287    version: u32,
288    buf: Vec<u8>,
289}
290
291impl<R: Read> StreamReader<R> {
292    /// Read and validate the stream header, returning a new reader.
293    pub fn new(mut reader: R) -> Result<Self> {
294        let mut header = [0u8; STREAM_HEADER_LEN];
295        reader
296            .read_exact(&mut header)
297            .map_err(StreamError::TruncatedHeader)?;
298
299        if &header[..SEND_STREAM_MAGIC_LEN] != SEND_STREAM_MAGIC {
300            return Err(StreamError::BadMagic);
301        }
302
303        let version = u32::from_le_bytes(
304            header[SEND_STREAM_MAGIC_LEN..STREAM_HEADER_LEN]
305                .try_into()
306                .unwrap(),
307        );
308
309        if version == 0 || version > 3 {
310            return Err(StreamError::UnsupportedVersion(version));
311        }
312
313        Ok(Self {
314            reader,
315            version,
316            buf: Vec::with_capacity(64 * 1024),
317        })
318    }
319
320    /// Return the stream protocol version.
321    pub fn version(&self) -> u32 {
322        self.version
323    }
324
325    /// Consume the underlying reader back out.
326    pub fn into_inner(self) -> R {
327        self.reader
328    }
329
330    /// Read the next command from the stream.
331    ///
332    /// Returns `Ok(None)` on clean EOF (no more data), `Ok(Some(End))` when
333    /// the stream contains an explicit end-of-stream marker.
334    pub fn next_command(&mut self) -> Result<Option<StreamCommand>> {
335        // Read command header.
336        let mut cmd_hdr = [0u8; CMD_HEADER_LEN];
337        if !read_exact_or_eof(&mut self.reader, &mut cmd_hdr)? {
338            return Ok(None); // clean EOF
339        }
340
341        let payload_len =
342            u32::from_le_bytes(cmd_hdr[0..4].try_into().unwrap()) as usize;
343        let cmd = u16::from_le_bytes(cmd_hdr[4..6].try_into().unwrap());
344        let expected_crc =
345            u32::from_le_bytes(cmd_hdr[6..10].try_into().unwrap());
346
347        // Read payload.
348        self.buf.resize(payload_len, 0);
349        self.reader
350            .read_exact(&mut self.buf)
351            .map_err(StreamError::TruncatedPayload)?;
352
353        // Validate CRC32C: compute over header (with crc field zeroed) + payload.
354        // The btrfs send stream uses a raw CRC-32C (init=0, xorout=0), not the
355        // standard ISO 3309 convention (init=0xFFFFFFFF, xorout=0xFFFFFFFF).
356        // The crc32c crate only exposes the standard version, so we recover the
357        // raw value: raw_crc32c(0, data) == !crc32c_append(!0, data).
358        let mut crc_buf = Vec::with_capacity(CMD_HEADER_LEN + payload_len);
359        crc_buf.extend_from_slice(&cmd_hdr[0..6]); // len + cmd
360        crc_buf.extend_from_slice(&[0u8; 4]); // zeroed crc field
361        crc_buf.extend_from_slice(&self.buf);
362        let computed_crc = !crc32c::crc32c_append(!0, &crc_buf);
363        if computed_crc != expected_crc {
364            return Err(StreamError::CrcMismatch {
365                cmd,
366                expected: expected_crc,
367                computed: computed_crc,
368            });
369        }
370
371        // Parse TLV attributes from payload.
372        let attrs = parse_tlv_attrs(&self.buf, self.version)?;
373
374        // Dispatch by command type.
375        match cmd {
376            BTRFS_SEND_C_SUBVOL => Ok(Some(StreamCommand::Subvol {
377                path: attr_string(
378                    &self.buf,
379                    &attrs,
380                    BTRFS_SEND_A_PATH,
381                    "path",
382                )?,
383                uuid: attr_uuid(&self.buf, &attrs, BTRFS_SEND_A_UUID, "uuid")?,
384                ctransid: attr_u64(
385                    &self.buf,
386                    &attrs,
387                    BTRFS_SEND_A_CTRANSID,
388                    "ctransid",
389                )?,
390            })),
391            BTRFS_SEND_C_SNAPSHOT => Ok(Some(StreamCommand::Snapshot {
392                path: attr_string(
393                    &self.buf,
394                    &attrs,
395                    BTRFS_SEND_A_PATH,
396                    "path",
397                )?,
398                uuid: attr_uuid(&self.buf, &attrs, BTRFS_SEND_A_UUID, "uuid")?,
399                ctransid: attr_u64(
400                    &self.buf,
401                    &attrs,
402                    BTRFS_SEND_A_CTRANSID,
403                    "ctransid",
404                )?,
405                clone_uuid: attr_uuid(
406                    &self.buf,
407                    &attrs,
408                    BTRFS_SEND_A_CLONE_UUID,
409                    "clone_uuid",
410                )?,
411                clone_ctransid: attr_u64(
412                    &self.buf,
413                    &attrs,
414                    BTRFS_SEND_A_CLONE_CTRANSID,
415                    "clone_ctransid",
416                )?,
417            })),
418            BTRFS_SEND_C_MKFILE => Ok(Some(StreamCommand::Mkfile {
419                path: attr_string(
420                    &self.buf,
421                    &attrs,
422                    BTRFS_SEND_A_PATH,
423                    "path",
424                )?,
425            })),
426            BTRFS_SEND_C_MKDIR => Ok(Some(StreamCommand::Mkdir {
427                path: attr_string(
428                    &self.buf,
429                    &attrs,
430                    BTRFS_SEND_A_PATH,
431                    "path",
432                )?,
433            })),
434            BTRFS_SEND_C_MKNOD => Ok(Some(StreamCommand::Mknod {
435                path: attr_string(
436                    &self.buf,
437                    &attrs,
438                    BTRFS_SEND_A_PATH,
439                    "path",
440                )?,
441                mode: attr_u64(&self.buf, &attrs, BTRFS_SEND_A_MODE, "mode")?,
442                rdev: attr_u64(&self.buf, &attrs, BTRFS_SEND_A_RDEV, "rdev")?,
443            })),
444            BTRFS_SEND_C_MKFIFO => Ok(Some(StreamCommand::Mkfifo {
445                path: attr_string(
446                    &self.buf,
447                    &attrs,
448                    BTRFS_SEND_A_PATH,
449                    "path",
450                )?,
451            })),
452            BTRFS_SEND_C_MKSOCK => Ok(Some(StreamCommand::Mksock {
453                path: attr_string(
454                    &self.buf,
455                    &attrs,
456                    BTRFS_SEND_A_PATH,
457                    "path",
458                )?,
459            })),
460            BTRFS_SEND_C_SYMLINK => Ok(Some(StreamCommand::Symlink {
461                path: attr_string(
462                    &self.buf,
463                    &attrs,
464                    BTRFS_SEND_A_PATH,
465                    "path",
466                )?,
467                target: attr_string(
468                    &self.buf,
469                    &attrs,
470                    BTRFS_SEND_A_PATH_LINK,
471                    "link_target",
472                )?,
473            })),
474            BTRFS_SEND_C_RENAME => Ok(Some(StreamCommand::Rename {
475                from: attr_string(
476                    &self.buf,
477                    &attrs,
478                    BTRFS_SEND_A_PATH,
479                    "path",
480                )?,
481                to: attr_string(
482                    &self.buf,
483                    &attrs,
484                    BTRFS_SEND_A_PATH_TO,
485                    "path_to",
486                )?,
487            })),
488            BTRFS_SEND_C_LINK => Ok(Some(StreamCommand::Link {
489                path: attr_string(
490                    &self.buf,
491                    &attrs,
492                    BTRFS_SEND_A_PATH,
493                    "path",
494                )?,
495                target: attr_string(
496                    &self.buf,
497                    &attrs,
498                    BTRFS_SEND_A_PATH_LINK,
499                    "link_target",
500                )?,
501            })),
502            BTRFS_SEND_C_UNLINK => Ok(Some(StreamCommand::Unlink {
503                path: attr_string(
504                    &self.buf,
505                    &attrs,
506                    BTRFS_SEND_A_PATH,
507                    "path",
508                )?,
509            })),
510            BTRFS_SEND_C_RMDIR => Ok(Some(StreamCommand::Rmdir {
511                path: attr_string(
512                    &self.buf,
513                    &attrs,
514                    BTRFS_SEND_A_PATH,
515                    "path",
516                )?,
517            })),
518            BTRFS_SEND_C_SET_XATTR => Ok(Some(StreamCommand::SetXattr {
519                path: attr_string(
520                    &self.buf,
521                    &attrs,
522                    BTRFS_SEND_A_PATH,
523                    "path",
524                )?,
525                name: attr_string(
526                    &self.buf,
527                    &attrs,
528                    BTRFS_SEND_A_XATTR_NAME,
529                    "xattr_name",
530                )?,
531                data: attr_data(
532                    &self.buf,
533                    &attrs,
534                    BTRFS_SEND_A_XATTR_DATA,
535                    "xattr_data",
536                )?,
537            })),
538            BTRFS_SEND_C_REMOVE_XATTR => Ok(Some(StreamCommand::RemoveXattr {
539                path: attr_string(
540                    &self.buf,
541                    &attrs,
542                    BTRFS_SEND_A_PATH,
543                    "path",
544                )?,
545                name: attr_string(
546                    &self.buf,
547                    &attrs,
548                    BTRFS_SEND_A_XATTR_NAME,
549                    "xattr_name",
550                )?,
551            })),
552            BTRFS_SEND_C_WRITE => Ok(Some(StreamCommand::Write {
553                path: attr_string(
554                    &self.buf,
555                    &attrs,
556                    BTRFS_SEND_A_PATH,
557                    "path",
558                )?,
559                offset: attr_u64(
560                    &self.buf,
561                    &attrs,
562                    BTRFS_SEND_A_FILE_OFFSET,
563                    "file_offset",
564                )?,
565                data: attr_data(&self.buf, &attrs, BTRFS_SEND_A_DATA, "data")?,
566            })),
567            BTRFS_SEND_C_CLONE => Ok(Some(StreamCommand::Clone {
568                path: attr_string(
569                    &self.buf,
570                    &attrs,
571                    BTRFS_SEND_A_PATH,
572                    "path",
573                )?,
574                offset: attr_u64(
575                    &self.buf,
576                    &attrs,
577                    BTRFS_SEND_A_FILE_OFFSET,
578                    "file_offset",
579                )?,
580                len: attr_u64(
581                    &self.buf,
582                    &attrs,
583                    BTRFS_SEND_A_CLONE_LEN,
584                    "clone_len",
585                )?,
586                clone_uuid: attr_uuid(
587                    &self.buf,
588                    &attrs,
589                    BTRFS_SEND_A_CLONE_UUID,
590                    "clone_uuid",
591                )?,
592                clone_ctransid: attr_u64(
593                    &self.buf,
594                    &attrs,
595                    BTRFS_SEND_A_CLONE_CTRANSID,
596                    "clone_ctransid",
597                )?,
598                clone_path: attr_string(
599                    &self.buf,
600                    &attrs,
601                    BTRFS_SEND_A_CLONE_PATH,
602                    "clone_path",
603                )?,
604                clone_offset: attr_u64(
605                    &self.buf,
606                    &attrs,
607                    BTRFS_SEND_A_CLONE_OFFSET,
608                    "clone_offset",
609                )?,
610            })),
611            BTRFS_SEND_C_TRUNCATE => Ok(Some(StreamCommand::Truncate {
612                path: attr_string(
613                    &self.buf,
614                    &attrs,
615                    BTRFS_SEND_A_PATH,
616                    "path",
617                )?,
618                size: attr_u64(&self.buf, &attrs, BTRFS_SEND_A_SIZE, "size")?,
619            })),
620            BTRFS_SEND_C_CHMOD => Ok(Some(StreamCommand::Chmod {
621                path: attr_string(
622                    &self.buf,
623                    &attrs,
624                    BTRFS_SEND_A_PATH,
625                    "path",
626                )?,
627                mode: attr_u64(&self.buf, &attrs, BTRFS_SEND_A_MODE, "mode")?,
628            })),
629            BTRFS_SEND_C_CHOWN => Ok(Some(StreamCommand::Chown {
630                path: attr_string(
631                    &self.buf,
632                    &attrs,
633                    BTRFS_SEND_A_PATH,
634                    "path",
635                )?,
636                uid: attr_u64(&self.buf, &attrs, BTRFS_SEND_A_UID, "uid")?,
637                gid: attr_u64(&self.buf, &attrs, BTRFS_SEND_A_GID, "gid")?,
638            })),
639            BTRFS_SEND_C_UTIMES => Ok(Some(StreamCommand::Utimes {
640                path: attr_string(
641                    &self.buf,
642                    &attrs,
643                    BTRFS_SEND_A_PATH,
644                    "path",
645                )?,
646                atime: attr_timespec(
647                    &self.buf,
648                    &attrs,
649                    BTRFS_SEND_A_ATIME,
650                    "atime",
651                )?,
652                mtime: attr_timespec(
653                    &self.buf,
654                    &attrs,
655                    BTRFS_SEND_A_MTIME,
656                    "mtime",
657                )?,
658                ctime: attr_timespec(
659                    &self.buf,
660                    &attrs,
661                    BTRFS_SEND_A_CTIME,
662                    "ctime",
663                )?,
664            })),
665            BTRFS_SEND_C_UPDATE_EXTENT => {
666                Ok(Some(StreamCommand::UpdateExtent {
667                    path: attr_string(
668                        &self.buf,
669                        &attrs,
670                        BTRFS_SEND_A_PATH,
671                        "path",
672                    )?,
673                    offset: attr_u64(
674                        &self.buf,
675                        &attrs,
676                        BTRFS_SEND_A_FILE_OFFSET,
677                        "file_offset",
678                    )?,
679                    len: attr_u64(
680                        &self.buf,
681                        &attrs,
682                        BTRFS_SEND_A_SIZE,
683                        "size",
684                    )?,
685                }))
686            }
687            BTRFS_SEND_C_FALLOCATE => Ok(Some(StreamCommand::Fallocate {
688                path: attr_string(
689                    &self.buf,
690                    &attrs,
691                    BTRFS_SEND_A_PATH,
692                    "path",
693                )?,
694                mode: attr_u32(
695                    &self.buf,
696                    &attrs,
697                    BTRFS_SEND_A_FALLOCATE_MODE,
698                    "fallocate_mode",
699                )?,
700                offset: attr_u64(
701                    &self.buf,
702                    &attrs,
703                    BTRFS_SEND_A_FILE_OFFSET,
704                    "file_offset",
705                )?,
706                len: attr_u64(&self.buf, &attrs, BTRFS_SEND_A_SIZE, "size")?,
707            })),
708            BTRFS_SEND_C_FILEATTR => Ok(Some(StreamCommand::Fileattr {
709                path: attr_string(
710                    &self.buf,
711                    &attrs,
712                    BTRFS_SEND_A_PATH,
713                    "path",
714                )?,
715                attr: attr_u64(
716                    &self.buf,
717                    &attrs,
718                    BTRFS_SEND_A_FILEATTR,
719                    "fileattr",
720                )?,
721            })),
722            BTRFS_SEND_C_ENCODED_WRITE => {
723                Ok(Some(StreamCommand::EncodedWrite {
724                    path: attr_string(
725                        &self.buf,
726                        &attrs,
727                        BTRFS_SEND_A_PATH,
728                        "path",
729                    )?,
730                    offset: attr_u64(
731                        &self.buf,
732                        &attrs,
733                        BTRFS_SEND_A_FILE_OFFSET,
734                        "file_offset",
735                    )?,
736                    unencoded_file_len: attr_u64(
737                        &self.buf,
738                        &attrs,
739                        BTRFS_SEND_A_UNENCODED_FILE_LEN,
740                        "unencoded_file_len",
741                    )?,
742                    unencoded_len: attr_u64(
743                        &self.buf,
744                        &attrs,
745                        BTRFS_SEND_A_UNENCODED_LEN,
746                        "unencoded_len",
747                    )?,
748                    unencoded_offset: attr_u64(
749                        &self.buf,
750                        &attrs,
751                        BTRFS_SEND_A_UNENCODED_OFFSET,
752                        "unencoded_offset",
753                    )?,
754                    // Compression and encryption default to 0 if absent.
755                    compression: attr_opt_u32(
756                        &self.buf,
757                        &attrs,
758                        BTRFS_SEND_A_COMPRESSION,
759                        0,
760                    ),
761                    encryption: attr_opt_u32(
762                        &self.buf,
763                        &attrs,
764                        BTRFS_SEND_A_ENCRYPTION,
765                        0,
766                    ),
767                    data: attr_data(
768                        &self.buf,
769                        &attrs,
770                        BTRFS_SEND_A_DATA,
771                        "data",
772                    )?,
773                }))
774            }
775            BTRFS_SEND_C_ENABLE_VERITY => {
776                Ok(Some(StreamCommand::EnableVerity {
777                    path: attr_string(
778                        &self.buf,
779                        &attrs,
780                        BTRFS_SEND_A_PATH,
781                        "path",
782                    )?,
783                    algorithm: attr_u8(
784                        &self.buf,
785                        &attrs,
786                        BTRFS_SEND_A_VERITY_ALGORITHM,
787                        "verity_algorithm",
788                    )?,
789                    block_size: attr_u32(
790                        &self.buf,
791                        &attrs,
792                        BTRFS_SEND_A_VERITY_BLOCK_SIZE,
793                        "verity_block_size",
794                    )?,
795                    salt: attr_data(
796                        &self.buf,
797                        &attrs,
798                        BTRFS_SEND_A_VERITY_SALT_DATA,
799                        "verity_salt",
800                    )?,
801                    sig: attr_data(
802                        &self.buf,
803                        &attrs,
804                        BTRFS_SEND_A_VERITY_SIG_DATA,
805                        "verity_sig",
806                    )?,
807                }))
808            }
809            BTRFS_SEND_C_END => Ok(Some(StreamCommand::End)),
810            _ => Err(StreamError::UnknownCommand(cmd)),
811        }
812    }
813}
814
815/// Read exactly `buf.len()` bytes, returning false on clean EOF (zero bytes read).
816fn read_exact_or_eof(reader: &mut impl Read, buf: &mut [u8]) -> Result<bool> {
817    let mut pos = 0;
818    while pos < buf.len() {
819        match reader.read(&mut buf[pos..]) {
820            Ok(0) => {
821                if pos == 0 {
822                    return Ok(false);
823                }
824                return Err(StreamError::TruncatedPayload(
825                    std::io::Error::new(
826                        std::io::ErrorKind::UnexpectedEof,
827                        format!("unexpected EOF after {pos} bytes"),
828                    ),
829                ));
830            }
831            Ok(n) => pos += n,
832            Err(e) => return Err(StreamError::TruncatedPayload(e)),
833        }
834    }
835    Ok(true)
836}
837
838/// Attribute storage: for each attribute type index, Option<(offset, len)> into
839/// the payload buffer.
840type AttrTable = [Option<(usize, usize)>; MAX_ATTRS];
841
842/// Parse TLV attributes from a command payload buffer into a lookup table.
843fn parse_tlv_attrs(payload: &[u8], version: u32) -> Result<AttrTable> {
844    let mut attrs: AttrTable = [None; MAX_ATTRS];
845    let mut pos = 0;
846
847    while pos < payload.len() {
848        if pos + 2 > payload.len() {
849            return Err(StreamError::TruncatedTlv {
850                detail: "not enough bytes for type field".into(),
851            });
852        }
853        let tlv_type =
854            u16::from_le_bytes(payload[pos..pos + 2].try_into().unwrap());
855        pos += 2;
856
857        if tlv_type == 0 || tlv_type as usize > MAX_ATTRS {
858            return Err(StreamError::InvalidTlvType(tlv_type));
859        }
860
861        // In v2+, the DATA attribute has no length field — it extends to the
862        // end of the command payload.
863        let tlv_len = if version >= 2 && tlv_type == BTRFS_SEND_A_DATA {
864            payload.len() - pos
865        } else {
866            if pos + 2 > payload.len() {
867                return Err(StreamError::TruncatedTlv {
868                    detail: "not enough bytes for length field".into(),
869                });
870            }
871            let len =
872                u16::from_le_bytes(payload[pos..pos + 2].try_into().unwrap())
873                    as usize;
874            pos += 2;
875            len
876        };
877
878        if pos + tlv_len > payload.len() {
879            return Err(StreamError::TruncatedTlv {
880                detail: format!(
881                    "attribute type {tlv_type} needs {tlv_len} bytes but only {} remain",
882                    payload.len() - pos
883                ),
884            });
885        }
886
887        attrs[(tlv_type - 1) as usize] = Some((pos, tlv_len));
888        pos += tlv_len;
889    }
890
891    Ok(attrs)
892}
893
894fn get_attr<'a>(
895    buf: &'a [u8],
896    attrs: &AttrTable,
897    attr_type: u16,
898    name: &'static str,
899) -> Result<&'a [u8]> {
900    let (offset, len) = attrs[(attr_type - 1) as usize]
901        .ok_or(StreamError::MissingAttribute(name))?;
902    Ok(&buf[offset..offset + len])
903}
904
905fn attr_u64(
906    buf: &[u8],
907    attrs: &AttrTable,
908    attr_type: u16,
909    name: &'static str,
910) -> Result<u64> {
911    let data = get_attr(buf, attrs, attr_type, name)?;
912    if data.len() < 8 {
913        return Err(StreamError::InvalidAttribute {
914            name,
915            detail: format!("too short for u64: {} bytes", data.len()),
916        });
917    }
918    Ok(u64::from_le_bytes(data[0..8].try_into().unwrap()))
919}
920
921fn attr_string(
922    buf: &[u8],
923    attrs: &AttrTable,
924    attr_type: u16,
925    name: &'static str,
926) -> Result<String> {
927    let data = get_attr(buf, attrs, attr_type, name)?;
928    // Strings in the stream are null-terminated; strip the trailing NUL.
929    let s = if data.last() == Some(&0) {
930        &data[..data.len() - 1]
931    } else {
932        data
933    };
934    String::from_utf8(s.to_vec()).map_err(|_| StreamError::InvalidAttribute {
935        name,
936        detail: "not valid UTF-8".into(),
937    })
938}
939
940fn attr_uuid(
941    buf: &[u8],
942    attrs: &AttrTable,
943    attr_type: u16,
944    name: &'static str,
945) -> Result<Uuid> {
946    let data = get_attr(buf, attrs, attr_type, name)?;
947    if data.len() < 16 {
948        return Err(StreamError::InvalidAttribute {
949            name,
950            detail: format!("too short for UUID: {} bytes", data.len()),
951        });
952    }
953    Ok(Uuid::from_bytes(data[0..16].try_into().unwrap()))
954}
955
956fn attr_timespec(
957    buf: &[u8],
958    attrs: &AttrTable,
959    attr_type: u16,
960    name: &'static str,
961) -> Result<Timespec> {
962    let data = get_attr(buf, attrs, attr_type, name)?;
963    if data.len() < 12 {
964        return Err(StreamError::InvalidAttribute {
965            name,
966            detail: format!("too short for timespec: {} bytes", data.len()),
967        });
968    }
969    Ok(Timespec {
970        sec: u64::from_le_bytes(data[0..8].try_into().unwrap()),
971        nsec: u32::from_le_bytes(data[8..12].try_into().unwrap()),
972    })
973}
974
975fn attr_u32(
976    buf: &[u8],
977    attrs: &AttrTable,
978    attr_type: u16,
979    name: &'static str,
980) -> Result<u32> {
981    let data = get_attr(buf, attrs, attr_type, name)?;
982    if data.len() < 4 {
983        return Err(StreamError::InvalidAttribute {
984            name,
985            detail: format!("too short for u32: {} bytes", data.len()),
986        });
987    }
988    Ok(u32::from_le_bytes(data[0..4].try_into().unwrap()))
989}
990
991fn attr_u8(
992    buf: &[u8],
993    attrs: &AttrTable,
994    attr_type: u16,
995    name: &'static str,
996) -> Result<u8> {
997    let data = get_attr(buf, attrs, attr_type, name)?;
998    if data.is_empty() {
999        return Err(StreamError::InvalidAttribute {
1000            name,
1001            detail: "is empty".into(),
1002        });
1003    }
1004    Ok(data[0])
1005}
1006
1007/// Like `get_attr` but returns `None` instead of an error when the attribute
1008/// is absent. Used for optional attributes in v2 commands.
1009fn get_attr_opt<'a>(
1010    buf: &'a [u8],
1011    attrs: &AttrTable,
1012    attr_type: u16,
1013) -> Option<&'a [u8]> {
1014    let (offset, len) = attrs[(attr_type - 1) as usize]?;
1015    Some(&buf[offset..offset + len])
1016}
1017
1018fn attr_opt_u32(
1019    buf: &[u8],
1020    attrs: &AttrTable,
1021    attr_type: u16,
1022    default: u32,
1023) -> u32 {
1024    match get_attr_opt(buf, attrs, attr_type) {
1025        Some(data) if data.len() >= 4 => {
1026            u32::from_le_bytes(data[0..4].try_into().unwrap())
1027        }
1028        _ => default,
1029    }
1030}
1031
1032fn attr_data(
1033    buf: &[u8],
1034    attrs: &AttrTable,
1035    attr_type: u16,
1036    name: &'static str,
1037) -> Result<Vec<u8>> {
1038    let data = get_attr(buf, attrs, attr_type, name)?;
1039    Ok(data.to_vec())
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044    use super::*;
1045    use std::io::Cursor;
1046
1047    /// Build a valid v1 stream header.
1048    fn v1_header() -> Vec<u8> {
1049        let mut buf = Vec::new();
1050        buf.extend_from_slice(SEND_STREAM_MAGIC);
1051        buf.extend_from_slice(&1u32.to_le_bytes());
1052        buf
1053    }
1054
1055    /// Build a v1 command with proper CRC. `cmd` is the command type code,
1056    /// `payload` is the raw TLV payload bytes.
1057    fn build_command(cmd: u16, payload: &[u8]) -> Vec<u8> {
1058        let len = payload.len() as u32;
1059        // Build header with zeroed CRC for checksum computation.
1060        // Uses raw CRC-32C (init=0, xorout=0) matching the btrfs send format.
1061        let mut crc_input = Vec::new();
1062        crc_input.extend_from_slice(&len.to_le_bytes());
1063        crc_input.extend_from_slice(&cmd.to_le_bytes());
1064        crc_input.extend_from_slice(&[0u8; 4]); // zeroed crc
1065        crc_input.extend_from_slice(payload);
1066        let crc = !crc32c::crc32c_append(!0, &crc_input);
1067
1068        let mut out = Vec::new();
1069        out.extend_from_slice(&len.to_le_bytes());
1070        out.extend_from_slice(&cmd.to_le_bytes());
1071        out.extend_from_slice(&crc.to_le_bytes());
1072        out.extend_from_slice(payload);
1073        out
1074    }
1075
1076    /// Build a TLV attribute with a u16 type, u16 length, and raw data.
1077    fn tlv(attr_type: u16, data: &[u8]) -> Vec<u8> {
1078        let mut buf = Vec::new();
1079        buf.extend_from_slice(&attr_type.to_le_bytes());
1080        buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
1081        buf.extend_from_slice(data);
1082        buf
1083    }
1084
1085    /// Build a TLV string attribute (null-terminated).
1086    fn tlv_string(attr_type: u16, s: &str) -> Vec<u8> {
1087        let mut data = s.as_bytes().to_vec();
1088        data.push(0); // null terminator
1089        tlv(attr_type, &data)
1090    }
1091
1092    /// Build a TLV u64 attribute (little-endian).
1093    fn tlv_u64(attr_type: u16, val: u64) -> Vec<u8> {
1094        tlv(attr_type, &val.to_le_bytes())
1095    }
1096
1097    /// Build a TLV UUID attribute.
1098    fn tlv_uuid(attr_type: u16, uuid: Uuid) -> Vec<u8> {
1099        tlv(attr_type, uuid.as_bytes())
1100    }
1101
1102    /// Build a TLV timespec attribute (8 bytes sec + 4 bytes nsec, LE).
1103    fn tlv_timespec(attr_type: u16, sec: u64, nsec: u32) -> Vec<u8> {
1104        let mut data = Vec::new();
1105        data.extend_from_slice(&sec.to_le_bytes());
1106        data.extend_from_slice(&nsec.to_le_bytes());
1107        tlv(attr_type, &data)
1108    }
1109
1110    // --- Header parsing ---
1111
1112    #[test]
1113    fn valid_v1_header() {
1114        let mut stream = v1_header();
1115        // Append an END command so we can actually read something.
1116        stream.extend_from_slice(&build_command(BTRFS_SEND_C_END, &[]));
1117        let reader = StreamReader::new(Cursor::new(stream)).unwrap();
1118        assert_eq!(reader.version(), 1);
1119    }
1120
1121    #[test]
1122    fn valid_v2_header() {
1123        let mut buf = Vec::new();
1124        buf.extend_from_slice(SEND_STREAM_MAGIC);
1125        buf.extend_from_slice(&2u32.to_le_bytes());
1126        buf.extend_from_slice(&build_command(BTRFS_SEND_C_END, &[]));
1127        let reader = StreamReader::new(Cursor::new(buf)).unwrap();
1128        assert_eq!(reader.version(), 2);
1129    }
1130
1131    #[test]
1132    fn bad_magic() {
1133        let buf = b"not-a-stream\0\x01\x00\x00\x00";
1134        let err = StreamReader::new(Cursor::new(buf.to_vec())).unwrap_err();
1135        assert!(
1136            format!("{err}").contains("bad magic"),
1137            "expected bad magic error, got: {err}"
1138        );
1139    }
1140
1141    #[test]
1142    fn unsupported_version_zero() {
1143        let mut buf = Vec::new();
1144        buf.extend_from_slice(SEND_STREAM_MAGIC);
1145        buf.extend_from_slice(&0u32.to_le_bytes());
1146        let err = StreamReader::new(Cursor::new(buf)).unwrap_err();
1147        assert!(format!("{err}").contains("unsupported"), "got: {err}");
1148    }
1149
1150    #[test]
1151    fn valid_v3_header() {
1152        let mut buf = Vec::new();
1153        buf.extend_from_slice(SEND_STREAM_MAGIC);
1154        buf.extend_from_slice(&3u32.to_le_bytes());
1155        buf.extend_from_slice(&build_command(BTRFS_SEND_C_END, &[]));
1156        let reader = StreamReader::new(Cursor::new(buf)).unwrap();
1157        assert_eq!(reader.version(), 3);
1158    }
1159
1160    #[test]
1161    fn unsupported_version_four() {
1162        let mut buf = Vec::new();
1163        buf.extend_from_slice(SEND_STREAM_MAGIC);
1164        buf.extend_from_slice(&4u32.to_le_bytes());
1165        let err = StreamReader::new(Cursor::new(buf)).unwrap_err();
1166        assert!(format!("{err}").contains("unsupported"), "got: {err}");
1167    }
1168
1169    #[test]
1170    fn truncated_header() {
1171        let buf = b"btrfs-str"; // too short
1172        let err = StreamReader::new(Cursor::new(buf.to_vec())).unwrap_err();
1173        assert!(matches!(err, StreamError::TruncatedHeader(_)), "got: {err}");
1174    }
1175
1176    // --- END command ---
1177
1178    #[test]
1179    fn end_command() {
1180        let mut stream = v1_header();
1181        stream.extend_from_slice(&build_command(BTRFS_SEND_C_END, &[]));
1182        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1183        let cmd = reader.next_command().unwrap().unwrap();
1184        assert!(matches!(cmd, StreamCommand::End));
1185    }
1186
1187    #[test]
1188    fn eof_after_end() {
1189        let mut stream = v1_header();
1190        stream.extend_from_slice(&build_command(BTRFS_SEND_C_END, &[]));
1191        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1192        reader.next_command().unwrap(); // END
1193        assert!(reader.next_command().unwrap().is_none()); // clean EOF
1194    }
1195
1196    // --- CRC validation ---
1197
1198    #[test]
1199    fn corrupted_crc() {
1200        let mut stream = v1_header();
1201        let mut cmd = build_command(BTRFS_SEND_C_END, &[]);
1202        cmd[8] ^= 0xFF; // flip a byte in the CRC field
1203        stream.extend_from_slice(&cmd);
1204        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1205        let err = reader.next_command().unwrap_err();
1206        assert!(format!("{err}").contains("CRC mismatch"), "got: {err}");
1207    }
1208
1209    #[test]
1210    fn corrupted_payload() {
1211        let mut payload = Vec::new();
1212        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "test"));
1213        let mut stream = v1_header();
1214        let mut cmd = build_command(BTRFS_SEND_C_MKFILE, &payload);
1215        // Corrupt a payload byte (after the 10-byte header).
1216        if cmd.len() > 11 {
1217            cmd[11] ^= 0xFF;
1218        }
1219        stream.extend_from_slice(&cmd);
1220        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1221        let err = reader.next_command().unwrap_err();
1222        assert!(format!("{err}").contains("CRC mismatch"), "got: {err}");
1223    }
1224
1225    // --- Simple commands ---
1226
1227    #[test]
1228    fn mkfile_command() {
1229        let payload = tlv_string(BTRFS_SEND_A_PATH, "hello.txt");
1230        let mut stream = v1_header();
1231        stream.extend_from_slice(&build_command(BTRFS_SEND_C_MKFILE, &payload));
1232        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1233        match reader.next_command().unwrap().unwrap() {
1234            StreamCommand::Mkfile { path } => assert_eq!(path, "hello.txt"),
1235            other => panic!("expected Mkfile, got {other:?}"),
1236        }
1237    }
1238
1239    #[test]
1240    fn mkdir_command() {
1241        let payload = tlv_string(BTRFS_SEND_A_PATH, "subdir");
1242        let mut stream = v1_header();
1243        stream.extend_from_slice(&build_command(BTRFS_SEND_C_MKDIR, &payload));
1244        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1245        match reader.next_command().unwrap().unwrap() {
1246            StreamCommand::Mkdir { path } => assert_eq!(path, "subdir"),
1247            other => panic!("expected Mkdir, got {other:?}"),
1248        }
1249    }
1250
1251    #[test]
1252    fn unlink_command() {
1253        let payload = tlv_string(BTRFS_SEND_A_PATH, "gone.txt");
1254        let mut stream = v1_header();
1255        stream.extend_from_slice(&build_command(BTRFS_SEND_C_UNLINK, &payload));
1256        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1257        match reader.next_command().unwrap().unwrap() {
1258            StreamCommand::Unlink { path } => assert_eq!(path, "gone.txt"),
1259            other => panic!("expected Unlink, got {other:?}"),
1260        }
1261    }
1262
1263    #[test]
1264    fn rmdir_command() {
1265        let payload = tlv_string(BTRFS_SEND_A_PATH, "old_dir");
1266        let mut stream = v1_header();
1267        stream.extend_from_slice(&build_command(BTRFS_SEND_C_RMDIR, &payload));
1268        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1269        match reader.next_command().unwrap().unwrap() {
1270            StreamCommand::Rmdir { path } => assert_eq!(path, "old_dir"),
1271            other => panic!("expected Rmdir, got {other:?}"),
1272        }
1273    }
1274
1275    // --- Commands with multiple attributes ---
1276
1277    #[test]
1278    fn rename_command() {
1279        let mut payload = Vec::new();
1280        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "old_name"));
1281        payload
1282            .extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH_TO, "new_name"));
1283        let mut stream = v1_header();
1284        stream.extend_from_slice(&build_command(BTRFS_SEND_C_RENAME, &payload));
1285        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1286        match reader.next_command().unwrap().unwrap() {
1287            StreamCommand::Rename { from, to } => {
1288                assert_eq!(from, "old_name");
1289                assert_eq!(to, "new_name");
1290            }
1291            other => panic!("expected Rename, got {other:?}"),
1292        }
1293    }
1294
1295    #[test]
1296    fn symlink_command() {
1297        let mut payload = Vec::new();
1298        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "link"));
1299        payload
1300            .extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH_LINK, "/target"));
1301        let mut stream = v1_header();
1302        stream
1303            .extend_from_slice(&build_command(BTRFS_SEND_C_SYMLINK, &payload));
1304        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1305        match reader.next_command().unwrap().unwrap() {
1306            StreamCommand::Symlink { path, target } => {
1307                assert_eq!(path, "link");
1308                assert_eq!(target, "/target");
1309            }
1310            other => panic!("expected Symlink, got {other:?}"),
1311        }
1312    }
1313
1314    #[test]
1315    fn chmod_command() {
1316        let mut payload = Vec::new();
1317        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "file.sh"));
1318        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_MODE, 0o755));
1319        let mut stream = v1_header();
1320        stream.extend_from_slice(&build_command(BTRFS_SEND_C_CHMOD, &payload));
1321        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1322        match reader.next_command().unwrap().unwrap() {
1323            StreamCommand::Chmod { path, mode } => {
1324                assert_eq!(path, "file.sh");
1325                assert_eq!(mode, 0o755);
1326            }
1327            other => panic!("expected Chmod, got {other:?}"),
1328        }
1329    }
1330
1331    #[test]
1332    fn chown_command() {
1333        let mut payload = Vec::new();
1334        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "owned"));
1335        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_UID, 1000));
1336        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_GID, 1000));
1337        let mut stream = v1_header();
1338        stream.extend_from_slice(&build_command(BTRFS_SEND_C_CHOWN, &payload));
1339        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1340        match reader.next_command().unwrap().unwrap() {
1341            StreamCommand::Chown { path, uid, gid } => {
1342                assert_eq!(path, "owned");
1343                assert_eq!(uid, 1000);
1344                assert_eq!(gid, 1000);
1345            }
1346            other => panic!("expected Chown, got {other:?}"),
1347        }
1348    }
1349
1350    #[test]
1351    fn truncate_command() {
1352        let mut payload = Vec::new();
1353        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "shrink.bin"));
1354        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_SIZE, 4096));
1355        let mut stream = v1_header();
1356        stream
1357            .extend_from_slice(&build_command(BTRFS_SEND_C_TRUNCATE, &payload));
1358        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1359        match reader.next_command().unwrap().unwrap() {
1360            StreamCommand::Truncate { path, size } => {
1361                assert_eq!(path, "shrink.bin");
1362                assert_eq!(size, 4096);
1363            }
1364            other => panic!("expected Truncate, got {other:?}"),
1365        }
1366    }
1367
1368    #[test]
1369    fn write_command() {
1370        let file_data = b"hello world";
1371        let mut payload = Vec::new();
1372        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "data.bin"));
1373        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_FILE_OFFSET, 512));
1374        payload.extend_from_slice(&tlv(BTRFS_SEND_A_DATA, file_data));
1375        let mut stream = v1_header();
1376        stream.extend_from_slice(&build_command(BTRFS_SEND_C_WRITE, &payload));
1377        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1378        match reader.next_command().unwrap().unwrap() {
1379            StreamCommand::Write { path, offset, data } => {
1380                assert_eq!(path, "data.bin");
1381                assert_eq!(offset, 512);
1382                assert_eq!(data, file_data);
1383            }
1384            other => panic!("expected Write, got {other:?}"),
1385        }
1386    }
1387
1388    #[test]
1389    fn set_xattr_command() {
1390        let xattr_value = b"\x01\x02\x03";
1391        let mut payload = Vec::new();
1392        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "file"));
1393        payload.extend_from_slice(&tlv_string(
1394            BTRFS_SEND_A_XATTR_NAME,
1395            "user.test",
1396        ));
1397        payload.extend_from_slice(&tlv(BTRFS_SEND_A_XATTR_DATA, xattr_value));
1398        let mut stream = v1_header();
1399        stream.extend_from_slice(&build_command(
1400            BTRFS_SEND_C_SET_XATTR,
1401            &payload,
1402        ));
1403        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1404        match reader.next_command().unwrap().unwrap() {
1405            StreamCommand::SetXattr { path, name, data } => {
1406                assert_eq!(path, "file");
1407                assert_eq!(name, "user.test");
1408                assert_eq!(data, xattr_value);
1409            }
1410            other => panic!("expected SetXattr, got {other:?}"),
1411        }
1412    }
1413
1414    #[test]
1415    fn utimes_command() {
1416        let mut payload = Vec::new();
1417        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "timed"));
1418        payload.extend_from_slice(&tlv_timespec(BTRFS_SEND_A_ATIME, 1000, 111));
1419        payload.extend_from_slice(&tlv_timespec(BTRFS_SEND_A_MTIME, 2000, 222));
1420        payload.extend_from_slice(&tlv_timespec(BTRFS_SEND_A_CTIME, 3000, 333));
1421        let mut stream = v1_header();
1422        stream.extend_from_slice(&build_command(BTRFS_SEND_C_UTIMES, &payload));
1423        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1424        match reader.next_command().unwrap().unwrap() {
1425            StreamCommand::Utimes {
1426                path,
1427                atime,
1428                mtime,
1429                ctime,
1430            } => {
1431                assert_eq!(path, "timed");
1432                assert_eq!(atime.sec, 1000);
1433                assert_eq!(atime.nsec, 111);
1434                assert_eq!(mtime.sec, 2000);
1435                assert_eq!(mtime.nsec, 222);
1436                assert_eq!(ctime.sec, 3000);
1437                assert_eq!(ctime.nsec, 333);
1438            }
1439            other => panic!("expected Utimes, got {other:?}"),
1440        }
1441    }
1442
1443    // --- Subvol and Snapshot ---
1444
1445    #[test]
1446    fn subvol_command() {
1447        let uuid =
1448            Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1449        let mut payload = Vec::new();
1450        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "my_subvol"));
1451        payload.extend_from_slice(&tlv_uuid(BTRFS_SEND_A_UUID, uuid));
1452        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_CTRANSID, 42));
1453        let mut stream = v1_header();
1454        stream.extend_from_slice(&build_command(BTRFS_SEND_C_SUBVOL, &payload));
1455        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1456        match reader.next_command().unwrap().unwrap() {
1457            StreamCommand::Subvol {
1458                path,
1459                uuid: u,
1460                ctransid,
1461            } => {
1462                assert_eq!(path, "my_subvol");
1463                assert_eq!(u, uuid);
1464                assert_eq!(ctransid, 42);
1465            }
1466            other => panic!("expected Subvol, got {other:?}"),
1467        }
1468    }
1469
1470    #[test]
1471    fn snapshot_command() {
1472        let uuid =
1473            Uuid::parse_str("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee").unwrap();
1474        let clone_uuid =
1475            Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap();
1476        let mut payload = Vec::new();
1477        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "my_snap"));
1478        payload.extend_from_slice(&tlv_uuid(BTRFS_SEND_A_UUID, uuid));
1479        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_CTRANSID, 100));
1480        payload
1481            .extend_from_slice(&tlv_uuid(BTRFS_SEND_A_CLONE_UUID, clone_uuid));
1482        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_CLONE_CTRANSID, 99));
1483        let mut stream = v1_header();
1484        stream
1485            .extend_from_slice(&build_command(BTRFS_SEND_C_SNAPSHOT, &payload));
1486        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1487        match reader.next_command().unwrap().unwrap() {
1488            StreamCommand::Snapshot {
1489                path,
1490                uuid: u,
1491                ctransid,
1492                clone_uuid: cu,
1493                clone_ctransid,
1494            } => {
1495                assert_eq!(path, "my_snap");
1496                assert_eq!(u, uuid);
1497                assert_eq!(ctransid, 100);
1498                assert_eq!(cu, clone_uuid);
1499                assert_eq!(clone_ctransid, 99);
1500            }
1501            other => panic!("expected Snapshot, got {other:?}"),
1502        }
1503    }
1504
1505    // --- Clone command ---
1506
1507    #[test]
1508    fn clone_command() {
1509        let clone_uuid =
1510            Uuid::parse_str("12345678-1234-1234-1234-123456789abc").unwrap();
1511        let mut payload = Vec::new();
1512        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "dest"));
1513        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_FILE_OFFSET, 0));
1514        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_CLONE_LEN, 4096));
1515        payload
1516            .extend_from_slice(&tlv_uuid(BTRFS_SEND_A_CLONE_UUID, clone_uuid));
1517        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_CLONE_CTRANSID, 7));
1518        payload
1519            .extend_from_slice(&tlv_string(BTRFS_SEND_A_CLONE_PATH, "source"));
1520        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_CLONE_OFFSET, 8192));
1521        let mut stream = v1_header();
1522        stream.extend_from_slice(&build_command(BTRFS_SEND_C_CLONE, &payload));
1523        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1524        match reader.next_command().unwrap().unwrap() {
1525            StreamCommand::Clone {
1526                path,
1527                offset,
1528                len,
1529                clone_uuid: cu,
1530                clone_ctransid,
1531                clone_path,
1532                clone_offset,
1533            } => {
1534                assert_eq!(path, "dest");
1535                assert_eq!(offset, 0);
1536                assert_eq!(len, 4096);
1537                assert_eq!(cu, clone_uuid);
1538                assert_eq!(clone_ctransid, 7);
1539                assert_eq!(clone_path, "source");
1540                assert_eq!(clone_offset, 8192);
1541            }
1542            other => panic!("expected Clone, got {other:?}"),
1543        }
1544    }
1545
1546    // --- Multi-command stream ---
1547
1548    #[test]
1549    fn multiple_commands_in_sequence() {
1550        let uuid = Uuid::nil();
1551        let mut stream = v1_header();
1552
1553        // SUBVOL
1554        let mut p = Vec::new();
1555        p.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "vol"));
1556        p.extend_from_slice(&tlv_uuid(BTRFS_SEND_A_UUID, uuid));
1557        p.extend_from_slice(&tlv_u64(BTRFS_SEND_A_CTRANSID, 1));
1558        stream.extend_from_slice(&build_command(BTRFS_SEND_C_SUBVOL, &p));
1559
1560        // MKFILE
1561        stream.extend_from_slice(&build_command(
1562            BTRFS_SEND_C_MKFILE,
1563            &tlv_string(BTRFS_SEND_A_PATH, "file.txt"),
1564        ));
1565
1566        // END
1567        stream.extend_from_slice(&build_command(BTRFS_SEND_C_END, &[]));
1568
1569        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1570
1571        assert!(matches!(
1572            reader.next_command().unwrap().unwrap(),
1573            StreamCommand::Subvol { .. }
1574        ));
1575        assert!(matches!(
1576            reader.next_command().unwrap().unwrap(),
1577            StreamCommand::Mkfile { .. }
1578        ));
1579        assert!(matches!(
1580            reader.next_command().unwrap().unwrap(),
1581            StreamCommand::End
1582        ));
1583        assert!(reader.next_command().unwrap().is_none());
1584    }
1585
1586    // --- Error cases ---
1587
1588    #[test]
1589    fn unknown_command_type() {
1590        let mut stream = v1_header();
1591        stream.extend_from_slice(&build_command(99, &[]));
1592        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1593        let err = reader.next_command().unwrap_err();
1594        assert!(
1595            format!("{err}").contains("unknown send stream command type 99"),
1596            "got: {err}"
1597        );
1598    }
1599
1600    #[test]
1601    fn missing_required_attribute() {
1602        // MKFILE with no PATH attribute.
1603        let mut stream = v1_header();
1604        stream.extend_from_slice(&build_command(BTRFS_SEND_C_MKFILE, &[]));
1605        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1606        let err = reader.next_command().unwrap_err();
1607        assert!(
1608            format!("{err}").contains("missing required attribute"),
1609            "got: {err}"
1610        );
1611    }
1612
1613    #[test]
1614    fn truncated_payload() {
1615        let mut stream = v1_header();
1616        // Write a command header claiming 100 bytes of payload, but don't
1617        // provide the payload data.
1618        let len = 100u32;
1619        let cmd = BTRFS_SEND_C_END;
1620        let mut crc_input = Vec::new();
1621        crc_input.extend_from_slice(&len.to_le_bytes());
1622        crc_input.extend_from_slice(&cmd.to_le_bytes());
1623        crc_input.extend_from_slice(&[0u8; 4]);
1624        let crc = crc32c::crc32c(&crc_input);
1625        stream.extend_from_slice(&len.to_le_bytes());
1626        stream.extend_from_slice(&cmd.to_le_bytes());
1627        stream.extend_from_slice(&crc.to_le_bytes());
1628        // No payload bytes follow.
1629        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1630        let err = reader.next_command().unwrap_err();
1631        assert!(format!("{err}").contains("truncated"), "got: {err}");
1632    }
1633
1634    #[test]
1635    fn into_inner_returns_reader() {
1636        let stream = v1_header();
1637        let cursor = Cursor::new(stream);
1638        let reader = StreamReader::new(cursor).unwrap();
1639        let _inner: Cursor<Vec<u8>> = reader.into_inner();
1640    }
1641
1642    // --- UpdateExtent ---
1643
1644    #[test]
1645    fn update_extent_command() {
1646        let mut payload = Vec::new();
1647        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "extent.dat"));
1648        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_FILE_OFFSET, 65536));
1649        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_SIZE, 131072));
1650        let mut stream = v1_header();
1651        stream.extend_from_slice(&build_command(
1652            BTRFS_SEND_C_UPDATE_EXTENT,
1653            &payload,
1654        ));
1655        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1656        match reader.next_command().unwrap().unwrap() {
1657            StreamCommand::UpdateExtent { path, offset, len } => {
1658                assert_eq!(path, "extent.dat");
1659                assert_eq!(offset, 65536);
1660                assert_eq!(len, 131072);
1661            }
1662            other => panic!("expected UpdateExtent, got {other:?}"),
1663        }
1664    }
1665
1666    // --- Mknod, Mkfifo, Mksock ---
1667
1668    #[test]
1669    fn mknod_command() {
1670        let mut payload = Vec::new();
1671        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "dev_node"));
1672        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_MODE, 0o660));
1673        payload.extend_from_slice(&tlv_u64(BTRFS_SEND_A_RDEV, 0x0801)); // 8:1
1674        let mut stream = v1_header();
1675        stream.extend_from_slice(&build_command(BTRFS_SEND_C_MKNOD, &payload));
1676        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1677        match reader.next_command().unwrap().unwrap() {
1678            StreamCommand::Mknod { path, mode, rdev } => {
1679                assert_eq!(path, "dev_node");
1680                assert_eq!(mode, 0o660);
1681                assert_eq!(rdev, 0x0801);
1682            }
1683            other => panic!("expected Mknod, got {other:?}"),
1684        }
1685    }
1686
1687    #[test]
1688    fn mkfifo_command() {
1689        let payload = tlv_string(BTRFS_SEND_A_PATH, "my_fifo");
1690        let mut stream = v1_header();
1691        stream.extend_from_slice(&build_command(BTRFS_SEND_C_MKFIFO, &payload));
1692        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1693        match reader.next_command().unwrap().unwrap() {
1694            StreamCommand::Mkfifo { path } => assert_eq!(path, "my_fifo"),
1695            other => panic!("expected Mkfifo, got {other:?}"),
1696        }
1697    }
1698
1699    #[test]
1700    fn mksock_command() {
1701        let payload = tlv_string(BTRFS_SEND_A_PATH, "my_sock");
1702        let mut stream = v1_header();
1703        stream.extend_from_slice(&build_command(BTRFS_SEND_C_MKSOCK, &payload));
1704        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1705        match reader.next_command().unwrap().unwrap() {
1706            StreamCommand::Mksock { path } => assert_eq!(path, "my_sock"),
1707            other => panic!("expected Mksock, got {other:?}"),
1708        }
1709    }
1710
1711    // --- Link and RemoveXattr ---
1712
1713    #[test]
1714    fn link_command() {
1715        let mut payload = Vec::new();
1716        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "hardlink"));
1717        payload
1718            .extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH_LINK, "original"));
1719        let mut stream = v1_header();
1720        stream.extend_from_slice(&build_command(BTRFS_SEND_C_LINK, &payload));
1721        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1722        match reader.next_command().unwrap().unwrap() {
1723            StreamCommand::Link { path, target } => {
1724                assert_eq!(path, "hardlink");
1725                assert_eq!(target, "original");
1726            }
1727            other => panic!("expected Link, got {other:?}"),
1728        }
1729    }
1730
1731    #[test]
1732    fn remove_xattr_command() {
1733        let mut payload = Vec::new();
1734        payload.extend_from_slice(&tlv_string(BTRFS_SEND_A_PATH, "file"));
1735        payload.extend_from_slice(&tlv_string(
1736            BTRFS_SEND_A_XATTR_NAME,
1737            "user.old",
1738        ));
1739        let mut stream = v1_header();
1740        stream.extend_from_slice(&build_command(
1741            BTRFS_SEND_C_REMOVE_XATTR,
1742            &payload,
1743        ));
1744        let mut reader = StreamReader::new(Cursor::new(stream)).unwrap();
1745        match reader.next_command().unwrap().unwrap() {
1746            StreamCommand::RemoveXattr { path, name } => {
1747                assert_eq!(path, "file");
1748                assert_eq!(name, "user.old");
1749            }
1750            other => panic!("expected RemoveXattr, got {other:?}"),
1751        }
1752    }
1753}