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