Skip to main content

btrfs_stream/
send.rs

1//! Binary send stream encoder, mirror image of [`crate::StreamReader`].
2//!
3//! Writes TLV-framed [`StreamCommand`] values to any `impl Write`,
4//! producing the same wire format the kernel emits via
5//! `BTRFS_IOC_SEND` and that `btrfs receive` consumes. Roundtrips
6//! through [`crate::StreamReader`] cleanly — that's the primary
7//! correctness target (byte-for-byte parity with kernel send is
8//! impossible because command ordering inside a transaction has
9//! flexibility).
10//!
11//! The protocol versions:
12//!
13//! - **v1**: base set of commands; all encoded data is uncompressed.
14//! - **v2**: adds `EncodedWrite`, `Fallocate`, `Fileattr`. Commands
15//!   are still framed identically; the writer accepts any version
16//!   and trusts the caller not to mix v2 commands into a v1 stream.
17//! - **v3**: adds `EnableVerity`.
18//!
19//! [`StreamWriter::write_command`] does not enforce that a command
20//! belongs to the negotiated version — that's a correctness concern
21//! at a higher layer (the walker or the CLI). This keeps the encoder
22//! simple and lets callers stream arbitrary command sequences for
23//! testing.
24
25use crate::{
26    consts::{
27        BTRFS_SEND_A_ATIME, BTRFS_SEND_A_CLONE_CTRANSID,
28        BTRFS_SEND_A_CLONE_LEN, BTRFS_SEND_A_CLONE_OFFSET,
29        BTRFS_SEND_A_CLONE_PATH, BTRFS_SEND_A_CLONE_UUID,
30        BTRFS_SEND_A_COMPRESSION, BTRFS_SEND_A_CTIME, BTRFS_SEND_A_CTRANSID,
31        BTRFS_SEND_A_DATA, BTRFS_SEND_A_ENCRYPTION,
32        BTRFS_SEND_A_FALLOCATE_MODE, BTRFS_SEND_A_FILE_OFFSET,
33        BTRFS_SEND_A_FILEATTR, BTRFS_SEND_A_GID, BTRFS_SEND_A_MODE,
34        BTRFS_SEND_A_MTIME, BTRFS_SEND_A_PATH, BTRFS_SEND_A_PATH_LINK,
35        BTRFS_SEND_A_PATH_TO, BTRFS_SEND_A_RDEV, BTRFS_SEND_A_SIZE,
36        BTRFS_SEND_A_UID, BTRFS_SEND_A_UNENCODED_FILE_LEN,
37        BTRFS_SEND_A_UNENCODED_LEN, BTRFS_SEND_A_UNENCODED_OFFSET,
38        BTRFS_SEND_A_UUID, BTRFS_SEND_A_VERITY_ALGORITHM,
39        BTRFS_SEND_A_VERITY_BLOCK_SIZE, BTRFS_SEND_A_VERITY_SALT_DATA,
40        BTRFS_SEND_A_VERITY_SIG_DATA, BTRFS_SEND_A_XATTR_DATA,
41        BTRFS_SEND_A_XATTR_NAME, BTRFS_SEND_C_CHMOD, BTRFS_SEND_C_CHOWN,
42        BTRFS_SEND_C_CLONE, BTRFS_SEND_C_ENABLE_VERITY,
43        BTRFS_SEND_C_ENCODED_WRITE, BTRFS_SEND_C_END, BTRFS_SEND_C_FALLOCATE,
44        BTRFS_SEND_C_FILEATTR, BTRFS_SEND_C_LINK, BTRFS_SEND_C_MKDIR,
45        BTRFS_SEND_C_MKFIFO, BTRFS_SEND_C_MKFILE, BTRFS_SEND_C_MKNOD,
46        BTRFS_SEND_C_MKSOCK, BTRFS_SEND_C_REMOVE_XATTR, BTRFS_SEND_C_RENAME,
47        BTRFS_SEND_C_RMDIR, BTRFS_SEND_C_SET_XATTR, BTRFS_SEND_C_SNAPSHOT,
48        BTRFS_SEND_C_SUBVOL, BTRFS_SEND_C_SYMLINK, BTRFS_SEND_C_TRUNCATE,
49        BTRFS_SEND_C_UNLINK, BTRFS_SEND_C_UPDATE_EXTENT, BTRFS_SEND_C_UTIMES,
50        BTRFS_SEND_C_WRITE, CMD_HEADER_LEN, SEND_STREAM_MAGIC,
51        SEND_STREAM_MAGIC_LEN, STREAM_HEADER_LEN,
52    },
53    stream::{StreamCommand, Timespec},
54};
55use std::io::{self, Write};
56use uuid::Uuid;
57
58/// TLV-framed encoder for the btrfs send stream. Construct with
59/// [`StreamWriter::new`] (writes the stream header), then call
60/// [`StreamWriter::write_command`] for each command. Drop or call
61/// [`StreamWriter::finish`] to release the inner writer.
62///
63/// Symmetric with [`crate::StreamReader`] — round trips of every
64/// [`StreamCommand`] variant are unit-tested.
65#[derive(Debug)]
66pub struct StreamWriter<W: Write> {
67    inner: W,
68    version: u32,
69}
70
71impl<W: Write> StreamWriter<W> {
72    /// Wrap `inner` and write the 17-byte stream header
73    /// (magic + version). `version` must be 1, 2, or 3.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error on `version == 0 || version > 3`, or if the
78    /// underlying writer fails.
79    pub fn new(mut inner: W, version: u32) -> io::Result<Self> {
80        if version == 0 || version > 3 {
81            return Err(io::Error::new(
82                io::ErrorKind::InvalidInput,
83                format!(
84                    "unsupported send stream version {version} (supported: 1-3)"
85                ),
86            ));
87        }
88        let mut header = [0u8; STREAM_HEADER_LEN];
89        header[..SEND_STREAM_MAGIC_LEN].copy_from_slice(SEND_STREAM_MAGIC);
90        header[SEND_STREAM_MAGIC_LEN..STREAM_HEADER_LEN]
91            .copy_from_slice(&version.to_le_bytes());
92        inner.write_all(&header)?;
93        Ok(Self { inner, version })
94    }
95
96    /// Negotiated stream protocol version.
97    #[must_use]
98    pub fn version(&self) -> u32 {
99        self.version
100    }
101
102    /// Flush and unwrap the inner writer. The caller is responsible
103    /// for sending an explicit [`StreamCommand::End`] terminator
104    /// beforehand if the consumer expects one (kernel-emitted
105    /// streams always terminate with `End`).
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if flushing fails.
110    pub fn finish(mut self) -> io::Result<W> {
111        self.inner.flush()?;
112        Ok(self.inner)
113    }
114
115    /// Encode `cmd` as a framed TLV command and write it to the
116    /// underlying writer. The frame's CRC32C (raw, init=0) is
117    /// computed over the header (with the CRC field zeroed) and
118    /// payload, matching the parser's verification.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if any individual attribute exceeds the
123    /// 16-bit length field's range, the total payload exceeds the
124    /// 32-bit length field's range, or the underlying writer fails.
125    pub fn write_command(&mut self, cmd: &StreamCommand) -> io::Result<()> {
126        let mut payload = Vec::new();
127        let cmd_id = encode_command(cmd, &mut payload, self.version)?;
128        self.write_framed(cmd_id, &payload)
129    }
130
131    fn write_framed(&mut self, cmd_id: u16, payload: &[u8]) -> io::Result<()> {
132        let payload_len = u32::try_from(payload.len()).map_err(|_| {
133            io::Error::new(
134                io::ErrorKind::InvalidInput,
135                format!(
136                    "command {cmd_id} payload exceeds u32 length field: {} bytes",
137                    payload.len(),
138                ),
139            )
140        })?;
141
142        let mut header = [0u8; CMD_HEADER_LEN];
143        header[0..4].copy_from_slice(&payload_len.to_le_bytes());
144        header[4..6].copy_from_slice(&cmd_id.to_le_bytes());
145        // CRC field stays zero for the computation; we patch it in
146        // afterwards.
147
148        // raw_crc32c(seed=0, data) == !crc32c::crc32c_append(!0, data).
149        // Computed incrementally to avoid copying the whole frame.
150        let crc = crc32c::crc32c_append(!0, &header[0..6]);
151        let crc = crc32c::crc32c_append(crc, &[0u8; 4]);
152        let crc = !crc32c::crc32c_append(crc, payload);
153        header[6..10].copy_from_slice(&crc.to_le_bytes());
154
155        self.inner.write_all(&header)?;
156        self.inner.write_all(payload)?;
157        Ok(())
158    }
159}
160
161// ── Per-command encoders ──────────────────────────────────────────
162
163/// Encode `cmd`'s attribute payload into `out` and return the wire
164/// command type id.
165///
166/// `version` controls one wire-format quirk: in v2+, the
167/// `BTRFS_SEND_A_DATA` attribute has no length field — it extends
168/// to the end of the command payload. The encoder mirrors that
169/// special case for `Write` / `EncodedWrite` (the only commands
170/// that carry DATA), and lets the larger v2 payload size be
171/// addressed by the outer u32 length field.
172#[allow(clippy::too_many_lines)]
173fn encode_command(
174    cmd: &StreamCommand,
175    out: &mut Vec<u8>,
176    version: u32,
177) -> io::Result<u16> {
178    match cmd {
179        StreamCommand::Subvol {
180            path,
181            uuid,
182            ctransid,
183        } => {
184            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
185            put_attr_uuid(out, BTRFS_SEND_A_UUID, uuid);
186            put_attr_u64(out, BTRFS_SEND_A_CTRANSID, *ctransid);
187            Ok(BTRFS_SEND_C_SUBVOL)
188        }
189        StreamCommand::Snapshot {
190            path,
191            uuid,
192            ctransid,
193            clone_uuid,
194            clone_ctransid,
195        } => {
196            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
197            put_attr_uuid(out, BTRFS_SEND_A_UUID, uuid);
198            put_attr_u64(out, BTRFS_SEND_A_CTRANSID, *ctransid);
199            put_attr_uuid(out, BTRFS_SEND_A_CLONE_UUID, clone_uuid);
200            put_attr_u64(out, BTRFS_SEND_A_CLONE_CTRANSID, *clone_ctransid);
201            Ok(BTRFS_SEND_C_SNAPSHOT)
202        }
203        StreamCommand::Mkfile { path } => {
204            path_only(out, path, BTRFS_SEND_C_MKFILE)
205        }
206        StreamCommand::Mkdir { path } => {
207            path_only(out, path, BTRFS_SEND_C_MKDIR)
208        }
209        StreamCommand::Mknod { path, mode, rdev } => {
210            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
211            put_attr_u64(out, BTRFS_SEND_A_MODE, *mode);
212            put_attr_u64(out, BTRFS_SEND_A_RDEV, *rdev);
213            Ok(BTRFS_SEND_C_MKNOD)
214        }
215        StreamCommand::Mkfifo { path } => {
216            path_only(out, path, BTRFS_SEND_C_MKFIFO)
217        }
218        StreamCommand::Mksock { path } => {
219            path_only(out, path, BTRFS_SEND_C_MKSOCK)
220        }
221        StreamCommand::Symlink { path, target } => {
222            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
223            put_attr_str(out, BTRFS_SEND_A_PATH_LINK, target)?;
224            Ok(BTRFS_SEND_C_SYMLINK)
225        }
226        StreamCommand::Rename { from, to } => {
227            put_attr_str(out, BTRFS_SEND_A_PATH, from)?;
228            put_attr_str(out, BTRFS_SEND_A_PATH_TO, to)?;
229            Ok(BTRFS_SEND_C_RENAME)
230        }
231        StreamCommand::Link { path, target } => {
232            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
233            put_attr_str(out, BTRFS_SEND_A_PATH_LINK, target)?;
234            Ok(BTRFS_SEND_C_LINK)
235        }
236        StreamCommand::Unlink { path } => {
237            path_only(out, path, BTRFS_SEND_C_UNLINK)
238        }
239        StreamCommand::Rmdir { path } => {
240            path_only(out, path, BTRFS_SEND_C_RMDIR)
241        }
242        StreamCommand::Write { path, offset, data } => {
243            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
244            put_attr_u64(out, BTRFS_SEND_A_FILE_OFFSET, *offset);
245            // DATA must be the last attribute in v2+ since it has
246            // no length field there; emitting it last is also fine
247            // for v1.
248            put_attr_data(out, data, version)?;
249            Ok(BTRFS_SEND_C_WRITE)
250        }
251        StreamCommand::Clone {
252            path,
253            offset,
254            len,
255            clone_uuid,
256            clone_ctransid,
257            clone_path,
258            clone_offset,
259        } => {
260            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
261            put_attr_u64(out, BTRFS_SEND_A_FILE_OFFSET, *offset);
262            put_attr_u64(out, BTRFS_SEND_A_CLONE_LEN, *len);
263            put_attr_uuid(out, BTRFS_SEND_A_CLONE_UUID, clone_uuid);
264            put_attr_u64(out, BTRFS_SEND_A_CLONE_CTRANSID, *clone_ctransid);
265            put_attr_str(out, BTRFS_SEND_A_CLONE_PATH, clone_path)?;
266            put_attr_u64(out, BTRFS_SEND_A_CLONE_OFFSET, *clone_offset);
267            Ok(BTRFS_SEND_C_CLONE)
268        }
269        StreamCommand::SetXattr { path, name, data } => {
270            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
271            put_attr_str(out, BTRFS_SEND_A_XATTR_NAME, name)?;
272            put_attr_bytes(out, BTRFS_SEND_A_XATTR_DATA, data)?;
273            Ok(BTRFS_SEND_C_SET_XATTR)
274        }
275        StreamCommand::RemoveXattr { path, name } => {
276            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
277            put_attr_str(out, BTRFS_SEND_A_XATTR_NAME, name)?;
278            Ok(BTRFS_SEND_C_REMOVE_XATTR)
279        }
280        StreamCommand::Truncate { path, size } => {
281            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
282            put_attr_u64(out, BTRFS_SEND_A_SIZE, *size);
283            Ok(BTRFS_SEND_C_TRUNCATE)
284        }
285        StreamCommand::Chmod { path, mode } => {
286            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
287            put_attr_u64(out, BTRFS_SEND_A_MODE, *mode);
288            Ok(BTRFS_SEND_C_CHMOD)
289        }
290        StreamCommand::Chown { path, uid, gid } => {
291            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
292            put_attr_u64(out, BTRFS_SEND_A_UID, *uid);
293            put_attr_u64(out, BTRFS_SEND_A_GID, *gid);
294            Ok(BTRFS_SEND_C_CHOWN)
295        }
296        StreamCommand::Utimes {
297            path,
298            atime,
299            mtime,
300            ctime,
301        } => {
302            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
303            put_attr_timespec(out, BTRFS_SEND_A_ATIME, atime);
304            put_attr_timespec(out, BTRFS_SEND_A_MTIME, mtime);
305            put_attr_timespec(out, BTRFS_SEND_A_CTIME, ctime);
306            Ok(BTRFS_SEND_C_UTIMES)
307        }
308        StreamCommand::UpdateExtent { path, offset, len } => {
309            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
310            put_attr_u64(out, BTRFS_SEND_A_FILE_OFFSET, *offset);
311            put_attr_u64(out, BTRFS_SEND_A_SIZE, *len);
312            Ok(BTRFS_SEND_C_UPDATE_EXTENT)
313        }
314        StreamCommand::EncodedWrite {
315            path,
316            offset,
317            unencoded_file_len,
318            unencoded_len,
319            unencoded_offset,
320            compression,
321            encryption,
322            data,
323        } => {
324            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
325            put_attr_u64(out, BTRFS_SEND_A_FILE_OFFSET, *offset);
326            put_attr_u64(
327                out,
328                BTRFS_SEND_A_UNENCODED_FILE_LEN,
329                *unencoded_file_len,
330            );
331            put_attr_u64(out, BTRFS_SEND_A_UNENCODED_LEN, *unencoded_len);
332            put_attr_u64(out, BTRFS_SEND_A_UNENCODED_OFFSET, *unencoded_offset);
333            put_attr_u32(out, BTRFS_SEND_A_COMPRESSION, *compression);
334            put_attr_u32(out, BTRFS_SEND_A_ENCRYPTION, *encryption);
335            put_attr_data(out, data, version)?;
336            Ok(BTRFS_SEND_C_ENCODED_WRITE)
337        }
338        StreamCommand::Fallocate {
339            path,
340            mode,
341            offset,
342            len,
343        } => {
344            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
345            put_attr_u32(out, BTRFS_SEND_A_FALLOCATE_MODE, *mode);
346            put_attr_u64(out, BTRFS_SEND_A_FILE_OFFSET, *offset);
347            put_attr_u64(out, BTRFS_SEND_A_SIZE, *len);
348            Ok(BTRFS_SEND_C_FALLOCATE)
349        }
350        StreamCommand::Fileattr { path, attr } => {
351            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
352            put_attr_u64(out, BTRFS_SEND_A_FILEATTR, *attr);
353            Ok(BTRFS_SEND_C_FILEATTR)
354        }
355        StreamCommand::EnableVerity {
356            path,
357            algorithm,
358            block_size,
359            salt,
360            sig,
361        } => {
362            put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
363            put_attr_u8(out, BTRFS_SEND_A_VERITY_ALGORITHM, *algorithm);
364            put_attr_u32(out, BTRFS_SEND_A_VERITY_BLOCK_SIZE, *block_size);
365            put_attr_bytes(out, BTRFS_SEND_A_VERITY_SALT_DATA, salt)?;
366            put_attr_bytes(out, BTRFS_SEND_A_VERITY_SIG_DATA, sig)?;
367            Ok(BTRFS_SEND_C_ENABLE_VERITY)
368        }
369        StreamCommand::End => Ok(BTRFS_SEND_C_END),
370    }
371}
372
373fn path_only(out: &mut Vec<u8>, path: &str, cmd_id: u16) -> io::Result<u16> {
374    put_attr_str(out, BTRFS_SEND_A_PATH, path)?;
375    Ok(cmd_id)
376}
377
378// ── TLV writers ───────────────────────────────────────────────────
379
380/// Write a TLV header (`type: u16 | len: u16`) followed by the
381/// payload bytes. Errors when `data.len() > u16::MAX` since the
382/// TLV length field can't represent it.
383fn put_attr_bytes(out: &mut Vec<u8>, attr: u16, data: &[u8]) -> io::Result<()> {
384    let len = u16::try_from(data.len()).map_err(|_| {
385        io::Error::new(
386            io::ErrorKind::InvalidInput,
387            format!(
388                "attribute {attr} payload exceeds u16 length field: {} bytes",
389                data.len(),
390            ),
391        )
392    })?;
393    out.extend_from_slice(&attr.to_le_bytes());
394    out.extend_from_slice(&len.to_le_bytes());
395    out.extend_from_slice(data);
396    Ok(())
397}
398
399fn put_attr_str(out: &mut Vec<u8>, attr: u16, s: &str) -> io::Result<()> {
400    put_attr_bytes(out, attr, s.as_bytes())
401}
402
403/// Encode `BTRFS_SEND_A_DATA` for `Write` / `EncodedWrite`. In v1
404/// streams this is a regular TLV attribute; in v2+ the length
405/// field is omitted and the data extends to the end of the
406/// command payload, which lets writes exceed the 64 KiB v1 cap.
407/// MUST be called last for the command — anything emitted after it
408/// would be parsed as part of the data on v2+.
409fn put_attr_data(
410    out: &mut Vec<u8>,
411    data: &[u8],
412    version: u32,
413) -> io::Result<()> {
414    if version >= 2 {
415        out.extend_from_slice(&BTRFS_SEND_A_DATA.to_le_bytes());
416        out.extend_from_slice(data);
417        Ok(())
418    } else {
419        put_attr_bytes(out, BTRFS_SEND_A_DATA, data)
420    }
421}
422
423fn put_attr_u64(out: &mut Vec<u8>, attr: u16, v: u64) {
424    put_attr_bytes(out, attr, &v.to_le_bytes())
425        .expect("8-byte payload always fits");
426}
427
428fn put_attr_u32(out: &mut Vec<u8>, attr: u16, v: u32) {
429    put_attr_bytes(out, attr, &v.to_le_bytes())
430        .expect("4-byte payload always fits");
431}
432
433fn put_attr_u8(out: &mut Vec<u8>, attr: u16, v: u8) {
434    put_attr_bytes(out, attr, &[v]).expect("1-byte payload always fits");
435}
436
437fn put_attr_uuid(out: &mut Vec<u8>, attr: u16, uuid: &Uuid) {
438    put_attr_bytes(out, attr, uuid.as_bytes())
439        .expect("16-byte UUID always fits");
440}
441
442fn put_attr_timespec(out: &mut Vec<u8>, attr: u16, t: &Timespec) {
443    let mut buf = [0u8; 12];
444    buf[0..8].copy_from_slice(&t.sec.to_le_bytes());
445    buf[8..12].copy_from_slice(&t.nsec.to_le_bytes());
446    put_attr_bytes(out, attr, &buf).expect("12-byte timespec always fits");
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::StreamReader;
453
454    /// Exercise every [`StreamCommand`] variant by writing it to a
455    /// buffer, parsing back, and asserting the parsed command
456    /// matches the original. The roundtrip proves both that the
457    /// encoder and parser agree on the wire format and that the
458    /// CRC32C produced by the encoder validates.
459    fn roundtrip(version: u32, cmds: &[StreamCommand]) {
460        let mut buf: Vec<u8> = Vec::new();
461        let mut writer = StreamWriter::new(&mut buf, version).unwrap();
462        for cmd in cmds {
463            writer.write_command(cmd).unwrap();
464        }
465        writer.finish().unwrap();
466
467        let mut reader = StreamReader::new(buf.as_slice()).unwrap();
468        assert_eq!(reader.version(), version);
469        for expected in cmds {
470            let got = reader.next_command().expect("parse").expect("not eof");
471            assert_eq!(format!("{got:?}"), format!("{expected:?}"));
472        }
473        assert!(reader.next_command().expect("eof check").is_none());
474    }
475
476    #[test]
477    fn header_only_roundtrips_each_version() {
478        for v in [1, 2, 3] {
479            let mut buf = Vec::new();
480            let writer = StreamWriter::new(&mut buf, v).unwrap();
481            writer.finish().unwrap();
482            let reader = StreamReader::new(buf.as_slice()).unwrap();
483            assert_eq!(reader.version(), v);
484        }
485    }
486
487    #[test]
488    fn invalid_versions_rejected() {
489        let mut buf = Vec::new();
490        assert!(StreamWriter::new(&mut buf, 0).is_err());
491        let mut buf = Vec::new();
492        assert!(StreamWriter::new(&mut buf, 4).is_err());
493    }
494
495    #[test]
496    fn subvol_and_snapshot_roundtrip() {
497        let uuid = Uuid::from_u128(0x0011_2233_4455_6677_8899_aabb_ccdd_eeff);
498        let clone_uuid =
499            Uuid::from_u128(0xdead_beef_dead_beef_dead_beef_dead_beef);
500        roundtrip(
501            1,
502            &[
503                StreamCommand::Subvol {
504                    path: "snap1".into(),
505                    uuid,
506                    ctransid: 42,
507                },
508                StreamCommand::Snapshot {
509                    path: "snap2".into(),
510                    uuid,
511                    ctransid: 100,
512                    clone_uuid,
513                    clone_ctransid: 50,
514                },
515                StreamCommand::End,
516            ],
517        );
518    }
519
520    #[test]
521    fn filesystem_object_creation_roundtrips() {
522        roundtrip(
523            1,
524            &[
525                StreamCommand::Mkfile {
526                    path: "f.txt".into(),
527                },
528                StreamCommand::Mkdir { path: "d".into() },
529                StreamCommand::Mknod {
530                    path: "n".into(),
531                    mode: 0o600,
532                    rdev: 0x102,
533                },
534                StreamCommand::Mkfifo { path: "p".into() },
535                StreamCommand::Mksock { path: "s".into() },
536                StreamCommand::Symlink {
537                    path: "l".into(),
538                    target: "f.txt".into(),
539                },
540            ],
541        );
542    }
543
544    #[test]
545    fn rename_link_unlink_rmdir_roundtrip() {
546        roundtrip(
547            1,
548            &[
549                StreamCommand::Rename {
550                    from: "a".into(),
551                    to: "b".into(),
552                },
553                StreamCommand::Link {
554                    path: "alias".into(),
555                    target: "real".into(),
556                },
557                StreamCommand::Unlink { path: "old".into() },
558                StreamCommand::Rmdir {
559                    path: "empty".into(),
560                },
561            ],
562        );
563    }
564
565    #[test]
566    fn write_clone_truncate_roundtrip() {
567        let clone_uuid =
568            Uuid::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
569        roundtrip(
570            1,
571            &[
572                StreamCommand::Write {
573                    path: "f".into(),
574                    offset: 4096,
575                    data: vec![0x42; 1024],
576                },
577                StreamCommand::Clone {
578                    path: "dst".into(),
579                    offset: 8192,
580                    len: 4096,
581                    clone_uuid,
582                    clone_ctransid: 7,
583                    clone_path: "src".into(),
584                    clone_offset: 0,
585                },
586                StreamCommand::Truncate {
587                    path: "f".into(),
588                    size: 65536,
589                },
590            ],
591        );
592    }
593
594    #[test]
595    fn xattr_perms_times_roundtrip() {
596        roundtrip(
597            1,
598            &[
599                StreamCommand::SetXattr {
600                    path: "f".into(),
601                    name: "user.greeting".into(),
602                    data: b"hello".to_vec(),
603                },
604                StreamCommand::RemoveXattr {
605                    path: "f".into(),
606                    name: "user.gone".into(),
607                },
608                StreamCommand::Chmod {
609                    path: "f".into(),
610                    mode: 0o644,
611                },
612                StreamCommand::Chown {
613                    path: "f".into(),
614                    uid: 1000,
615                    gid: 1001,
616                },
617                StreamCommand::Utimes {
618                    path: "f".into(),
619                    atime: Timespec { sec: 100, nsec: 1 },
620                    mtime: Timespec { sec: 200, nsec: 2 },
621                    ctime: Timespec { sec: 300, nsec: 3 },
622                },
623                StreamCommand::UpdateExtent {
624                    path: "f".into(),
625                    offset: 0,
626                    len: 4096,
627                },
628            ],
629        );
630    }
631
632    #[test]
633    fn v2_commands_roundtrip() {
634        roundtrip(
635            2,
636            &[
637                StreamCommand::EncodedWrite {
638                    path: "f".into(),
639                    offset: 0,
640                    unencoded_file_len: 4096,
641                    unencoded_len: 4096,
642                    unencoded_offset: 0,
643                    compression: 2, // zstd
644                    encryption: 0,
645                    data: vec![0xab; 256],
646                },
647                StreamCommand::Fallocate {
648                    path: "f".into(),
649                    mode: 3, // PUNCH_HOLE | KEEP_SIZE
650                    offset: 4096,
651                    len: 4096,
652                },
653                StreamCommand::Fileattr {
654                    path: "f".into(),
655                    attr: 0x40, // FS_NOCOW_FL
656                },
657            ],
658        );
659    }
660
661    #[test]
662    fn v3_enable_verity_roundtrips() {
663        roundtrip(
664            3,
665            &[StreamCommand::EnableVerity {
666                path: "f".into(),
667                algorithm: 1,
668                block_size: 4096,
669                salt: vec![1, 2, 3, 4],
670                sig: vec![],
671            }],
672        );
673    }
674
675    #[test]
676    fn end_command_roundtrips() {
677        roundtrip(1, &[StreamCommand::End]);
678    }
679
680    #[test]
681    fn corrupted_payload_fails_crc_check() {
682        let mut buf = Vec::new();
683        let mut writer = StreamWriter::new(&mut buf, 1).unwrap();
684        writer
685            .write_command(&StreamCommand::Mkfile { path: "f".into() })
686            .unwrap();
687        writer.finish().unwrap();
688
689        // Flip a bit in the first command's payload (after the
690        // 17-byte stream header + 10-byte command header).
691        let payload_start = STREAM_HEADER_LEN + CMD_HEADER_LEN;
692        buf[payload_start + 4] ^= 0x01;
693
694        let mut reader = StreamReader::new(buf.as_slice()).unwrap();
695        let err = reader.next_command().unwrap_err();
696        assert!(
697            matches!(err, crate::StreamError::CrcMismatch { .. }),
698            "expected CrcMismatch, got {err:?}",
699        );
700    }
701}