error handling and other stuff

This commit is contained in:
Florian Stecker
2025-12-16 22:37:09 -05:00
parent 6a6cbdef2c
commit 86d0915fae
2 changed files with 113 additions and 48 deletions

View File

@@ -6,3 +6,4 @@ edition = "2024"
[dependencies] [dependencies]
chrono = "0.4.40" chrono = "0.4.40"
num_enum = "0.7.3" num_enum = "0.7.3"
thiserror = "2.0.17"

View File

@@ -2,9 +2,11 @@ use std::fs::File;
use std::error::Error; use std::error::Error;
use std::io::{Read, Write, ErrorKind}; use std::io::{Read, Write, ErrorKind};
use std::collections::{HashMap, BTreeMap}; use std::collections::{HashMap, BTreeMap};
use num_enum::TryFromPrimitive; use std::string::FromUtf8Error;
use std::fmt::Display; use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
use std::fmt::{Debug, Display};
use chrono::DateTime; use chrono::DateTime;
use thiserror::Error;
#[derive(TryFromPrimitive,Debug,PartialEq,Eq,Clone,Copy,PartialOrd,Ord)] #[derive(TryFromPrimitive,Debug,PartialEq,Eq,Clone,Copy,PartialOrd,Ord)]
#[repr(u16)] #[repr(u16)]
@@ -40,13 +42,13 @@ pub enum CommandType {
#[derive(Debug,Clone)] #[derive(Debug,Clone)]
pub enum Command { pub enum Command {
Unspec, Unspec,
Subvol, Subvol { path: String, uuid: UUID, transid: u64 },
Snapshot { clone_transid: u64, clone_uuid: UUID, uuid: UUID, path: String, transid: u64 }, Snapshot { clone_transid: u64, clone_uuid: UUID, uuid: UUID, path: String, transid: u64 },
MkFile { path: String, inode: u64 }, MkFile { path: String, inode: u64 },
MkDir { path: String, inode: u64 }, MkDir { path: String, inode: u64 },
MkNod, MkNod { path: String, mode: u64, rdev: u64 },
MkFifo, MkFifo { path: String, inode: u64 },
MkSock, MkSock { path: String, inode: u64 },
Symlink { path: String, inode: u64, link: String }, Symlink { path: String, inode: u64, link: String },
Rename { from: String, to: String }, Rename { from: String, to: String },
Link { path: String, link: String }, Link { path: String, link: String },
@@ -61,8 +63,8 @@ pub enum Command {
Chown { path: String, uid: u64, gid: u64 }, Chown { path: String, uid: u64, gid: u64 },
UTimes { path: String, atime: Time, mtime: Time, ctime: Time }, UTimes { path: String, atime: Time, mtime: Time, ctime: Time },
End, End,
UpdateExtent, UpdateExtent { path: String, offset: u64, size: u64 },
FAllocate, FAllocate { path: String, mode: u32, offset: u64, size: u64 },
FileAttr, FileAttr,
EncodedWrite, EncodedWrite,
} }
@@ -107,8 +109,8 @@ pub enum TLVType {
/// `u128` wrapper for UUIDs /// `u128` wrapper for UUIDs
/// ///
/// This mostly just overrides the `Display` trait to use the typical UUID format `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` /// This mostly just overrides the `Display` trait to use the typical UUID format `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`
#[derive(Debug,Clone)] #[derive(Clone)]
pub struct UUID(u128); pub struct UUID(pub u128);
impl Display for UUID { impl Display for UUID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 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 /// time format for BTRFS stream
/// ///
/// Consists of a 64 bit UNIX timestamp and a 32 bit nanoseconds field. /// Consists of a 64 bit UNIX timestamp and a 32 bit nanoseconds field.
#[derive(Debug,Clone)] #[derive(Debug,Clone)]
pub struct Time { pub struct Time {
secs: i64, pub secs: i64,
nanos: u32, pub nanos: u32,
} }
impl Display for Time { impl Display for Time {
@@ -196,7 +208,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut offset: usize = 17; let mut offset: usize = 17;
fn do_op(files: &mut BTreeMap<String, FileChange>, path: &String, op: FileOp) -> Result<(), Box<dyn Error>> { fn do_op(files: &mut BTreeMap<String, FileChange>, path: &String, op: FileOp) -> Result<(), String> {
let old = *files.get(path).unwrap_or(&FileChange::Unchanged); let old = *files.get(path).unwrap_or(&FileChange::Unchanged);
let new = state_change(old, op) let new = state_change(old, op)
.ok_or(format!("Invalid operation: {:?} on {:?}, path = {}", op, old, path))?; .ok_or(format!("Invalid operation: {:?} on {:?}, path = {}", op, old, path))?;
@@ -205,6 +217,7 @@ fn main() -> Result<(), Box<dyn Error>> {
} else { } else {
files.insert(path.clone(), new); files.insert(path.clone(), new);
} }
Ok(()) Ok(())
} }
@@ -212,7 +225,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let (cmd, new_offset) = Command::parse(&contents[offset..])?; let (cmd, new_offset) = Command::parse(&contents[offset..])?;
offset += new_offset; offset += new_offset;
// out!("{:?}", cmd.ty()); if cmd.ty() == CommandType::Snapshot {
out!("{:#?}", &cmd);
}
match &cmd { match &cmd {
Command::MkFile { path, inode: _ } | Command::MkFile { path, inode: _ } |
Command::MkDir { path, inode: _ } | Command::MkDir { path, inode: _ } |
@@ -254,37 +270,37 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
pub trait TlvData: Sized { pub trait TlvData: Sized {
fn parse_tlv_data(data: &[u8]) -> Result<Self, Box<dyn Error>>; fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError>;
} }
impl TlvData for u64 { impl TlvData for u64 {
fn parse_tlv_data(data: &[u8]) -> Result<Self, Box<dyn Error>> { fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(u64::from_le_bytes(data.try_into()?)) Ok(u64::from_le_bytes(data.try_into().unwrap()))
} }
} }
impl TlvData for u32 { impl TlvData for u32 {
fn parse_tlv_data(data: &[u8]) -> Result<Self, Box<dyn Error>> { fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(u32::from_le_bytes(data.try_into()?)) Ok(u32::from_le_bytes(data.try_into().unwrap()))
} }
} }
impl TlvData for String { impl TlvData for String {
fn parse_tlv_data(data: &[u8]) -> Result<Self, Box<dyn Error>> { fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
String::from_utf8(data.to_vec()).map_err(Into::into) String::from_utf8(data.to_vec()).map_err(Into::into)
} }
} }
impl TlvData for UUID { impl TlvData for UUID {
fn parse_tlv_data(data: &[u8]) -> Result<Self, Box<dyn Error>> { fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(UUID(u128::from_le_bytes(data.try_into()?))) Ok(UUID(u128::from_le_bytes(data.try_into().unwrap())))
} }
} }
impl TlvData for Time { impl TlvData for Time {
fn parse_tlv_data(data: &[u8]) -> Result<Self, Box<dyn Error>> { fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
let secs = i64::from_le_bytes(data[0..8].try_into()?); let secs = i64::from_le_bytes(data[0..8].try_into().unwrap());
let nanos = u32::from_le_bytes(data[8..12].try_into()?); let nanos = u32::from_le_bytes(data[8..12].try_into().unwrap());
Ok(Time {secs, nanos}) Ok(Time {secs, nanos})
} }
} }
@@ -292,13 +308,13 @@ impl TlvData for Time {
impl Command { impl Command {
pub fn ty(&self) -> CommandType { pub fn ty(&self) -> CommandType {
match self { 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::Snapshot { clone_transid: _, clone_uuid: _, uuid: _, path: _, transid: _ } => CommandType::Snapshot,
Command::MkFile { path: _, inode: _ } => CommandType::MkFile, Command::MkFile { path: _, inode: _ } => CommandType::MkFile,
Command::MkDir { path: _, inode: _ } => CommandType::MkDir, Command::MkDir { path: _, inode: _ } => CommandType::MkDir,
Command::MkNod => CommandType::MkNod, Command::MkNod { path: _, mode: _, rdev: _ } => CommandType::MkNod,
Command::MkFifo => CommandType::MkFifo, Command::MkFifo { path: _, inode: _ } => CommandType::MkFifo,
Command::MkSock => CommandType::MkSock, Command::MkSock { path: _, inode: _ } => CommandType::MkSock,
Command::Symlink { path: _, inode: _, link: _ } => CommandType::Symlink, Command::Symlink { path: _, inode: _, link: _ } => CommandType::Symlink,
Command::Rename { from: _, to: _ } => CommandType::Rename, Command::Rename { from: _, to: _ } => CommandType::Rename,
Command::Link { path: _, link: _ } => CommandType::Link, Command::Link { path: _, link: _ } => CommandType::Link,
@@ -313,8 +329,8 @@ impl Command {
Command::Chown { path: _, uid: _, gid: _ } => CommandType::Chown, Command::Chown { path: _, uid: _, gid: _ } => CommandType::Chown,
Command::UTimes { path: _, atime: _, mtime: _, ctime: _ } => CommandType::UTimes, Command::UTimes { path: _, atime: _, mtime: _, ctime: _ } => CommandType::UTimes,
Command::End => CommandType::End, Command::End => CommandType::End,
Command::UpdateExtent => CommandType::UpdateExtent, Command::UpdateExtent { path: _, offset: _, size: _ } => CommandType::UpdateExtent,
Command::FAllocate => CommandType::FAllocate, Command::FAllocate { path: _, mode: _, offset: _, size: _ } => CommandType::FAllocate,
Command::FileAttr => CommandType::FileAttr, Command::FileAttr => CommandType::FileAttr,
Command::EncodedWrite => CommandType::EncodedWrite, Command::EncodedWrite => CommandType::EncodedWrite,
_ => CommandType::Unspec, _ => CommandType::Unspec,
@@ -335,34 +351,46 @@ impl Command {
} }
} }
pub fn parse(data: &[u8]) -> Result<(Self, usize), Box<dyn Error>> { pub fn parse(data: &[u8]) -> Result<(Self, usize), BtrfsStreamError> {
let len = u32::from_le_bytes(data[0..4].try_into()?) as usize; 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( 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<TLVType, (usize, usize)> = HashMap::new(); let mut tlvs: HashMap<TLVType, (usize, usize)> = HashMap::new();
let mut inner_offset = 0; let mut inner_offset = 0;
while inner_offset < len { while inner_offset < len {
if data.len() < inner_offset + 14 {
return Err(BtrfsStreamError::IncompleteCommand);
}
let tlvtype = TLVType::try_from_primitive( 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)); tlvs.insert(tlvtype, (inner_offset+14, inner_offset+14+tlvlen));
inner_offset += tlvlen + 4; inner_offset += tlvlen + 4;
} }
let tlv = |ty: TLVType| -> Result<&[u8], Box<dyn Error>> { let tlv = |ty: TLVType| -> Result<&[u8], BtrfsStreamError> {
let (s, e) = *tlvs.get(&ty).ok_or(format!("Command of type {cmd:?} needs a TLV of type {ty:?}"))?; let (s, e) = *tlvs.get(&ty).ok_or(BtrfsStreamError::MissingTLV(cmd, ty))?;
Ok(&data[s..e]) Ok(&data[s..e])
}; };
let result = match cmd { 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 { CommandType::Snapshot => Command::Snapshot {
clone_transid: u64::parse_tlv_data(tlv(TLVType::CloneCTransID)?)?, clone_transid: u64::parse_tlv_data(tlv(TLVType::CloneCTransID)?)?,
clone_uuid: UUID::parse_tlv_data(tlv(TLVType::CloneUUID)?)?, clone_uuid: UUID::parse_tlv_data(tlv(TLVType::CloneUUID)?)?,
@@ -378,9 +406,19 @@ impl Command {
path: String::parse_tlv_data(tlv(TLVType::Path)?)?, path: String::parse_tlv_data(tlv(TLVType::Path)?)?,
inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?,
}, },
// CommandType::MkNod => Command::MkNod, CommandType::MkNod => Command::MkNod {
// CommandType::MkFifo => Command::MkFifo, path: String::parse_tlv_data(tlv(TLVType::Path)?)?,
// CommandType::MkSock => Command::MkSock, 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 { CommandType::Symlink => Command::Symlink {
path: String::parse_tlv_data(tlv(TLVType::Path)?)?, path: String::parse_tlv_data(tlv(TLVType::Path)?)?,
inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?, inode: u64::parse_tlv_data(tlv(TLVType::Ino)?)?,
@@ -400,8 +438,6 @@ impl Command {
CommandType::RmDir => Command::RmDir { CommandType::RmDir => Command::RmDir {
path: String::parse_tlv_data(tlv(TLVType::Path)?)?, path: String::parse_tlv_data(tlv(TLVType::Path)?)?,
}, },
// CommandType::SetXAttr => Command::SetXAttr,
// CommandType::RemoveXAttr => Command::RemoveXAttr,
CommandType::Write => Command::Write { CommandType::Write => Command::Write {
path: String::parse_tlv_data(tlv(TLVType::Path)?)?, path: String::parse_tlv_data(tlv(TLVType::Path)?)?,
offset: u64::parse_tlv_data(tlv(TLVType::FileOffset)?)?, offset: u64::parse_tlv_data(tlv(TLVType::FileOffset)?)?,
@@ -436,14 +472,42 @@ impl Command {
ctime: Time::parse_tlv_data(tlv(TLVType::CTime)?)?, ctime: Time::parse_tlv_data(tlv(TLVType::CTime)?)?,
}, },
CommandType::End => Command::End, CommandType::End => Command::End,
// CommandType::UpdateExtent => Command::UpdateExtent, CommandType::UpdateExtent => Command::UpdateExtent {
// CommandType::FAllocate => Command::FAllocate, 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::FileAttr => Command::FileAttr,
// CommandType::EncodedWrite => Command::EncodedWrite, // 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)) 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<TLVType, (usize, usize)>),
#[error("command ended prematurely")]
IncompleteCommand,
#[error("invalid command type: {}", .0.number)]
InvalidCommandType(#[from] TryFromPrimitiveError<CommandType>),
#[error("invalid TLV type: {}", .0.number)]
InvalidTLVType(#[from] TryFromPrimitiveError<TLVType>),
#[error("invalid UTF-8 string: {:?}", .0.as_bytes())]
InvalidString(#[from] FromUtf8Error),
}