use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use strop_core::id::{BufferRevision, DocumentId};
pub const FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DraftOrigin {
LocalFile {
#[serde(with = "strop_core::path_serde")]
path: PathBuf,
#[serde(with = "strop_core::path_serde::option")]
canonical: Option<PathBuf>,
},
RemoteFile { label: String },
Scratch { id: u64 },
}
impl DraftOrigin {
pub fn label(&self) -> String {
match self {
Self::LocalFile { path, .. } => path.display().to_string(),
Self::RemoteFile { label } => label.clone(),
Self::Scratch { id } => format!("[scratch #{id}]"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceObservation {
pub modified_ms: u64,
pub len: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Snapshot {
Captured { text_len: u64 },
OverBound { bytes: u64 },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DraftRecord {
pub origin: DraftOrigin,
pub revision: BufferRevision,
pub document: DocumentId,
pub source: Option<SourceObservation>,
pub snapshot: Snapshot,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceBinding {
pub filesystem: strop_workspace::Filesystem,
#[serde(with = "strop_core::path_serde::option")]
pub root: Option<PathBuf>,
pub incarnation: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CohortHeader {
pub version: u32,
pub cohort: u64,
pub captured_ms: u64,
pub workspace: WorkspaceBinding,
pub records: Vec<DraftRecord>,
}
impl CohortHeader {
pub fn validate(&self) -> Result<(), crate::session::SessionError> {
if self.version != FORMAT_VERSION {
return Err(invalid(format!(
"unsupported recovery format version {}",
self.version
)));
}
for record in &self.records {
if let DraftOrigin::LocalFile { path, canonical } = &record.origin {
strop_core::path_serde::validate(path)?;
if let Some(canonical) = canonical {
strop_core::path_serde::validate(canonical)?;
}
}
}
Ok(())
}
}
pub fn invalid(reason: String) -> crate::session::SessionError {
crate::session::SessionError::Invalid(format!("recovery checkpoint: {reason}"))
}
pub fn millis(time: SystemTime) -> u64 {
time.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_millis() as u64)
}