diff --git a/src/btrfs_stream.rs b/src/btrfs_stream.rs new file mode 100644 index 0000000..4cf3fe3 --- /dev/null +++ b/src/btrfs_stream.rs @@ -0,0 +1,446 @@ +use std::io::{ErrorKind, Read}; +use std::collections::HashMap; +use std::string::FromUtf8Error; +use num_enum::{TryFromPrimitive, TryFromPrimitiveError}; +use std::fmt::{Debug, Display}; +use chrono::DateTime; +use thiserror::Error; + +#[derive(TryFromPrimitive,Debug,PartialEq,Eq,Clone,Copy,PartialOrd,Ord)] +#[repr(u16)] +pub enum CommandType { + Unspec = 0, + Subvol = 1, + Snapshot = 2, + MkFile = 3, + MkDir = 4, + MkNod = 5, + MkFifo = 6, + MkSock = 7, + Symlink = 8, + Rename = 9, + Link = 10, + Unlink = 11, + RmDir = 12, + SetXAttr = 13, + RemoveXAttr = 14, + Write = 15, + Clone = 16, + Truncate = 17, + Chmod = 18, + Chown = 19, + UTimes = 20, + End = 21, + UpdateExtent = 22, + FAllocate = 23, + FileAttr = 24, + EncodedWrite = 25, +} + +#[derive(Debug,Clone)] +pub enum Command { + Unspec, + Subvol { path: String, uuid: UUID, transid: u64 }, + Snapshot { clone_transid: u64, clone_uuid: UUID, uuid: UUID, path: String, transid: u64 }, + MkFile { path: String, inode: u64 }, + MkDir { path: String, inode: u64 }, + MkNod { path: String, mode: u64, rdev: u64 }, + MkFifo { path: String, inode: u64 }, + MkSock { path: String, inode: u64 }, + Symlink { path: String, inode: u64, link: String }, + Rename { from: String, to: String }, + Link { path: String, link: String }, + Unlink { path: String }, + RmDir { path: String }, + SetXAttr, + RemoveXAttr, + Write { path: String, offset: u64, data: Vec }, + Clone { path: String, offset: u64, clone_path: String, clone_uuid: UUID, clone_len: u64, clone_transid: u64, clone_offset: u64 }, + Truncate { path: String, size: u64 }, + Chmod { path: String, mode: u64 }, + Chown { path: String, uid: u64, gid: u64 }, + UTimes { path: String, atime: Time, mtime: Time, ctime: Time }, + End, + UpdateExtent { path: String, offset: u64, size: u64 }, + FAllocate { path: String, mode: u32, offset: u64, size: u64 }, + FileAttr, + EncodedWrite, +} + +#[derive(TryFromPrimitive,Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Hash)] +#[repr(u16)] +pub enum TLVType { + Unspec = 0, + UUID = 1, + CTransID = 2, + Ino = 3, + Size = 4, + Mode = 5, + UID = 6, + GID = 7, + RDev = 8, + CTime = 9, + MTime = 10, + ATime = 11, + OTime = 12, + XAttrName = 13, + XAttrData = 14, + Path = 15, + PathTo = 16, + PathLink = 17, + FileOffset = 18, + Data = 19, + CloneUUID = 20, + CloneCTransID = 21, + ClonePath = 22, + CloneOffset = 23, + CloneLen = 24, + FAllocateMode = 25, + FileAttr = 26, + UnencodedFileLen = 27, + UnencodedLen = 28, + UnencodedOffset = 29, + Compression = 30, + Encryption = 31, +} + +/// `u128` wrapper for UUIDs +/// +/// This mostly just overrides the `Display` trait to use the typical UUID format `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` +#[derive(Clone)] +pub struct UUID(pub u128); + +impl Display for UUID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let x = self.0.to_le_bytes(); + write!(f, + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12], x[13], x[14], x[15], + ) + } +} + +impl Debug for UUID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let x = self.0.to_le_bytes(); + write!(f, + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12], x[13], x[14], x[15], + ) + } +} + +/// time format for BTRFS stream +/// +/// Consists of a 64 bit UNIX timestamp and a 32 bit nanoseconds field. +#[derive(Debug,Clone)] +pub struct Time { + pub secs: i64, + pub nanos: u32, +} + +impl Display for Time { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", DateTime::from_timestamp(self.secs, self.nanos).ok_or(std::fmt::Error)?) + } +} + +pub trait TlvData: Sized { + fn parse_tlv_data(data: &[u8]) -> Result; +} + +impl TlvData for u64 { + fn parse_tlv_data(data: &[u8]) -> Result { + Ok(u64::from_le_bytes(data.try_into().unwrap())) + } +} + +impl TlvData for u32 { + fn parse_tlv_data(data: &[u8]) -> Result { + Ok(u32::from_le_bytes(data.try_into().unwrap())) + } +} + +impl TlvData for String { + fn parse_tlv_data(data: &[u8]) -> Result { + String::from_utf8(data.to_vec()).map_err(Into::into) + } +} + +impl TlvData for UUID { + fn parse_tlv_data(data: &[u8]) -> Result { + Ok(UUID(u128::from_le_bytes(data.try_into().unwrap()))) + } +} + +impl TlvData for Time { + fn parse_tlv_data(data: &[u8]) -> Result { + let secs = i64::from_le_bytes(data[0..8].try_into().unwrap()); + let nanos = u32::from_le_bytes(data[8..12].try_into().unwrap()); + Ok(Time {secs, nanos}) + } +} + +impl Command { + pub fn ty(&self) -> CommandType { + match self { + Command::Subvol { path: _, uuid: _, transid: _ } => CommandType::Subvol, + Command::Snapshot { clone_transid: _, clone_uuid: _, uuid: _, path: _, transid: _ } => CommandType::Snapshot, + Command::MkFile { path: _, inode: _ } => CommandType::MkFile, + Command::MkDir { path: _, inode: _ } => CommandType::MkDir, + Command::MkNod { path: _, mode: _, rdev: _ } => CommandType::MkNod, + Command::MkFifo { path: _, inode: _ } => CommandType::MkFifo, + Command::MkSock { path: _, inode: _ } => CommandType::MkSock, + Command::Symlink { path: _, inode: _, link: _ } => CommandType::Symlink, + Command::Rename { from: _, to: _ } => CommandType::Rename, + Command::Link { path: _, link: _ } => CommandType::Link, + Command::Unlink { path: _ } => CommandType::Unlink, + Command::RmDir { path: _ } => CommandType::RmDir, + Command::SetXAttr => CommandType::SetXAttr, + Command::RemoveXAttr => CommandType::RemoveXAttr, + Command::Write { path: _, offset: _, data: _ } => CommandType::Write, + Command::Clone { path: _, offset: _, clone_path: _, clone_uuid: _, clone_len: _, clone_transid: _, clone_offset: _ } => CommandType::Clone, + Command::Truncate { path: _, size: _ } => CommandType::Truncate, + Command::Chmod { path: _, mode: _ } => CommandType::Chmod, + Command::Chown { path: _, uid: _, gid: _ } => CommandType::Chown, + Command::UTimes { path: _, atime: _, mtime: _, ctime: _ } => CommandType::UTimes, + Command::End => CommandType::End, + Command::UpdateExtent { path: _, offset: _, size: _ } => CommandType::UpdateExtent, + Command::FAllocate { path: _, mode: _, offset: _, size: _ } => CommandType::FAllocate, + Command::FileAttr => CommandType::FileAttr, + Command::EncodedWrite => CommandType::EncodedWrite, + _ => CommandType::Unspec, + } + } + + pub fn path(&self) -> Option<&str> { + match self { + Command::Symlink { path, inode: _, link: _ } | + Command::Rename { from: path, to: _ } | + Command::Link { path , link: _ } | + Command::Unlink { path } | + Command::RmDir { path } | + Command::MkFile { path, inode: _ } | + Command::MkDir { path, inode: _ } => + Some(path), + _ => None, + } + } + + pub fn parse(data: &mut R) -> Result { + let mut buf: [u8; 10] = [0; 10]; + + if let Err(e) = data.read_exact(&mut buf) { + if e.kind() == ErrorKind::UnexpectedEof { + return Err(BtrfsStreamError::EmptyStream); + } else { + return Err(e.into()); + } + } + + let len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize; + let cmd = CommandType::try_from_primitive( + u16::from_le_bytes(buf[4..6].try_into().unwrap()) + )?; + let _checksum = u32::from_le_bytes(buf[6..10].try_into().unwrap()); + + let mut payload: Vec = vec![0; len]; + data.read_exact(&mut payload)?; + + let mut tlvs: HashMap = HashMap::new(); + let mut offset: usize = 0; + + while offset < len { + let tlvtype = TLVType::try_from_primitive( + u16::from_le_bytes(payload[offset..offset+2].try_into().unwrap()) + )?; + let tlvlen = u16::from_le_bytes(payload[offset+2..offset+4].try_into().unwrap()) as usize; + + tlvs.insert(tlvtype, &payload[offset+4..offset+4+tlvlen]); + offset += tlvlen + 4; + } + + macro_rules! tlv { + ($name:ident, $ty:ty) => { + <$ty>::parse_tlv_data( + tlvs.get(&TLVType::$name) + .ok_or(BtrfsStreamError::MissingTLV(cmd, TLVType::$name))? + )? + }; + } + + let result = match cmd { + CommandType::Subvol => Command::Subvol { + path: tlv!(Path, String), + uuid: tlv!(UUID, UUID), + transid: tlv!(CTransID, u64), + }, + CommandType::Snapshot => Command::Snapshot { + clone_transid: tlv!(CloneCTransID, u64), + clone_uuid: tlv!(CloneUUID, UUID), + uuid: tlv!(UUID, UUID), + path: tlv!(Path, String), + transid: tlv!(CTransID, u64), + }, + CommandType::MkFile => Command::MkFile { + path: tlv!(Path, String), + inode: tlv!(Ino, u64), + }, + CommandType::MkDir => Command::MkDir { + path: tlv!(Path, String), + inode: tlv!(Ino, u64), + }, + CommandType::MkNod => Command::MkNod { + path: tlv!(Path, String), + mode: tlv!(Mode, u64), + rdev: tlv!(RDev, u64), + }, + CommandType::MkFifo => Command::MkFifo { + path: tlv!(Path, String), + inode: tlv!(Ino, u64), + }, + CommandType::MkSock => Command::MkSock { + path: tlv!(Path, String), + inode: tlv!(Ino, u64), + }, + CommandType::Symlink => Command::Symlink { + path: tlv!(Path, String), + inode: tlv!(Ino, u64), + link: tlv!(PathLink, String), + }, + CommandType::Rename => Command::Rename { + from: tlv!(Path, String), + to: tlv!(PathTo, String), + }, + CommandType::Link => Command::Link { + path: tlv!(Path, String), + link: tlv!(PathLink, String), + }, + CommandType::Unlink => Command::Unlink { + path: tlv!(Path, String), + }, + CommandType::RmDir => Command::RmDir { + path: tlv!(Path, String), + }, + CommandType::Write => Command::Write { + path: tlv!(Path, String), + offset: tlv!(FileOffset, u64), + data: tlvs.get(&TLVType::Data) + .ok_or(BtrfsStreamError::MissingTLV(CommandType::Write, TLVType::Data))? + .to_vec(), + }, + CommandType::Clone => Command::Clone { + path: tlv!(Path, String), + offset: tlv!(FileOffset, u64), + clone_path: tlv!(ClonePath, String), + clone_uuid: tlv!(CloneUUID, UUID), + clone_len: tlv!(CloneLen, u64), + clone_transid: tlv!(CloneCTransID, u64), + clone_offset: tlv!(CloneOffset, u64), + }, + CommandType::Truncate => Command::Truncate { + path: tlv!(Path, String), + size: tlv!(Size, u64), + }, + CommandType::Chmod => Command::Chmod { + path: tlv!(Path, String), + mode: tlv!(Mode, u64), + }, + CommandType::Chown => Command::Chown { + path: tlv!(Path, String), + uid: tlv!(UID, u64), + gid: tlv!(GID, u64), + }, + CommandType::UTimes => Command::UTimes { + path: tlv!(Path, String), + atime: tlv!(ATime, Time), + mtime: tlv!(MTime, Time), + ctime: tlv!(CTime, Time), + }, + CommandType::End => Command::End, + CommandType::UpdateExtent => Command::UpdateExtent { + path: tlv!(Path, String), + offset: tlv!(FileOffset, u64), + size: tlv!(Size, u64), + }, + CommandType::FAllocate => Command::FAllocate { + path: tlv!(Path, String), + mode: tlv!(FAllocateMode, u32), + offset: tlv!(FileOffset, u64), + size: tlv!(Size, u64), + }, +// CommandType::SetXAttr => Command::SetXAttr, +// CommandType::RemoveXAttr => Command::RemoveXAttr, +// CommandType::FileAttr => Command::FileAttr, +// CommandType::EncodedWrite => Command::EncodedWrite, + _ => return Err(BtrfsStreamError::UnimplementedCommandType(cmd, tlvs.keys().cloned().collect())), + }; + + Ok(result) + } +} + +pub struct BtrfsSendStream { + reader: R, + version: u32, +} + +impl BtrfsSendStream { + pub fn new(mut reader: R) -> Result { + let mut header: [u8; 17] = [0; 17]; + reader.read(&mut header)?; + + let magic = &header[..12]; + let version = u32::from_le_bytes(header[13..17].try_into().unwrap()); + + if magic != "btrfs-stream".as_bytes() { + return Err(BtrfsStreamError::NotABtrfsStream); + } + + Ok(BtrfsSendStream { reader, version }) + } + + pub fn version(&self) -> u32 { + self.version + } + + pub fn commands(&mut self) -> Commands<'_, R> { + Commands { reader: &mut self.reader } + } +} + +pub struct Commands<'a, R> { + reader: &'a mut R +} + +impl<'a, R: Read> Iterator for Commands<'a, R> { + type Item = Result; + + fn next(&mut self) -> Option { + match Command::parse(&mut self.reader) { + Ok(x) => Some(Ok(x)), + Err(BtrfsStreamError::EmptyStream) => None, + Err(e) => Some(Err(e)), + } + } +} + +#[derive(Error, Debug)] +pub enum BtrfsStreamError { + #[error("missing TLV type {0:?} for command {1:?}")] + MissingTLV(CommandType, TLVType), + #[error("unimplemented command type {0:?} with TLVs: {1:?}")] + UnimplementedCommandType(CommandType, Vec), + #[error("I/O error")] + IOError(#[from] std::io::Error), + #[error("Empty stream")] + EmptyStream, + #[error("invalid command type: {}", .0.number)] + InvalidCommandType(#[from] TryFromPrimitiveError), + #[error("invalid TLV type: {}", .0.number)] + InvalidTLVType(#[from] TryFromPrimitiveError), + #[error("invalid UTF-8 string: {:?}", .0.as_bytes())] + InvalidString(#[from] FromUtf8Error), + #[error("not a valid BTRFS Send stream")] + NotABtrfsStream +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ee38110 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod btrfs_stream; diff --git a/src/main.rs b/src/main.rs index b6dd316..34be1a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,151 +1,9 @@ use std::fs::File; use std::error::Error; -use std::io::{Read, Write, ErrorKind}; -use std::collections::{HashMap, BTreeMap}; -use std::string::FromUtf8Error; -use num_enum::{TryFromPrimitive, TryFromPrimitiveError}; -use std::fmt::{Debug, Display}; -use chrono::DateTime; -use thiserror::Error; - -#[derive(TryFromPrimitive,Debug,PartialEq,Eq,Clone,Copy,PartialOrd,Ord)] -#[repr(u16)] -pub enum CommandType { - Unspec = 0, - Subvol = 1, - Snapshot = 2, - MkFile = 3, - MkDir = 4, - MkNod = 5, - MkFifo = 6, - MkSock = 7, - Symlink = 8, - Rename = 9, - Link = 10, - Unlink = 11, - RmDir = 12, - SetXAttr = 13, - RemoveXAttr = 14, - Write = 15, - Clone = 16, - Truncate = 17, - Chmod = 18, - Chown = 19, - UTimes = 20, - End = 21, - UpdateExtent = 22, - FAllocate = 23, - FileAttr = 24, - EncodedWrite = 25, -} - -#[derive(Debug,Clone)] -pub enum Command { - Unspec, - Subvol { path: String, uuid: UUID, transid: u64 }, - Snapshot { clone_transid: u64, clone_uuid: UUID, uuid: UUID, path: String, transid: u64 }, - MkFile { path: String, inode: u64 }, - MkDir { path: String, inode: u64 }, - MkNod { path: String, mode: u64, rdev: u64 }, - MkFifo { path: String, inode: u64 }, - MkSock { path: String, inode: u64 }, - Symlink { path: String, inode: u64, link: String }, - Rename { from: String, to: String }, - Link { path: String, link: String }, - Unlink { path: String }, - RmDir { path: String }, - SetXAttr, - RemoveXAttr, - Write { path: String, offset: u64, data: Vec }, - Clone { path: String, offset: u64, clone_path: String, clone_uuid: UUID, clone_len: u64, clone_transid: u64, clone_offset: u64 }, - Truncate { path: String, size: u64 }, - Chmod { path: String, mode: u64 }, - Chown { path: String, uid: u64, gid: u64 }, - UTimes { path: String, atime: Time, mtime: Time, ctime: Time }, - End, - UpdateExtent { path: String, offset: u64, size: u64 }, - FAllocate { path: String, mode: u32, offset: u64, size: u64 }, - FileAttr, - EncodedWrite, -} - -#[derive(TryFromPrimitive,Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Hash)] -#[repr(u16)] -pub enum TLVType { - Unspec = 0, - UUID = 1, - CTransID = 2, - Ino = 3, - Size = 4, - Mode = 5, - UID = 6, - GID = 7, - RDev = 8, - CTime = 9, - MTime = 10, - ATime = 11, - OTime = 12, - XAttrName = 13, - XAttrData = 14, - Path = 15, - PathTo = 16, - PathLink = 17, - FileOffset = 18, - Data = 19, - CloneUUID = 20, - CloneCTransID = 21, - ClonePath = 22, - CloneOffset = 23, - CloneLen = 24, - FAllocateMode = 25, - FileAttr = 26, - UnencodedFileLen = 27, - UnencodedLen = 28, - UnencodedOffset = 29, - Compression = 30, - Encryption = 31, -} - -/// `u128` wrapper for UUIDs -/// -/// This mostly just overrides the `Display` trait to use the typical UUID format `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` -#[derive(Clone)] -pub struct UUID(pub u128); - -impl Display for UUID { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - let x = self.0.to_le_bytes(); - write!(f, - "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", - x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12], x[13], x[14], x[15], - ) - } -} - -impl Debug for UUID { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - let x = self.0.to_le_bytes(); - write!(f, - "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", - x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12], x[13], x[14], x[15], - ) - } -} - -/// time format for BTRFS stream -/// -/// Consists of a 64 bit UNIX timestamp and a 32 bit nanoseconds field. -#[derive(Debug,Clone)] -pub struct Time { - pub secs: i64, - pub nanos: u32, -} - -impl Display for Time { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", DateTime::from_timestamp(self.secs, self.nanos).ok_or(std::fmt::Error)?) - } -} +use std::io::{BufReader, ErrorKind, Write}; +use std::collections::BTreeMap; +use std::fmt::Debug; +use unitree::btrfs_stream::{BtrfsSendStream, Command, CommandType}; /// `writeln`, but behaving more gracefully on shutdown macro_rules! out { @@ -195,35 +53,31 @@ fn state_change(old: FileChange, op: FileOp) -> Option { Some(new) } +fn do_op(files: &mut BTreeMap, path: &String, op: FileOp) -> Result<(), String> { + let old = *files.get(path).unwrap_or(&FileChange::Unchanged); + let new = state_change(old, op) + .ok_or(format!("Invalid operation: {:?} on {:?}, path = {}", op, old, path))?; + if new == FileChange::Unchanged { + files.remove(path); + } else { + files.insert(path.clone(), new); + } + + Ok(()) +} + fn main() -> Result<(), Box> { - let mut contents: Vec = Vec::new(); - File::open("../sync_diff_20250525_20251130")?.read_to_end(&mut contents)?; - - out!("Magic: {}", &String::from_utf8(contents[..12].to_vec())?); - out!("Version: {}", u32::from_le_bytes(contents[13..17].try_into()?)); - out!(""); + let mut stream = BtrfsSendStream::new( + BufReader::new( + File::open("../sync_diff_20250208_20251130")? + ) + )?; let mut files: BTreeMap = BTreeMap::new(); - let mut offset: usize = 17; - - fn do_op(files: &mut BTreeMap, path: &String, op: FileOp) -> Result<(), String> { - let old = *files.get(path).unwrap_or(&FileChange::Unchanged); - let new = state_change(old, op) - .ok_or(format!("Invalid operation: {:?} on {:?}, path = {}", op, old, path))?; - if new == FileChange::Unchanged { - files.remove(path); - } else { - files.insert(path.clone(), new); - } - - Ok(()) - } - - while offset < contents.len() { - let (cmd, new_offset) = Command::parse(&contents[offset..])?; - offset += new_offset; + for cmd in stream.commands() { + let cmd = cmd?; if cmd.ty() == CommandType::Snapshot { out!("{:#?}", &cmd); @@ -235,27 +89,55 @@ fn main() -> Result<(), Box> { Command::Symlink { path, inode: _, link: _ } | Command::Link { path, link: _ } => { do_op(&mut files, path, FileOp::Create)?; + + if path.starts_with("o2585216-208524-0") { + out!("{cmd:#?}"); + } }, Command::Rename { from, to } if from != to => { do_op(&mut files, from, FileOp::Delete)?; do_op(&mut files, to, FileOp::Create)?; + + if from.starts_with("o2585216-208524-0") || to.starts_with("o2585216-208524-0") { + out!("{cmd:#?}"); + } + }, Command::Unlink { path } | Command::RmDir { path } => { do_op(&mut files, path, FileOp::Delete)?; + + if path.starts_with("o2585216-208524-0") { + out!("{cmd:#?}"); + } + }, + Command::Write { path, offset, data } => { + do_op(&mut files, path, FileOp::Modify)?; + + if path.starts_with("o2585216-208524-0") { + out!("WRITE {} {} {}", path, offset, data.len()); + } }, - Command::Write { path, offset: _, data: _ } | Command::Truncate { path, size: _ } => { do_op(&mut files, path, FileOp::Modify)?; + + if path.starts_with("o2585216-208524-0") { + out!("{cmd:#?}"); + } }, Command::Chmod { path, mode: _ } | Command::Chown { path, uid: _, gid: _ } => { do_op(&mut files, path, FileOp::ModifyAttributes)?; + + if path.starts_with("o2585216-208524-0") { + out!("{cmd:#?}"); + } }, _ => (), } } + /* let mut last: Option = None; for (file, &change) in &files { if let Some(last_) = &last && file.starts_with(last_) { @@ -264,250 +146,8 @@ fn main() -> Result<(), Box> { last = Some(file.clone()); out!("{file}"); } - } +} + */ Ok(()) } - -pub trait TlvData: Sized { - fn parse_tlv_data(data: &[u8]) -> Result; -} - -impl TlvData for u64 { - fn parse_tlv_data(data: &[u8]) -> Result { - Ok(u64::from_le_bytes(data.try_into().unwrap())) - } -} - -impl TlvData for u32 { - fn parse_tlv_data(data: &[u8]) -> Result { - Ok(u32::from_le_bytes(data.try_into().unwrap())) - } -} - -impl TlvData for String { - fn parse_tlv_data(data: &[u8]) -> Result { - String::from_utf8(data.to_vec()).map_err(Into::into) - } -} - -impl TlvData for UUID { - fn parse_tlv_data(data: &[u8]) -> Result { - Ok(UUID(u128::from_le_bytes(data.try_into().unwrap()))) - } -} - -impl TlvData for Time { - fn parse_tlv_data(data: &[u8]) -> Result { - let secs = i64::from_le_bytes(data[0..8].try_into().unwrap()); - let nanos = u32::from_le_bytes(data[8..12].try_into().unwrap()); - Ok(Time {secs, nanos}) - } -} - -impl Command { - pub fn ty(&self) -> CommandType { - match self { - Command::Subvol { path: _, uuid: _, transid: _ } => CommandType::Subvol, - Command::Snapshot { clone_transid: _, clone_uuid: _, uuid: _, path: _, transid: _ } => CommandType::Snapshot, - Command::MkFile { path: _, inode: _ } => CommandType::MkFile, - Command::MkDir { path: _, inode: _ } => CommandType::MkDir, - Command::MkNod { path: _, mode: _, rdev: _ } => CommandType::MkNod, - Command::MkFifo { path: _, inode: _ } => CommandType::MkFifo, - Command::MkSock { path: _, inode: _ } => CommandType::MkSock, - Command::Symlink { path: _, inode: _, link: _ } => CommandType::Symlink, - Command::Rename { from: _, to: _ } => CommandType::Rename, - Command::Link { path: _, link: _ } => CommandType::Link, - Command::Unlink { path: _ } => CommandType::Unlink, - Command::RmDir { path: _ } => CommandType::RmDir, - Command::SetXAttr => CommandType::SetXAttr, - Command::RemoveXAttr => CommandType::RemoveXAttr, - Command::Write { path: _, offset: _, data: _ } => CommandType::Write, - Command::Clone { path: _, offset: _, clone_path: _, clone_uuid: _, clone_len: _, clone_transid: _, clone_offset: _ } => CommandType::Clone, - Command::Truncate { path: _, size: _ } => CommandType::Truncate, - Command::Chmod { path: _, mode: _ } => CommandType::Chmod, - Command::Chown { path: _, uid: _, gid: _ } => CommandType::Chown, - Command::UTimes { path: _, atime: _, mtime: _, ctime: _ } => CommandType::UTimes, - Command::End => CommandType::End, - Command::UpdateExtent { path: _, offset: _, size: _ } => CommandType::UpdateExtent, - Command::FAllocate { path: _, mode: _, offset: _, size: _ } => CommandType::FAllocate, - Command::FileAttr => CommandType::FileAttr, - Command::EncodedWrite => CommandType::EncodedWrite, - _ => CommandType::Unspec, - } - } - - pub fn path(&self) -> Option<&str> { - match self { - Command::Symlink { path, inode: _, link: _ } | - Command::Rename { from: path, to: _ } | - Command::Link { path , link: _ } | - Command::Unlink { path } | - Command::RmDir { path } | - Command::MkFile { path, inode: _ } | - Command::MkDir { path, inode: _ } => - Some(path), - _ => None, - } - } - - pub fn parse(data: &[u8]) -> Result<(Self, usize), BtrfsStreamError> { - if data.len() < 10 { - return Err(BtrfsStreamError::IncompleteCommand); - } - - let len = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize; - let cmd = CommandType::try_from_primitive( - u16::from_le_bytes(data[4..6].try_into().unwrap()) - )?; - let _checksum = u32::from_le_bytes(data[6..10].try_into().unwrap()); - - let mut tlvs: HashMap = HashMap::new(); - - let mut inner_offset = 0; - while inner_offset < len { - if data.len() < inner_offset + 14 { - return Err(BtrfsStreamError::IncompleteCommand); - } - - let tlvtype = TLVType::try_from_primitive( - u16::from_le_bytes(data[inner_offset+10 .. inner_offset+12].try_into().unwrap()) - )?; - let tlvlen = u16::from_le_bytes(data[inner_offset+12 .. inner_offset+14].try_into().unwrap()) as usize; - - tlvs.insert(tlvtype, (inner_offset+14, inner_offset+14+tlvlen)); - - inner_offset += tlvlen + 4; - } - - let tlv = |ty: TLVType| -> Result<&[u8], BtrfsStreamError> { - let (s, e) = *tlvs.get(&ty).ok_or(BtrfsStreamError::MissingTLV(cmd, ty))?; - Ok(&data[s..e]) - }; - - let result = match cmd { - CommandType::Subvol => Command::Subvol { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - uuid: UUID::parse_tlv_data(tlv(TLVType::UUID)?)?, - transid: u64::parse_tlv_data(tlv(TLVType::CTransID)?)?, - }, - CommandType::Snapshot => Command::Snapshot { - clone_transid: u64::parse_tlv_data(tlv(TLVType::CloneCTransID)?)?, - clone_uuid: UUID::parse_tlv_data(tlv(TLVType::CloneUUID)?)?, - uuid: UUID::parse_tlv_data(tlv(TLVType::UUID)?)?, - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - transid: u64::parse_tlv_data(tlv(TLVType::CTransID)?)?, - }, - CommandType::MkFile => Command::MkFile { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, - }, - CommandType::MkDir => Command::MkDir { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, - }, - CommandType::MkNod => Command::MkNod { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - mode: u64::parse_tlv_data(tlv(TLVType::Mode)?)?, - rdev: u64::parse_tlv_data(tlv(TLVType::RDev)?)?, - }, - CommandType::MkFifo => Command::MkFifo { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, - }, - CommandType::MkSock => Command::MkSock { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, - }, - CommandType::Symlink => Command::Symlink { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, - link: String::parse_tlv_data(tlv(TLVType::PathLink)?)?, - }, - CommandType::Rename => Command::Rename { - from: String::parse_tlv_data(tlv(TLVType::Path)?)?, - to: String::parse_tlv_data(tlv(TLVType::PathTo)?)?, - }, - CommandType::Link => Command::Link { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - link: String::parse_tlv_data(tlv(TLVType::PathLink)?)?, - }, - CommandType::Unlink => Command::Unlink { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - }, - CommandType::RmDir => Command::RmDir { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - }, - CommandType::Write => Command::Write { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - offset: u64::parse_tlv_data(tlv(TLVType::FileOffset)?)?, - data: tlv(TLVType::Data)?.to_vec(), - }, - CommandType::Clone => Command::Clone { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - offset: u64::parse_tlv_data(tlv(TLVType::FileOffset)?)?, - clone_path: String::parse_tlv_data(tlv(TLVType::ClonePath)?)?, - clone_uuid: UUID::parse_tlv_data(tlv(TLVType::CloneUUID)?)?, - clone_len: u64::parse_tlv_data(tlv(TLVType::CloneLen)?)?, - clone_transid: u64::parse_tlv_data(tlv(TLVType::CloneCTransID)?)?, - clone_offset: u64::parse_tlv_data(tlv(TLVType::CloneOffset)?)?, - }, - CommandType::Truncate => Command::Truncate { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - size: u64::parse_tlv_data(tlv(TLVType::Size)?)?, - }, - CommandType::Chmod => Command::Chmod { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - mode: u64::parse_tlv_data(tlv(TLVType::Mode)?)?, - }, - CommandType::Chown => Command::Chown { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - uid: u64::parse_tlv_data(tlv(TLVType::UID)?)?, - gid: u64::parse_tlv_data(tlv(TLVType::GID)?)?, - }, - CommandType::UTimes => Command::UTimes { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - atime: Time::parse_tlv_data(tlv(TLVType::ATime)?)?, - mtime: Time::parse_tlv_data(tlv(TLVType::MTime)?)?, - ctime: Time::parse_tlv_data(tlv(TLVType::CTime)?)?, - }, - CommandType::End => Command::End, - CommandType::UpdateExtent => Command::UpdateExtent { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - offset: u64::parse_tlv_data(tlv(TLVType::FileOffset)?)?, - size: u64::parse_tlv_data(tlv(TLVType::Size)?)?, - }, - CommandType::FAllocate => Command::FAllocate { - path: String::parse_tlv_data(tlv(TLVType::Path)?)?, - mode: u32::parse_tlv_data(tlv(TLVType::FAllocateMode)?)?, - offset: u64::parse_tlv_data(tlv(TLVType::FileOffset)?)?, - size: u64::parse_tlv_data(tlv(TLVType::Size)?)?, - }, -// CommandType::SetXAttr => Command::SetXAttr, -// CommandType::RemoveXAttr => Command::RemoveXAttr, -// CommandType::FileAttr => Command::FileAttr, -// CommandType::EncodedWrite => Command::EncodedWrite, - _ => return Err(BtrfsStreamError::UnimplementedCommandType(cmd, tlvs)), - }; - - Ok((result, len+10)) - } - -} - - -#[derive(Error, Debug)] -pub enum BtrfsStreamError { - #[error("missing TLV type {0:?} for command {1:?}")] - MissingTLV(CommandType, TLVType), - #[error("unimplemented command type {0:?} with TLVs: {1:?}")] - UnimplementedCommandType(CommandType, HashMap), - #[error("command ended prematurely")] - IncompleteCommand, - #[error("invalid command type: {}", .0.number)] - InvalidCommandType(#[from] TryFromPrimitiveError), - #[error("invalid TLV type: {}", .0.number)] - InvalidTLVType(#[from] TryFromPrimitiveError), - #[error("invalid UTF-8 string: {:?}", .0.as_bytes())] - InvalidString(#[from] FromUtf8Error), -}