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