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