use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use super::types::{SnapshotInfo, TimeError, TimeSpec};
const SECS_PER_MINUTE: u64 = 60;
const SECS_PER_HOUR: u64 = 3600;
const SECS_PER_DAY: u64 = 86400;
const SECS_PER_WEEK: u64 = 604800;
#[derive(Debug, Clone, Copy)]
pub struct TxgTimestamp {
pub txg: u64,
pub timestamp: u64,
}
pub trait TxgHistoryProvider {
fn current_txg(&self) -> u64;
fn current_timestamp(&self) -> u64;
fn min_txg(&self) -> u64;
fn txg_to_timestamp(&self, txg: u64) -> Option<u64>;
fn timestamp_to_txg(&self, timestamp: u64) -> Option<u64>;
fn txg_history(&self) -> Vec<TxgTimestamp>;
fn lookup_snapshot(&self, name: &str) -> Option<SnapshotInfo>;
fn list_snapshots(&self) -> Vec<SnapshotInfo>;
}
pub struct TimeSpecResolver<'a, P: TxgHistoryProvider> {
provider: &'a P,
}
impl<'a, P: TxgHistoryProvider> TimeSpecResolver<'a, P> {
pub fn new(provider: &'a P) -> Self {
Self { provider }
}
pub fn resolve(&self, spec: &TimeSpec) -> Result<u64, TimeError> {
match spec {
TimeSpec::Txg(txg) => {
if *txg < self.provider.min_txg() || *txg > self.provider.current_txg() {
return Err(TimeError::TxgNotFound(*txg));
}
Ok(*txg)
}
TimeSpec::Now => Ok(self.provider.current_txg()),
TimeSpec::Timestamp(ts) => self.resolve_timestamp(*ts),
TimeSpec::DateTime(s) => {
let ts = parse_datetime(s)?;
self.resolve_timestamp(ts)
}
TimeSpec::Relative(s) => {
let offset = parse_relative_time(s)?;
let now = self.provider.current_timestamp();
let target = now.saturating_sub(offset);
self.resolve_timestamp(target)
}
TimeSpec::Snapshot(name) => {
let snap = self
.provider
.lookup_snapshot(name)
.ok_or_else(|| TimeError::SnapshotNotFound(name.clone()))?;
Ok(snap.txg)
}
}
}
fn resolve_timestamp(&self, timestamp: u64) -> Result<u64, TimeError> {
let history = self.provider.txg_history();
if history.is_empty() {
return Err(TimeError::NoHistory);
}
let idx = binary_search_txg(&history, timestamp);
if idx < history.len() {
Ok(history[idx].txg)
} else if !history.is_empty() {
Ok(history[history.len() - 1].txg)
} else {
Err(TimeError::NoHistory)
}
}
pub fn validate_txg(&self, txg: u64) -> Result<(), TimeError> {
if txg < self.provider.min_txg() {
return Err(TimeError::TxgNotFound(txg));
}
if txg > self.provider.current_txg() {
return Err(TimeError::TxgNotFound(txg));
}
Ok(())
}
pub fn txg_timestamp(&self, txg: u64) -> Option<u64> {
self.provider.txg_to_timestamp(txg)
}
}
fn binary_search_txg(history: &[TxgTimestamp], target: u64) -> usize {
if history.is_empty() {
return 0;
}
let mut left = 0;
let mut right = history.len();
while left < right {
let mid = left + (right - left) / 2;
if history[mid].timestamp <= target {
left = mid + 1;
} else {
right = mid;
}
}
if left > 0 { left - 1 } else { 0 }
}
fn parse_datetime(s: &str) -> Result<u64, TimeError> {
let s = s.trim();
let s = s.strip_suffix('Z').unwrap_or(s);
let s = s.strip_suffix('z').unwrap_or(s);
let (date_part, time_part) = if s.contains('T') {
let parts: Vec<&str> = s.splitn(2, 'T').collect();
(parts[0], parts.get(1).copied())
} else if s.contains(' ') {
let parts: Vec<&str> = s.splitn(2, ' ').collect();
(parts[0], parts.get(1).copied())
} else {
(s, None)
};
let date_parts: Vec<&str> = date_part.split('-').collect();
if date_parts.len() != 3 {
return Err(TimeError::InvalidTimeSpec(alloc::format!(
"invalid date format: {}",
s
)));
}
let year: i32 = date_parts[0]
.parse()
.map_err(|_| TimeError::InvalidTimeSpec("invalid year".into()))?;
let month: u32 = date_parts[1]
.parse()
.map_err(|_| TimeError::InvalidTimeSpec("invalid month".into()))?;
let day: u32 = date_parts[2]
.parse()
.map_err(|_| TimeError::InvalidTimeSpec("invalid day".into()))?;
if !(1970..=2100).contains(&year) {
return Err(TimeError::InvalidTimeSpec("year out of range".into()));
}
if !(1..=12).contains(&month) {
return Err(TimeError::InvalidTimeSpec("month out of range".into()));
}
if !(1..=31).contains(&day) {
return Err(TimeError::InvalidTimeSpec("day out of range".into()));
}
let (hour, minute, second) = if let Some(time) = time_part {
let time_parts: Vec<&str> = time.split(':').collect();
if time_parts.len() >= 2 {
let h: u32 = time_parts[0]
.parse()
.map_err(|_| TimeError::InvalidTimeSpec("invalid hour".into()))?;
let m: u32 = time_parts[1]
.parse()
.map_err(|_| TimeError::InvalidTimeSpec("invalid minute".into()))?;
let s: u32 = time_parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
if h > 23 || m > 59 || s > 59 {
return Err(TimeError::InvalidTimeSpec("time out of range".into()));
}
(h, m, s)
} else {
(0, 0, 0)
}
} else {
(0, 0, 0)
};
let timestamp = datetime_to_unix(year, month, day, hour, minute, second);
Ok(timestamp)
}
fn datetime_to_unix(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u64 {
let mut days = 0i64;
for y in 1970..year {
days += if is_leap_year(y) { 366 } else { 365 };
}
if year < 1970 {
return 0;
}
let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for m in 1..month {
days += days_in_month[(m - 1) as usize] as i64;
if m == 2 && is_leap_year(year) {
days += 1; }
}
days += (day - 1) as i64;
let total_seconds =
days * SECS_PER_DAY as i64 + hour as i64 * 3600 + minute as i64 * 60 + second as i64;
total_seconds.max(0) as u64
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn parse_relative_time(s: &str) -> Result<u64, TimeError> {
let s = s.trim().to_lowercase();
if s == "yesterday" {
return Ok(SECS_PER_DAY);
}
if s == "today" || s == "now" {
return Ok(0);
}
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() < 2 {
return Err(TimeError::InvalidTimeSpec(alloc::format!(
"invalid relative time: {}",
s
)));
}
let has_ago = parts.last().is_some_and(|p| *p == "ago");
let unit_idx = if has_ago && parts.len() >= 3 {
parts.len() - 2
} else {
parts.len() - 1
};
let number: u64 = parts[0]
.parse()
.map_err(|_| TimeError::InvalidTimeSpec(alloc::format!("invalid number: {}", parts[0])))?;
let unit = if unit_idx < parts.len() {
parts[unit_idx]
} else {
return Err(TimeError::InvalidTimeSpec("missing time unit".into()));
};
let multiplier = match unit {
"second" | "seconds" | "sec" | "secs" | "s" => 1,
"minute" | "minutes" | "min" | "mins" | "m" => SECS_PER_MINUTE,
"hour" | "hours" | "hr" | "hrs" | "h" => SECS_PER_HOUR,
"day" | "days" | "d" => SECS_PER_DAY,
"week" | "weeks" | "w" => SECS_PER_WEEK,
_ => {
return Err(TimeError::InvalidTimeSpec(alloc::format!(
"unknown time unit: {}",
unit
)));
}
};
Ok(number * multiplier)
}
#[derive(Debug, Default)]
pub struct InMemoryTxgHistory {
pub entries: Vec<TxgTimestamp>,
pub snapshots: Vec<SnapshotInfo>,
pub current_txg: u64,
pub current_timestamp: u64,
pub min_txg: u64,
}
impl InMemoryTxgHistory {
pub fn new() -> Self {
Self::default()
}
pub fn add_txg(&mut self, txg: u64, timestamp: u64) {
self.entries.push(TxgTimestamp { txg, timestamp });
self.entries.sort_by_key(|e| e.txg);
if txg > self.current_txg {
self.current_txg = txg;
self.current_timestamp = timestamp;
}
}
pub fn add_snapshot(&mut self, info: SnapshotInfo) {
self.snapshots.push(info);
}
}
impl TxgHistoryProvider for InMemoryTxgHistory {
fn current_txg(&self) -> u64 {
self.current_txg
}
fn current_timestamp(&self) -> u64 {
self.current_timestamp
}
fn min_txg(&self) -> u64 {
self.min_txg
}
fn txg_to_timestamp(&self, txg: u64) -> Option<u64> {
self.entries
.iter()
.find(|e| e.txg == txg)
.map(|e| e.timestamp)
}
fn timestamp_to_txg(&self, timestamp: u64) -> Option<u64> {
let idx = binary_search_txg(&self.entries, timestamp);
if idx < self.entries.len() {
Some(self.entries[idx].txg)
} else {
None
}
}
fn txg_history(&self) -> Vec<TxgTimestamp> {
self.entries.clone()
}
fn lookup_snapshot(&self, name: &str) -> Option<SnapshotInfo> {
self.snapshots.iter().find(|s| s.name == name).cloned()
}
fn list_snapshots(&self) -> Vec<SnapshotInfo> {
self.snapshots.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_history() -> InMemoryTxgHistory {
let mut history = InMemoryTxgHistory::new();
history.add_txg(100, 1704067200);
history.add_txg(200, 1705276800);
history.add_txg(300, 1706745600);
history.add_txg(400, 1709251200);
history.add_txg(500, 1717200000);
history.min_txg = 100;
history.add_snapshot(SnapshotInfo {
name: "daily-backup".into(),
creation_time: 1705276800,
txg: 200,
referenced: 1024 * 1024,
used: 512 * 1024,
});
history
}
#[test]
fn test_resolve_txg() {
let history = create_test_history();
let resolver = TimeSpecResolver::new(&history);
let spec = TimeSpec::Txg(300);
assert_eq!(resolver.resolve(&spec).unwrap(), 300);
}
#[test]
fn test_resolve_now() {
let history = create_test_history();
let resolver = TimeSpecResolver::new(&history);
let spec = TimeSpec::Now;
assert_eq!(resolver.resolve(&spec).unwrap(), 500);
}
#[test]
fn test_resolve_timestamp() {
let history = create_test_history();
let resolver = TimeSpecResolver::new(&history);
let spec = TimeSpec::Timestamp(1705276800);
assert_eq!(resolver.resolve(&spec).unwrap(), 200);
let spec = TimeSpec::Timestamp(1706000000);
assert_eq!(resolver.resolve(&spec).unwrap(), 200);
}
#[test]
fn test_resolve_snapshot() {
let history = create_test_history();
let resolver = TimeSpecResolver::new(&history);
let spec = TimeSpec::Snapshot("daily-backup".into());
assert_eq!(resolver.resolve(&spec).unwrap(), 200);
}
#[test]
fn test_resolve_snapshot_not_found() {
let history = create_test_history();
let resolver = TimeSpecResolver::new(&history);
let spec = TimeSpec::Snapshot("nonexistent".into());
assert!(matches!(
resolver.resolve(&spec),
Err(TimeError::SnapshotNotFound(_))
));
}
#[test]
fn test_parse_datetime_date_only() {
let ts = parse_datetime("2024-01-15").unwrap();
assert_eq!(ts, 1705276800);
}
#[test]
fn test_parse_datetime_with_time() {
let ts = parse_datetime("2024-01-15 12:30:45").unwrap();
assert_eq!(ts, 1705276800 + 12 * 3600 + 30 * 60 + 45);
}
#[test]
fn test_parse_datetime_iso8601() {
let ts = parse_datetime("2024-01-15T12:30:45Z").unwrap();
assert_eq!(ts, 1705276800 + 12 * 3600 + 30 * 60 + 45);
}
#[test]
fn test_parse_relative_hours_ago() {
let offset = parse_relative_time("3 hours ago").unwrap();
assert_eq!(offset, 3 * SECS_PER_HOUR);
}
#[test]
fn test_parse_relative_days_ago() {
let offset = parse_relative_time("7 days ago").unwrap();
assert_eq!(offset, 7 * SECS_PER_DAY);
}
#[test]
fn test_parse_relative_yesterday() {
let offset = parse_relative_time("yesterday").unwrap();
assert_eq!(offset, SECS_PER_DAY);
}
#[test]
fn test_parse_relative_minutes() {
let offset = parse_relative_time("30 minutes ago").unwrap();
assert_eq!(offset, 30 * SECS_PER_MINUTE);
}
#[test]
fn test_parse_relative_weeks() {
let offset = parse_relative_time("2 weeks ago").unwrap();
assert_eq!(offset, 2 * SECS_PER_WEEK);
}
#[test]
fn test_binary_search_exact_match() {
let history = vec![
TxgTimestamp {
txg: 100,
timestamp: 1000,
},
TxgTimestamp {
txg: 200,
timestamp: 2000,
},
TxgTimestamp {
txg: 300,
timestamp: 3000,
},
];
assert_eq!(binary_search_txg(&history, 2000), 1);
}
#[test]
fn test_binary_search_between() {
let history = vec![
TxgTimestamp {
txg: 100,
timestamp: 1000,
},
TxgTimestamp {
txg: 200,
timestamp: 2000,
},
TxgTimestamp {
txg: 300,
timestamp: 3000,
},
];
assert_eq!(binary_search_txg(&history, 1500), 0);
assert_eq!(binary_search_txg(&history, 2500), 1);
}
#[test]
fn test_binary_search_before_first() {
let history = vec![
TxgTimestamp {
txg: 100,
timestamp: 1000,
},
TxgTimestamp {
txg: 200,
timestamp: 2000,
},
];
assert_eq!(binary_search_txg(&history, 500), 0);
}
#[test]
fn test_binary_search_after_last() {
let history = vec![
TxgTimestamp {
txg: 100,
timestamp: 1000,
},
TxgTimestamp {
txg: 200,
timestamp: 2000,
},
];
assert_eq!(binary_search_txg(&history, 3000), 1);
}
#[test]
fn test_resolve_datetime() {
let history = create_test_history();
let resolver = TimeSpecResolver::new(&history);
let spec = TimeSpec::DateTime("2024-01-15".into());
assert_eq!(resolver.resolve(&spec).unwrap(), 200);
}
#[test]
fn test_is_leap_year() {
assert!(is_leap_year(2000)); assert!(is_leap_year(2024)); assert!(!is_leap_year(1900)); assert!(!is_leap_year(2023)); }
#[test]
fn test_datetime_to_unix_epoch() {
assert_eq!(datetime_to_unix(1970, 1, 1, 0, 0, 0), 0);
}
#[test]
fn test_datetime_to_unix_known_date() {
let ts = datetime_to_unix(2024, 1, 1, 0, 0, 0);
assert_eq!(ts, 1704067200);
}
#[test]
fn test_invalid_datetime() {
assert!(parse_datetime("not-a-date").is_err());
assert!(parse_datetime("2024-13-01").is_err()); assert!(parse_datetime("2024-01-32").is_err()); }
#[test]
fn test_invalid_relative() {
assert!(parse_relative_time("not relative").is_err());
assert!(parse_relative_time("abc days ago").is_err());
}
#[test]
fn test_txg_out_of_range() {
let history = create_test_history();
let resolver = TimeSpecResolver::new(&history);
let spec = TimeSpec::Txg(50);
assert!(matches!(
resolver.resolve(&spec),
Err(TimeError::TxgNotFound(50))
));
let spec = TimeSpec::Txg(1000);
assert!(matches!(
resolver.resolve(&spec),
Err(TimeError::TxgNotFound(1000))
));
}
}