use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq)]
pub enum TimeSpec {
Timestamp(u64),
DateTime(String),
Snapshot(String),
Relative(String),
Txg(u64),
Now,
}
impl TimeSpec {
pub fn is_relative(&self) -> bool {
matches!(self, TimeSpec::Relative(_))
}
pub fn is_snapshot(&self) -> bool {
matches!(self, TimeSpec::Snapshot(_))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TimeQuery {
Select {
columns: Vec<Column>,
path: String,
time: TimeSpec,
filter: Option<Filter>,
limit: Option<usize>,
order_by: Option<OrderBy>,
},
Diff {
path: String,
from: TimeSpec,
to: TimeSpec,
change_types: Option<Vec<ChangeType>>,
},
Versions {
path: String,
limit: Option<usize>,
},
Restore {
path: String,
time: TimeSpec,
dest_path: Option<String>,
},
ShowSnapshots {
path: String,
},
Aggregate {
functions: Vec<AggregateFunc>,
path: String,
time: TimeSpec,
filter: Option<Filter>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum Column {
All,
Named(String),
Aliased {
name: String,
alias: String,
},
}
impl Column {
pub fn name(&self) -> &str {
match self {
Column::All => "*",
Column::Named(n) => n,
Column::Aliased { name, .. } => name,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AggregateFunc {
Count(Option<String>),
Sum(String),
Avg(String),
Min(String),
Max(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct OrderBy {
pub column: String,
pub ascending: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Filter {
Eq {
column: String,
value: Value,
},
Ne {
column: String,
value: Value,
},
Lt {
column: String,
value: Value,
},
Le {
column: String,
value: Value,
},
Gt {
column: String,
value: Value,
},
Ge {
column: String,
value: Value,
},
Like {
column: String,
pattern: String,
},
In {
column: String,
values: Vec<Value>,
},
Between {
column: String,
low: Value,
high: Value,
},
IsNull {
column: String,
},
IsNotNull {
column: String,
},
And(Box<Filter>, Box<Filter>),
Or(Box<Filter>, Box<Filter>),
Not(Box<Filter>),
}
impl Filter {
pub fn and(self, other: Filter) -> Filter {
Filter::And(Box::new(self), Box::new(other))
}
pub fn or(self, other: Filter) -> Filter {
Filter::Or(Box::new(self), Box::new(other))
}
pub fn not(self) -> Filter {
Filter::Not(Box::new(self))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
String(String),
Integer(i64),
Unsigned(u64),
Bool(bool),
Null,
}
impl Value {
pub fn as_str(&self) -> Option<&str> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Value::Integer(n) => Some(*n),
Value::Unsigned(n) => i64::try_from(*n).ok(),
_ => None,
}
}
pub fn as_u64(&self) -> Option<u64> {
match self {
Value::Unsigned(n) => Some(*n),
Value::Integer(n) => u64::try_from(*n).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChangeType {
Created,
Modified,
Deleted,
Renamed {
old_path: String,
},
MetadataChanged,
}
impl ChangeType {
pub fn name(&self) -> &'static str {
match self {
ChangeType::Created => "created",
ChangeType::Modified => "modified",
ChangeType::Deleted => "deleted",
ChangeType::Renamed { .. } => "renamed",
ChangeType::MetadataChanged => "metadata_changed",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"created" | "create" | "new" => Some(ChangeType::Created),
"modified" | "modify" | "changed" => Some(ChangeType::Modified),
"deleted" | "delete" | "removed" => Some(ChangeType::Deleted),
"renamed" | "rename" | "moved" => Some(ChangeType::Renamed {
old_path: String::new(),
}),
"metadata" | "metadata_changed" => Some(ChangeType::MetadataChanged),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub enum QueryResult {
Rows(Vec<QueryRow>),
Diffs(Vec<DiffEntry>),
Versions(Vec<FileVersion>),
Snapshots(Vec<SnapshotInfo>),
Aggregate(AggregateResult),
Restored {
path: String,
txg: u64,
},
Empty,
}
impl QueryResult {
pub fn len(&self) -> usize {
match self {
QueryResult::Rows(rows) => rows.len(),
QueryResult::Diffs(diffs) => diffs.len(),
QueryResult::Versions(versions) => versions.len(),
QueryResult::Snapshots(snaps) => snaps.len(),
QueryResult::Aggregate(_) => 1,
QueryResult::Restored { .. } => 1,
QueryResult::Empty => 0,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone)]
pub struct QueryRow {
pub path: String,
pub object_id: u64,
pub size: u64,
pub mtime: u64,
pub ctime: u64,
pub atime: u64,
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub txg: u64,
pub file_type: FileType,
pub checksum: [u64; 4],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
Regular,
Directory,
Symlink,
BlockDevice,
CharDevice,
Fifo,
Socket,
}
impl FileType {
pub fn name(&self) -> &'static str {
match self {
FileType::Regular => "file",
FileType::Directory => "directory",
FileType::Symlink => "symlink",
FileType::BlockDevice => "block",
FileType::CharDevice => "char",
FileType::Fifo => "fifo",
FileType::Socket => "socket",
}
}
}
#[derive(Debug, Clone)]
pub struct DiffEntry {
pub path: String,
pub change_type: ChangeType,
pub old_size: Option<u64>,
pub new_size: Option<u64>,
pub old_checksum: Option<[u64; 4]>,
pub new_checksum: Option<[u64; 4]>,
pub old_mtime: Option<u64>,
pub new_mtime: Option<u64>,
pub txg: u64,
}
#[derive(Debug, Clone)]
pub struct FileVersion {
pub txg: u64,
pub timestamp: u64,
pub snapshot_name: Option<String>,
pub size: u64,
pub checksum: [u64; 4],
pub change_type: ChangeType,
}
#[derive(Debug, Clone)]
pub struct SnapshotInfo {
pub name: String,
pub creation_time: u64,
pub txg: u64,
pub referenced: u64,
pub used: u64,
}
#[derive(Debug, Clone)]
pub struct AggregateResult {
pub count: Option<u64>,
pub sum: Option<u64>,
pub avg: Option<f64>,
pub min: Option<u64>,
pub max: Option<u64>,
}
#[derive(Debug, Clone)]
pub enum TimeError {
ParseError(String),
InvalidTimeSpec(String),
SnapshotNotFound(String),
PathNotFound(String),
TxgNotFound(u64),
NoHistory,
DatasetNotFound(String),
PermissionDenied,
IoError(String),
NotSupported(String),
}
impl core::fmt::Display for TimeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
TimeError::ParseError(msg) => write!(f, "parse error: {}", msg),
TimeError::InvalidTimeSpec(msg) => write!(f, "invalid time specification: {}", msg),
TimeError::SnapshotNotFound(name) => write!(f, "snapshot not found: {}", name),
TimeError::PathNotFound(path) => write!(f, "path not found: {}", path),
TimeError::TxgNotFound(txg) => write!(f, "TXG {} not found", txg),
TimeError::NoHistory => write!(f, "no historical data available"),
TimeError::DatasetNotFound(name) => write!(f, "dataset not found: {}", name),
TimeError::PermissionDenied => write!(f, "permission denied"),
TimeError::IoError(msg) => write!(f, "IO error: {}", msg),
TimeError::NotSupported(msg) => write!(f, "not supported: {}", msg),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_spec_variants() {
let ts = TimeSpec::Timestamp(1704067200);
assert!(!ts.is_relative());
assert!(!ts.is_snapshot());
let rel = TimeSpec::Relative("1 hour ago".into());
assert!(rel.is_relative());
let snap = TimeSpec::Snapshot("daily-backup".into());
assert!(snap.is_snapshot());
}
#[test]
fn test_change_type_from_str() {
assert_eq!(ChangeType::from_str("created"), Some(ChangeType::Created));
assert_eq!(ChangeType::from_str("MODIFIED"), Some(ChangeType::Modified));
assert_eq!(ChangeType::from_str("deleted"), Some(ChangeType::Deleted));
assert_eq!(ChangeType::from_str("invalid"), None);
}
#[test]
fn test_filter_combinators() {
let f1 = Filter::Eq {
column: "size".into(),
value: Value::Unsigned(100),
};
let f2 = Filter::Eq {
column: "type".into(),
value: Value::String("file".into()),
};
let combined = f1.clone().and(f2.clone());
assert!(matches!(combined, Filter::And(_, _)));
let either = f1.clone().or(f2);
assert!(matches!(either, Filter::Or(_, _)));
let negated = f1.not();
assert!(matches!(negated, Filter::Not(_)));
}
#[test]
fn test_value_conversions() {
let s = Value::String("test".into());
assert_eq!(s.as_str(), Some("test"));
assert_eq!(s.as_i64(), None);
let n = Value::Integer(42);
assert_eq!(n.as_i64(), Some(42));
assert_eq!(n.as_u64(), Some(42));
let u = Value::Unsigned(100);
assert_eq!(u.as_u64(), Some(100));
}
#[test]
fn test_query_result_len() {
let empty = QueryResult::Empty;
assert!(empty.is_empty());
let rows = QueryResult::Rows(vec![QueryRow {
path: "/test".into(),
object_id: 1,
size: 100,
mtime: 0,
ctime: 0,
atime: 0,
mode: 0o644,
uid: 0,
gid: 0,
txg: 1,
file_type: FileType::Regular,
checksum: [0; 4],
}]);
assert_eq!(rows.len(), 1);
}
}