diff --git a/Cargo.toml b/Cargo.toml index e200bd3..3e603c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] chrono = "0.4.40" num_enum = "0.7.3" +thiserror = "2.0.17" diff --git a/src/main.rs b/src/main.rs index d885291..b6dd316 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,11 @@ use std::fs::File; use std::error::Error; use std::io::{Read, Write, ErrorKind}; use std::collections::{HashMap, BTreeMap}; -use num_enum::TryFromPrimitive; -use std::fmt::Display; +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)] @@ -40,13 +42,13 @@ pub enum CommandType { #[derive(Debug,Clone)] pub enum Command { Unspec, - Subvol, + 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, - MkFifo, - MkSock, + 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 }, @@ -61,8 +63,8 @@ pub enum Command { Chown { path: String, uid: u64, gid: u64 }, UTimes { path: String, atime: Time, mtime: Time, ctime: Time }, End, - UpdateExtent, - FAllocate, + UpdateExtent { path: String, offset: u64, size: u64 }, + FAllocate { path: String, mode: u32, offset: u64, size: u64 }, FileAttr, EncodedWrite, } @@ -107,8 +109,8 @@ pub enum TLVType { /// `u128` wrapper for UUIDs /// /// This mostly just overrides the `Display` trait to use the typical UUID format `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` -#[derive(Debug,Clone)] -pub struct UUID(u128); +#[derive(Clone)] +pub struct UUID(pub u128); impl Display for UUID { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { @@ -120,13 +122,23 @@ impl Display for UUID { } } +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 { - secs: i64, - nanos: u32, + pub secs: i64, + pub nanos: u32, } impl Display for Time { @@ -196,7 +208,7 @@ fn main() -> Result<(), Box> { let mut offset: usize = 17; - fn do_op(files: &mut BTreeMap, path: &String, op: FileOp) -> Result<(), Box> { + 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))?; @@ -205,6 +217,7 @@ fn main() -> Result<(), Box> { } else { files.insert(path.clone(), new); } + Ok(()) } @@ -212,7 +225,10 @@ fn main() -> Result<(), Box> { let (cmd, new_offset) = Command::parse(&contents[offset..])?; offset += new_offset; - // out!("{:?}", cmd.ty()); + if cmd.ty() == CommandType::Snapshot { + out!("{:#?}", &cmd); + } + match &cmd { Command::MkFile { path, inode: _ } | Command::MkDir { path, inode: _ } | @@ -254,37 +270,37 @@ fn main() -> Result<(), Box> { } pub trait TlvData: Sized { - fn parse_tlv_data(data: &[u8]) -> Result>; + 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()?)) + 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()?)) + 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> { + 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()?))) + 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()?); - let nanos = u32::from_le_bytes(data[8..12].try_into()?); + 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}) } } @@ -292,13 +308,13 @@ impl TlvData for Time { impl Command { pub fn ty(&self) -> CommandType { match self { - Command::Subvol => CommandType::Subvol, + 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 => CommandType::MkNod, - Command::MkFifo => CommandType::MkFifo, - Command::MkSock => CommandType::MkSock, + 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, @@ -313,8 +329,8 @@ impl Command { Command::Chown { path: _, uid: _, gid: _ } => CommandType::Chown, Command::UTimes { path: _, atime: _, mtime: _, ctime: _ } => CommandType::UTimes, Command::End => CommandType::End, - Command::UpdateExtent => CommandType::UpdateExtent, - Command::FAllocate => CommandType::FAllocate, + 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, @@ -335,34 +351,46 @@ impl Command { } } - pub fn parse(data: &[u8]) -> Result<(Self, usize), Box> { - let len = u32::from_le_bytes(data[0..4].try_into()?) as usize; + 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()?) + u16::from_le_bytes(data[4..6].try_into().unwrap()) )?; - let _checksum = u32::from_le_bytes(data[6..10].try_into()?); + 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()?) + 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()?) as usize; + 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], Box> { - let (s, e) = *tlvs.get(&ty).ok_or(format!("Command of type {cmd:?} needs a TLV of type {ty:?}"))?; + 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, + 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)?)?, @@ -378,9 +406,19 @@ impl Command { path: String::parse_tlv_data(tlv(TLVType::Path)?)?, inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, }, -// CommandType::MkNod => Command::MkNod, -// CommandType::MkFifo => Command::MkFifo, -// CommandType::MkSock => Command::MkSock, + 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)?)?, @@ -400,8 +438,6 @@ impl Command { CommandType::RmDir => Command::RmDir { path: String::parse_tlv_data(tlv(TLVType::Path)?)?, }, -// CommandType::SetXAttr => Command::SetXAttr, -// CommandType::RemoveXAttr => Command::RemoveXAttr, CommandType::Write => Command::Write { path: String::parse_tlv_data(tlv(TLVType::Path)?)?, offset: u64::parse_tlv_data(tlv(TLVType::FileOffset)?)?, @@ -436,14 +472,42 @@ impl Command { ctime: Time::parse_tlv_data(tlv(TLVType::CTime)?)?, }, CommandType::End => Command::End, -// CommandType::UpdateExtent => Command::UpdateExtent, -// CommandType::FAllocate => Command::FAllocate, + 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(format!("Command type {cmd:?} not implemented! tlvdata = {tlvs:?}").into()), + _ => 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), +}