move stream parsing into library + some tests

This commit is contained in:
Florian Stecker
2025-12-20 10:19:54 -05:00
parent 86d0915fae
commit 310379c760
3 changed files with 502 additions and 415 deletions

446
src/btrfs_stream.rs Normal file
View File

@@ -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<u8> },
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<Self, BtrfsStreamError>;
}
impl TlvData for u64 {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(u64::from_le_bytes(data.try_into().unwrap()))
}
}
impl TlvData for u32 {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(u32::from_le_bytes(data.try_into().unwrap()))
}
}
impl TlvData for String {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
String::from_utf8(data.to_vec()).map_err(Into::into)
}
}
impl TlvData for UUID {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(UUID(u128::from_le_bytes(data.try_into().unwrap())))
}
}
impl TlvData for Time {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
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<R: Read>(data: &mut R) -> Result<Self, BtrfsStreamError> {
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<u8> = vec![0; len];
data.read_exact(&mut payload)?;
let mut tlvs: HashMap<TLVType, &[u8]> = 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<R> {
reader: R,
version: u32,
}
impl<R: Read> BtrfsSendStream<R> {
pub fn new(mut reader: R) -> Result<Self, BtrfsStreamError> {
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<Command, BtrfsStreamError>;
fn next(&mut self) -> Option<Self::Item> {
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<TLVType>),
#[error("I/O error")]
IOError(#[from] std::io::Error),
#[error("Empty stream")]
EmptyStream,
#[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),
#[error("not a valid BTRFS Send stream")]
NotABtrfsStream
}

1
src/lib.rs Normal file
View File

@@ -0,0 +1 @@
pub mod btrfs_stream;

View File

@@ -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<u8> },
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<FileChange> {
Some(new)
}
fn do_op(files: &mut BTreeMap<String, FileChange>, 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<dyn Error>> {
let mut contents: Vec<u8> = 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<String, FileChange> = BTreeMap::new();
let mut offset: usize = 17;
fn do_op(files: &mut BTreeMap<String, FileChange>, 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<dyn Error>> {
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<String> = None;
for (file, &change) in &files {
if let Some(last_) = &last && file.starts_with(last_) {
@@ -264,250 +146,8 @@ fn main() -> Result<(), Box<dyn Error>> {
last = Some(file.clone());
out!("{file}");
}
}
}
*/
Ok(())
}
pub trait TlvData: Sized {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError>;
}
impl TlvData for u64 {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(u64::from_le_bytes(data.try_into().unwrap()))
}
}
impl TlvData for u32 {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(u32::from_le_bytes(data.try_into().unwrap()))
}
}
impl TlvData for String {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
String::from_utf8(data.to_vec()).map_err(Into::into)
}
}
impl TlvData for UUID {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
Ok(UUID(u128::from_le_bytes(data.try_into().unwrap())))
}
}
impl TlvData for Time {
fn parse_tlv_data(data: &[u8]) -> Result<Self, BtrfsStreamError> {
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<TLVType, (usize, usize)> = 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<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),
}