//! Durable session storage: append-only writer, incremental bounded loader, //! and atomic repair publication (`SMH-SPEC-SPEC0001`, Sessions). use crate::frame::{ FILE_PROLOGUE, Frame, Recovery, encode_entry, index_bytes, visit_frames, visit_frames_reporting, visit_metadata_reporting, write_frame, }; use crate::session::{Entry, EntryFrame, Session}; use smith::config::Config; use smith::error::{Result, SmithError}; use smith::id::{EntryId, SessionId}; use smith::tool::CancelHandle; use std::fs::{File, OpenOptions}; use std::io::{BufReader, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; /// Exclusive access to one session, held on a sidecar lock file. /// /// The lock lives beside the session file rather than on it, because repair /// publishes a replacement inode: a lock taken on the data file would leave /// a waiter holding a descriptor on the replaced inode, and its appends /// would vanish. The sidecar is never replaced, so every participant /// serializes on the same object and opens the data file only after winning. #[derive(Debug)] struct SessionLock { file: File, path: PathBuf, } impl SessionLock { /// Acquire the lock for `path`, creating parent directories and the /// sidecar as needed. Blocks while another holder owns the session. fn acquire(path: &Path) -> Result { Self::lock(path, false) } fn try_acquire(path: &Path) -> Result { Self::lock(path, true) } fn lock(path: &Path, nonblocking: bool) -> Result { if let Some(parent) = path.parent() && !parent.as_os_str().is_empty() { std::fs::create_dir_all(parent) .map_err(io_error(smith::error::StorageCode::StoreMkdir, parent))?; } let path = canonical_session_path(path)?; let file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(lock_path(&path)) .map_err(io_error( smith::error::StorageCode::StoreLockOpen, &lock_path(&path), ))?; if nonblocking { fs2::FileExt::try_lock_exclusive(&file).map_err(io_error( smith::error::StorageCode::StoreBusy, &lock_path(&path), ))?; } else { fs2::FileExt::lock_exclusive(&file).map_err(io_error( smith::error::StorageCode::StoreLock, &lock_path(&path), ))?; } Ok(Self { file, path }) } fn path(&self) -> &Path { &self.path } } impl Drop for SessionLock { fn drop(&mut self) { // Best-effort unlock; the OS also releases on close. let _unlock_result: std::io::Result<()> = fs2::FileExt::unlock(&self.file); } } fn scratch_file(operation: &str) -> Result<(PathBuf, File)> { let (path, file) = new_temp_file( &std::env::temp_dir().join("smith-scratch"), operation, SessionId::new(), )?; std::fs::remove_file(&path).map_err(io_error( smith::error::StorageCode::StoreScratchRemove, &path, ))?; Ok((path, file)) } /// Sidecar lock file path for a session file. fn readers_path(path: &Path) -> PathBuf { let mut name = path.as_os_str().to_os_string(); name.push(".readers.lock"); PathBuf::from(name) } fn lock_path(path: &Path) -> PathBuf { let mut name = path.as_os_str().to_os_string(); name.push(".lock"); PathBuf::from(name) } /// Resolve existing data symlinks, or a new file's parent symlinks, before /// deriving sidecar paths. /// /// Unix rejects data files with multiple hard links. /// Bind mounts and non-Unix hard-link aliases have no portable safe identity /// query, so callers must not expose one session through them. fn canonical_session_path(path: &Path) -> Result { match std::fs::canonicalize(path) { Ok(path) => { reject_ambiguous_data_alias(&path)?; Ok(path) } Err(error) if error.kind() == std::io::ErrorKind::NotFound => { if std::fs::symlink_metadata(path).is_ok() { return Err(io_error(smith::error::StorageCode::StoreCanonicalize, path)(error)); } let parent = path .parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")); let parent = std::fs::canonicalize(parent) .map_err(io_error(smith::error::StorageCode::StoreCanonicalize, path))?; let name = path.file_name().ok_or_else(|| { storage_fault( smith::error::StorageCode::StoreCanonicalize, path, format!("{} has no file name", path.display()), ) })?; Ok(parent.join(name)) } Err(error) => Err(io_error(smith::error::StorageCode::StoreCanonicalize, path)(error)), } } #[cfg(unix)] fn reject_ambiguous_data_alias(path: &Path) -> Result<()> { use std::os::unix::fs::MetadataExt; let metadata = std::fs::metadata(path).map_err(io_error(smith::error::StorageCode::StoreStat, path))?; if metadata.is_file() && metadata.nlink() > 1 { return Err(SmithError::Session { code: smith::error::SessionCode::SessionAmbiguousAlias, message: format!("{} has multiple hard links", path.display()), }); } Ok(()) } #[cfg(not(unix))] fn reject_ambiguous_data_alias(_: &Path) -> Result<()> { Ok(()) } fn validate_named_identity(path: &Path, session_id: SessionId) -> Result<()> { if let Some(named_id) = path .file_stem() .and_then(|stem| stem.to_str()) .and_then(|stem| stem.parse::().ok()) && named_id != session_id { return Err(SmithError::Session { code: smith::error::SessionCode::SessionIdMismatch, message: format!( "{} names session {named_id}, not {session_id}", path.display() ), }); } Ok(()) } fn reject_existing_target(path: &Path) -> Result<()> { match std::fs::symlink_metadata(path) { Ok(_) => Err(SmithError::Session { code: smith::error::SessionCode::SessionExists, message: format!("{} already exists", path.display()), }), Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(error) => Err(io_error(smith::error::StorageCode::CreateStat, path)(error)), } } #[cfg(test)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum PublicationCheckpoint { TempCreated, PartialWrite, CompletedWrite, TempSynced, Published, ParentSynced, Cleanup, } #[cfg(test)] type PublicationHook = Box; #[cfg(test)] thread_local! { static PUBLICATION_HOOK: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; static PARENT_SYNC_FAILURE: std::cell::Cell = const { std::cell::Cell::new(false) }; } #[cfg(test)] fn run_publication_hook(point: PublicationCheckpoint) { PUBLICATION_HOOK.with(|hook| { if let Some(hook) = hook.borrow_mut().as_mut() { hook(point); } }); } macro_rules! publication_checkpoint { ($point:ident) => {{ #[cfg(test)] run_publication_hook(PublicationCheckpoint::$point); }}; } #[cfg(test)] fn set_publication_hook(hook: impl FnMut(PublicationCheckpoint) + 'static) { PUBLICATION_HOOK.with(|slot| *slot.borrow_mut() = Some(Box::new(hook))); } #[cfg(test)] fn clear_publication_hook() { PUBLICATION_HOOK.with(|slot| *slot.borrow_mut() = None); } #[cfg(test)] fn set_parent_sync_failure(fail: bool) { PARENT_SYNC_FAILURE.with(|slot| slot.set(fail)); } fn new_temp_file(path: &Path, operation: &str, id: SessionId) -> Result<(PathBuf, File)> { let name = path.file_name().ok_or_else(|| { storage_fault( smith::error::StorageCode::CreateTmp, path, format!("{} has no file name", path.display()), ) })?; let mut last = path.to_path_buf(); for attempt in 0..128 { let mut temporary = name.to_os_string(); temporary.push(format!(".smith-{operation}-{id}-{attempt}.tmp")); let temporary = path.with_file_name(temporary); last.clone_from(&temporary); let mut options = OpenOptions::new(); options.read(true).write(true).create_new(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600); } match options.open(&temporary) { Ok(file) => return Ok((temporary, file)), Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} Err(error) => { return Err(io_error(smith::error::StorageCode::CreateTmp, &temporary)( error, )); } } } Err(storage_fault( smith::error::StorageCode::CreateTmp, &last, format!( "could not allocate a unique temporary beside {}", path.display() ), )) } /// Append-only session writer holding exclusive access to its session. /// /// Opening repairs a torn tail (truncating to the last complete frame /// boundary) before appending, so acknowledged appends are always /// reachable by the loader. The lock serializes writers against each other /// and against repair. #[derive(Debug)] pub struct SessionWriter { file: File, config: Config, path: PathBuf, /// A failed write or sync may have appended an unreachable frame prefix. poisoned: bool, /// Last known entry boundary, used only for reconstructible hints. last: Option<(EntryId, u64)>, /// Hints since the last reconstructible selected-tip checkpoint. since_checkpoint: u16, /// Held for the writer's lifetime; released on drop. _lock: SessionLock, } impl SessionWriter { /// Open (creating parent directories) and lock the session file. /// /// # Errors /// /// Returns a configuration, locking, or input/output error. pub fn open>(path: P, config: Config) -> Result { Self::open_with_recovery(path, config).map(|(writer, _)| writer) } /// Open and lock the session file, returning bounded damage/tail metadata. /// No frame payloads are retained by the durable open path. fn open_with_recovery>(path: P, config: Config) -> Result<(Self, Recovery)> { // Win the lock before opening the data file, so a repair that is // publishing a replacement cannot hand out a stale descriptor. let lock = SessionLock::try_acquire(path.as_ref())?; Self::attach(lock.path().to_path_buf(), config, lock) } /// Take over an already-locked session file. fn attach(path: PathBuf, config: Config, lock: SessionLock) -> Result<(Self, Recovery)> { // Validation already names the offending field path. config.validate()?; let mut file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(&path) .map_err(io_error(smith::error::StorageCode::StoreOpen, &path))?; // Drop a torn tail before appending so new frames stay reachable. // Complete but damaged frames are left in place and stay reported. if file .metadata() .map_err(io_error(smith::error::StorageCode::StoreStat, &path))? .len() == 0 { file.write_all(&FILE_PROLOGUE) .map_err(io_error(smith::error::StorageCode::StoreWrite, &path))?; file.sync_data() .map_err(io_error(smith::error::StorageCode::StoreSync, &path))?; } let recovery = walk_file_bounded(&file, &path, config.frame_bytes_max)?; let (first, _) = SessionReader::open(&path)?.first(config.frame_bytes_max)?; if let Some(named_id) = path .file_stem() .and_then(|stem| stem.to_str()) .and_then(|stem| stem.parse::().ok()) && let Some(Frame::Header { header }) = first && header.session_id != named_id { return Err(SmithError::Session { code: smith::error::SessionCode::SessionIdMismatch, message: format!( "{} records session {}, not {named_id}", path.display(), header.session_id ), }); } let len = file .metadata() .map_err(io_error(smith::error::StorageCode::StoreStat, &path))? .len(); if recovery.good_end < len { let coordination = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(readers_path(&path)) .map_err(io_error( smith::error::StorageCode::StoreReaderLockOpen, &readers_path(&path), ))?; fs2::FileExt::try_lock_exclusive(&coordination).map_err(io_error( smith::error::StorageCode::StoreReadersActive, &readers_path(&path), ))?; file.set_len(recovery.good_end) .map_err(io_error(smith::error::StorageCode::StoreTruncate, &path))?; file.sync_data() .map_err(io_error(smith::error::StorageCode::StoreSync, &path))?; fs2::FileExt::unlock(&coordination).map_err(io_error( smith::error::StorageCode::StoreReaderUnlock, &readers_path(&path), ))?; } let since_checkpoint = recovery_checkpoint_count(&file, &path, config.frame_bytes_max)?; file.seek(SeekFrom::Start(recovery.good_end)) .map_err(io_error(smith::error::StorageCode::StoreSeek, &path))?; Ok(( Self { file, config, path, poisoned: false, last: None, since_checkpoint, _lock: lock, }, recovery, )) } /// Append one frame. The encoded size is checked against the configured /// limit before any bytes are written; a rejected frame leaves the file /// unchanged. /// /// # Errors /// /// Returns a framing error for oversized frames, or an input/output error. /// A write or sync error poisons this writer; later appends fail. pub fn append_frame(&mut self, frame: &Frame) -> Result<()> { let bytes = frame.encode(self.config.frame_bytes_max)?; self.write(&bytes) } /// Append one session record as an entry frame, framing its arena /// `content` bytes directly. Size limits apply as in /// [`SessionWriter::append_frame`]. /// /// # Errors /// /// Returns a framing error for oversized frames, or an input/output error. /// A write or sync error poisons this writer; later appends fail. pub fn append_entry(&mut self, entry: &Entry, content: &[u8]) -> Result<()> { let bytes = encode_entry(entry, content, self.config.frame_bytes_max)?; self.write_entry_bytes(&bytes, entry.id, entry.parent) } #[expect( clippy::large_types_passed_by_value, reason = "Copy parent ID is consumed once to resolve an index hint" )] fn write_entry_bytes( &mut self, bytes: &[u8], id: EntryId, parent: Option, ) -> Result<()> { let parent_offset = parent.and_then(|id| { self.last .filter(|(last, _)| *last == id) .map(|(_, offset)| offset) }); let offset = self .file .stream_position() .map_err(io_error(smith::error::StorageCode::StoreSeek, &self.path))?; self.write(bytes)?; self.last = Some((id, offset)); if (parent.is_none() || parent_offset.is_some()) && self.config.frame_bytes_max >= 48 { self.write(&index_bytes(id, offset, parent, parent_offset))?; } self.since_checkpoint += 1; if self.since_checkpoint == 64 && self.config.frame_bytes_max >= 24 { self.append_frame(&Frame::Checkpoint { id, offset })?; self.since_checkpoint = 0; } Ok(()) } /// Persist a branch-tip selection, including when no new entry was appended. /// /// # Errors /// Returns `ENTRY_NOT_FOUND` for an absent entry or a storage fault. pub fn select_entry(&mut self, id: EntryId) -> Result<()> { let (_, offset) = SessionReader::open(&self.path)? .find_entry(id, self.config.frame_bytes_max)? .ok_or_else(|| missing_entry(id))?; self.append_frame(&Frame::Selection { id })?; if self.config.frame_bytes_max >= 24 { self.append_frame(&Frame::Checkpoint { id, offset })?; self.since_checkpoint = 0; } self.last = Some((id, offset)); Ok(()) } fn write(&mut self, bytes: &[u8]) -> Result<()> { self.io_step(smith::error::StorageCode::StoreWrite, |file| { file.write_all(bytes) })?; self.io_step(smith::error::StorageCode::StoreSync, |file| { file.sync_data() }) } fn io_step( &mut self, code: smith::error::StorageCode, step: impl FnOnce(&mut File) -> std::io::Result<()>, ) -> Result<()> { if self.poisoned { return Err(storage_fault( smith::error::StorageCode::StorePoisoned, &self.path, format!("{} may contain a partial append", self.path.display()), )); } match step(&mut self.file) { Ok(()) => Ok(()), Err(error) => { self.poisoned = true; Err(io_error(code, &self.path)(error)) } } } #[cfg(test)] fn write_step(&mut self, step: impl FnOnce(&mut File) -> Result<()>) -> Result<()> { if self.poisoned { return Err(storage_fault( smith::error::StorageCode::StorePoisoned, &self.path, format!("{} may contain a partial append", self.path.display()), )); } match step(&mut self.file) { Ok(()) => Ok(()), Err(error) => { self.poisoned = true; Err(error) } } } /// Path of the underlying session file. #[must_use] pub fn path(&self) -> &Path { &self.path } } /// A session file opened for use. #[derive(Debug)] pub struct OpenSession { /// Restored, or newly created, session state. pub session: Session, /// Locked writer positioned after the last complete frame. pub writer: SessionWriter, /// Damage and torn-tail metadata found while loading; `frames` is empty /// on bounded durable open, not a second copy of session history. pub recovery: Recovery, } /// Open a session file, writing a header frame when the file is new. /// /// Reopening restores the persisted session and branch IDs, so state /// reconstructed from the same file is identical on every load. A file that /// holds frames but no header cannot be attributed to a session and fails /// explicitly instead of silently becoming a new session. /// /// # Errors /// /// Returns [`SmithError::Session`] when the file holds frames but no header, /// or a locking, framing, or input/output error. pub fn open_session(path: &Path, config: Config) -> Result { let (mut writer, recovery) = SessionWriter::open_with_recovery(path, config)?; let reader = SessionReader::open(&writer.path)?; let session = if recovery.good_end > FILE_PROLOGUE.len() as u64 { reader.selected_session(writer.config.frame_bytes_max)? } else if recovery.damage.is_empty() { let session = path .file_stem() .and_then(|stem| stem.to_str()) .and_then(|stem| stem.parse().ok()) .map_or_else(Session::new, Session::new_with_id); writer.append_frame(&Frame::Header { header: session.header(), })?; session } else { return Err(SmithError::Session { code: smith::error::SessionCode::SessionHeaderMissing, message: format!("{} holds frames but no session header", path.display()), }); }; if let Some(id) = session.selected_id() { writer.last = reader .find_entry(id, writer.config.frame_bytes_max)? .map(|(_, offset)| (id, offset)); } Ok(OpenSession { session, writer, recovery, }) } /// Publish `session` as a new session file and open it for appending. /// /// The file is written completely and then published by a no-replace hard /// link, so an interrupted fork leaves no half-written session behind and an /// existing path is never overwritten. /// /// # Errors /// /// Returns [`SmithError::Session`] when the target already holds a session, /// or a locking, framing, or input/output error. pub fn create_session(path: &Path, session: &Session, config: Config) -> Result { config.validate()?; if session .active_branch() .entries() .first() .is_some_and(|root| root.parent.is_some()) { return Err(session_fault( smith::error::SessionCode::SessionIncompleteContext, "effective context omits earlier ancestors; fork through SessionReader", )); } let lock = SessionLock::acquire(path)?; let path = lock.path().to_path_buf(); validate_named_identity(&path, session.id())?; reject_existing_target(&path)?; let (temporary, mut out) = new_temp_file(&path, "create", session.id())?; publication_checkpoint!(TempCreated); let written = (|| { out.write_all(&FILE_PROLOGUE) .map_err(io_error(smith::error::StorageCode::CreateWrite, &temporary))?; publication_checkpoint!(PartialWrite); write_frame( &mut out, &Frame::Header { header: session.header(), }, config.frame_bytes_max, )?; for entry in session.active_branch().entries() { let bytes = encode_entry(entry, session.bytes(entry), config.frame_bytes_max)?; out.write_all(&bytes) .map_err(io_error(smith::error::StorageCode::CreateWrite, &temporary))?; } if let Some(id) = session.selected_id() && session .active_branch() .entries() .last() .map(|entry| entry.id) != Some(id) { write_frame(&mut out, &Frame::Selection { id }, config.frame_bytes_max)?; } publication_checkpoint!(CompletedWrite); out.sync_all() .map_err(io_error(smith::error::StorageCode::CreateSync, &temporary))?; publication_checkpoint!(TempSynced); Ok(()) })(); drop(out); if let Err(error) = written { // Preserve the write failure; temporary-file removal is best-effort cleanup. let _cleanup_result: std::io::Result<()> = std::fs::remove_file(&temporary); return Err(error); } publish_no_clobber(&temporary, &path)?; publication_checkpoint!(Cleanup); let (writer, recovery) = SessionWriter::attach(path, config, lock)?; let session = SessionReader::open(writer.path())?.selected_session(writer.config.frame_bytes_max)?; Ok(OpenSession { session, writer, recovery, }) } /// Visit frames within the file size observed at open, without retaining history. /// /// A live incomplete tail is reported but never repaired by this read-only path. /// Callers must inspect damage and torn tails before treating a scan as complete. /// /// # Errors /// Returns an input/output error or the visitor's error. pub fn scan_session( path: &Path, frame_bytes_max: usize, visit: impl FnMut(Frame) -> Result<()>, ) -> Result { SessionReader::open(path)?.scan(frame_bytes_max, visit) } /// Whether the per-session exclusive writer lock is currently held. /// /// # Errors /// Returns an I/O error if the lock file cannot be probed. pub fn writer_active(path: &Path) -> Result { let path = canonical_session_path(path)?; let file = match OpenOptions::new() .read(true) .write(true) .open(lock_path(&path)) { Ok(file) => file, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false), Err(e) => { return Err(io_error( smith::error::StorageCode::StoreLockOpen, &lock_path(&path), )(e)); } }; match fs2::FileExt::try_lock_exclusive(&file) { Ok(()) => { fs2::FileExt::unlock(&file).map_err(io_error( smith::error::StorageCode::StoreUnlock, &lock_path(&path), ))?; Ok(false) } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(true), Err(e) => Err(io_error( smith::error::StorageCode::StoreLock, &lock_path(&path), )(e)), } } /// A pinned file generation with a shared maintenance lock, independent of writer ownership. /// The snapshot length is captured at scan start; subsequent appends require a new scan. #[derive(Debug)] pub struct SessionReader { file: File, readers: File, snapshot_len: u64, writer_active: bool, } impl Drop for SessionReader { fn drop(&mut self) { // A fork shares this lock's open-file description until exec. Unlock it // explicitly so a dropped snapshot cannot delay tail repair in that window. let _unlock_result: std::io::Result<()> = fs2::FileExt::unlock(&self.readers); } } struct SnapshotRead<'a> { file: &'a File, at: u64, end: u64, } impl Read for SnapshotRead<'_> { fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { let available = usize::try_from(self.end.saturating_sub(self.at)) .unwrap_or(usize::MAX) .min(buffer.len()); if available == 0 { return Ok(0); } #[cfg(unix)] let n = std::os::unix::fs::FileExt::read_at(self.file, &mut buffer[..available], self.at)?; #[cfg(windows)] let n = std::os::windows::fs::FileExt::seek_read(self.file, &mut buffer[..available], self.at)?; #[cfg(not(any(unix, windows)))] let n = { let mut clone = self.file.try_clone()?; clone.seek(SeekFrom::Start(self.at))?; clone.read(&mut buffer[..available])? }; debug_assert!( n <= available, "snapshot reader must not advance beyond its validated byte window" ); self.at += n as u64; Ok(n) } } #[cfg(test)] thread_local! { static METADATA_PASSES: std::cell::Cell = const { std::cell::Cell::new(0) }; static PROOF_VISITS: std::cell::Cell = const { std::cell::Cell::new(0) }; static CHECKPOINT_HITS: std::cell::Cell = const { std::cell::Cell::new(0) }; } // Fixed-width, offset-sorted structural proof. Scratch is unlinked before // scanning; file size grows, resident memory does not. Only the sequential // prologue walk can populate these records, never a magic-shaped payload. struct Proof { file: File, path: PathBuf, len: u64, selected_checkpoint: Option, cancel: Option, } #[derive(Clone, Copy)] struct ProofEntry { id: EntryId, parent: Option, offset: u64, parent_offset: Option, compaction: bool, } impl Proof { const WIDTH: u64 = 49; fn check_cancelled(&self) -> Result<()> { if self.cancel.as_ref().is_some_and(CancelHandle::is_cancelled) { return Err(SmithError::Cancelled); } Ok(()) } fn get(&mut self, index: u64) -> Result { self.check_cancelled()?; #[cfg(test)] PROOF_VISITS.with(|visits| visits.set(visits.get() + 1)); self.file .seek(SeekFrom::Start(index * Self::WIDTH)) .map_err(io_error( smith::error::StorageCode::StoreProofSeek, &self.path, ))?; let mut bytes = [0; 49]; self.file.read_exact(&mut bytes).map_err(io_error( smith::error::StorageCode::StoreProofRead, &self.path, ))?; let id = EntryId::from_u128(u128::from_le_bytes(bytes[..16].try_into().map_err( |_| session_fault(smith::error::SessionCode::StoreProof, "short id"), )?)); let parent = u128::from_le_bytes(bytes[16..32].try_into().map_err(|_| { session_fault(smith::error::SessionCode::StoreProof, "short parent") })?); let offset = u64::from_le_bytes(bytes[32..40].try_into().map_err(|_| { session_fault(smith::error::SessionCode::StoreProof, "short offset") })?); let parent_offset = u64::from_le_bytes( bytes[40..48] .try_into() .map_err(|_| session_fault(smith::error::SessionCode::StoreProof, "short hint"))?, ); Ok(ProofEntry { id, parent: (parent != 0).then(|| EntryId::from_u128(parent)), offset, parent_offset: (parent_offset != 0).then_some(parent_offset), compaction: bytes[48] != 0, }) } fn push(&mut self, entry: &ProofEntry) -> Result<()> { self.check_cancelled()?; self.file .seek(SeekFrom::Start(self.len * Self::WIDTH)) .map_err(io_error( smith::error::StorageCode::StoreProofSeek, &self.path, ))?; self.file .write_all(&entry.id.as_u128().to_le_bytes()) .map_err(io_error( smith::error::StorageCode::StoreProofWrite, &self.path, ))?; self.file .write_all(&entry.parent.map_or(0, |id| id.as_u128()).to_le_bytes()) .map_err(io_error( smith::error::StorageCode::StoreProofWrite, &self.path, ))?; self.file .write_all(&entry.offset.to_le_bytes()) .map_err(io_error( smith::error::StorageCode::StoreProofWrite, &self.path, ))?; self.file .write_all(&entry.parent_offset.unwrap_or(0).to_le_bytes()) .map_err(io_error( smith::error::StorageCode::StoreProofWrite, &self.path, ))?; self.file .write_all(&[u8::from(entry.compaction)]) .map_err(io_error( smith::error::StorageCode::StoreProofWrite, &self.path, ))?; self.len += 1; Ok(()) } fn set_hint(&mut self, at: u64) -> Result<()> { self.check_cancelled()?; self.file .seek(SeekFrom::Start((self.len - 1) * Self::WIDTH + 40)) .map_err(io_error( smith::error::StorageCode::StoreProofSeek, &self.path, ))?; self.file.write_all(&at.to_le_bytes()).map_err(io_error( smith::error::StorageCode::StoreProofWrite, &self.path, )) } fn find(&mut self, id: EntryId) -> Result> { let mut found = None; for i in 0..self.len { self.check_cancelled()?; let record = self.get(i)?; if record.id == id { if found.is_some() { return Err(session_fault( smith::error::SessionCode::SessionDuplicateId, format!("duplicate entry {id}"), )); } found = Some(record); } } Ok(found) } fn at_offset(&mut self, offset: u64) -> Result> { let (mut start, mut end) = (0, self.len); while start < end { self.check_cancelled()?; let middle = start + (end - start) / 2; let record = self.get(middle)?; match record.offset.cmp(&offset) { std::cmp::Ordering::Less => start = middle + 1, std::cmp::Ordering::Greater => end = middle, std::cmp::Ordering::Equal => return Ok(Some(record)), } } Ok(None) } fn parent(&mut self, child: &ProofEntry) -> Result> { self.check_cancelled()?; let Some(id) = child.parent else { return Ok(None); }; // The hint is usable only if it names a structurally proven entry, // matches the parent identity, and precedes the child. Identity lookup also // rejects duplicate IDs, including outside the selected branch. let hinted = child .parent_offset .and_then(|at| (at < child.offset).then_some(at)); let target = hinted.map(|at| self.at_offset(at)).transpose()?.flatten(); let found = self.find(id)?.ok_or_else(|| missing_entry(id))?; let parent = target .filter(|candidate| candidate.id == id && candidate.offset == found.offset) .unwrap_or(found); if parent.offset >= child.offset { return Err(session_fault( smith::error::SessionCode::SessionParent, "parent does not precede child", )); } Ok(Some(parent)) } } impl SessionReader { const fn snapshot_read(&self, at: u64) -> SnapshotRead<'_> { SnapshotRead { file: &self.file, at, end: self.snapshot_len, } } fn proof( &self, max: usize, cancel: Option, ) -> Result<(Proof, crate::session::SessionHeader, Option)> { let (path, file) = new_temp_file( &std::env::temp_dir().join("smith-proof"), "scan", SessionId::new(), )?; std::fs::remove_file(&path) .map_err(io_error(smith::error::StorageCode::StoreProofRemove, &path))?; let mut proof = Proof { file, path, len: 0, selected_checkpoint: None, cancel, }; let mut header = None; let mut selected = None; let mut first = true; self.scan_metadata_positions(max, |offset, frame| { proof.check_cancelled()?; match frame { Frame::Header { header: h } if first => header = Some(h), Frame::Header { .. } => { return Err(session_fault( smith::error::SessionCode::SessionHeaderDuplicate, "duplicate header", )); } Frame::Known { entry } if header.is_some() => { selected = Some(entry.id); proof.selected_checkpoint = None; proof.push(&ProofEntry { id: entry.id, parent: entry.parent, offset, parent_offset: None, compaction: entry.kind == (crate::session::EntryKind::Meta { compaction: true }), })?; } Frame::Index { id, offset: target, parent, parent_offset, } if header.is_some() => { if let Some(previous) = (proof.len > 0) .then(|| proof.get(proof.len - 1)) .transpose()? && previous.id == id && previous.offset == target && previous.parent == parent && let Some(parent_at) = parent_offset.filter(|at| *at < target) { proof.set_hint(parent_at)?; } } Frame::Selection { id } if header.is_some() => { selected = Some(id); proof.selected_checkpoint = None; } Frame::Checkpoint { id, offset: target } if header.is_some() => { if selected == Some(id) && target < offset { proof.selected_checkpoint = proof.at_offset(target)?.filter(|entry| entry.id == id); } } Frame::Unknown { .. } if header.is_some() => {} _ => { return Err(session_fault( smith::error::SessionCode::SessionHeaderMissing, "missing first header", )); } } first = false; Ok(()) })?; Ok(( proof, header.ok_or_else(|| { session_fault( smith::error::SessionCode::SessionHeaderMissing, "missing first header", ) })?, selected, )) } fn scan_metadata_positions( &self, max: usize, mut visit: impl FnMut(u64, Frame) -> Result<()>, ) -> Result { #[cfg(test)] METADATA_PASSES.with(|passes| passes.set(passes.get() + 1)); let mut reader = BufReader::with_capacity(64 * 1024, self.snapshot_read(0)); read_prologue(&mut reader)?; visit_metadata_reporting( &mut reader, max, |offset, frame| visit(offset + FILE_PROLOGUE.len() as u64, frame), |_| Ok(()), ) .map(file_offsets) } fn entry_at(&self, offset: u64, max: usize) -> Result { let mut file = self.snapshot_read(offset); let mut frame = None; let scan = visit_frames(&mut file, max, |found| { frame = Some(found); false })?; if let Some(fault) = scan.damage.into_iter().next() { return Err(fault); } match frame { Some(Frame::Known { entry }) => Ok(entry), _ => Err(session_fault( smith::error::SessionCode::SessionParent, "entry boundary no longer contains entry", )), } } /// Scan structural frame boundaries from the fixed prologue, reporting /// absolute offsets without retaining a per-file index. fn scan_positions( &self, max: usize, mut visit: impl FnMut(u64, Frame) -> Result<()>, ) -> Result { struct Counted<'a, R> { inner: R, bytes: &'a std::cell::Cell, } impl Read for Counted<'_, R> { fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { let n = self.inner.read(buffer)?; self.bytes.set(self.bytes.get() + n as u64); Ok(n) } } let bytes = std::cell::Cell::new(0); let mut reader = Counted { inner: BufReader::with_capacity(64 * 1024, self.snapshot_read(0)), bytes: &bytes, }; read_prologue(&mut reader)?; let offset = std::cell::Cell::new(FILE_PROLOGUE.len() as u64); let mut failure = None; let mut damage = None; let mut scan = visit_frames_reporting( &mut reader, max, |frame| { let current = offset.replace(bytes.get()); match visit(current, frame) { Ok(()) => true, Err(error) => { failure = Some(error); false } } }, |fault| { offset.set(bytes.get()); if damage.is_none() { damage = Some(fault); } true }, )?; if let Some(error) = failure { return Err(error); } if let Some(error) = damage { scan.damage.push(error); } Ok(file_offsets(scan)) } /// Find an entry identity by walking authoritative entry frames at structural /// boundaries; embedded indexes are hints, never the source of identity. /// /// # Errors /// Returns storage faults or a duplicate identity error. pub fn find_entry(&self, id: EntryId, max: usize) -> Result> { let mut found = None; self.scan_metadata_positions(max, |offset, frame| { if let Frame::Known { entry } = frame && entry.id == id { if found.is_some() { return Err(session_fault( smith::error::SessionCode::SessionDuplicateId, format!("duplicate entry {id}"), )); } found = Some(offset); } Ok(()) })?; found .map(|offset| self.entry_at(offset, max).map(|entry| (entry, offset))) .transpose() } /// Durable session and root-branch identity without materializing content. /// /// # Errors /// Returns a structural fault for an invalid or missing header. pub fn header(&self, max: usize) -> Result { self.proof(max, None).map(|(_, header, _)| header) } /// Selected entry identity without materializing any content. /// /// # Errors /// Returns a structural fault for invalid selection or ancestry. pub fn selected_tip(&self, max: usize) -> Result> { self.selected_tip_with_cancel(max, None) } /// Selected entry identity, checking cancellation at each bounded traversal boundary. /// /// # Errors /// Returns [`SmithError::Cancelled`] or a structural fault for invalid selection or ancestry. pub fn selected_tip_cancelled( &self, max: usize, cancel: &CancelHandle, ) -> Result> { self.selected_tip_with_cancel(max, Some(cancel.clone())) } fn selected_tip_with_cancel( &self, max: usize, cancel: Option, ) -> Result> { let (mut proof, _, selected) = self.proof(max, cancel)?; if let Some(id) = selected { proof.find(id)?.ok_or_else(|| missing_entry(id))?; } Ok(selected) } /// Selected effective context from latest compaction boundary to tip. /// Rootward ancestry is validated using a bounded, file-backed proof; /// only effective payloads become resident. /// /// # Errors /// Returns a fault for missing ancestry, invalid header or selection. pub fn selected_session(&self, max: usize) -> Result { self.selected_session_with_cancel(max, None) } fn selected_session_with_cancel( &self, max: usize, cancel: Option, ) -> Result { let (mut proof, header, selected) = self.proof(max, cancel)?; let (scratch_path, mut offsets) = scratch_file("context")?; let mut length = 0u64; if let Some(id) = selected { let mut cursor = proof.find(id)?.ok_or_else(|| missing_entry(id))?; if let Some(hint) = proof .selected_checkpoint .filter(|hint| hint.id == id && hint.offset == cursor.offset) { cursor = hint; #[cfg(test)] CHECKPOINT_HITS.with(|hits| hits.set(hits.get() + 1)); } let mut keep = true; loop { proof.check_cancelled()?; if keep { offsets .write_all(&cursor.offset.to_le_bytes()) .map_err(io_error( smith::error::StorageCode::StoreScratchWrite, &scratch_path, ))?; length += 1; if cursor.compaction { keep = false; } } match proof.parent(&cursor)? { Some(parent) => cursor = parent, None => break, } } } let mut session = Session::from_chain(header, Vec::new())?; for i in (0..length).rev() { proof.check_cancelled()?; offsets.seek(SeekFrom::Start(i * 8)).map_err(io_error( smith::error::StorageCode::StoreScratchSeek, &scratch_path, ))?; let mut bytes = [0; 8]; offsets.read_exact(&mut bytes).map_err(io_error( smith::error::StorageCode::StoreScratchRead, &scratch_path, ))?; session.push_restored(self.entry_at(u64::from_le_bytes(bytes), max)?)?; } Ok(session) } /// Replay a rooted chain in chronological order with bounded retained /// payload. One metadata pass validates structural offsets into a /// file-backed proof; parent lookups scan fixed-width proof records. /// /// # Errors /// Returns a missing-entry, ancestry, storage, or callback fault. pub fn replay_at( &self, id: EntryId, max: usize, visit: impl FnMut(EntryFrame) -> Result<()>, ) -> Result<()> { self.replay_at_with_cancel(id, max, None, visit) } /// Replay a rooted chain, observing cancellation between bounded reads. /// /// # Errors /// Returns [`SmithError::Cancelled`], a storage fault, or a callback fault. pub fn replay_at_cancelled( &self, id: EntryId, max: usize, cancel: &CancelHandle, visit: impl FnMut(EntryFrame) -> Result<()>, ) -> Result<()> { self.replay_at_with_cancel(id, max, Some(cancel.clone()), visit) } fn replay_at_with_cancel( &self, id: EntryId, max: usize, cancel: Option, mut visit: impl FnMut(EntryFrame) -> Result<()>, ) -> Result<()> { // Spill only physical addresses, never payloads. Unlink immediately: // the open descriptor owns the scratch lifetime even on early errors. let (mut proof, _, _) = self.proof(max, cancel)?; let (scratch_path, mut scratch) = scratch_file("replay")?; let mut cursor = proof.find(id)?.ok_or_else(|| missing_entry(id))?; let mut depth = 0u64; loop { proof.check_cancelled()?; scratch .write_all(&cursor.offset.to_le_bytes()) .map_err(io_error( smith::error::StorageCode::StoreScratchWrite, &scratch_path, ))?; depth += 1; match proof.parent(&cursor)? { Some(parent) => cursor = parent, None => break, } } for position in (0..depth).rev() { proof.check_cancelled()?; scratch .seek(SeekFrom::Start(position * 8)) .map_err(io_error( smith::error::StorageCode::StoreScratchSeek, &scratch_path, ))?; let mut address = [0; 8]; scratch.read_exact(&mut address).map_err(io_error( smith::error::StorageCode::StoreScratchRead, &scratch_path, ))?; visit(self.entry_at(u64::from_le_bytes(address), max)?)?; } Ok(()) } /// Fork one structurally reachable chain to a new independently named file. /// /// # Errors /// Returns a missing-entry, ancestry, publication or storage fault. pub fn fork_at(&self, id: EntryId, path: &Path, config: Config) -> Result { self.fork_at_with_cancel(id, path, config, None) } /// Fork one rooted chain, observing cancellation between bounded traversal work. /// /// # Errors /// Returns [`SmithError::Cancelled`], a storage fault, or a publication fault. pub fn fork_at_cancelled( &self, id: EntryId, path: &Path, config: Config, cancel: &CancelHandle, ) -> Result { self.fork_at_with_cancel(id, path, config, Some(cancel.clone())) } fn fork_at_with_cancel( &self, id: EntryId, path: &Path, config: Config, cancel: Option, ) -> Result { if cancel.as_ref().is_some_and(CancelHandle::is_cancelled) { return Err(SmithError::Cancelled); } config.validate()?; let source = path .file_stem() .and_then(|stem| stem.to_str()) .and_then(|stem| stem.parse::().ok()) .map_or_else(Session::new, Session::new_with_id); let lock = SessionLock::acquire(path)?; let target = lock.path().to_path_buf(); validate_named_identity(&target, source.id())?; reject_existing_target(&target)?; let (temporary, mut out) = new_temp_file(&target, "fork", source.id())?; publication_checkpoint!(TempCreated); let written = (|| { out.write_all(&FILE_PROLOGUE) .map_err(io_error(smith::error::StorageCode::CreateWrite, &temporary))?; publication_checkpoint!(PartialWrite); write_frame( &mut out, &Frame::Header { header: source.header(), }, config.frame_bytes_max, )?; self.replay_at_with_cancel(id, config.frame_bytes_max, cancel.clone(), |entry| { write_frame(&mut out, &Frame::Known { entry }, config.frame_bytes_max) })?; publication_checkpoint!(CompletedWrite); out.sync_all() .map_err(io_error(smith::error::StorageCode::CreateSync, &temporary))?; publication_checkpoint!(TempSynced); Ok(()) })(); drop(out); if let Err(error) = written { // Preserve the write failure; temporary-file removal is best-effort cleanup. let _cleanup_result: std::io::Result<()> = std::fs::remove_file(&temporary); return Err(error); } publish_no_clobber(&temporary, &target)?; publication_checkpoint!(Cleanup); let (writer, recovery) = SessionWriter::attach(target, config, lock)?; let session = Self::open(writer.path())? .selected_session_with_cancel(writer.config.frame_bytes_max, cancel)?; Ok(OpenSession { session, writer, recovery, }) } #[expect( clippy::large_types_passed_by_value, reason = "Copy IDs and offsets are compared once while validating an index record" )] fn verify_index( &self, last: Option<(EntryId, u64, Option)>, id: EntryId, target: u64, parent: Option, parent_offset: Option, max: usize, ) -> Result<()> { if last != Some((id, target, parent)) { return Err(session_fault( smith::error::SessionCode::SessionIndex, "index target is not preceding entry boundary", )); } if let Some(parent) = parent { let (_, actual) = self .find_entry(parent, max)? .ok_or_else(|| missing_entry(parent))?; if parent_offset != Some(actual) || actual >= target { return Err(session_fault( smith::error::SessionCode::SessionIndex, "index parent offset or identity mismatch", )); } } else if parent_offset.is_some() { return Err(session_fault( smith::error::SessionCode::SessionIndex, "root index has parent offset", )); } Ok(()) } /// Check every frame and content body in the pinned snapshot, including /// unrelated branches and all embedded location hints. /// /// # Errors /// Reports damaged frames, invalid ancestry/offsets, or a torn tail. pub fn verify_full(&self, max: usize) -> Result<()> { let mut first = true; let mut last_entry = None; let scan = self.scan_positions(max, |offset, frame| { match frame { Frame::Header { .. } if first => {} Frame::Header { .. } => { return Err(session_fault( smith::error::SessionCode::SessionHeaderDuplicate, "duplicate header", )); } Frame::Known { entry } if !first => { // Full verification rejects duplicate identities even when the // selected branch never references either copy. self.find_entry(entry.id, max)? .ok_or_else(|| missing_entry(entry.id))?; let content = crate::content_codec::EntryContentView::decode(&entry.content)?; if content.kind() != entry.kind { return Err(session_fault( smith::error::SessionCode::EntryKindMismatch, format!("entry {} kind mismatch", entry.id), )); } if let Some(parent) = entry.parent { let (_, parent_offset) = self .find_entry(parent, max)? .ok_or_else(|| missing_entry(parent))?; if parent_offset >= offset { return Err(session_fault( smith::error::SessionCode::SessionParent, "parent does not precede child", )); } } last_entry = Some((entry.id, offset, entry.parent)); } Frame::Index { id, offset: target, parent, parent_offset, } if !first => { self.verify_index(last_entry, id, target, parent, parent_offset, max)?; last_entry = None; } Frame::Selection { id } if !first => { self.find_entry(id, max)?.ok_or_else(|| missing_entry(id))?; last_entry = None; } Frame::Checkpoint { id, offset: target } if !first => { if self .find_entry(id, max)? .is_none_or(|(_, actual)| actual != target) || target >= offset { return Err(session_fault( smith::error::SessionCode::SessionCheckpoint, "checkpoint target is not an earlier entry boundary", )); } last_entry = None; } Frame::Unknown { .. } if !first => last_entry = None, _ => { return Err(session_fault( smith::error::SessionCode::SessionHeaderMissing, "missing first header", )); } } first = false; Ok(()) })?; if first { return Err(session_fault( smith::error::SessionCode::SessionHeaderMissing, "missing first header", )); } if let Some(fault) = scan.damage.into_iter().next() { return Err(fault); } if scan.torn_tail_at.is_some() { return Err(session_fault( smith::error::SessionCode::SessionTorn, "incomplete tail", )); } Ok(()) } /// Open without taking the writer lock or modifying session content. /// /// # Errors /// Returns an input/output error for missing data or failed coordination. pub fn open(path: &Path) -> Result { Self::open_with_cancel(path, None) } /// Open a reader snapshot, observing cancellation while maintenance holds the reader lock. /// /// # Errors /// Returns [`SmithError::Cancelled`] or an input/output error for failed coordination. pub fn open_cancelled(path: &Path, cancel: &CancelHandle) -> Result { Self::open_with_cancel(path, Some(cancel)) } fn open_with_cancel(path: &Path, cancel: Option<&CancelHandle>) -> Result { let path = canonical_session_path(path)?; let readers = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(readers_path(&path)) .map_err(io_error( smith::error::StorageCode::StoreReaderLockOpen, &readers_path(&path), ))?; if let Some(cancel) = cancel { loop { if cancel.is_cancelled() { return Err(SmithError::Cancelled); } match fs2::FileExt::try_lock_shared(&readers) { Ok(()) => break, Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { std::thread::sleep(std::time::Duration::from_millis(10)); } Err(error) => { return Err(io_error( smith::error::StorageCode::StoreReaderLock, &readers_path(&path), )(error)); } } } } else { fs2::FileExt::lock_shared(&readers).map_err(io_error( smith::error::StorageCode::StoreReaderLock, &readers_path(&path), ))?; } let file = File::open(&path).map_err(io_error(smith::error::StorageCode::StoreOpen, &path))?; let snapshot_len = file .metadata() .map_err(io_error(smith::error::StorageCode::StoreStat, &path))? .len(); let writer_active = writer_active(&path)?; Ok(Self { file, readers, snapshot_len, writer_active, }) } /// Visit a consistent file-size snapshot with per-frame bounded allocation. /// /// # Errors /// Returns an I/O error or the visitor's error. pub fn scan( &self, frame_bytes_max: usize, mut visit: impl FnMut(Frame) -> Result<()>, ) -> Result { let mut reader = BufReader::with_capacity(64 * 1024, self.snapshot_read(0)); read_prologue(&mut reader)?; let mut failure = None; let scan = visit_frames(&mut reader, frame_bytes_max, |frame| match visit(frame) { Ok(()) => true, Err(error) => { failure = Some(error); false } }); let scan = file_offsets(scan?); if let Some(error) = failure { return Err(error); } Ok(scan) } /// Decode the first frame only, without reading the history. /// /// # Errors /// Returns an I/O error when the file cannot be read. pub fn first(&self, frame_bytes_max: usize) -> Result<(Option, Recovery)> { let first_limit = u64::try_from(frame_bytes_max) .unwrap_or(u64::MAX) .saturating_add( u64::try_from( FILE_PROLOGUE.len() + crate::frame::HEADER_LEN + crate::frame::BODY_PREFIX_LEN, ) .unwrap_or(u64::MAX), ) .min(self.snapshot_len); let mut reader = BufReader::new(self.snapshot_read(0).take(first_limit)); read_prologue(&mut reader)?; let mut first = None; let scan = visit_frames(&mut reader, frame_bytes_max, |frame| { first = Some(frame); false })?; Ok((first, file_offsets(scan))) } /// Whether a writer owned this session when this reader acquired its snapshot. #[must_use] pub const fn writer_active(&self) -> bool { self.writer_active } /// File size of the pinned generation at reader acquisition. #[must_use] pub const fn file_size(&self) -> u64 { self.snapshot_len } /// Scan a snapshot while reporting damage through `report_damage`. /// /// Unlike [`SessionReader::scan`], this method retains no damage, keeping /// callers that report or reject faults incrementally bounded. /// /// # Errors /// Returns an I/O error, visitor error, or damage-reporting error. pub fn scan_reporting( &self, frame_bytes_max: usize, mut visit: impl FnMut(Frame) -> Result, mut report_damage: impl FnMut(SmithError) -> Result<()>, ) -> Result { let mut reader = BufReader::with_capacity(64 * 1024, self.snapshot_read(0)); read_prologue(&mut reader)?; let failure = std::cell::RefCell::new(None); let scan = visit_frames_reporting( &mut reader, frame_bytes_max, |frame| match visit(frame) { Ok(keep_going) => keep_going, Err(error) => { *failure.borrow_mut() = Some(error); false } }, |damage| match report_damage(shift_damage(damage)) { Ok(()) => true, Err(error) => { *failure.borrow_mut() = Some(error); false } }, )?; if let Some(error) = failure.into_inner() { return Err(error); } Ok(file_offsets(scan)) } } /// Repair a session file by republishing only its complete frames. /// /// Damaged frames are dropped; [`Recovery::damage_count`] counts all faults /// while [`Recovery::damage`] retains only the first report. /// An explicit selection whose entry is dropped fails before publication. /// The original content stays intact if any step fails before publication. /// Repair takes the session lock, so it waits for a live writer to finish /// instead of replacing the file underneath it. /// /// # Errors /// /// Returns a locking, framing, or input/output error. pub fn repair_session(path: &Path, frame_bytes_max: usize) -> Result { let lock = SessionLock::acquire(path)?; let path = lock.path(); let _coordination = maintenance_lock(path)?; let (temporary, mut out) = new_temp_file(path, "repair", SessionId::new())?; publication_checkpoint!(TempCreated); let written = (|| { out.write_all(&FILE_PROLOGUE) .map_err(io_error(smith::error::StorageCode::RepairWrite, &temporary))?; publication_checkpoint!(PartialWrite); let mut source = BufReader::with_capacity( 64 * 1024, File::open(path).map_err(io_error(smith::error::StorageCode::StoreOpen, path))?, ); read_prologue(&mut source)?; let mut last = None; let mut count = 0u16; let mut selection = None; let mut has_header = false; let mut first_damage = None; let mut failure = None; let mut recovery = visit_frames_reporting( &mut source, frame_bytes_max, |frame| { let result = (|| { match frame { Frame::Index { .. } | Frame::Checkpoint { .. } => {} Frame::Selection { id } => selection = Some(id), Frame::Header { header } => { has_header = true; write_frame(&mut out, &Frame::Header { header }, frame_bytes_max)?; } Frame::Known { entry } => { repair_entry( &mut out, path, entry, frame_bytes_max, has_header, &mut last, &mut count, )?; } other @ Frame::Unknown { .. } => { write_frame(&mut out, &other, frame_bytes_max)?; } } Ok(()) })(); if let Err(error) = result { failure = Some(error); false } else { true } }, |damage| { if first_damage.is_none() { first_damage = Some(shift_damage(damage)); } true }, )?; if let Some(error) = failure { return Err(error); } finish_repair_selection(&temporary, &mut out, selection.as_ref(), frame_bytes_max)?; if let Some(damage) = first_damage { recovery.damage.push(damage); } publication_checkpoint!(CompletedWrite); out.sync_all() .map_err(io_error(smith::error::StorageCode::RepairSync, &temporary))?; publication_checkpoint!(TempSynced); Ok(file_offsets(recovery)) })(); drop(out); let recovery = match written { Ok(recovery) => recovery, Err(error) => { // Preserve the recovery failure; temporary-file removal is best-effort cleanup. let _cleanup_result: std::io::Result<()> = std::fs::remove_file(&temporary); return Err(error); } }; if let Err(error) = std::fs::rename(&temporary, path) { // Preserve the publish failure; temporary-file removal is best-effort cleanup. let _cleanup_result: std::io::Result<()> = std::fs::remove_file(&temporary); return Err(io_error(smith::error::StorageCode::RepairPublish, path)( error, )); } publication_checkpoint!(Published); sync_parent(path)?; publication_checkpoint!(ParentSynced); // `rename` removed the temporary name as it published the replacement. publication_checkpoint!(Cleanup); drop(lock); Ok(recovery) } fn repair_entry( out: &mut File, path: &Path, entry: EntryFrame, max: usize, has_header: bool, last: &mut Option<(EntryId, u64)>, count: &mut u16, ) -> Result<()> { let offset = out .stream_position() .map_err(io_error(smith::error::StorageCode::RepairSeek, path))?; let id = entry.id; let parent = entry.parent; write_frame(out, &Frame::Known { entry }, max)?; let parent_offset = parent.and_then(|parent| last.filter(|(id, _)| *id == parent).map(|(_, at)| at)); *last = Some((id, offset)); if has_header && (parent.is_none() || parent_offset.is_some()) && max >= 48 { out.write_all(&index_bytes(id, offset, parent, parent_offset)) .map_err(io_error(smith::error::StorageCode::RepairWrite, path))?; } *count += 1; if has_header && *count == 64 && max >= 24 { write_frame(out, &Frame::Checkpoint { id, offset }, max)?; *count = 0; } Ok(()) } fn maintenance_lock(path: &Path) -> Result { let file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(readers_path(path)) .map_err(io_error( smith::error::StorageCode::StoreReaderLockOpen, &readers_path(path), ))?; fs2::FileExt::try_lock_exclusive(&file).map_err(io_error( smith::error::StorageCode::StoreReadersActive, &readers_path(path), ))?; Ok(file) } fn finish_repair_selection( path: &Path, out: &mut File, selection: Option<&EntryId>, max: usize, ) -> Result<()> { let Some(&id) = selection else { return Ok(()); }; let mut source = BufReader::new( File::open(path).map_err(io_error(smith::error::StorageCode::RepairOpen, path))?, ); read_prologue(&mut source)?; let mut found = None; visit_metadata_reporting( &mut source, max, |at, frame| { if let Frame::Known { entry } = frame && entry.id == id { found = Some(at + FILE_PROLOGUE.len() as u64); } Ok(()) }, |_| Ok(()), )?; let offset = found.ok_or_else(|| missing_entry(id))?; write_frame(out, &Frame::Selection { id }, max)?; if max >= 24 { write_frame(out, &Frame::Checkpoint { id, offset }, max)?; } Ok(()) } fn recovery_checkpoint_count(file: &File, path: &Path, max: usize) -> Result { let mut reader = BufReader::new( file.try_clone() .map_err(io_error(smith::error::StorageCode::StoreOpen, path))?, ); reader .seek(SeekFrom::Start(0)) .map_err(io_error(smith::error::StorageCode::StoreSeek, path))?; read_prologue(&mut reader)?; let (path, file) = scratch_file("cadence")?; let mut proof = Proof { file, path, len: 0, selected_checkpoint: None, cancel: None, }; let mut count = 0; visit_metadata_reporting( &mut reader, max, |offset, frame| { match frame { Frame::Known { entry } => { proof.push(&ProofEntry { id: entry.id, parent: entry.parent, offset: offset + FILE_PROLOGUE.len() as u64, parent_offset: None, compaction: false, })?; count = (count + 1) % 64; } Frame::Checkpoint { id, offset: at } if proof.at_offset(at)?.is_some_and(|entry| entry.id == id) => { count = 0; } _ => {} } Ok(()) }, |_| Ok(()), )?; Ok(count) } /// Walk an already-open file from its start using the shared frame reader. /// /// # Errors /// /// Returns an input/output error when the file cannot be re-read. fn walk_file_bounded(file: &File, path: &Path, frame_bytes_max: usize) -> Result { let mut handle = file .try_clone() .map_err(io_error(smith::error::StorageCode::StoreOpen, path))?; handle .seek(SeekFrom::Start(0)) .map_err(io_error(smith::error::StorageCode::StoreSeek, path))?; let mut reader = BufReader::with_capacity(64 * 1024, handle); read_prologue(&mut reader)?; let mut damage = None; let mut recovery = visit_frames_reporting( &mut reader, frame_bytes_max, |_| true, |fault| { if damage.is_none() { damage = Some(fault); } true }, ) .map(file_offsets)?; if let Some(fault) = damage { recovery.damage.push(shift_damage(fault)); } Ok(recovery) } fn read_prologue(reader: &mut impl Read) -> Result<()> { let mut magic = [0; FILE_PROLOGUE.len()]; reader.read_exact(&mut magic).map_err(io_error( smith::error::StorageCode::SessionPrologue, Path::new(""), ))?; if magic != FILE_PROLOGUE { return Err(SmithError::Session { code: smith::error::SessionCode::SessionPrologue, message: "invalid session format or version; expected SMH3 prologue".into(), }); } Ok(()) } fn file_offsets(mut recovery: Recovery) -> Recovery { recovery.good_end += FILE_PROLOGUE.len() as u64; recovery.torn_tail_at = recovery .torn_tail_at .map(|n| n + FILE_PROLOGUE.len() as u64); for damage in &mut recovery.damage { if let SmithError::Frame { offset: Some(n), .. } = damage { *n += FILE_PROLOGUE.len() as u64; } } recovery } const fn shift_damage(mut damage: SmithError) -> SmithError { if let SmithError::Frame { offset: Some(n), .. } = &mut damage { *n += FILE_PROLOGUE.len() as u64; } damage } /// Rename a synced temporary file into an absent target without any fallback. /// /// `renamore::rename_exclusive` is `renameat2(RENAME_NOREPLACE)` on Linux, /// `renamex_np(RENAME_EXCL)` on macOS, and `MoveFileExW` without replace flags /// on Windows. Unsupported filesystems fail rather than weakening no-clobber. fn publish_no_clobber(temporary: &Path, target: &Path) -> Result<()> { renamore::rename_exclusive(temporary, target) .map_err(io_error(smith::error::StorageCode::CreatePublish, target))?; publication_checkpoint!(Published); sync_parent(target)?; publication_checkpoint!(ParentSynced); Ok(()) } fn sync_parent(path: &Path) -> Result<()> { let parent = path .parent() .filter(|p| !p.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")); #[cfg(test)] if PARENT_SYNC_FAILURE.with(std::cell::Cell::get) { return Err(published_parent_sync_error( parent, "injected parent directory sync failure", )); } sync_directory(parent).map_err(|error| published_parent_sync_error(parent, error)) } fn published_parent_sync_error(path: &Path, error: impl std::fmt::Display) -> SmithError { SmithError::Storage { code: smith::error::StorageCode::StoreDirSync, operation: smith::error::StorageOperation::SyncParentDirectoryAfterPublication, path: path.to_path_buf(), message: error.to_string(), publication: Some(smith::error::PublicationState::PublishedDurabilityUncertain), } } #[cfg(not(windows))] fn sync_directory(path: &Path) -> std::io::Result<()> { File::open(path)?.sync_all() } #[cfg(windows)] fn sync_directory(path: &Path) -> std::io::Result<()> { use std::os::windows::fs::OpenOptionsExt; // FILE_FLAG_BACKUP_SEMANTICS permits opening a directory handle safely. OpenOptions::new() .read(true) .custom_flags(0x0200_0000) .open(path)? .sync_all() } fn session_fault(code: smith::error::SessionCode, message: impl Into) -> SmithError { SmithError::Session { code, message: message.into(), } } fn missing_entry(id: EntryId) -> SmithError { session_fault( smith::error::SessionCode::EntryNotFound, format!("entry {id} not found"), ) } fn io_error(code: smith::error::StorageCode, path: &Path) -> impl Fn(std::io::Error) -> SmithError { storage_error(code, storage_operation(code), path) } const fn storage_operation(code: smith::error::StorageCode) -> smith::error::StorageOperation { use smith::error::{StorageCode, StorageOperation}; match code { StorageCode::StoreCanonicalize => StorageOperation::Canonicalize, StorageCode::StoreStat | StorageCode::CreateStat => StorageOperation::Stat, StorageCode::StoreOpen | StorageCode::StoreLockOpen | StorageCode::StoreReaderLockOpen | StorageCode::RepairOpen => StorageOperation::Open, StorageCode::StoreRead | StorageCode::StoreProofRead | StorageCode::StoreScratchRead | StorageCode::FrameRead | StorageCode::SessionPrologue | StorageCode::FrameBoundary | StorageCode::AuthRead => StorageOperation::Read, StorageCode::StoreWrite | StorageCode::StoreProofWrite | StorageCode::StoreScratchWrite | StorageCode::CreateWrite | StorageCode::RepairWrite | StorageCode::StorePoisoned => StorageOperation::Write, StorageCode::StoreSeek | StorageCode::StoreProofSeek | StorageCode::StoreScratchSeek | StorageCode::RepairSeek => StorageOperation::Seek, StorageCode::StoreSync | StorageCode::CreateSync | StorageCode::RepairSync | StorageCode::StoreDirSync => StorageOperation::Sync, StorageCode::StoreTruncate => StorageOperation::Truncate, StorageCode::StoreMkdir | StorageCode::RuntimeDir => StorageOperation::CreateDirectory, StorageCode::AuthStore => StorageOperation::AuthStoreOperation, StorageCode::StoreLock | StorageCode::StoreBusy | StorageCode::StoreReaderLock | StorageCode::StoreReadersActive => StorageOperation::Lock, StorageCode::StoreUnlock | StorageCode::StoreReaderUnlock => StorageOperation::Unlock, StorageCode::StoreProofRemove | StorageCode::StoreScratchRemove | StorageCode::CreateTempRemove => StorageOperation::Remove, StorageCode::CreateTmp => StorageOperation::CreateTemporaryFile, StorageCode::CreatePublish | StorageCode::RepairPublish => StorageOperation::Publish, } } fn storage_fault( code: smith::error::StorageCode, path: &Path, message: impl Into, ) -> SmithError { SmithError::Storage { code, operation: storage_operation(code), path: path.to_path_buf(), message: message.into(), publication: None, } } fn storage_error( code: smith::error::StorageCode, operation: smith::error::StorageOperation, path: &Path, ) -> impl Fn(std::io::Error) -> SmithError + '_ { move |e| SmithError::Storage { code, operation, path: path.to_path_buf(), message: e.to_string(), publication: None, } } #[cfg(test)] #[expect( clippy::unwrap_used, clippy::panic, reason = "tests may panic on invariant violations" )] mod tests { use super::*; use crate::session::COMPACTION_KIND; use crate::session::{EntryContent, EntryFrame, EntryKind, Session}; use smith::message::{ContentBlock, Message, Role}; use smith::structured::{StructuredData, ToolArguments}; use smith_alloc::{Scope, measure}; use std::sync::Mutex; static MEASURING: Mutex<()> = Mutex::new(()); fn tmp_path(name: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("smith_store_tests_{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); dir.join(name) } fn message(text: &str) -> EntryContent { EntryContent::Message(Message::with_text(Role::User, text)) } fn entry(text: &str) -> EntryFrame { EntryFrame::new(None, &message(text)).unwrap() } fn text(entry: &EntryFrame) -> Option { match EntryContent::decode(&entry.content).unwrap() { EntryContent::Message(m) => match &m.blocks[0] { ContentBlock::Text(t) => Some(t.clone()), _ => None, }, _ => None, } } fn frame(text: &str) -> Frame { Frame::Known { entry: entry(text) } } fn file_bytes(frames: impl IntoIterator) -> Vec { FILE_PROLOGUE.into_iter().chain(frames).collect() } /// Eager frame collection belongs only in recovery assertions; durable /// product paths use `SessionReader`'s bounded readers directly. fn scanned_frames(path: &Path, frame_bytes_max: usize) -> Recovery { let reader = SessionReader::open(path).unwrap(); let mut frames = Vec::new(); let mut recovery = reader .scan(frame_bytes_max, |frame| { frames.push(frame); Ok(()) }) .unwrap(); recovery.frames = frames; recovery } #[test] fn durable_selection_replays_and_forks_a_sibling_without_loading_unrelated_content() { let path = tmp_path("durable-selection.smh"); let fork_path = tmp_path("durable-selection-fork.smh"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&fork_path); let cfg = Config::default_valid(); let root = entry("root"); let left = EntryFrame::new(Some(root.id), &message("left")).unwrap(); let right = EntryFrame::new(Some(root.id), &message("right")).unwrap(); { let mut opened = open_session(&path, cfg.clone()).unwrap(); for entry in [&root, &left, &right] { opened .writer .append_frame(&Frame::Known { entry: entry.clone(), }) .unwrap(); } opened.writer.select_entry(left.id).unwrap(); } let reopened = open_session(&path, cfg.clone()).unwrap(); assert_eq!(reopened.session.selected_id(), Some(left.id)); assert_eq!(reopened.session.active_branch().entries().len(), 2); assert_eq!( reopened .session .content(&reopened.session.active_branch().entries()[1]) .unwrap(), EntryContent::decode(&left.content).unwrap() ); drop(reopened); let reader = SessionReader::open(&path).unwrap(); let fork = reader.fork_at(right.id, &fork_path, cfg).unwrap(); assert_eq!(fork.session.selected_id(), Some(right.id)); assert_ne!( fork.session.id(), SessionReader::open(&path) .unwrap() .selected_session(Config::default_valid().frame_bytes_max) .unwrap() .id() ); assert_eq!( fork.session .content(&fork.session.active_branch().entries()[1]) .unwrap(), EntryContent::decode(&right.content).unwrap() ); drop(fork); drop(reader); let _ = std::fs::remove_file(path); let _ = std::fs::remove_file(fork_path); } #[test] fn full_verification_detects_duplicate_ids_outside_selected_branch() { let path = tmp_path("duplicate-unselected.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let unselected = entry("one"); let mut duplicate = entry("two"); duplicate.id = unselected.id; for entry in [unselected, duplicate] { opened.writer.append_frame(&Frame::Known { entry }).unwrap(); } drop(opened); let reader = SessionReader::open(&path).unwrap(); assert_eq!( reader.verify_full(cfg.frame_bytes_max).unwrap_err().code(), "SESSION_DUPLICATE_ID" ); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn missing_session_file_reports_storage_operation_path_and_source() { let path = tmp_path("missing-storage-error.smh"); let _ = std::fs::remove_file(&path); let error = SessionReader::open(&path).unwrap_err(); match error { SmithError::Storage { code, operation, path: actual, message, .. } => { assert_eq!(code, smith::error::StorageCode::StoreOpen); assert_eq!(operation.as_str(), "open"); assert_eq!(actual, path); assert!(!message.is_empty()); } other => panic!("expected storage error, got {other:?}"), } let _ = std::fs::remove_file(readers_path(&path)); } #[test] fn missing_selected_entry_fails_open_without_retargeting_a_branch() { let path = tmp_path("missing-selected.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); opened .writer .append_frame(&Frame::Known { entry: entry("valid"), }) .unwrap(); opened .writer .append_frame(&Frame::Selection { id: EntryId::new() }) .unwrap(); drop(opened); assert_eq!( open_session(&path, cfg).unwrap_err().code(), "ENTRY_NOT_FOUND" ); let _ = std::fs::remove_file(path); } #[test] fn damaged_index_does_not_hide_following_entries() { let path = tmp_path("damaged-index.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let session = Session::new(); let root = entry("root"); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); let mut bytes = FILE_PROLOGUE.to_vec(); bytes.extend( Frame::Header { header: session.header(), } .encode(cfg.frame_bytes_max) .unwrap(), ); bytes.extend( Frame::Known { entry: root.clone(), } .encode(cfg.frame_bytes_max) .unwrap(), ); let mut invalid = Frame::Index { id: root.id, offset: 8, parent: None, parent_offset: None, } .encode(cfg.frame_bytes_max) .unwrap(); invalid[crate::frame::HEADER_LEN + 4] ^= 0x80; bytes.extend(invalid); bytes.extend( Frame::Known { entry: child.clone(), } .encode(cfg.frame_bytes_max) .unwrap(), ); std::fs::write(&path, bytes).unwrap(); let reader = SessionReader::open(&path).unwrap(); assert_eq!( reader .selected_session(cfg.frame_bytes_max) .unwrap() .selected_id(), Some(child.id) ); assert_eq!( reader.verify_full(cfg.frame_bytes_max).unwrap_err().code(), "FRAME_CHECKSUM" ); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn repair_discards_stale_offsets_without_losing_authoritative_entries() { let path = tmp_path("repair-index-offset.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let record = *opened.session.append(&message("kept")).unwrap(); opened .writer .append_entry(&record, opened.session.bytes(&record)) .unwrap(); let selected = record.id; drop(opened); let original = std::fs::read(&path).unwrap(); let header_end = FILE_PROLOGUE.len() + Frame::Header { header: SessionReader::open(&path) .unwrap() .selected_session(cfg.frame_bytes_max) .unwrap() .header(), } .encode(cfg.frame_bytes_max) .unwrap() .len(); let mut bytes = original[..header_end].to_vec(); let mut damaged = Frame::Unknown { kind: 99, version: 1, payload: b"bad".to_vec(), } .encode(cfg.frame_bytes_max) .unwrap(); damaged[crate::frame::HEADER_LEN + 4] ^= 1; bytes.extend(damaged); bytes.extend(&original[header_end..]); std::fs::write(&path, bytes).unwrap(); repair_session(&path, cfg.frame_bytes_max).unwrap(); let reader = SessionReader::open(&path).unwrap(); reader.verify_full(cfg.frame_bytes_max).unwrap(); assert_eq!( reader .selected_session(cfg.frame_bytes_max) .unwrap() .selected_id(), Some(selected) ); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn reopen_keeps_only_effective_payload_after_compaction() { let path = tmp_path("effective-compaction.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let before = *opened .session .append(&message(&"x".repeat(400_000))) .unwrap(); opened .writer .append_entry(&before, opened.session.bytes(&before)) .unwrap(); let compact = EntryContent::Meta { kind: COMPACTION_KIND.into(), detail: StructuredData::from_serializable(&serde_json::json!({"summary": ""})).unwrap(), }; let boundary = *opened.session.append(&compact).unwrap(); opened .writer .append_entry(&boundary, opened.session.bytes(&boundary)) .unwrap(); let after = *opened.session.append(&message("after")).unwrap(); opened .writer .append_entry(&after, opened.session.bytes(&after)) .unwrap(); drop(opened); let reopened = open_session(&path, cfg).unwrap(); assert_eq!(reopened.session.selected_id(), Some(after.id)); assert_eq!(reopened.session.compaction_boundary(), Some(boundary.id)); assert_eq!(reopened.session.active_branch().entries().len(), 2); assert!(reopened.session.bytes(&before).is_empty()); drop(reopened); let _ = std::fs::remove_file(path); } #[test] fn replay_visits_file_metadata_once_for_linear_history() { let path = tmp_path("replay-pass-count.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let mut parent = None; for _ in 0..96 { let e = EntryFrame::new(parent, &message("x")).unwrap(); parent = Some(e.id); opened .writer .append_frame(&Frame::Known { entry: e }) .unwrap(); } drop(opened); let reader = SessionReader::open(&path).unwrap(); METADATA_PASSES.with(|passes| passes.set(0)); let mut visited = 0; reader .replay_at(parent.unwrap(), cfg.frame_bytes_max, |_| { visited += 1; Ok(()) }) .unwrap(); assert_eq!(visited, 96); METADATA_PASSES .with(|passes| assert!(passes.get() <= 2, "{} metadata passes", passes.get())); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn effective_context_cannot_publish_a_broken_rooted_fork() { let path = tmp_path("partial-source.smh"); let target = tmp_path("partial-target.smh"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&target); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let first = *opened.session.append(&message("before")).unwrap(); opened .writer .append_entry(&first, opened.session.bytes(&first)) .unwrap(); let compact = EntryContent::Meta { kind: COMPACTION_KIND.into(), detail: StructuredData::from_serializable(&serde_json::json!({"summary": ""})).unwrap(), }; let boundary = *opened.session.append(&compact).unwrap(); opened .writer .append_entry(&boundary, opened.session.bytes(&boundary)) .unwrap(); drop(opened); let resumed = open_session(&path, cfg.clone()).unwrap(); assert_eq!( create_session(&target, &resumed.session, cfg) .unwrap_err() .code(), "SESSION_INCOMPLETE_CONTEXT" ); assert!(!target.exists()); drop(resumed); let _ = std::fs::remove_file(path); } #[test] fn snapshot_reads_pinned_inode_after_uncoordinated_replacement() { let path = tmp_path("pinned-generation.smh"); let replacement = tmp_path("pinned-generation-replacement.smh"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&replacement); let cfg = Config::default_valid(); let mut first = open_session(&path, cfg.clone()).unwrap(); let root = entry("old"); first .writer .append_frame(&Frame::Known { entry: root.clone(), }) .unwrap(); let old_id = first.session.id(); drop(first); let snapshot = SessionReader::open(&path).unwrap(); let newer = open_session(&replacement, cfg.clone()).unwrap(); let new_id = newer.session.id(); drop(newer); std::fs::rename(&replacement, &path).unwrap(); assert_eq!( snapshot.selected_session(cfg.frame_bytes_max).unwrap().id(), old_id ); assert_eq!( snapshot .find_entry(root.id, cfg.frame_bytes_max) .unwrap() .unwrap() .0 .id, root.id ); assert_eq!( SessionReader::open(&path) .unwrap() .selected_session(cfg.frame_bytes_max) .unwrap() .id(), new_id ); drop(snapshot); let _ = std::fs::remove_file(path); } #[test] fn replay_and_fork_visit_growth_is_quadratic_not_cubic() { fn visits(depth: usize) -> (usize, usize) { let path = tmp_path(&format!("growth-{depth}.smh")); let target = tmp_path(&format!("growth-fork-{depth}.smh")); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&target); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let mut parent = None; for _ in 0..depth { let entry = EntryFrame::new(parent, &message("x")).unwrap(); parent = Some(entry.id); opened.writer.append_frame(&Frame::Known { entry }).unwrap(); } drop(opened); let reader = SessionReader::open(&path).unwrap(); PROOF_VISITS.with(|visits| visits.set(0)); reader .replay_at(parent.unwrap(), cfg.frame_bytes_max, |_| Ok(())) .unwrap(); let replay = PROOF_VISITS.with(std::cell::Cell::get); PROOF_VISITS.with(|visits| visits.set(0)); let fork = reader.fork_at(parent.unwrap(), &target, cfg).unwrap(); assert_eq!(fork.session.selected_id(), parent); let fork_visits = PROOF_VISITS.with(std::cell::Cell::get); drop(fork); drop(reader); let _ = std::fs::remove_file(path); let _ = std::fs::remove_file(target); (replay, fork_visits) } let small = visits(64); let large = visits(128); assert!( large.0 <= 5 * small.0, "replay proof visits: {small:?} -> {large:?}" ); assert!( large.1 <= 5 * small.1, "fork proof visits: {small:?} -> {large:?}" ); } #[test] fn create_keeps_selection_of_existing_ancestor() { let path = tmp_path("create-ancestor-selection.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let root = entry("root"); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); let source = Session::from_frames(&[ Frame::Header { header: Session::new().header(), }, Frame::Known { entry: root.clone(), }, Frame::Known { entry: child }, Frame::Selection { id: root.id }, ]) .unwrap(); let opened = create_session(&path, &source, cfg.clone()).unwrap(); assert_eq!(opened.session.selected_id(), Some(root.id)); drop(opened); let reopened = open_session(&path, cfg).unwrap(); assert_eq!(reopened.session.selected_id(), Some(root.id)); drop(reopened); let _ = std::fs::remove_file(path); } #[test] fn valid_checkpoint_locates_selected_ancestor() { let path = tmp_path("selected-checkpoint-hint.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let root = entry("root"); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); let mut opened = open_session(&path, cfg.clone()).unwrap(); for record in [&root, &child] { opened .writer .append_frame(&Frame::Known { entry: record.clone(), }) .unwrap(); } opened.writer.select_entry(root.id).unwrap(); drop(opened); CHECKPOINT_HITS.with(|hits| hits.set(0)); let reader = SessionReader::open(&path).unwrap(); assert_eq!( reader .selected_session(cfg.frame_bytes_max) .unwrap() .selected_id(), Some(root.id) ); CHECKPOINT_HITS.with(|hits| assert_eq!(hits.get(), 1)); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn invalid_checkpoint_does_not_reset_writer_cadence() { let path = tmp_path("invalid-cadence.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); for _ in 0..63 { let record = *opened.session.append(&message("turn")).unwrap(); opened .writer .append_entry(&record, opened.session.bytes(&record)) .unwrap(); } opened .writer .append_frame(&Frame::Checkpoint { id: EntryId::new(), offset: 123, }) .unwrap(); drop(opened); let mut reopened = open_session(&path, cfg.clone()).unwrap(); let record = *reopened.session.append(&message("last")).unwrap(); reopened .writer .append_entry(&record, reopened.session.bytes(&record)) .unwrap(); drop(reopened); let mut count = 0; SessionReader::open(&path) .unwrap() .scan(cfg.frame_bytes_max, |frame| { if matches!(frame, Frame::Checkpoint { .. }) { count += 1; } Ok(()) }) .unwrap(); assert_eq!(count, 2); let _ = std::fs::remove_file(path); } #[test] fn writer_publishes_reconstructible_checkpoints_at_the_cadence() { let path = tmp_path("checkpoint-cadence.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); for _ in 0..63 { let record = *opened.session.append(&message("turn")).unwrap(); opened .writer .append_entry(&record, opened.session.bytes(&record)) .unwrap(); } drop(opened); let mut reopened = open_session(&path, cfg.clone()).unwrap(); let record = *reopened.session.append(&message("turn")).unwrap(); reopened .writer .append_entry(&record, reopened.session.bytes(&record)) .unwrap(); let selected = reopened.session.selected_id(); drop(reopened); let reader = SessionReader::open(&path).unwrap(); let mut checkpoints = 0; reader .scan(cfg.frame_bytes_max, |frame| { if matches!(frame, Frame::Checkpoint { .. }) { checkpoints += 1; } Ok(()) }) .unwrap(); assert_eq!(checkpoints, 1); reader.verify_full(cfg.frame_bytes_max).unwrap(); assert_eq!( reader .selected_session(cfg.frame_bytes_max) .unwrap() .selected_id(), selected ); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn forged_checkpoint_is_ignored_for_resume_but_fails_full_verification() { let path = tmp_path("forged-checkpoint.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let root = entry("root"); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); opened .writer .append_frame(&Frame::Known { entry: root.clone(), }) .unwrap(); opened .writer .append_frame(&Frame::Checkpoint { id: root.id, offset: FILE_PROLOGUE.len() as u64 + 1, }) .unwrap(); opened .writer .append_frame(&Frame::Known { entry: child.clone(), }) .unwrap(); drop(opened); let reader = SessionReader::open(&path).unwrap(); assert_eq!( reader .selected_session(cfg.frame_bytes_max) .unwrap() .selected_id(), Some(child.id) ); assert_eq!( reader.verify_full(cfg.frame_bytes_max).unwrap_err().code(), "SESSION_CHECKPOINT" ); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn forged_index_never_hides_later_entries_and_full_verification_rejects_it() { let path = tmp_path("forged-index.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut opened = open_session(&path, cfg.clone()).unwrap(); let root = entry("root"); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); opened .writer .append_frame(&Frame::Known { entry: root.clone(), }) .unwrap(); opened .writer .append_frame(&Frame::Index { id: root.id, offset: FILE_PROLOGUE.len() as u64 + 1, parent: None, parent_offset: None, }) .unwrap(); opened .writer .append_frame(&Frame::Known { entry: child.clone(), }) .unwrap(); drop(opened); let reader = SessionReader::open(&path).unwrap(); assert_eq!( reader .selected_session(cfg.frame_bytes_max) .unwrap() .selected_id(), Some(child.id) ); assert_eq!( reader.verify_full(cfg.frame_bytes_max).unwrap_err().code(), "SESSION_INDEX" ); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn create_validates_config_before_creating_session_files() { let path = tmp_path("config-before-effects/nested/session.smh"); let _ = std::fs::remove_dir_all(path.parent().unwrap().parent().unwrap()); let mut config = Config::default_valid(); config.frame_bytes_max = 0; assert!(create_session(&path, &Session::new(), config).is_err()); assert!(!path.parent().unwrap().exists()); } #[test] fn create_rejects_named_identity_mismatch_before_publication() { let session = Session::new(); let path = tmp_path(&format!("{}.smh", smith::id::SessionId::new())); let _ = std::fs::remove_file(&path); assert_eq!( create_session(&path, &session, Config::default_valid()) .unwrap_err() .code(), "SESSION_ID_MISMATCH" ); assert!(!path.exists()); let _ = std::fs::remove_file(lock_path(&path)); } #[cfg(unix)] #[test] fn create_temp_symlink_cannot_truncate_its_target() { let path = tmp_path("create-temp-link.smh"); let temporary = path.with_extension("smith-create.tmp"); let victim = tmp_path("create-temp-victim"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&temporary); std::fs::write(&victim, b"do not truncate").unwrap(); std::os::unix::fs::symlink(&victim, &temporary).unwrap(); create_session(&path, &Session::new(), Config::default_valid()).unwrap(); assert_eq!(std::fs::read(&victim).unwrap(), b"do not truncate"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&temporary); let _ = std::fs::remove_file(&victim); } #[cfg(unix)] #[test] fn repair_temp_symlink_cannot_truncate_its_target() { let path = tmp_path("repair-temp-link.smh"); let temporary = path.with_extension("smith-repair.tmp"); let victim = tmp_path("repair-temp-victim"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&temporary); std::fs::write(&path, file_bytes(frame("intact").encode(1 << 20).unwrap())).unwrap(); std::fs::write(&victim, b"do not truncate").unwrap(); std::os::unix::fs::symlink(&victim, &temporary).unwrap(); repair_session(&path, Config::default_valid().frame_bytes_max).unwrap(); assert_eq!(std::fs::read(&victim).unwrap(), b"do not truncate"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&temporary); let _ = std::fs::remove_file(&victim); } fn text_of(frame: &Frame) -> String { match frame { Frame::Known { entry } => text(entry).unwrap(), other => panic!("unexpected frame {other:?}"), } } #[cfg(target_arch = "x86_64")] #[test] fn observer_handle_has_constant_size_independent_of_history() { assert_eq!(std::mem::size_of::(), 24); } #[test] fn reporting_scan_does_not_retain_streamed_damage() { let path = tmp_path("reporting-damage.smh"); let _ = std::fs::remove_file(&path); let mut malformed = frame("bad").encode(1 << 20).unwrap(); malformed[crate::frame::HEADER_LEN + 4] ^= 0xff; std::fs::write(&path, file_bytes(malformed.repeat(128))).unwrap(); let reader = SessionReader::open(&path).unwrap(); let mut reported = 0; let recovery = reader .scan_reporting( Config::default_valid().frame_bytes_max, |_| Ok(true), |_| { reported += 1; Ok(()) }, ) .unwrap(); assert_eq!(reported, 128); assert!(recovery.damage.is_empty()); let _ = std::fs::remove_file(path); } #[test] fn reader_snapshot_keeps_initial_writer_ownership_and_length() { let path = tmp_path("reader-snapshot.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut writer = SessionWriter::open(&path, cfg.clone()).unwrap(); writer.append_frame(&frame("before")).unwrap(); let reader = SessionReader::open(&path).unwrap(); assert!(reader.writer_active()); let snapshot_len = reader.file_size(); writer.append_frame(&frame("after")).unwrap(); drop(writer); assert_eq!(reader.file_size(), snapshot_len); let mut frames = 0; reader .scan(cfg.frame_bytes_max, |_| { frames += 1; Ok(()) }) .unwrap(); assert_eq!(frames, 1); let _ = std::fs::remove_file(path); } #[test] fn streaming_scan_visits_entries_without_retaining_history() { // SMH-SPEC-SPEC0001: Sessions, Working sets; a snapshot never follows live appends. let path = tmp_path("stream_scan.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut writer = SessionWriter::open(&path, cfg.clone()).unwrap(); for _ in 0..128 { writer.append_frame(&frame("entry")).unwrap(); } let mut count = 0; let scan = scan_session(&path, cfg.frame_bytes_max, |frame| { assert!(matches!(frame, Frame::Known { .. })); count += 1; Ok(()) }) .unwrap(); assert_eq!(count, 128); assert!(scan.damage.is_empty()); assert_eq!(scan.torn_tail_at, None); let mut corrupt = File::options().append(true).open(&path).unwrap(); corrupt.write_all(&[0, 1]).unwrap(); let scan = scan_session(&path, cfg.frame_bytes_max, |_| Ok(())).unwrap(); assert!(scan.torn_tail_at.is_some()); drop(writer); let _ = std::fs::remove_file(path); } #[test] fn append_load_round_trip_preserves_entries() { let path = tmp_path("round_trip.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let first = entry("one"); let second = EntryFrame::new(Some(first.id), &message("two")).unwrap(); { let mut opened = open_session(&path, cfg.clone()).unwrap(); opened .writer .append_frame(&Frame::Known { entry: first.clone(), }) .unwrap(); opened .writer .append_frame(&Frame::Known { entry: second.clone(), }) .unwrap(); } let session = SessionReader::open(&path) .unwrap() .selected_session(cfg.frame_bytes_max) .unwrap(); assert_eq!( session .active_branch() .entries() .iter() .map(|e| e.id) .collect::>(), vec![first.id, second.id] ); assert_eq!(session.selected_id(), Some(second.id)); let _ = std::fs::remove_file(&path); } #[test] fn successful_io_error_context_setup_does_not_allocate() { let path = Path::new("/tmp/smith-store-allocation-check"); let _serial = MEASURING .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let (_, cost) = measure(Scope::IoRequest, || { let error = io_error(smith::error::StorageCode::StoreWrite, path); drop(std::hint::black_box(error)); }); assert_eq!(cost.allocations, 0); assert_eq!(cost.bytes, 0); } #[test] fn successful_write_does_not_allocate_error_context() { let path = tmp_path("write-allocation-check.smh"); let _ = std::fs::remove_file(&path); let mut writer = SessionWriter::open(&path, Config::default_valid()).unwrap(); let bytes = frame("allocation-check") .encode(Config::default_valid().frame_bytes_max) .unwrap(); let _serial = MEASURING .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let (result, cost) = measure(Scope::IoRequest, || writer.write(&bytes)); result.unwrap(); assert_eq!(cost.allocations, 0); assert_eq!(cost.bytes, 0); drop(writer); let _ = std::fs::remove_file(path); } #[test] fn torn_tail_is_truncated_on_open_and_appends_stay_visible() { let path = tmp_path("torn_tail.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); { let mut w = SessionWriter::open(&path, cfg.clone()).unwrap(); w.append_frame(&frame("kept")).unwrap(); } // Simulate a crash mid-append: partial header + payload bytes. let good = Frame::Known { entry: entry("kept"), } .encode(cfg.frame_bytes_max) .unwrap(); let mut torn = good; torn.extend_from_slice(&[0x10, 0x00, 0x00, 0x00, 0xAA, 0xBB]); std::fs::write(&path, file_bytes(torn)).unwrap(); { // Open repairs the tail, then a new append must be loadable. let mut w = SessionWriter::open(&path, cfg.clone()).unwrap(); w.append_frame(&frame("after")).unwrap(); } let frames = scanned_frames(&path, cfg.frame_bytes_max).frames; let texts: Vec = frames .iter() .filter_map(|f| match f { Frame::Known { entry } => text(entry), _ => None, }) .collect(); assert_eq!(texts, vec!["kept".to_string(), "after".to_string()]); let _ = std::fs::remove_file(&path); } #[test] fn loader_stops_at_torn_tail_without_header_fabrication() { let path = tmp_path("loader_torn.smh"); let cfg = Config::default_valid(); let good = frame("a").encode(cfg.frame_bytes_max).unwrap(); let mut bytes = good; bytes.extend_from_slice(&[0xFF, 0xFF]); // partial header std::fs::write(&path, file_bytes(bytes)).unwrap(); let frames = scanned_frames(&path, cfg.frame_bytes_max).frames; assert_eq!(frames.len(), 1); let _ = std::fs::remove_file(&path); } #[test] fn reopening_a_multi_frame_session_keeps_every_frame() { let path = tmp_path("reopen_multi.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); { let mut w = SessionWriter::open(&path, cfg.clone()).unwrap(); w.append_frame(&frame("one")).unwrap(); w.append_frame(&frame("two")).unwrap(); } { // Reopening must not mistake a later frame boundary for a torn tail. let mut w = SessionWriter::open(&path, cfg.clone()).unwrap(); w.append_frame(&frame("three")).unwrap(); } let recovery = scanned_frames(&path, cfg.frame_bytes_max); assert_eq!(recovery.frames.len(), 3); assert!(recovery.damage.is_empty()); assert_eq!(recovery.torn_tail_at, None); let _ = std::fs::remove_file(&path); } #[test] fn oversized_complete_frame_is_skipped_and_following_frames_reachable() { let path = tmp_path("oversized_skip.smh"); // declared = 5000 > limit 1024, payload fully present, then a valid frame. let mut bytes = Frame::Unknown { kind: 9, version: 1, payload: vec![0xAB; 5000], } .encode(6000) .unwrap(); bytes.extend_from_slice(&frame("tail").encode(1 << 20).unwrap()); std::fs::write(&path, file_bytes(bytes)).unwrap(); let recovery = scanned_frames(&path, 1024); assert_eq!(recovery.frames.len(), 1); assert!(matches!(recovery.frames[0], Frame::Known { .. })); assert_eq!(recovery.damage[0].code(), "FRAME_TOO_LARGE"); let _ = std::fs::remove_file(&path); } #[test] fn repair_reports_all_faults_without_retaining_their_frames() { let path = tmp_path("repair-multiple-faults.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut bytes = FILE_PROLOGUE.to_vec(); for _ in 0..2 { let mut broken = frame("damaged").encode(cfg.frame_bytes_max).unwrap(); broken[crate::frame::HEADER_LEN + 4] ^= 1; bytes.extend(broken); } std::fs::write(&path, bytes).unwrap(); let recovery = repair_session(&path, cfg.frame_bytes_max).unwrap(); assert_eq!(recovery.damage_count, 2); assert_eq!(recovery.damage.len(), 1); assert!(recovery.frames.is_empty()); let _ = std::fs::remove_file(path); } #[test] fn repair_streams_unknown_frames_and_keeps_final_selection() { let path = tmp_path("repair-stream-selection.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let root = entry("root"); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); let unknown = Frame::Unknown { kind: 0xfabc, version: 17, payload: vec![42; 1024], }; let mut opened = open_session(&path, cfg.clone()).unwrap(); for entry in [&root, &child] { opened .writer .append_frame(&Frame::Known { entry: entry.clone(), }) .unwrap(); } opened.writer.append_frame(&unknown).unwrap(); opened.writer.select_entry(root.id).unwrap(); drop(opened); let recovery = repair_session(&path, cfg.frame_bytes_max).unwrap(); assert!( recovery.frames.is_empty(), "repair must not retain historical frames" ); let mut seen_unknown = false; SessionReader::open(&path) .unwrap() .scan(cfg.frame_bytes_max, |frame| { if frame == unknown { seen_unknown = true; } Ok(()) }) .unwrap(); assert!(seen_unknown); let reopened = open_session(&path, cfg).unwrap(); assert_eq!(reopened.session.selected_id(), Some(root.id)); drop(reopened); let _ = std::fs::remove_file(path); } #[test] fn repair_rejects_damaged_explicit_selection_without_publication() { let dir = tmp_path("repair-selected-damaged"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("session.smh"); let cfg = Config::default_valid(); let session = Session::new(); let root = entry("root"); let selected = EntryFrame::new(Some(root.id), &message("selected")).unwrap(); let mut bytes = FILE_PROLOGUE.to_vec(); for frame in [ Frame::Header { header: session.header(), }, Frame::Known { entry: root.clone(), }, Frame::Known { entry: selected.clone(), }, Frame::Selection { id: selected.id }, ] { bytes.extend(frame.encode(cfg.frame_bytes_max).unwrap()); } let selected_start = FILE_PROLOGUE.len() + Frame::Header { header: session.header(), } .encode(cfg.frame_bytes_max) .unwrap() .len() + Frame::Known { entry: root } .encode(cfg.frame_bytes_max) .unwrap() .len(); let selected_checksum = selected_start + crate::frame::HEADER_LEN + 4; bytes[selected_checksum] ^= 1; std::fs::write(&path, &bytes).unwrap(); let before_temps = std::fs::read_dir(&dir) .unwrap() .filter_map(std::result::Result::ok) .filter(|entry| { entry .file_name() .to_string_lossy() .starts_with("session.smh.smith-repair-") }) .count(); let error = repair_session(&path, cfg.frame_bytes_max).unwrap_err(); assert_eq!(error.code(), "ENTRY_NOT_FOUND"); assert_eq!(std::fs::read(&path).unwrap(), bytes); let after_temps = std::fs::read_dir(&dir) .unwrap() .filter_map(std::result::Result::ok) .filter(|entry| { entry .file_name() .to_string_lossy() .starts_with("session.smh.smith-repair-") }) .count(); assert_eq!(after_temps, before_temps); let _ = std::fs::remove_dir_all(dir); } #[test] fn repair_republishes_file_without_torn_tail() { let path = tmp_path("repair.smh"); let cfg = Config::default_valid(); let good = frame("a").encode(cfg.frame_bytes_max).unwrap(); let mut bytes = good; bytes.extend_from_slice(&frame("b").encode(cfg.frame_bytes_max).unwrap()[..6]); std::fs::write(&path, file_bytes(bytes)).unwrap(); let before = std::fs::metadata(&path).unwrap().len(); let recovery = repair_session(&path, cfg.frame_bytes_max).unwrap(); assert!(recovery.frames.is_empty()); assert_eq!(recovery.torn_tail_at, Some(before - 6)); let after = std::fs::metadata(&path).unwrap().len(); assert!(after < before); assert_eq!(scanned_frames(&path, cfg.frame_bytes_max).frames.len(), 1); let _ = std::fs::remove_file(&path); } #[test] fn writer_rejects_oversized_frame_without_touching_file() { let path = tmp_path("oversize_reject.smh"); let _ = std::fs::remove_file(&path); let mut cfg = Config::default_valid(); cfg.frame_bytes_max = 4096; { let mut w = SessionWriter::open(&path, cfg.clone()).unwrap(); w.append_frame(&frame("small")).unwrap(); let big = EntryFrame::new( None, &EntryContent::Message({ let mut m = Message::with_text(Role::User, "x"); m.add_block(ContentBlock::text("y".repeat(8192))); m }), ) .unwrap(); let error = w.append_frame(&Frame::Known { entry: big }).unwrap_err(); assert_eq!(error.code(), "FRAME_TOO_LARGE"); } assert_eq!(scanned_frames(&path, cfg.frame_bytes_max).frames.len(), 1); let _ = std::fs::remove_file(&path); } #[test] fn session_identity_mismatch_does_not_mutate_source() { // SMH-SPEC-SPEC0001: Sessions, identity independent of offsets and filenames. let original = Session::new(); let path = tmp_path(&format!("{}.smh", original.id())); let renamed = tmp_path(&format!("{}.smh", smith::id::SessionId::new())); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&renamed); drop(open_session(&path, Config::default_valid()).unwrap()); std::fs::rename(&path, &renamed).unwrap(); let before = std::fs::read(&renamed).unwrap(); assert_eq!( open_session(&renamed, Config::default_valid()) .unwrap_err() .code(), "SESSION_ID_MISMATCH" ); assert_eq!(std::fs::read(&renamed).unwrap(), before); let _ = std::fs::remove_file(renamed); } #[test] fn observer_prevents_implicit_tail_repair() { // SMH-SPEC-SPEC0001: Sessions, maintenance cannot invalidate a pinned reader. let path = tmp_path("observer_torn.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut writer = SessionWriter::open(&path, cfg.clone()).unwrap(); writer.append_frame(&frame("intact")).unwrap(); drop(writer); let mut tail = File::options().append(true).open(&path).unwrap(); tail.write_all(&[0, 1]).unwrap(); let before = std::fs::read(&path).unwrap(); let observer = SessionReader::open(&path).unwrap(); assert_eq!( SessionWriter::open(&path, cfg).unwrap_err().code(), "STORE_READERS_ACTIVE" ); assert_eq!(std::fs::read(&path).unwrap(), before); drop(observer); let _ = std::fs::remove_file(path); } #[test] fn observer_coexists_with_writer_but_blocks_replacement() { // SMH-SPEC-SPEC0001: Sessions, Access ownership and repair coordination. let path = tmp_path("observer_lock.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let mut writer = SessionWriter::open(&path, cfg.clone()).unwrap(); writer.append_frame(&frame("one")).unwrap(); let observer = SessionReader::open(&path).unwrap(); assert_eq!( observer .scan(cfg.frame_bytes_max, |_| Ok(())) .unwrap() .good_end, std::fs::metadata(&path).unwrap().len() ); assert_eq!( SessionWriter::open(&path, cfg.clone()).unwrap_err().code(), "STORE_BUSY" ); drop(writer); assert_eq!( repair_session(&path, cfg.frame_bytes_max) .unwrap_err() .code(), "STORE_READERS_ACTIVE" ); drop(observer); repair_session(&path, cfg.frame_bytes_max).unwrap(); let _ = std::fs::remove_file(path); } #[cfg(unix)] #[test] fn symlinked_data_and_parent_paths_share_one_writer_lock() { let dir = tmp_path("symlinked-writer-lock"); let real_dir = dir.join("real"); let parent_alias = dir.join("parent-alias"); let data_alias = dir.join("data-alias.smh"); let real_path = real_dir.join("session.smh"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&real_dir).unwrap(); std::os::unix::fs::symlink(&real_dir, &parent_alias).unwrap(); let writer = SessionWriter::open(&real_path, Config::default_valid()).unwrap(); std::os::unix::fs::symlink(&real_path, &data_alias).unwrap(); for alias in [&parent_alias.join("session.smh"), &data_alias] { assert_eq!( SessionWriter::open(alias, Config::default_valid()) .unwrap_err() .code(), "STORE_BUSY" ); } drop(writer); let _ = std::fs::remove_dir_all(dir); } #[cfg(unix)] #[test] fn multiply_linked_session_data_is_rejected() { let dir = tmp_path("multiply-linked-session"); let path = dir.join("session.smh"); let alias = dir.join("alias.smh"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); drop(SessionWriter::open(&path, Config::default_valid()).unwrap()); std::fs::hard_link(&path, &alias).unwrap(); assert_eq!( SessionWriter::open(&path, Config::default_valid()) .unwrap_err() .code(), "SESSION_AMBIGUOUS_ALIAS" ); let _ = std::fs::remove_dir_all(dir); } #[test] fn failed_write_poisoning_rejects_later_appends() { let path = tmp_path("poisoned-writer.smh"); let _ = std::fs::remove_file(&path); let mut writer = SessionWriter::open(&path, Config::default_valid()).unwrap(); let healthy = writer.file.try_clone().unwrap(); writer.file = File::open(path.parent().unwrap()).unwrap(); assert_eq!( writer.append_frame(&frame("partial")).unwrap_err().code(), "STORE_WRITE" ); writer.file = healthy; assert_eq!( writer.append_frame(&frame("later")).unwrap_err().code(), "STORE_POISONED" ); let _ = std::fs::remove_file(path); } #[test] fn fault_injected_partial_write_and_sync_failure_poison_writers() { let partial_path = tmp_path("poisoned-partial-writer.smh"); let sync_path = tmp_path("poisoned-sync-writer.smh"); let _ = std::fs::remove_file(&partial_path); let _ = std::fs::remove_file(&sync_path); let bytes = frame("pending") .encode(Config::default_valid().frame_bytes_max) .unwrap(); let mut partial = SessionWriter::open(&partial_path, Config::default_valid()).unwrap(); assert_eq!( partial .write_step(|file| { file.write_all(&bytes[..bytes.len() / 2]).map_err(io_error( smith::error::StorageCode::StoreWrite, &partial_path, ))?; Err(storage_fault( smith::error::StorageCode::StoreWrite, &partial_path, "injected partial write failure", )) }) .unwrap_err() .code(), "STORE_WRITE" ); assert_eq!( partial.append_frame(&frame("later")).unwrap_err().code(), "STORE_POISONED" ); drop(partial); let mut sync = SessionWriter::open(&sync_path, Config::default_valid()).unwrap(); assert_eq!( sync.write_step(|file| { file.write_all(&bytes) .map_err(io_error(smith::error::StorageCode::StoreWrite, &sync_path))?; Err(storage_fault( smith::error::StorageCode::StoreSync, &sync_path, "injected sync failure", )) }) .unwrap_err() .code(), "STORE_SYNC" ); assert_eq!( sync.append_frame(&frame("later")).unwrap_err().code(), "STORE_POISONED" ); drop(sync); let _ = std::fs::remove_file(partial_path); let _ = std::fs::remove_file(sync_path); } #[test] fn lock_serializes_second_writer_handle() { let path = tmp_path("locked.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let writer = SessionWriter::open(&path, cfg).unwrap(); // While the first writer holds the session, an independent handle // cannot take it: writers cannot interleave frames. let probe = OpenOptions::new() .read(true) .write(true) .open(lock_path(&path)) .unwrap(); assert!(fs2::FileExt::try_lock_exclusive(&probe).is_err()); drop(writer); assert!(fs2::FileExt::try_lock_exclusive(&probe).is_ok()); let _ = std::fs::remove_file(&path); } #[test] fn repair_waits_for_a_live_writer_and_keeps_its_entries() { let path = tmp_path("repair_race.smh"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(lock_path(&path)); let cfg = Config::default_valid(); let mut writer = SessionWriter::open(&path, cfg.clone()).unwrap(); writer.append_frame(&frame("one")).unwrap(); let repair_path = path.clone(); let limit = cfg.frame_bytes_max; let (entered, repair_started) = std::sync::mpsc::sync_channel(1); let repair = std::thread::spawn(move || { entered.send(()).unwrap(); repair_session(&repair_path, limit).unwrap() }); // synchronize: the repair has started; the writer's lock holds it // until the drop, so an append now is still committed to the file // the repair will republish. repair_started .recv_timeout(std::time::Duration::from_secs(10)) .unwrap(); writer.append_frame(&frame("two")).unwrap(); drop(writer); let recovery = repair.join().unwrap(); assert!(recovery.frames.is_empty()); let loaded = scanned_frames(&path, cfg.frame_bytes_max); let texts: Vec = loaded.frames.iter().map(text_of).collect(); assert_eq!(texts, vec!["one".to_string(), "two".to_string()]); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(lock_path(&path)); } #[test] fn unknown_frames_survive_write_and_load() { let path = tmp_path("unknown_frames.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let unknown = Frame::Unknown { kind: 0x4242, version: 3, payload: vec![1, 2, 3, 4, 5, 6], }; { let mut w = SessionWriter::open(&path, cfg.clone()).unwrap(); w.append_frame(&unknown).unwrap(); w.append_frame(&frame("known")).unwrap(); } let frames = scanned_frames(&path, cfg.frame_bytes_max).frames; assert_eq!(frames.len(), 2); match &frames[0] { Frame::Unknown { kind, version, payload, } => { assert_eq!(payload, &vec![1, 2, 3, 4, 5, 6]); assert_eq!((*kind, *version), (0x4242, 3)); } other => panic!("expected unknown frame, got {other:?}"), } match &frames[1] { Frame::Known { entry } => assert_eq!(text(entry).as_deref(), Some("known")), other => panic!("expected known frame, got {other:?}"), } let _ = std::fs::remove_file(&path); } #[test] fn end_to_end_tool_activity_persists_and_reconstructs() { let dir = tmp_path("e2e"); std::fs::create_dir_all(&dir).unwrap(); let workdir = dir.join("work"); std::fs::create_dir_all(&workdir).unwrap(); let session_path = dir.join("e2e.smh"); let _ = std::fs::remove_file(&session_path); let cfg = Config::default_valid(); let opened_id; { let opened = open_session(&session_path, cfg.clone()).unwrap(); opened_id = opened.session.id(); let mut ts = crate::tools::ToolSession::with_session( workdir.clone(), opened.session, opened.writer, ); ts.invoke( "write", &ToolArguments::from_serializable( &serde_json::json!({"path": "out/hello.txt", "content": "hi"}), ) .unwrap(), ) .unwrap(); ts.invoke( "read", &ToolArguments::from_serializable(&serde_json::json!({"path": "out/hello.txt"})) .unwrap(), ) .unwrap(); } let restored = SessionReader::open(&session_path) .unwrap() .selected_session(cfg.frame_bytes_max) .unwrap(); assert_eq!(restored.id(), opened_id); assert_eq!(restored.active_branch().entries().len(), 6); // Tool call/result/completion triples stay adjacent and ordered. let kinds: Vec<&str> = restored .active_branch() .entries() .iter() .map(|e| match e.kind { EntryKind::ToolCall { .. } => "call", EntryKind::ToolResult { .. } => "result", _ => "other", }) .collect(); assert_eq!( kinds, vec!["call", "result", "other", "call", "result", "other"] ); // The file effect actually happened under the invocation workdir. assert_eq!( std::fs::read_to_string(workdir.join("out/hello.txt")).unwrap(), "hi" ); let _ = std::fs::remove_file(&session_path); let _ = std::fs::remove_dir_all(&dir); } #[test] fn reopened_session_keeps_its_identity_and_replays_identically() { let path = tmp_path("identity.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let created = { let mut opened = open_session(&path, cfg.clone()).unwrap(); opened.writer.append_frame(&frame("one")).unwrap(); opened.session }; let first = SessionReader::open(&path).unwrap(); let second = SessionReader::open(&path).unwrap(); let first_header = first.header(cfg.frame_bytes_max).unwrap(); let second_header = second.header(cfg.frame_bytes_max).unwrap(); assert_eq!(first_header.session_id, created.id()); assert_eq!(first_header.branch_id, created.active_branch().id()); assert_eq!(second_header.session_id, first_header.session_id); // Replay evidence is identical across independent durable readers. let records = |reader: &SessionReader| { let mut records = Vec::new(); crate::trace::replay(reader, cfg.frame_bytes_max, |record| { records.push(record); Ok(()) }) .unwrap(); records }; assert_eq!(records(&first), records(&second)); let _ = std::fs::remove_file(&path); } #[test] fn forking_publishes_a_new_session_file_and_leaves_the_source_intact() { let dir = tmp_path("fork_dir"); std::fs::create_dir_all(&dir).unwrap(); let source_path = dir.join("source.smh"); let fork_path = dir.join("fork.smh"); let cfg = Config::default_valid(); let (source_session, boundary_id, leaf_id) = { let mut opened = open_session(&source_path, cfg.clone()).unwrap(); let root = *opened.session.append(&message("root")).unwrap(); opened .writer .append_entry(&root, opened.session.bytes(&root)) .unwrap(); let boundary = *opened .session .append(&EntryContent::Meta { kind: COMPACTION_KIND.to_string(), detail: StructuredData::from_serializable( &serde_json::json!({"summary": "", "kept": 1}), ) .unwrap(), }) .unwrap(); opened .writer .append_entry(&boundary, opened.session.bytes(&boundary)) .unwrap(); let tip = *opened.session.append(&message("after")).unwrap(); opened .writer .append_entry(&tip, opened.session.bytes(&tip)) .unwrap(); assert_ne!(root.id, tip.id); (opened.session, boundary.id, tip.id) }; let source_reader = SessionReader::open(&source_path).unwrap(); let fork_id = { let forked = source_reader .fork_at(boundary_id, &fork_path, cfg.clone()) .unwrap(); assert_ne!(forked.session.id(), source_session.id()); assert_eq!(forked.session.selected_id(), Some(boundary_id)); assert_eq!(forked.session.compaction_boundary(), Some(boundary_id)); assert!(forked.recovery.damage.is_empty()); forked.session.id() }; // The fork is durable: reopening restores the same identity and leaf. { let reopened = open_session(&fork_path, cfg.clone()).unwrap(); assert_eq!(reopened.session.id(), fork_id); assert_eq!(reopened.session.selected_id(), Some(boundary_id)); assert_eq!(reopened.session.active_branch().entries().len(), 1); assert_eq!(reopened.session.compaction_boundary(), Some(boundary_id)); // Full historical chain remains on disk, including pre-compaction content. let mut history = Vec::new(); SessionReader::open(&fork_path) .unwrap() .replay_at(boundary_id, cfg.frame_bytes_max, |entry| { history.push(entry); Ok(()) }) .unwrap(); assert_eq!(history.len(), 2); assert_eq!( history[0].content, source_session.bytes(&source_session.active_branch().entries()[0]) ); } // The source keeps its own identity, tip, and entry count. { let source_again = open_session(&source_path, cfg.clone()).unwrap(); assert_eq!(source_again.session.id(), source_session.id()); assert_eq!(source_again.session.selected_id(), Some(leaf_id)); assert_eq!(source_again.session.active_branch().entries().len(), 2); let mut count = 0; SessionReader::open(&source_path) .unwrap() .replay_at(leaf_id, cfg.frame_bytes_max, |_| { count += 1; Ok(()) }) .unwrap(); assert_eq!(count, 3); } // A populated target is refused instead of overwritten. let error = source_reader .fork_at(boundary_id, &fork_path, cfg) .unwrap_err(); assert_eq!(error.code(), "SESSION_EXISTS"); let _ = std::fs::remove_dir_all(&dir); } #[test] fn legacy_session_without_prologue_is_rejected_without_repair() { let path = tmp_path("legacy-no-prologue.smh"); let bytes = frame("legacy").encode(1 << 20).unwrap(); std::fs::write(&path, &bytes).unwrap(); assert_eq!( open_session(&path, Config::default_valid()) .unwrap_err() .code(), "SESSION_PROLOGUE" ); assert_eq!(std::fs::read(&path).unwrap(), bytes); let _ = std::fs::remove_file(&path); } #[test] fn session_file_without_header_is_rejected() { let path = tmp_path("headerless.smh"); let cfg = Config::default_valid(); std::fs::write( &path, file_bytes(frame("orphan").encode(cfg.frame_bytes_max).unwrap()), ) .unwrap(); let error = open_session(&path, cfg).unwrap_err(); assert_eq!(error.code(), "SESSION_HEADER_MISSING"); let _ = std::fs::remove_file(&path); } #[test] fn cancellation_stops_replay_at_entry_boundary_and_leaves_no_fork() { let path = tmp_path("cancel-replay.smh"); let fork = tmp_path("cancel-replay-fork.smh"); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&fork); let config = Config::default_valid(); let mut opened = open_session(&path, config.clone()).unwrap(); let root = entry("root"); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); for entry in [&root, &child] { opened .writer .append_frame(&Frame::Known { entry: entry.clone(), }) .unwrap(); } drop(opened); let reader = SessionReader::open(&path).unwrap(); let cancel = CancelHandle::new(); let mut visited = 0; let error = reader .replay_at_cancelled(child.id, config.frame_bytes_max, &cancel, |_| { visited += 1; cancel.cancel(); Ok(()) }) .unwrap_err(); assert_eq!(error.code(), "CANCELLED"); assert_eq!(visited, 1); assert_eq!( reader .fork_at_cancelled(child.id, &fork, config, &cancel) .unwrap_err() .code(), "CANCELLED" ); assert!(!fork.exists()); drop(reader); let _ = std::fs::remove_file(path); } #[test] fn reader_drop_releases_an_inherited_snapshot_lock() { let path = tmp_path("reader-drop-unlocks-inherited-lock.smh"); let _ = std::fs::remove_file(&path); let opened = open_session(&path, Config::default_valid()).unwrap(); drop(opened); let reader = SessionReader::open(&path).unwrap(); // A fork inherits this open-file description. Closing only this process's // descriptor must still release the maintenance lock before the child execs. let inherited_readers = reader.readers.try_clone().unwrap(); drop(reader); let coordination = maintenance_lock(&path).unwrap(); drop(coordination); drop(inherited_readers); let _ = std::fs::remove_file(path); } #[test] fn checkpoint_tail_snapshots_reopen_to_the_last_authoritative_entry() { let path = tmp_path("checkpoint-tail-snapshots.smh"); let _ = std::fs::remove_file(&path); let cfg = Config::default_valid(); let (before_checkpoint, selected, boundary, ancestry) = { let mut opened = open_session(&path, cfg.clone()).unwrap(); let root = *opened.session.append(&message("root")).unwrap(); opened .writer .append_entry(&root, opened.session.bytes(&root)) .unwrap(); let boundary = *opened .session .append(&EntryContent::Meta { kind: COMPACTION_KIND.to_string(), detail: StructuredData::from_serializable( &serde_json::json!({"summary": "", "kept": 1}), ) .unwrap(), }) .unwrap(); opened .writer .append_entry(&boundary, opened.session.bytes(&boundary)) .unwrap(); let mut ancestry = vec![root.id, boundary.id]; for _ in 0..61 { let record = *opened.session.append(&message("turn")).unwrap(); opened .writer .append_entry(&record, opened.session.bytes(&record)) .unwrap(); ancestry.push(record.id); } let record = *opened.session.append(&message("selected")).unwrap(); opened .writer .append_entry(&record, opened.session.bytes(&record)) .unwrap(); ancestry.push(record.id); ( std::fs::read(&path).unwrap(), record.id, boundary.id, ancestry, ) }; let checkpoint = Frame::Checkpoint { id: selected, offset: 0, } .encode(cfg.frame_bytes_max) .unwrap(); let checkpoint_at = before_checkpoint.len() - checkpoint.len(); for end in checkpoint_at..before_checkpoint.len() { std::fs::write(&path, &before_checkpoint[..end]).unwrap(); if end > checkpoint_at { assert_eq!( SessionReader::open(&path) .unwrap() .scan(cfg.frame_bytes_max, |_| Ok(())) .unwrap() .torn_tail_at, Some(u64::try_from(checkpoint_at).unwrap()) ); } let reopened = open_session(&path, cfg.clone()).unwrap(); assert_eq!(reopened.session.selected_id(), Some(selected)); assert_eq!(reopened.session.compaction_boundary(), Some(boundary)); drop(reopened); let mut restored_ancestry = Vec::new(); SessionReader::open(&path) .unwrap() .replay_at(selected, cfg.frame_bytes_max, |entry| { restored_ancestry.push(entry.id); Ok(()) }) .unwrap(); assert_eq!(restored_ancestry, ancestry); assert_eq!( SessionReader::open(&path) .unwrap() .scan(cfg.frame_bytes_max, |_| Ok(())) .unwrap() .torn_tail_at, None ); } let _ = std::fs::remove_file(path); } fn publication_fixture(path: &Path, cfg: &Config) -> (Vec, [EntryId; 3]) { let mut opened = open_session(path, cfg.clone()).unwrap(); let root = *opened.session.append(&message("root")).unwrap(); opened .writer .append_entry(&root, opened.session.bytes(&root)) .unwrap(); let boundary = *opened .session .append(&EntryContent::Meta { kind: COMPACTION_KIND.to_string(), detail: StructuredData::from_serializable( &serde_json::json!({"summary": "", "kept": 1}), ) .unwrap(), }) .unwrap(); opened .writer .append_entry(&boundary, opened.session.bytes(&boundary)) .unwrap(); let leaf = *opened.session.append(&message("leaf")).unwrap(); opened .writer .append_entry(&leaf, opened.session.bytes(&leaf)) .unwrap(); opened .writer .append_frame(&Frame::Unknown { kind: 99, version: 1, payload: b"preserve me".to_vec(), }) .unwrap(); drop(opened); ( std::fs::read(path).unwrap(), [root.id, boundary.id, leaf.id], ) } fn assert_complete_publication(path: &Path, cfg: &Config, ids: [EntryId; 3], unknown: bool) { let reader = SessionReader::open(path).unwrap(); reader.verify_full(cfg.frame_bytes_max).unwrap(); let session = reader.selected_session(cfg.frame_bytes_max).unwrap(); assert_eq!(session.selected_id(), Some(ids[2])); assert_eq!(session.compaction_boundary(), Some(ids[1])); let mut ancestry = Vec::new(); reader .replay_at(ids[2], cfg.frame_bytes_max, |entry| { ancestry.push(entry.id); Ok(()) }) .unwrap(); assert_eq!(ancestry, ids); assert_eq!( scanned_frames(path, cfg.frame_bytes_max) .frames .iter() .any(|frame| matches!(frame, Frame::Unknown { kind: 99, .. })), unknown ); } fn publication_temps(dir: &Path, operation: &str) -> Vec { std::fs::read_dir(dir) .unwrap() .map(|entry| entry.unwrap().path()) .filter(|path| { path.file_name() .and_then(|name| name.to_str()) .is_some_and(|name| name.contains(&format!(".smith-{operation}-"))) }) .collect() } #[test] fn fork_published_checkpoint_reopens_complete_history() { let dir = tmp_path("fork-hard-link-interval"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let source = dir.join("source.smh"); let target = dir.join("fork.smh"); let cfg = Config::default_valid(); let (_, ids) = publication_fixture(&source, &cfg); let observed = std::rc::Rc::new(std::cell::Cell::new(false)); let observed_at_hook = std::rc::Rc::clone(&observed); let target_at_hook = target.clone(); set_publication_hook(move |point| { if point == PublicationCheckpoint::Published { observed_at_hook.set(true); assert!(SessionReader::open(&target_at_hook).is_ok()); } }); SessionReader::open(&source) .unwrap() .fork_at(ids[2], &target, cfg) .unwrap(); clear_publication_hook(); assert!(observed.get()); let _ = std::fs::remove_dir_all(dir); } #[test] fn fork_and_repair_publication_interruptions_never_expose_partial_sessions() { use std::panic::{AssertUnwindSafe, catch_unwind, panic_any}; let cfg = Config::default_valid(); let points = [ PublicationCheckpoint::TempCreated, PublicationCheckpoint::PartialWrite, PublicationCheckpoint::CompletedWrite, PublicationCheckpoint::TempSynced, PublicationCheckpoint::Published, PublicationCheckpoint::ParentSynced, PublicationCheckpoint::Cleanup, ]; for point in points { let dir = tmp_path(&format!("publication-interrupt-{point:?}")); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let source = dir.join("source.smh"); let target = dir.join("fork.smh"); let (before, ids) = publication_fixture(&source, &cfg); let reader = SessionReader::open(&source).unwrap(); set_publication_hook(move |actual| { if actual == point { // Panic simulates abrupt control-flow loss: fork's error cleanup is not run. panic_any(actual); } }); assert!( catch_unwind(AssertUnwindSafe(|| reader.fork_at( ids[2], &target, cfg.clone() ))) .is_err() ); clear_publication_hook(); assert_eq!(std::fs::read(&source).unwrap(), before); assert_complete_publication(&source, &cfg, ids, true); if matches!( point, PublicationCheckpoint::TempCreated | PublicationCheckpoint::PartialWrite | PublicationCheckpoint::CompletedWrite | PublicationCheckpoint::TempSynced ) { assert!(!target.exists()); assert!(SessionReader::open(&target).is_err()); assert_eq!(publication_temps(&dir, "fork").len(), 1); } else { assert_complete_publication(&target, &cfg, ids, false); } drop(reader); let _ = std::fs::remove_dir_all(dir); } for point in points { let dir = tmp_path(&format!("repair-publication-interrupt-{point:?}")); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let source = dir.join("source.smh"); let (before, ids) = publication_fixture(&source, &cfg); set_publication_hook(move |actual| { if actual == point { // Panic simulates abrupt control-flow loss: repair's error cleanup is not run. panic_any(actual); } }); assert!( catch_unwind(AssertUnwindSafe(|| repair_session( &source, cfg.frame_bytes_max ))) .is_err() ); clear_publication_hook(); if matches!( point, PublicationCheckpoint::TempCreated | PublicationCheckpoint::PartialWrite | PublicationCheckpoint::CompletedWrite | PublicationCheckpoint::TempSynced ) { assert_eq!(std::fs::read(&source).unwrap(), before); assert_complete_publication(&source, &cfg, ids, true); assert_eq!(publication_temps(&dir, "repair").len(), 1); } else { assert_complete_publication(&source, &cfg, ids, true); assert!(publication_temps(&dir, "repair").is_empty()); } let _ = std::fs::remove_dir_all(dir); } } #[test] fn fork_publish_collision_preserves_target_and_temporary_source() { let dir = tmp_path("fork-publish-failure"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let source = dir.join("source.smh"); let target = dir.join("fork.smh"); let cfg = Config::default_valid(); let (before, ids) = publication_fixture(&source, &cfg); let occupied = target.clone(); set_publication_hook(move |point| { if point == PublicationCheckpoint::TempSynced { std::fs::write(&occupied, b"occupied during publish").unwrap(); } }); let error = SessionReader::open(&source) .unwrap() .fork_at(ids[2], &target, cfg) .unwrap_err(); clear_publication_hook(); match error { SmithError::Storage { code, operation, path, message, publication, } => { assert_eq!(code, smith::error::StorageCode::CreatePublish); assert_eq!(publication, None); assert_eq!(operation.as_str(), "publish"); assert_eq!(path, target); assert!(!message.is_empty()); } other => panic!("expected publication storage error, got {other:?}"), } assert_eq!(std::fs::read(&source).unwrap(), before); assert_eq!(std::fs::read(&target).unwrap(), b"occupied during publish"); assert_eq!(publication_temps(&dir, "fork").len(), 1); let _ = std::fs::remove_dir_all(dir); } #[test] fn concurrent_publishers_choose_exactly_one_winner_without_overwrite() { use std::sync::{Arc, Barrier}; let dir = tmp_path("concurrent-no-clobber-publication"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let target = dir.join("target.smh"); let barrier = Arc::new(Barrier::new(4)); let mut workers = Vec::new(); for index in 0..4 { let temp = dir.join(format!("temp-{index}.smh")); let bytes = format!("publisher-{index}").into_bytes(); let mut file = File::create(&temp).unwrap(); file.write_all(&bytes).unwrap(); file.sync_all().unwrap(); let target = target.clone(); let barrier = Arc::clone(&barrier); workers.push(std::thread::spawn(move || { barrier.wait(); (bytes, temp.clone(), publish_no_clobber(&temp, &target)) })); } let results: Vec<_> = workers .into_iter() .map(|worker| worker.join().unwrap()) .collect(); let winners: Vec<_> = results .iter() .filter(|(_, _, result)| result.is_ok()) .collect(); assert_eq!(winners.len(), 1); assert_eq!(std::fs::read(&target).unwrap(), winners[0].0); for (_, temp, result) in results { assert_eq!(temp.exists(), result.is_err()); } let _ = std::fs::remove_dir_all(dir); } #[cfg(unix)] #[test] fn no_clobber_publication_never_leaves_an_alias() { use std::os::unix::fs::MetadataExt; let dir = tmp_path("no-clobber-publication-nlink"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let temp = dir.join("temp.smh"); let target = dir.join("target.smh"); std::fs::write(&temp, b"new").unwrap(); publish_no_clobber(&temp, &target).unwrap(); assert!(!temp.exists()); assert_eq!(std::fs::metadata(&target).unwrap().nlink(), 1); std::fs::write(&temp, b"retry").unwrap(); let before = std::fs::read(&target).unwrap(); assert!(publish_no_clobber(&temp, &target).is_err()); assert_eq!(std::fs::read(&target).unwrap(), before); assert_eq!(std::fs::metadata(&temp).unwrap().nlink(), 1); let _ = std::fs::remove_dir_all(dir); } #[test] fn parent_sync_failure_reports_published_durability_uncertain_on_wire() { let dir = tmp_path("fork-parent-sync-failure"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let source = dir.join("source.smh"); let target = dir.join("fork.smh"); let cfg = Config::default_valid(); let (before, ids) = publication_fixture(&source, &cfg); set_parent_sync_failure(true); let error = SessionReader::open(&source) .unwrap() .fork_at(ids[2], &target, cfg.clone()) .unwrap_err(); set_parent_sync_failure(false); assert_eq!(std::fs::read(&source).unwrap(), before); assert_complete_publication(&target, &cfg, ids, false); assert_eq!( serde_json::to_string(&error).unwrap(), format!( r#"{{"Storage":{{"code":"STORE_DIR_SYNC","operation":"sync parent directory after publication","path":"{}","message":"injected parent directory sync failure","publication":"published-durability-uncertain"}}}}"#, dir.display() ) ); match error { SmithError::Storage { publication, .. } => assert_eq!( publication, Some(smith::error::PublicationState::PublishedDurabilityUncertain) ), other => panic!("expected published storage error, got {other:?}"), } let _ = std::fs::remove_dir_all(dir); } #[test] fn temporary_name_exhaustion_reports_final_attempt_without_mutation_or_leak() { let dir = tmp_path("temporary-name-exhaustion"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("session.smh"); let id = SessionId::from_u128(7); for attempt in 0..128 { std::fs::create_dir( path.with_file_name(format!("session.smh.smith-create-{id}-{attempt}.tmp")), ) .unwrap(); } let before = std::fs::read_dir(&dir).unwrap().count(); let error = new_temp_file(&path, "create", id).unwrap_err(); match error { SmithError::Storage { code, operation, path: attempted, message, .. } => { assert_eq!(code, smith::error::StorageCode::CreateTmp); assert_eq!(operation.as_str(), "create temporary file"); assert_eq!( attempted, path.with_file_name(format!("session.smh.smith-create-{id}-127.tmp")) ); assert!(!message.is_empty()); } other => panic!("expected storage error, got {other:?}"), } assert!(!path.exists()); assert_eq!(std::fs::read_dir(&dir).unwrap().count(), before); let _ = std::fs::remove_dir_all(dir); } #[test] fn header_len_matches_protocol() { assert_eq!(crate::frame::HEADER_LEN, 12); assert_eq!(crate::frame::BODY_PREFIX_LEN, 8); } }