use crate::core::disclosure::{gate_for_model, may_persist, DisclosureClass, ModelVisibility};
use crate::core::stream_types::SharedMessage;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::cell::Cell;
use std::collections::HashMap;
use std::io::{self, Write};
#[cfg(unix)]
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError, Weak};
use std::time::{Duration, Instant};
pub const SHELF_FILES: usize = 128;
const LAST_SHELF: u64 = 999_999;
pub const MAX_SEGMENTS: usize = SHELF_FILES;
pub const MAX_SEGMENT_BYTES: usize = 16 * 1024 * 1024;
pub const SEARCH_SCAN_FILES: usize = 1024;
pub const SEARCH_SCAN_BYTES: usize = 256 * 1024 * 1024;
pub const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024;
pub const MAX_MESSAGES: usize = 4096;
pub const MAX_NOTE_BYTES: usize = 8192;
pub const MAX_FETCH_MESSAGES: usize = 128;
pub const MAX_SEARCH_RESULTS: usize = 32;
pub const REDUCED_KEY: &str = "_archive_reduced";
pub const INPUT_JSON_KEY: &str = "_archive_json";
const REDUCED_TEXT_BYTES: usize = 4096;
const REDUCED_BLOCKS: usize = 256;
pub const LOCK_WAIT: Duration = Duration::from_secs(30);
const ABORT_POLL: Duration = Duration::from_millis(20);
pub const PROCESS_LOCK_WAIT: Duration = Duration::from_secs(5);
const PROCESS_LOCK_POLL: Duration = Duration::from_millis(25);
const STALE_TEMP: Duration = Duration::from_secs(60 * 60);
const WITHHELD: &str = "[archive: withheld]";
pub const NOT_FOUND: &str = "archive not available in this scope";
const BUSY: &str = "archive busy; retain source and retry";
pub type ArchiveRedactor = dyn Fn(&str) -> String + Send + Sync;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchiveRef {
pub id: String,
pub message_count: usize,
pub source_message_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArchivePart {
pub archive: ArchiveRef,
pub first_source_index: usize,
pub reduced_messages: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SealedWindow {
pub parts: Vec<ArchivePart>,
pub note: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Forgotten {
pub ids: Vec<String>,
pub unread: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchiveDescriptor {
pub id: String,
pub message_count: usize,
pub source_message_count: usize,
pub snippet: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ArchivedMessage {
pub source_index: usize,
pub block_indices: Vec<usize>,
pub message: SharedMessage,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ArchiveProjection {
pub logical_id: String,
pub source_message_count: usize,
pub messages: Vec<ArchivedMessage>,
pub note: String,
pub digest: String,
}
pub fn project_archive(
logical_id: &str,
messages: &[SharedMessage],
note: &str,
redactor: Option<Arc<ArchiveRedactor>>,
) -> io::Result<ArchiveProjection> {
validate_key(logical_id)?;
project_hashed(
&digest_parts(&[logical_id.as_bytes()]),
messages,
note,
redactor,
Reduce::None,
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Reduce {
None,
LargeInputs,
InputsAsText,
Inputs,
Text,
}
impl Reduce {
fn next(self) -> Option<Self> {
match self {
Reduce::None => Some(Reduce::LargeInputs),
Reduce::LargeInputs => Some(Reduce::InputsAsText),
Reduce::InputsAsText => Some(Reduce::Inputs),
Reduce::Inputs => Some(Reduce::Text),
Reduce::Text => None,
}
}
fn in_place_of(self, input: &Value) -> Option<usize> {
let as_text = || input_text_cost(input).unwrap_or(0);
match self {
Reduce::None => None,
Reduce::LargeInputs => InputBudget::alone(input).is_err().then(as_text),
Reduce::InputsAsText => Some(as_text()),
Reduce::Inputs | Reduce::Text => Some(0),
}
}
}
fn input_text_cost(input: &Value) -> Option<usize> {
let cost = json_len(input).saturating_add(INPUT_JSON_KEY.len() + 16);
(cost <= MAX_INPUT_BYTES).then_some(cost)
}
fn input_eligible(input: &Value) -> bool {
let mut pending = vec![input];
while let Some(value) = pending.pop() {
if eligible_class(value).is_none() {
return false;
}
match value {
Value::Array(values) => pending.extend(values),
Value::Object(values) => pending.extend(values.values()),
_ => {}
}
}
true
}
fn project_hashed(
logical_id: &str,
messages: &[SharedMessage],
note: &str,
redactor: Option<Arc<ArchiveRedactor>>,
reduce: Reduce,
) -> io::Result<ArchiveProjection> {
debug_assert!(reduce == Reduce::None || messages.len() <= 1);
if note.len() > MAX_NOTE_BYTES {
return Err(invalid("archive message/note budget exceeded"));
}
if messages.len() > MAX_MESSAGES {
return Err(over("archive message/note budget exceeded"));
}
if reduce != Reduce::Text {
let mut budget = InputBudget {
bytes: 0,
nodes: 0,
reduce,
};
for message in messages {
budget.message(message, redactor.is_some())?;
}
}
let projector = Projector {
redactor,
reduce,
omitted: Cell::new(0),
as_text: Cell::new(0),
};
let mut eligible = Vec::new();
for (source_index, message) in messages.iter().enumerate() {
if let Some((message, block_indices)) = projector.project(message)? {
eligible.push(ArchivedMessage {
source_index,
block_indices,
message: Arc::new(message),
});
}
}
let digest = projection_digest(logical_id, messages.len(), &eligible)?;
let note = projector.screen(note, DisclosureClass::ModelVisible)?;
if note.len() > MAX_NOTE_BYTES {
return Err(invalid("redacted note exceeds archive budget"));
}
Ok(ArchiveProjection {
logical_id: logical_id.to_owned(),
source_message_count: messages.len(),
messages: eligible,
note,
digest,
})
}
struct Projector {
redactor: Option<Arc<ArchiveRedactor>>,
reduce: Reduce,
omitted: Cell<usize>,
as_text: Cell<usize>,
}
pub struct ArchiveStore {
fs: PrivateDir,
scope: String,
logical: String,
redactor: Option<Arc<ArchiveRedactor>>,
abort: Option<Arc<AtomicBool>>,
wait: Duration,
process_wait: Duration,
directory: PathBuf,
}
struct Gate {
held: Mutex<bool>,
released: Condvar,
#[cfg(test)]
waited: std::sync::atomic::AtomicUsize,
}
struct Locked {
_file: Option<FlockGuard>,
_gate: GateGuard,
}
struct GateGuard(Arc<Gate>);
type DirIdentity = (u64, u64);
type Gates = Mutex<HashMap<DirIdentity, Weak<Gate>>>;
impl Gate {
fn for_directory(identity: DirIdentity) -> Arc<Gate> {
static GATES: OnceLock<Gates> = OnceLock::new();
let mut gates = GATES
.get_or_init(Default::default)
.lock()
.unwrap_or_else(PoisonError::into_inner);
if let Some(gate) = gates.get(&identity).and_then(Weak::upgrade) {
return gate;
}
gates.retain(|_, gate| gate.strong_count() > 0);
let gate = Arc::new(Gate {
held: Mutex::new(false),
released: Condvar::new(),
#[cfg(test)]
waited: Default::default(),
});
gates.insert(identity, Arc::downgrade(&gate));
gate
}
fn enter(
gate: Arc<Gate>,
wait: Duration,
abort: Option<&AtomicBool>,
busy: &'static str,
) -> io::Result<GateGuard> {
let deadline = Instant::now() + wait;
{
let mut held = gate.held.lock().unwrap_or_else(PoisonError::into_inner);
let mut waited = false;
loop {
if abort.is_some_and(|abort| abort.load(Ordering::Acquire)) {
return Err(canceled());
}
let now = Instant::now();
if waited && now >= deadline {
return Err(io::Error::new(io::ErrorKind::WouldBlock, busy));
}
if !*held {
*held = true;
break;
}
if now >= deadline {
return Err(io::Error::new(io::ErrorKind::WouldBlock, busy));
}
let mut slice = deadline - now;
if abort.is_some() {
slice = slice.min(ABORT_POLL);
}
#[cfg(test)]
if !waited {
gate.waited.fetch_add(1, Ordering::AcqRel);
}
waited = true;
held = gate
.released
.wait_timeout(held, slice)
.unwrap_or_else(PoisonError::into_inner)
.0;
}
}
Ok(GateGuard(gate))
}
}
impl Drop for GateGuard {
fn drop(&mut self) {
*self.0.held.lock().unwrap_or_else(PoisonError::into_inner) = false;
self.0.released.notify_all();
}
}
struct FlockGuard(std::fs::File);
impl Drop for FlockGuard {
fn drop(&mut self) {
let _ = fs4::fs_std::FileExt::unlock(&self.0);
}
}
#[derive(Debug)]
struct LockRefused(&'static str);
impl std::fmt::Display for LockRefused {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "archive lock file {}", self.0)
}
}
impl std::error::Error for LockRefused {}
struct Located {
shelf: String,
record: Record,
forgotten: bool,
}
enum Shelved {
Nothing,
This(PrivateDir, Record),
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unscanned {
pub stopped_after: Option<usize>,
pub unread: Option<String>,
}
#[derive(Default)]
struct Unread {
places: usize,
first: Option<(String, io::Error)>,
}
impl Unread {
fn pass(&mut self, place: &str, error: io::Error) {
tracing::warn!(%place, %error, "archive lookup passed over what it could not read");
self.places += 1;
self.first.get_or_insert_with(|| (place.to_owned(), error));
}
fn notice(&self) -> Option<String> {
let (place, error) = self.first.as_ref()?;
let places = self.places;
Some(format!("{places} place(s) could not be read, first {place}: {error}"))
}
fn notice_in(&self, directory: &Path) -> Option<String> {
let notice = self.notice()?;
Some(format!("{notice} (in {})", directory.display()))
}
fn not_found(self) -> io::Error {
let Some((place, error)) = self.first else {
return not_found();
};
let kind = match error.kind() {
io::ErrorKind::NotFound => io::ErrorKind::Other,
kind => kind,
};
let places = self.places;
io::Error::new(
kind,
format!(
"archive not found in what could be read; {places} place(s) that may hold it \
could not be read, first {place}: {error}"
),
)
}
}
#[derive(Debug)]
enum Fault {
Parse(serde_json::Error),
Version(u32),
Scope,
Id,
Logical,
Digest,
Name,
Limit,
Order,
Mismatch { record: String, content: String },
TombstoneCreated,
Size(io::Error),
}
impl Fault {
fn into_read_error(self) -> io::Error {
match self {
Fault::Size(error) => error,
_ => corrupt(),
}
}
}
impl std::fmt::Display for Fault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Fault::Parse(error) => {
let text = error.to_string();
let what = match error.classify() {
serde_json::error::Category::Data if text.starts_with("unknown field") => {
"an unknown field"
}
serde_json::error::Category::Data if text.starts_with("missing field") => {
"a field is missing"
}
serde_json::error::Category::Data if text.starts_with("duplicate field") => {
"a field is there twice"
}
serde_json::error::Category::Data if text.starts_with("unknown variant") => {
"an unknown state"
}
serde_json::error::Category::Data => "a field of the wrong type",
serde_json::error::Category::Eof => "the JSON ends early",
_ => "not JSON",
};
write!(f, "not an archive record: {what} (column {})", error.column())
}
Fault::Version(version) => write!(f, "version {version} is not supported"),
Fault::Scope => f.write_str("scope is not this project's"),
Fault::Id => f.write_str("invalid id"),
Fault::Logical => f.write_str("invalid logical"),
Fault::Digest => f.write_str("invalid digest"),
Fault::Name => f.write_str("file name is not the record's id"),
Fault::Limit => f.write_str("over a per-archive limit"),
Fault::Order => f.write_str("source_index out of order"),
Fault::Mismatch { record, content } => {
write!(f, "digest does not match content (record {record}, content {content})")
}
Fault::TombstoneCreated => f.write_str("a tombstone carries no created_ms"),
Fault::Size(_) => f.write_str("over the 16 MiB archive size"),
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Record {
version: u32,
scope: String,
logical: String,
id: String,
digest: String,
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
created_ms: Option<u64>,
body: Body,
}
fn present<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<u64>, D::Error> {
u64::deserialize(deserializer).map(Some)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Written {
pub(crate) created_ms: Option<u64>,
pub(crate) modified: Option<std::time::SystemTime>,
}
impl Written {
fn now() -> Self {
let since_epoch = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
Written {
created_ms: since_epoch.ok().and_then(|since| u64::try_from(since.as_millis()).ok()),
modified: None,
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
enum Body {
Live {
source_message_count: usize,
messages: Vec<ArchivedMessage>,
note: String,
},
Tombstone,
}
impl Record {
fn reference(&self) -> io::Result<ArchiveRef> {
match &self.body {
Body::Live {
source_message_count,
messages,
..
} => Ok(ArchiveRef {
id: self.id.clone(),
message_count: messages.len(),
source_message_count: *source_message_count,
}),
Body::Tombstone => Err(not_found()),
}
}
}
const CREATED_AHEAD_MS: u64 = 24 * 60 * 60 * 1000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportOutcome {
Stored,
Present { note_differs: bool },
Forgotten,
KeptForgotten,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Imported {
pub outcome: ImportOutcome,
pub id: String,
pub live: bool,
pub message_count: usize,
pub shelf: Option<String>,
pub same_content_as: Option<String>,
pub unread: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportRefused {
pub reason: String,
pub id: Option<String>,
}
#[derive(Default)]
pub struct ImportSeen {
projects: HashMap<String, Planned>,
}
#[derive(Default)]
struct Planned {
ids: HashMap<String, Held>,
forgotten: std::collections::HashSet<String>,
shelf: Option<(u64, usize)>,
}
#[derive(Clone)]
struct Held {
digest: String,
note: Option<String>,
shelf: Option<String>,
}
#[derive(Default)]
struct Probe {
forgotten: bool,
live: Option<String>,
held: bool,
taken: [bool; 2],
}
impl Probe {
fn guard(&self, digest: &str) -> Option<String> {
let free = self.taken.iter().position(|taken| !taken)?;
(!self.held).then(|| digest[free * 32..(free + 1) * 32].to_owned())
}
}
impl Planned {
fn shelve(&mut self, store: Option<&ArchiveStore>, shelves: &[String]) -> String {
let (number, held) = self.shelf.get_or_insert_with(|| {
let newest = shelves.first().and_then(|name| Some((shelf_number(name)?, name)));
let Some((number, name)) = newest else {
return (0, SHELF_FILES);
};
let names = store
.and_then(|store| store.fs.open_child(name, true).ok().flatten())
.and_then(|shelf| shelf.names().ok());
let held = names.map_or(SHELF_FILES, |names| {
names.iter().filter(|(name, ..)| name.ends_with(".json")).count()
});
(number, held)
});
if *held >= SHELF_FILES {
*number += 1;
*held = 0;
}
*held += 1;
shelf_name(*number)
}
}
pub fn project_scope(scope_key: &str) -> io::Result<String> {
validate_key(scope_key)?;
Ok(digest_parts(&[scope_key.as_bytes()]))
}
pub fn record_scope(line: &[u8]) -> Result<String, ImportRefused> {
#[derive(Deserialize)]
struct Scoped {
scope: String,
}
let refused = |reason: String| ImportRefused { reason, id: None };
let scoped: Scoped =
serde_json::from_slice(line).map_err(|error| refused(Fault::Parse(error).to_string()))?;
if !is_scope(&scoped.scope) {
return Err(refused("invalid scope".to_owned()));
}
Ok(scoped.scope)
}
fn is_scope(scope: &str) -> bool {
scope.len() == 64 && lower_hex(scope)
}
fn lower_hex(text: &str) -> bool {
text.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
impl ArchiveStore {
pub fn new(base: &Path, scope_key: &str, logical_id: &str) -> io::Result<Self> {
let (scope, logical) = Self::keys(scope_key, logical_id)?;
let fs = PrivateDir::new(base, &scope)?;
Ok(Self::opened(fs, base, scope, logical))
}
pub fn open_existing(
base: &Path,
scope_key: &str,
logical_id: &str,
) -> io::Result<Option<Self>> {
let (scope, logical) = Self::keys(scope_key, logical_id)?;
match PrivateDir::open_existing(base, &scope) {
Ok(fs) => Ok(Some(Self::opened(fs, base, scope, logical))),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
}
fn keys(scope_key: &str, logical_id: &str) -> io::Result<(String, String)> {
for key in [scope_key, logical_id] {
validate_key(key)?;
}
let scope = digest_parts(&[scope_key.as_bytes()]);
Ok((scope, digest_parts(&[logical_id.as_bytes()])))
}
fn opened(fs: PrivateDir, base: &Path, scope: String, logical: String) -> Self {
Self {
directory: base.join("context-archives").join(&scope),
fs,
scope,
logical,
redactor: None,
abort: None,
wait: LOCK_WAIT,
process_wait: PROCESS_LOCK_WAIT,
}
}
pub fn with_redactor(mut self, redactor: Arc<ArchiveRedactor>) -> Self {
self.redactor = Some(redactor);
self
}
pub fn with_abort(mut self, abort: Arc<AtomicBool>) -> Self {
self.abort = Some(abort);
self
}
fn exclusive(&self) -> io::Result<Locked> {
self.locked(|fs| fs.lock().map(Some))
}
fn lock_error(&self, error: io::Error) -> io::Error {
let refused = error.get_ref().and_then(|inner| inner.downcast_ref::<LockRefused>());
let Some(LockRefused(what)) = refused else {
return error;
};
let file = self.directory.join("lock");
let file = file.display();
io::Error::new(
error.kind(),
format!(
"context archive lock file {file} {what}; nothing is saved or forgotten while \
it is. It holds no data: remove it (rm '{file}') and try again"
),
)
}
fn shared(&self) -> io::Result<Locked> {
self.locked(PrivateDir::lock_existing)
}
fn locked(
&self,
lock: impl Fn(&PrivateDir) -> io::Result<Option<FlockGuard>>,
) -> io::Result<Locked> {
let gate = self.enter(BUSY)?;
let file = crate::core::lock_file::lock_within(
self.process_wait,
PROCESS_LOCK_POLL,
|| match lock(&self.fs) {
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
self.check_abort()?;
Ok(None)
}
result => result.map(Some).map_err(|error| self.lock_error(error)),
},
)?
.ok_or_else(|| io::Error::new(io::ErrorKind::WouldBlock, BUSY))?;
Ok(Locked { _file: file, _gate: gate })
}
fn check_abort(&self) -> io::Result<()> {
match &self.abort {
Some(abort) if abort.load(Ordering::Acquire) => Err(canceled()),
_ => Ok(()),
}
}
fn enter(&self, busy: &'static str) -> io::Result<GateGuard> {
let gate = Gate::for_directory(self.fs.identity()?);
Gate::enter(gate, self.wait, self.abort.as_deref(), busy)
}
pub fn seal(&self, messages: &[SharedMessage], note: &str) -> io::Result<ArchiveRef> {
self.seal_with_note(messages, note)
.map(|(reference, _)| reference)
}
pub fn seal_with_note(
&self,
messages: &[SharedMessage],
note: &str,
) -> io::Result<(ArchiveRef, String)> {
let (reference, note, _) = self.seal_one(messages, note, Reduce::None)?;
Ok((reference, note))
}
pub fn seal_window(&self, messages: &[SharedMessage], note: &str) -> io::Result<SealedWindow> {
let redactor = self.redactor.clone();
let screened = project_hashed(&self.logical, &[], note, redactor, Reduce::None)?.note;
let mut window = SealedWindow {
parts: Vec::new(),
note: String::new(),
};
let mut start = 0;
loop {
self.check_abort()?;
let mut end = self.fill(messages, start);
loop {
let note = if end == messages.len() { note } else { "" };
match self.seal_part(messages, start..end, note, &mut window) {
Ok(()) => break,
Err(error) if over_budget(&error) && end - start > 1 => {
end = start + (1 << (end - start - 1).ilog2());
}
Err(error) => return Err(error),
}
}
start = end;
if start == messages.len() {
break;
}
}
if let Some((last, earlier)) = window.parts.split_last() {
if earlier.iter().any(|part| part.archive.id == last.archive.id) {
window.note = screened;
}
}
Ok(window)
}
fn fill(&self, messages: &[SharedMessage], start: usize) -> usize {
let mut budget = InputBudget {
bytes: 0,
nodes: 0,
reduce: Reduce::None,
};
let limit = messages.len().min(start.saturating_add(MAX_MESSAGES));
let redactor = self.redactor.is_some();
let over = (start..limit).find(|&index| budget.message(&messages[index], redactor).is_err());
let end = over.unwrap_or(limit).max(start + 1).min(messages.len());
before_results(messages, start, end)
}
fn seal_part(
&self,
messages: &[SharedMessage],
range: std::ops::Range<usize>,
note: &str,
window: &mut SealedWindow,
) -> io::Result<()> {
let mut reduce = Reduce::None;
loop {
match self.seal_one(&messages[range.clone()], note, reduce) {
Ok((archive, stored, reduced_messages)) => {
window.parts.push(ArchivePart {
archive,
first_source_index: range.start,
reduced_messages,
});
window.note = stored;
return Ok(());
}
Err(error) if !over_budget(&error) => return Err(error),
Err(error) => match reduce.next().filter(|_| range.len() == 1) {
Some(next) => reduce = next,
None => return Err(error),
},
}
}
}
fn seal_one(
&self,
messages: &[SharedMessage],
note: &str,
reduce: Reduce,
) -> io::Result<(ArchiveRef, String, usize)> {
let redactor = self.redactor.clone();
let projection = project_hashed(&self.logical, messages, note, redactor, reduce)?;
let reduced = projection
.messages
.iter()
.filter(|archived| archived.message.get(REDUCED_KEY).is_some())
.count();
let (reference, note) = self.seal_projection(projection, None)?;
Ok((reference, note, reduced))
}
pub(crate) fn seal_projection(
&self,
projection: ArchiveProjection,
written: Option<Written>,
) -> io::Result<(ArchiveRef, String)> {
self.admit(&projection)?;
let _lock = self.exclusive()?;
self.store(projection, written.unwrap_or_else(Written::now))
}
fn admit(&self, projection: &ArchiveProjection) -> io::Result<()> {
if self.logical.is_empty() {
return Err(invalid("archive store was opened for a project, not a chat: no seal"));
}
let count = projection.source_message_count;
Self::admit_content(
&self.logical,
&projection.logical_id,
count,
&projection.messages,
&projection.digest,
)
}
fn admit_content(
chat: &str,
logical: &str,
count: usize,
messages: &[ArchivedMessage],
digest: &str,
) -> io::Result<()> {
if digest.len() != 64 || !digest.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(invalid("archive projection digest is not 64 hex characters"));
}
if logical != chat {
return Err(invalid("archive projection is of another chat"));
}
if projection_digest(logical, count, messages)? != digest {
return Err(invalid("archive projection digest is not the digest of its content"));
}
Ok(())
}
fn store(
&self,
projection: ArchiveProjection,
written: Written,
) -> io::Result<(ArchiveRef, String)> {
let ArchiveProjection {
digest,
source_message_count,
messages,
note,
..
} = projection;
let mut record = Record {
version: 1,
scope: self.scope.clone(),
logical: self.logical.clone(),
id: String::new(),
digest,
created_ms: written.created_ms,
body: Body::Live {
source_message_count,
messages,
note: note.clone(),
},
};
let mut legacy_read = false;
for _ in 0..3 {
let shelves = self.shelves()?;
let (mut stored, mut free) = (None, None);
for half in [0..32, 32..64] {
let id = &record.digest[half];
match self.shelved(&shelves, id, &record.digest, false)? {
Shelved::This(_, found) if matches!(found.body, Body::Tombstone) => {
return Err(forgotten());
}
Shelved::This(shelf, found) => {
stored.get_or_insert((shelf, found));
}
Shelved::Other => {}
Shelved::Nothing => {
free.get_or_insert_with(|| id.to_owned());
}
}
}
if let Some((shelf, found)) = stored {
return Self::already(&shelf, &found);
}
if !legacy_read {
if let Some(found) = self.legacy_projection(&record.digest, false)? {
return Self::already(&self.fs, &found);
}
legacy_read = true;
}
let Some(id) = free else {
break;
};
record.id = id;
let bytes = bounded_json(&record, MAX_SEGMENT_BYTES)?;
let name = archive_name(&record.id);
self.readable(&name, &bytes)?;
let (shelf, _) = self.writable_shelf(&shelves)?;
match shelf.publish_dated(&name, &bytes, true, written.modified) {
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
result => return result.and_then(|()| Ok((record.reference()?, note))),
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"both archive ids of this projection are taken by other files",
))
}
fn readable(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
match Self::parsed(&self.scope, name, bytes) {
Ok(_) => Ok(()),
Err(fault) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("archive record would not be read back and is not written: {fault}"),
)),
}
}
fn shelved(
&self,
shelves: &[String],
id: &str,
digest: &str,
readonly: bool,
) -> io::Result<Shelved> {
let name = archive_name(id);
let mut taken = false;
for shelf in shelves {
let dir = match self.fs.open_child(shelf, readonly) {
Ok(Some(dir)) => dir,
Ok(None) => continue,
Err(error) if PrivateDir::passing(&error) => return Err(error),
Err(error) => {
tracing::warn!(%shelf, %error, "seal passed over an archive shelf");
continue;
}
};
let raw = match dir.read_with(&name, MAX_SEGMENT_BYTES, readonly) {
Ok(Some(raw)) => raw,
Ok(None) => continue,
Err(error) if PrivateDir::not_a_segment(&error) => {
tracing::warn!(%shelf, file = %name, %error, "seal found its archive id taken");
taken = true;
continue;
}
Err(error) => return Err(error),
};
match self.record(&name, &raw) {
Ok(record) if record.digest == digest => return Ok(Shelved::This(dir, record)),
Err(_) if says_forgotten(&raw, digest) => return Err(forgotten()),
_ => {
tracing::warn!(%shelf, file = %name, "seal found its archive id taken");
taken = true;
}
}
}
Ok(if taken { Shelved::Other } else { Shelved::Nothing })
}
fn already(dir: &PrivateDir, record: &Record) -> io::Result<(ArchiveRef, String)> {
let Body::Live { note, .. } = &record.body else {
return Err(forgotten());
};
dir.sync()?;
Ok((record.reference()?, note.clone()))
}
pub fn fetch(&self, id: &str, start: usize, limit: usize) -> io::Result<Vec<SharedMessage>> {
Ok(self
.fetch_with_provenance(id, start, limit)?
.into_iter()
.map(|m| m.message)
.collect())
}
pub fn fetch_with_provenance(
&self,
id: &str,
start: usize,
limit: usize,
) -> io::Result<Vec<ArchivedMessage>> {
validate_id(id)?;
if limit > MAX_FETCH_MESSAGES {
return Err(invalid("archive fetch limit exceeds 128"));
}
match self.find(id)?.body {
Body::Live { messages, .. } => {
Ok(messages.into_iter().skip(start).take(limit).collect())
}
Body::Tombstone => Err(not_found()),
}
}
pub fn fetch_note(&self, id: &str) -> io::Result<String> {
validate_id(id)?;
match self.find(id)?.body {
Body::Live { note, .. } => Ok(note),
Body::Tombstone => Err(not_found()),
}
}
pub fn search(&self, query: &str, limit: usize) -> io::Result<Vec<ArchiveDescriptor>> {
self.search_scan(query, limit).map(|(rows, _)| rows)
}
pub fn search_scan(
&self,
query: &str,
limit: usize,
) -> io::Result<(Vec<ArchiveDescriptor>, Option<Unscanned>)> {
if query.len() > 512 || limit > MAX_SEARCH_RESULTS {
return Err(invalid("archive search query/result budget exceeded"));
}
if limit == 0 {
return Ok((Vec::new(), None));
}
let query = query.to_lowercase();
self.check_abort()?;
let (shelves, mut slots) = self.layout()?;
let mut rows = Vec::new();
let (mut files, mut bytes) = (0, 0);
let mut legacy_lock = None;
let mut unread = Unread::default();
let done = |rows: Vec<ArchiveDescriptor>, stopped_after: Option<usize>, unread: &Unread| {
let unread = unread.notice();
let unscanned = (stopped_after.is_some() || unread.is_some()).then_some(Unscanned {
stopped_after,
unread,
});
Ok::<_, io::Error>((rows, unscanned))
};
for shelf in shelves.iter().map(Some).chain([None]) {
let opened;
let (dir, names) = match shelf {
Some(shelf) => {
let listed = self.fs.open_child(shelf, true).and_then(|dir| match dir {
Some(dir) => Ok(Some((dir.names()?, dir))),
None => Ok(None),
});
let (mut entries, dir) = match listed {
Ok(Some(listed)) => listed,
Ok(None) => continue,
Err(error) => {
unread.pass(shelf, error);
continue;
}
};
entries.retain(|(name, ..)| archive_id(name).is_some());
entries.sort_unstable_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
opened = dir;
let names: Vec<_> = entries.into_iter().map(|(name, ..)| name).collect();
(&opened, names)
}
None => (&self.fs, std::mem::take(&mut slots)),
};
for name in names {
if rows.len() == limit {
return done(rows, None, &unread);
}
if files == SEARCH_SCAN_FILES || bytes >= SEARCH_SCAN_BYTES {
return done(rows, Some(files), &unread);
}
if shelf.is_none() && legacy_lock.is_none() {
legacy_lock = Some(self.shared()?);
}
let place = || match shelf {
Some(shelf) => format!("{shelf}/{name}"),
None => name.clone(),
};
let raw = match dir.read_with(&name, MAX_SEGMENT_BYTES, true) {
Ok(Some(raw)) => raw,
Ok(None) => continue,
Err(error) => {
unread.pass(&place(), error);
files += 1;
continue;
}
};
files += 1;
bytes += raw.len();
match self.record(&name, &raw) {
Ok(record) => rows.extend(describe(record, &query)?),
Err(error) => unread.pass(&place(), error),
}
}
}
done(rows, None, &unread)
}
pub fn forget(&self, id: &str) -> io::Result<Forgotten> {
validate_id(id)?;
let _lock = self.exclusive()?;
self.forget_locked(id)
}
fn write_guard(
&self,
shelves: &[String],
logical: &str,
digest: &str,
taken: [bool; 2],
) -> io::Result<()> {
for (index, id) in [&digest[..32], &digest[32..]].into_iter().enumerate() {
if taken[index] {
continue;
}
let guard = Record {
version: 1,
scope: self.scope.clone(),
logical: logical.to_owned(),
id: id.to_owned(),
digest: digest.to_owned(),
created_ms: None,
body: Body::Tombstone,
};
let bytes = bounded_json(&guard, MAX_SEGMENT_BYTES)?;
let name = archive_name(id);
self.readable(&name, &bytes)?;
let (shelf, _) = self.writable_shelf(shelves)?;
match shelf.publish(&name, &bytes, true) {
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
result => return result,
}
}
Ok(())
}
fn forget_locked(&self, id: &str) -> io::Result<Forgotten> {
let (shelves, slots) = self.layout()?;
let mut unread = Unread::default();
let asked = match self.locate(&shelves, id, false, &mut unread)? {
Some(found) => found.record,
None => match self.legacy_segment(&slots, id, false, &mut unread) {
Some((_, record)) => record,
None => return Err(unread.not_found()),
},
};
let digest = asked.digest.as_str();
let derived = [&digest[..32], &digest[32..]];
let mut names: Vec<_> = derived.iter().map(|id| archive_name(id)).collect();
if !derived.contains(&id) {
names.push(archive_name(id));
}
let mut unread = Unread::default();
let mut copies = Vec::new();
let (mut guarded, mut taken) = (false, [false; 2]);
for shelf in &shelves {
let dir = match self.fs.open_child(shelf, false) {
Ok(Some(dir)) => dir,
Ok(None) => continue,
Err(error) => {
unread.pass(shelf, error);
continue;
}
};
for (index, name) in names.iter().enumerate() {
let raw = match dir.read_with(name, MAX_SEGMENT_BYTES, false) {
Ok(Some(raw)) => Some(raw),
Ok(None) => continue,
Err(error) => {
unread.pass(&format!("{shelf}/{name}"), error);
None
}
};
if let Some(held) = taken.get_mut(index) {
*held = true;
}
let Some(raw) = raw else {
continue;
};
match self.record(name, &raw) {
Ok(record) if record.digest == digest => {
copies.push((Some(shelf), name.clone(), record));
}
Ok(_) => {}
Err(_) if says_forgotten(&raw, digest) => guarded |= index < 2,
Err(error) => unread.pass(&format!("{shelf}/{name}"), error),
}
}
for (name, record) in self.twins(&dir, shelf, digest, &names, false, &mut unread) {
copies.push((Some(shelf), name, record));
}
}
for slot in &slots {
let raw = self.fs.read_with(slot, MAX_SEGMENT_BYTES, false);
match raw.and_then(|raw| raw.map(|raw| self.record(slot, &raw)).transpose()) {
Ok(Some(record)) if record.digest == digest => {
copies.push((None, slot.clone(), record));
}
Ok(_) => {}
Err(error) => unread.pass(slot, error),
}
}
let seen_by_a_seal = |(shelf, name, _): &(Option<&String>, String, Record)| {
shelf.is_some() && archive_id(name).is_some_and(|id| derived.contains(&id))
};
copies.sort_by_key(|copy| !seen_by_a_seal(copy));
guarded |= copies.first().is_some_and(seen_by_a_seal);
if !guarded {
self.write_guard(&shelves, &asked.logical, digest, taken)?;
}
let mut ids = Vec::new();
for (shelf, name, mut record) in copies {
let opened;
let dir = match shelf {
Some(shelf) => {
opened = self.fs.open_child(shelf, false)?.ok_or_else(not_found)?;
&opened
}
None => &self.fs,
};
if matches!(record.body, Body::Tombstone) {
dir.sync()?;
continue;
}
record.body = Body::Tombstone;
record.created_ms = None;
dir.publish(&name, &bounded_json(&record, MAX_SEGMENT_BYTES)?, false)?;
ids.push(record.id);
}
ids.sort_by_key(|forgotten| forgotten != id);
Ok(Forgotten {
ids,
unread: unread.notice_in(&self.directory),
})
}
fn twins(
&self,
dir: &PrivateDir,
shelf: &str,
digest: &str,
names: &[String],
readonly: bool,
unread: &mut Unread,
) -> Vec<(String, Record)> {
let listed = match dir.names() {
Ok(listed) => listed,
Err(error) => {
unread.pass(shelf, error);
return Vec::new();
}
};
let mut twins = Vec::new();
for (name, is_dir, _) in listed {
if is_dir || archive_id(&name).is_none() || names.contains(&name) {
continue;
}
let raw = match dir.read_with(&name, MAX_SEGMENT_BYTES, readonly) {
Ok(Some(raw)) => raw,
Ok(None) => continue,
Err(error) if PrivateDir::not_a_segment(&error) => continue,
Err(error) => {
unread.pass(&format!("{shelf}/{name}"), error);
continue;
}
};
if !names_digest(&raw, digest) {
continue;
}
match self.record(&name, &raw) {
Ok(record) => twins.push((name, record)),
Err(_) if says_forgotten(&raw, digest) => {}
Err(error) => unread.pass(&format!("{shelf}/{name}"), error),
}
}
twins
}
fn live_twin(
&self,
shelves: &[String],
digest: &str,
names: &[String],
readonly: bool,
) -> io::Result<Option<String>> {
let mut unread = Unread::default();
let mut live = None;
for shelf in shelves {
let dir = match self.fs.open_child(shelf, readonly) {
Ok(Some(dir)) => dir,
Ok(None) => continue,
Err(error) => {
unread.pass(shelf, error);
continue;
}
};
for (_, record) in self.twins(&dir, shelf, digest, names, readonly, &mut unread) {
if matches!(record.body, Body::Live { .. }) {
live.get_or_insert(record.id);
}
}
}
match (live, unread.notice()) {
(Some(id), _) => Ok(Some(id)),
(None, None) => Ok(None),
(None, Some(notice)) => Err(io::Error::other(format!(
"a tombstone is not imported while a file that may hold its content live cannot be read: {notice}"
))),
}
}
pub fn for_scope(base: &Path, scope: &str) -> io::Result<Self> {
if !is_scope(scope) {
return Err(invalid("archive scope must be 64 lowercase hex characters"));
}
let fs = PrivateDir::new(base, scope)?;
Ok(Self::opened(fs, base, scope.to_owned(), String::new()))
}
pub fn existing_scope(base: &Path, scope: &str) -> io::Result<Option<Self>> {
if !is_scope(scope) {
return Err(invalid("archive scope must be 64 lowercase hex characters"));
}
match PrivateDir::open_existing(base, scope) {
Ok(fs) => Ok(Some(Self::opened(fs, base, scope.to_owned(), String::new()))),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
}
pub fn scope(&self) -> &str {
&self.scope
}
pub fn import(
&self,
line: &[u8],
rehome: bool,
dry_run: bool,
seen: &mut ImportSeen,
) -> io::Result<Result<Imported, ImportRefused>> {
Self::import_in(Some(self), &self.scope, line, rehome, dry_run, seen)
}
pub fn import_dry_absent(
scope: &str,
line: &[u8],
rehome: bool,
seen: &mut ImportSeen,
) -> io::Result<Result<Imported, ImportRefused>> {
if !is_scope(scope) {
return Err(invalid("archive scope must be 64 lowercase hex characters"));
}
Self::import_in(None, scope, line, rehome, true, seen)
}
fn import_in(
store: Option<&Self>,
scope: &str,
line: &[u8],
rehome: bool,
dry_run: bool,
seen: &mut ImportSeen,
) -> io::Result<Result<Imported, ImportRefused>> {
let refuse = |reason: String, id: Option<&str>| {
Ok(Err(ImportRefused {
reason,
id: id.map(str::to_owned),
}))
};
let mut record: Record = match serde_json::from_slice(line) {
Ok(record) => record,
Err(error) => return refuse(Fault::Parse(error).to_string(), None),
};
let id = validate_id(&record.id).is_ok().then(|| record.id.clone());
let id = id.as_deref();
if !is_scope(&record.scope) {
return refuse("invalid scope".to_owned(), id);
}
if rehome {
scope.clone_into(&mut record.scope);
}
let Ok(bytes) = bounded_json(&record, MAX_SEGMENT_BYTES) else {
return refuse("over the 16 MiB archive size".to_owned(), id);
};
let name = archive_name(&record.id);
let record = match Self::parsed(scope, &name, &bytes) {
Ok(record) => record,
Err(fault) => return refuse(fault.to_string(), id),
};
if !lower_hex(&record.digest) {
return refuse(Fault::Digest.to_string(), id);
}
let mut modified = None;
if let Body::Live {
source_message_count,
messages,
..
} = &record.body
{
if !messages.iter().all(|archived| archived.message.is_object()) {
return refuse("message is not an object".to_owned(), id);
}
let (logical, digest) = (&record.logical, &record.digest);
let admitted =
Self::admit_content(logical, logical, *source_message_count, messages, digest);
if let Err(error) = admitted {
return refuse(error.to_string(), id);
}
if let Some(created_ms) = record.created_ms {
let now = Written::now().created_ms.unwrap_or(0);
if created_ms > now.saturating_add(CREATED_AHEAD_MS) {
let reason = "created_ms is more than a day after this machine's clock";
return refuse(reason.to_owned(), id);
}
let time = std::time::UNIX_EPOCH.checked_add(Duration::from_millis(created_ms));
let Some(time) = time else {
return refuse("created_ms is not a time a file can have".to_owned(), id);
};
modified = Some(time);
}
}
let incoming = match &record.body {
Body::Live { note, .. } => Some(note.as_str()),
Body::Tombstone => None,
};
let message_count = match &record.body {
Body::Live { messages, .. } => messages.len(),
Body::Tombstone => 0,
};
let digest = record.digest.as_str();
let mut plan = match dry_run {
true => Some(seen.projects.entry(scope.to_owned()).or_default()),
false => None,
};
let mut _lock = None;
let (shelves, slots) = match (store, dry_run) {
(Some(store), false) => {
_lock = Some(store.exclusive()?);
store.layout()?
}
(Some(store), true) => {
store.check_abort()?;
let layout = store.layout()?;
if !layout.1.is_empty() {
_lock = Some(store.shared()?);
}
layout
}
(None, true) => Default::default(),
(None, false) => return Err(invalid("an import that writes needs a store")),
};
let mut found = plan.as_ref().and_then(|plan| plan.ids.get(&record.id).cloned());
if let (None, Some(store)) = (&found, store) {
let mut unread = Unread::default();
let located = match store.locate(&shelves, &record.id, dry_run, &mut unread)? {
Some(located) if located.forgotten => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"a damaged file under this archive id says its content was forgotten, \
over a valid record on an older shelf",
));
}
Some(located) => Some((Some(located.shelf), located.record)),
None => store
.legacy_segment(&slots, &record.id, dry_run, &mut unread)
.map(|(_, record)| (None, record)),
};
if located.is_none() && unread.places > 0 {
return Err(unread.not_found());
}
found = located.map(|(shelf, held)| Held {
digest: held.digest,
note: match held.body {
Body::Live { note, .. } => Some(note),
Body::Tombstone => None,
},
shelf,
});
}
if let (Some(held), Some(plan)) = (&mut found, &plan) {
if plan.forgotten.contains(&held.digest) {
held.note = None;
}
}
let imported = |outcome, shelf, same_content_as, unread| {
Ok(Ok(Imported {
outcome,
id: record.id.clone(),
live: incoming.is_some(),
message_count,
shelf,
same_content_as,
unread,
}))
};
if let Some(held) = found {
if held.digest != digest {
return refuse("id already holds different content".to_owned(), id);
}
let mut unread = None;
let outcome = match (held.note.as_deref(), incoming) {
(Some(stored), Some(note)) => ImportOutcome::Present {
note_differs: stored != note,
},
(None, None) => ImportOutcome::Present { note_differs: false },
(None, Some(_)) => ImportOutcome::KeptForgotten,
(Some(_), None) => {
match (&mut plan, store) {
(Some(plan), _) => {
let probe =
Self::probe(store, Some(&**plan), &shelves, digest, true)?;
if let Some(free) = probe.guard(digest) {
let shelf = Some(plan.shelve(store, &shelves));
let guard = Held {
digest: digest.to_owned(),
note: None,
shelf,
};
plan.ids.insert(free, guard);
}
plan.forgotten.insert(digest.to_owned());
for planned in plan.ids.values_mut() {
if planned.digest == digest {
planned.note = None;
}
}
}
(None, Some(store)) => unread = store.forget_locked(&record.id)?.unread,
(None, None) => {}
}
ImportOutcome::Forgotten
}
};
if let (false, Some(store)) = (dry_run, store) {
if outcome != ImportOutcome::Forgotten {
match &held.shelf {
Some(shelf) => {
store.fs.open_child(shelf, false)?.ok_or_else(not_found)?.sync()?;
}
None => store.fs.sync()?,
}
}
}
return imported(outcome, held.shelf, None, unread);
}
let mut probe = Self::probe(store, plan.as_deref(), &shelves, digest, dry_run)?;
let mut same_content_as = None;
if incoming.is_some() {
if probe.forgotten {
return refuse("this content was forgotten in this store".to_owned(), id);
}
same_content_as = probe.live.take();
} else if let Some(other) =
Self::live_copy(store, plan.as_deref(), &probe, &shelves, &record, dry_run)?
{
let reason = format!(
"a tombstone of content that is live in this store as {other}: forget that \
archive here, then import again"
);
return refuse(reason, id);
}
let derived = record.id == digest[..32] || record.id == digest[32..];
let guard = match incoming.is_none() && !derived {
true => probe.guard(digest),
false => None,
};
let shelf = match (plan, store) {
(Some(plan), _) => {
if let Some(guard) = guard {
let held = Held {
digest: digest.to_owned(),
note: None,
shelf: Some(plan.shelve(store, &shelves)),
};
plan.ids.insert(guard, held);
}
let shelf = plan.shelve(store, &shelves);
let held = Held {
digest: digest.to_owned(),
note: incoming.map(str::to_owned),
shelf: Some(shelf.clone()),
};
plan.ids.insert(record.id.clone(), held);
shelf
}
(None, Some(store)) => {
let mut shelves = shelves;
if guard.is_some() {
store.write_guard(&shelves, &record.logical, digest, probe.taken)?;
shelves = store.shelves()?;
}
let (dir, shelf) = store.writable_shelf(&shelves)?;
dir.publish_dated(&name, &bytes, true, modified)?;
shelf
}
(None, None) => return Err(invalid("an import that writes needs a store")),
};
imported(ImportOutcome::Stored, Some(shelf), same_content_as, None)
}
fn live_copy(
store: Option<&Self>,
plan: Option<&Planned>,
probe: &Probe,
shelves: &[String],
record: &Record,
readonly: bool,
) -> io::Result<Option<String>> {
let digest = record.digest.as_str();
if probe.live.is_some() {
return Ok(probe.live.clone());
}
if let Some(plan) = plan {
if plan.forgotten.contains(digest) {
return Ok(None);
}
let live = |(_, held): &(&String, &Held)| held.digest == digest && held.note.is_some();
if let Some((id, _)) = plan.ids.iter().filter(live).min_by_key(|(id, _)| *id) {
return Ok(Some(id.clone()));
}
}
let Some(store) = store else {
return Ok(None);
};
let names = [&digest[..32], &digest[32..], &record.id].map(archive_name);
store.live_twin(shelves, digest, &names, readonly)
}
fn probe(
store: Option<&Self>,
plan: Option<&Planned>,
shelves: &[String],
digest: &str,
readonly: bool,
) -> io::Result<Probe> {
let mut probe = Probe::default();
for (index, half) in [0..32, 32..64].into_iter().enumerate() {
let id = &digest[half];
probe.taken[index] = true;
let shelved = match store {
Some(store) => store.shelved(shelves, id, digest, readonly),
None => Ok(Shelved::Nothing),
};
match shelved {
Ok(Shelved::This(_, record)) => {
probe.held = true;
match record.body {
Body::Tombstone => probe.forgotten = true,
Body::Live { .. } => {
probe.live.get_or_insert(record.id);
}
}
}
Ok(Shelved::Other) => {}
Ok(Shelved::Nothing) => match plan.and_then(|plan| plan.ids.get(id)) {
Some(planned) if planned.digest == digest => {
probe.held = true;
match planned.note {
None => probe.forgotten = true,
Some(_) => {
probe.live.get_or_insert_with(|| id.to_owned());
}
}
}
Some(_) => {}
None => probe.taken[index] = false,
},
Err(error) if is_forgotten(&error) => {
probe.held = true;
probe.forgotten = true;
}
Err(error) => return Err(error),
}
}
if let Some(store) = store {
match store.legacy_projection(digest, readonly) {
Ok(Some(record)) => match record.body {
Body::Tombstone => probe.forgotten = true,
Body::Live { .. } => {
probe.live.get_or_insert(record.id);
}
},
Ok(None) => {}
Err(error) if is_forgotten(&error) => probe.forgotten = true,
Err(error) => return Err(error),
}
}
if plan.is_some_and(|plan| plan.forgotten.contains(digest)) {
probe.forgotten = true;
probe.live = None;
}
Ok(probe)
}
fn shelves(&self) -> io::Result<Vec<String>> {
Ok(self.layout()?.0)
}
fn layout(&self) -> io::Result<(Vec<String>, Vec<String>)> {
let (mut shelves, mut slots) = (Vec::new(), Vec::new());
for (name, ..) in self.fs.names()? {
if let Some(number) = shelf_number(&name) {
shelves.push((number, name));
} else if slot_number(&name).is_some() {
slots.push(name);
}
}
shelves.sort_unstable();
slots.sort_unstable();
let shelves = shelves.into_iter().rev().map(|(_, name)| name).collect();
slots.reverse();
Ok((shelves, slots))
}
fn writable_shelf(&self, shelves: &[String]) -> io::Result<(PrivateDir, String)> {
let highest = shelves.first().and_then(|name| shelf_number(name));
let highest = highest.unwrap_or(0);
let last = highest >= LAST_SHELF;
for (index, name) in shelves.iter().enumerate() {
let shelf = self.fs.child(name, true);
match shelf.and_then(|shelf| Ok((shelf.names()?, shelf))) {
Ok((names, shelf)) => {
sweep(&shelf, &names);
let held = names.iter().filter(|(name, ..)| name.ends_with(".json"));
if last || (index == 0 && held.count() < SHELF_FILES) {
return Ok((shelf, name.clone()));
}
break;
}
Err(error) if PrivateDir::passing(&error) => return Err(error),
Err(error) => {
tracing::warn!(shelf = %name, %error, "seal passed over an archive shelf");
if !last {
break;
}
}
}
}
if last {
return Err(invalid("archive shelf numbers exhausted"));
}
let name = shelf_name(highest + 1);
Ok((self.fs.child(&name, true)?, name))
}
fn locate(
&self,
shelves: &[String],
id: &str,
readonly: bool,
unread: &mut Unread,
) -> io::Result<Option<Located>> {
let name = archive_name(id);
let (mut first, mut passed) = (None, Vec::<Vec<u8>>::new());
for shelf in shelves {
let dir = match self.fs.open_child(shelf, readonly) {
Ok(Some(dir)) => dir,
Ok(None) => continue,
Err(error) => {
unread.pass(shelf, error);
continue;
}
};
let error = match dir.read_with(&name, MAX_SEGMENT_BYTES, readonly) {
Ok(None) => continue,
Ok(Some(raw)) => match self.record(&name, &raw) {
Ok(record) => {
let forgotten =
passed.iter().any(|raw| says_forgotten(raw, &record.digest));
return Ok(Some(Located {
shelf: shelf.clone(),
record,
forgotten,
}));
}
Err(error) => {
passed.push(raw);
error
}
},
Err(error) if PrivateDir::not_a_segment(&error) => error,
Err(error) => return Err(error),
};
tracing::warn!(%shelf, file = %name, %error, "archive lookup passed over a file under its id");
first.get_or_insert_with(|| {
io::Error::new(
error.kind(),
format!("the newest file under this id is not its record: {shelf}/{name}: {error}"),
)
});
}
first.map_or(Ok(None), Err)
}
fn find(&self, id: &str) -> io::Result<Record> {
self.check_abort()?;
let (shelves, slots) = self.layout()?;
let mut unread = Unread::default();
if let Some(found) = self.locate(&shelves, id, true, &mut unread)? {
return if found.forgotten { Err(not_found()) } else { Ok(found.record) };
}
if !slots.is_empty() {
let _lock = self.shared()?;
if let Some((_, record)) = self.legacy_segment(&slots, id, true, &mut unread) {
return Ok(record);
}
}
Err(unread.not_found())
}
fn legacy_segment(
&self,
slots: &[String],
id: &str,
readonly: bool,
unread: &mut Unread,
) -> Option<(String, Record)> {
for name in slots {
let raw = self.fs.read_with(name, MAX_SEGMENT_BYTES, readonly);
match raw.and_then(|raw| raw.map(|raw| self.record(name, &raw)).transpose()) {
Ok(Some(record)) if record.id == id => return Some((name.clone(), record)),
Ok(_) => {}
Err(error) => unread.pass(name, error),
}
}
None
}
fn record(&self, name: &str, raw: &[u8]) -> io::Result<Record> {
Self::parsed(&self.scope, name, raw).map_err(Fault::into_read_error)
}
fn parsed(scope: &str, name: &str, raw: &[u8]) -> Result<Record, Fault> {
let record: Record = serde_json::from_slice(raw).map_err(Fault::Parse)?;
Self::check(scope, &record, name)?;
Ok(record)
}
fn check(scope: &str, record: &Record, name: &str) -> Result<(), Fault> {
let hex = |text: &str| text.len() == 64 && text.bytes().all(|b| b.is_ascii_hexdigit());
if record.version != 1 {
return Err(Fault::Version(record.version));
}
if record.scope != scope {
return Err(Fault::Scope);
}
if validate_id(&record.id).is_err() {
return Err(Fault::Id);
}
if !hex(&record.logical) {
return Err(Fault::Logical);
}
if !hex(&record.digest) {
return Err(Fault::Digest);
}
if name != archive_name(&record.id) && slot_number(name).is_none() {
return Err(Fault::Name);
}
let Body::Live {
messages,
source_message_count,
note,
} = &record.body
else {
return match record.created_ms {
Some(_) => Err(Fault::TombstoneCreated),
None => Ok(()),
};
};
if messages.len() > MAX_MESSAGES
|| *source_message_count > MAX_MESSAGES
|| note.len() > MAX_NOTE_BYTES
{
return Err(Fault::Limit);
}
if messages.iter().any(|m| m.source_index >= *source_message_count)
|| messages.windows(2).any(|m| m[0].source_index >= m[1].source_index)
{
return Err(Fault::Order);
}
let content = projection_digest(&record.logical, *source_message_count, messages)
.map_err(Fault::Size)?;
if content != record.digest {
return Err(Fault::Mismatch {
record: record.digest.clone(),
content,
});
}
Ok(())
}
fn legacy_projection(&self, digest: &str, readonly: bool) -> io::Result<Option<Record>> {
for slot in 0..MAX_SEGMENTS {
let name = slot_name(slot);
let record = match self.fs.read_with(&name, MAX_SEGMENT_BYTES, readonly) {
Ok(Some(raw)) => match self.record(&name, &raw) {
Err(_) if says_forgotten(&raw, digest) => return Err(forgotten()),
record => record,
},
Ok(None) => continue,
Err(error) => Err(error),
};
match record {
Ok(record) if record.digest == digest => return Ok(Some(record)),
Ok(_) => {}
Err(error) if PrivateDir::passing(&error) => return Err(error),
Err(error) => {
tracing::warn!(slot = %name, %error, "seal passed over a legacy archive slot");
}
}
}
Ok(None)
}
}
fn describe(record: Record, query: &str) -> io::Result<Option<ArchiveDescriptor>> {
let Body::Live {
messages,
source_message_count,
..
} = &record.body
else {
return Ok(None);
};
let mut snippet = None;
for message in messages {
let text = serde_json::to_string(&message.message).map_err(json_error)?;
if query.is_empty() || text.to_lowercase().contains(query) {
snippet = Some(prefix(&text, 256).to_owned());
break;
}
}
if snippet.is_none() && !(query.is_empty() && messages.is_empty()) {
return Ok(None);
}
Ok(Some(ArchiveDescriptor {
message_count: messages.len(),
source_message_count: *source_message_count,
snippet: snippet.unwrap_or_default(),
id: record.id,
}))
}
impl Projector {
fn screen(&self, text: &str, class: DisclosureClass) -> io::Result<String> {
if class == DisclosureClass::ModelVisibleAfterRedaction && self.redactor.is_none() {
return Ok(WITHHELD.to_owned());
}
let redactor = |text: &str| {
let text = self
.redactor
.as_ref()
.map_or_else(|| text.to_owned(), |r| r(text));
if credential_like(&text) {
WITHHELD.to_owned()
} else {
text
}
};
let text = redactor(text);
if text.len() > MAX_SEGMENT_BYTES && self.reduce != Reduce::Text {
return Err(over("redactor output exceeds archive budget"));
}
match gate_for_model(class, &text, false, Some(&|s| s.to_owned())) {
ModelVisibility::Visible(text) => Ok(text),
ModelVisibility::Withheld(_) => Ok(WITHHELD.to_owned()),
}
}
fn text(&self, text: &str, class: DisclosureClass) -> io::Result<String> {
let text = self.screen(text, class)?;
if self.reduce != Reduce::Text || text.len() <= REDUCED_TEXT_BYTES {
return Ok(text);
}
let kept = prefix(&text, REDUCED_TEXT_BYTES);
let omitted = self.omission(format_args!("{} bytes", text.len() - kept.len()));
Ok(format!("{kept}\n{omitted}"))
}
fn omission(&self, what: std::fmt::Arguments<'_>) -> String {
self.omitted.set(self.omitted.get() + 1);
format!("[archive: {what} omitted, over the per-archive limit]")
}
fn stored(&self, role: &str, content: Value) -> Value {
let mut message = json!({"role": role, "content": content});
let (omitted, as_text) = (self.omitted.replace(0), self.as_text.replace(0));
let changed = match (omitted, as_text) {
(0, 0) => return message,
(omitted, 0) => format!("{omitted} omission(s)"),
(0, as_text) => format!("{as_text} tool input(s) as JSON text"),
(omitted, as_text) => {
format!("{omitted} omission(s) and {as_text} tool input(s) as JSON text")
}
};
message[REDUCED_KEY] = json!(format!(
"this message alone is over the per-archive limit; it is stored with {changed}, \
each marked in place"
));
message
}
fn project(&self, message: &Value) -> io::Result<Option<(Value, Vec<usize>)>> {
let Some((class, role, content)) = projection_content(message) else {
return Ok(None);
};
self.omitted.set(0);
self.as_text.set(0);
if let Some(text) = content.as_str() {
let content = json!(self.text(text, class)?);
return Ok(Some((self.stored(role, content), Vec::new())));
}
let Some(blocks) = content.as_array() else {
return Ok(None);
};
let mut output = Vec::new();
let mut indices = Vec::new();
for (index, block) in blocks.iter().enumerate() {
if self.reduce == Reduce::Text && output.len() == REDUCED_BLOCKS {
let rest = self.omission(format_args!("{} blocks", blocks.len() - index));
output.push(json!({"type":"text", "text":rest}));
indices.push(index);
break;
}
let Some(mut block_class) = eligible_class(block) else {
continue;
};
if class == DisclosureClass::ModelVisibleAfterRedaction {
block_class = class;
}
let kept = match block.get("type").and_then(Value::as_str) {
Some("text") => block
.get("text")
.and_then(Value::as_str)
.map(|text| {
self.text(text, block_class)
.map(|text| json!({"type":"text", "text":text}))
})
.transpose()?,
Some("tool_use") if role == "assistant" => {
if let (Some(id), Some(name)) =
(identifier(block, "id"), identifier(block, "name"))
{
let input = if name == "context_checkpoint" {
json!({"_archive_withheld":true})
} else if self.redactor.is_some() {
self.tool_input(block.get("input"), block_class)?
} else {
json!({"_archive_withheld":true})
};
Some(json!({"type":"tool_use", "id":id, "name":name, "input":input}))
} else {
None
}
}
Some("tool_result") if role == "user" => {
if let Some(id) = identifier(block, "tool_use_id") {
let content = if self.redactor.is_some() {
self.tool_result(&block["content"], block_class)?
} else {
Value::String(WITHHELD.to_owned())
};
let mut result =
json!({"type":"tool_result", "tool_use_id":id, "content":content});
if let Some(error) = block.get("is_error").and_then(Value::as_bool) {
result["is_error"] = json!(error);
}
Some(result)
} else {
None
}
}
_ => None,
};
if let Some(kept) = kept {
output.push(kept);
indices.push(index);
}
}
if output.is_empty() {
Ok(None)
} else {
Ok(Some((self.stored(role, json!(output)), indices)))
}
}
fn tool_input(&self, input: Option<&Value>, class: DisclosureClass) -> io::Result<Value> {
match (self.reduce, input) {
(Reduce::Inputs | Reduce::Text, Some(input)) => Ok(self.omitted_input(input)),
(Reduce::InputsAsText, Some(input)) => self.input_as_text(input, class),
(Reduce::LargeInputs, Some(value)) => match self.exact_input(input, class) {
Err(error) if over_budget(&error) => self.input_as_text(value, class),
exact => exact,
},
_ => self.exact_input(input, class),
}
}
fn omitted_input(&self, input: &Value) -> Value {
let bytes = json_len(input);
let marker = self.omission(format_args!("tool input of {bytes} bytes"));
json!({"_archive_omitted": marker})
}
fn input_as_text(&self, input: &Value, class: DisclosureClass) -> io::Result<Value> {
if !input_eligible(input) {
return Ok(json!({"_archive_withheld":true}));
}
if input_text_cost(input).is_none() {
return Ok(self.omitted_input(input));
}
let encoded = bounded_json(input, MAX_INPUT_BYTES)?;
let text = std::str::from_utf8(&encoded).map_err(|_| corrupt())?;
match self.screen(text, class) {
Ok(text) if text == WITHHELD => Ok(json!({"_archive_withheld":true})),
Ok(text) => {
self.as_text.set(self.as_text.get() + 1);
Ok(json!({INPUT_JSON_KEY: text}))
}
Err(error) if over_budget(&error) => Ok(self.omitted_input(input)),
Err(error) => Err(error),
}
}
fn exact_input(&self, input: Option<&Value>, class: DisclosureClass) -> io::Result<Value> {
if let Some(input) = input {
if let Some(bytes) = InputBudget::alone(input)? {
let limit = bytes
.checked_mul(6)
.ok_or_else(|| over("archive input byte budget exceeded"))?;
let encoded = bounded_json(input, limit)?;
let text =
self.screen(std::str::from_utf8(&encoded).map_err(|_| corrupt())?, class)?;
return Ok(serde_json::from_str(&text)
.unwrap_or_else(|_| json!({"_archive_withheld":true})));
}
}
Ok(json!({"_archive_withheld":true}))
}
fn tool_result(&self, content: &Value, class: DisclosureClass) -> io::Result<Value> {
if let Some(text) = content.as_str() {
return Ok(json!(self.text(text, class)?));
}
let mut output = Vec::new();
if let Some(blocks) = content.as_array() {
for (index, block) in blocks.iter().enumerate() {
if let Some(class) = eligible_class(block) {
if block["type"] == "text" {
if let Some(text) = block["text"].as_str() {
let text = self.text(text, class)?;
if self.reduce == Reduce::Text {
let rest = blocks.len() - index - 1;
if rest == 0 {
return Ok(json!(text));
}
let rest = self.omission(format_args!("{rest} blocks"));
return Ok(json!(format!("{text}\n{rest}")));
}
output.push(json!({"type":"text", "text":text}));
}
}
}
}
}
if output.is_empty() {
Ok(json!(WITHHELD))
} else {
Ok(json!(output))
}
}
}
const READER: &str = "project-reader";
pub fn archive_file(base: &Path, scope_key: &str, id: &str) -> io::Result<Option<PathBuf>> {
validate_id(id)?;
let Some(store) = ArchiveStore::open_existing(base, scope_key, READER)? else {
return Ok(None);
};
let directory = base.join("context-archives").join(&store.scope);
let mut unread = Unread::default();
let (shelves, slots) = store.layout()?;
if let Some(found) = store.locate(&shelves, id, true, &mut unread)? {
if found.forgotten {
return Err(corrupt());
}
return Ok(Some(directory.join(found.shelf).join(archive_name(id))));
}
match store.legacy_segment(&slots, id, true, &mut unread) {
Some((slot, _)) => Ok(Some(directory.join(slot))),
None if unread.places == 0 => Ok(None),
None => Err(unread.not_found()),
}
}
pub fn project_search(
base: &Path,
scope_key: &str,
query: &str,
limit: usize,
) -> io::Result<Vec<ArchiveDescriptor>> {
match ArchiveStore::open_existing(base, scope_key, READER)? {
Some(store) => store.search(query, limit),
None => Ok(Vec::new()),
}
}
pub fn project_fetch(
base: &Path,
scope_key: &str,
id: &str,
start: usize,
limit: usize,
) -> io::Result<Vec<SharedMessage>> {
validate_id(id)?;
match ArchiveStore::open_existing(base, scope_key, READER)? {
Some(store) => store.fetch(id, start, limit),
None => Err(not_found()),
}
}
fn eligible_class(value: &Value) -> Option<DisclosureClass> {
if value
.get("channel")
.is_some_and(|v| !matches!(v.as_str(), Some("final")))
|| value.get("content_class").is_some_and(|v| {
!matches!(
v.as_str(),
Some("user_text" | "assistant_text" | "tool_calls" | "tool_results")
)
})
|| value
.get("private")
.is_some_and(|v| v != &Value::Bool(false))
{
return None;
}
for key in ["sensitivity", "retention", "retention_class"] {
if let Some(v) = value.get(key) {
if !matches!(v.as_str(), Some("normal" | "standard" | "model_visible")) {
return None;
}
}
}
let mut class = DisclosureClass::ModelVisible;
for key in ["disclosure", "disclosure_class"] {
if let Some(v) = value.get(key) {
let parsed = DisclosureClass::parse(v.as_str()?)?;
if !may_persist(parsed)
|| !matches!(
parsed,
DisclosureClass::ModelVisible | DisclosureClass::ModelVisibleAfterRedaction
)
{
return None;
}
class = parsed;
}
}
Some(class)
}
fn projection_content(message: &Value) -> Option<(DisclosureClass, &str, &Value)> {
if message.get("_synaps_context").is_some()
|| crate::core::host_note::is_tool_in_flight_note(message)
{
return None;
}
let class = eligible_class(message)?;
let role @ ("user" | "assistant") = message.get("role")?.as_str()? else {
return None;
};
Some((class, role, message.get("content")?))
}
fn identifier<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
let text = value.get(key)?.as_str()?;
(!text.is_empty()
&& text.len() <= 128
&& !credential_like(text)
&& text
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"_-.:".contains(&b)))
.then_some(text)
}
fn credential_like(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
[
"password",
"passwd",
"api_key",
"apikey",
"api-key",
"access_token",
"refresh_token",
"secret",
"authorization",
"bearer ",
"private key",
"sk-",
"ghp_",
"github_pat_",
"xoxb-",
"akia",
"eyjh",
]
.iter()
.any(|needle| lower.contains(needle))
}
struct InputBudget {
bytes: usize,
nodes: usize,
reduce: Reduce,
}
impl InputBudget {
fn node(&mut self, depth: usize) -> io::Result<()> {
self.nodes += 1;
if depth > 32 || self.nodes > 65536 {
return Err(over("archive input structural budget exceeded"));
}
Ok(())
}
fn charge(&mut self, bytes: usize) -> io::Result<()> {
self.bytes = self.bytes.saturating_add(bytes);
if self.bytes > MAX_INPUT_BYTES {
return Err(over("archive input byte budget exceeded"));
}
Ok(())
}
fn alone(input: &Value) -> io::Result<Option<usize>> {
let mut budget = InputBudget {
bytes: 0,
nodes: 0,
reduce: Reduce::None,
};
let bytes = budget.input(input, 3)?;
if let Some(bytes) = bytes {
budget.charge(bytes)?;
}
Ok(bytes)
}
fn field(&mut self, key: &str, text: &str, depth: usize) -> io::Result<()> {
self.node(depth)?;
self.charge(key.len().saturating_add(8).saturating_add(text.len()))
}
fn text_field(
&mut self,
key: &str,
text: &str,
class: DisclosureClass,
redactor: bool,
depth: usize,
) -> io::Result<()> {
if class == DisclosureClass::ModelVisibleAfterRedaction && !redactor {
return self.node(depth); }
self.field(key, text, depth)
}
fn message(&mut self, message: &Value, redactor: bool) -> io::Result<()> {
self.node(0)?;
let Some((class, role, content)) = projection_content(message) else {
return Ok(());
};
self.charge(8)?;
self.field("role", role, 1)?;
if let Some(text) = content.as_str() {
return self.text_field("content", text, class, redactor, 1);
}
self.node(1)?;
let Some(blocks) = content.as_array() else {
return Ok(());
};
self.charge("content".len() + 8)?;
for block in blocks {
self.node(2)?;
let Some(mut block_class) = eligible_class(block) else {
continue;
};
if class == DisclosureClass::ModelVisibleAfterRedaction {
block_class = class;
}
match block.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(text) = block.get("text").and_then(Value::as_str) {
self.charge(8)?;
self.field("type", "text", 3)?;
self.text_field("text", text, block_class, redactor, 3)?;
}
}
Some("tool_use") if role == "assistant" => {
if let (Some(id), Some(name)) =
(identifier(block, "id"), identifier(block, "name"))
{
self.charge(8)?;
self.field("type", "tool_use", 3)?;
self.field("id", id, 3)?;
self.field("name", name, 3)?;
if name != "context_checkpoint" && redactor {
let input = block.get("input");
let in_place = input.and_then(|input| self.reduce.in_place_of(input));
match (input, in_place) {
(Some(input), None) => {
if let Some(bytes) = self.input(input, 3)? {
self.charge("input".len().saturating_add(bytes))?;
}
}
(_, None | Some(0)) => {}
(_, Some(bytes)) => {
self.node(4)?;
self.charge("input".len().saturating_add(bytes))?;
}
}
}
}
}
Some("tool_result") if role == "user" => {
if let Some(id) = identifier(block, "tool_use_id") {
self.charge(8)?;
self.field("type", "tool_result", 3)?;
self.field("tool_use_id", id, 3)?;
if block.get("is_error").and_then(Value::as_bool).is_some() {
self.field("is_error", "", 3)?;
}
if redactor {
if let Some(content) = block.get("content") {
self.tool_result(content, block_class)?;
}
}
}
}
_ => {}
}
}
Ok(())
}
fn tool_result(&mut self, content: &Value, class: DisclosureClass) -> io::Result<()> {
if let Some(text) = content.as_str() {
return self.text_field("content", text, class, true, 3);
}
self.node(3)?;
if let Some(blocks) = content.as_array() {
self.charge("content".len() + 8)?;
for block in blocks {
self.node(4)?;
if let Some(class) = eligible_class(block) {
if block["type"] == "text" {
if let Some(text) = block["text"].as_str() {
self.charge(8)?;
self.field("type", "text", 5)?;
self.text_field("text", text, class, true, 5)?;
}
}
}
}
}
Ok(())
}
fn input(&mut self, value: &Value, depth: usize) -> io::Result<Option<usize>> {
self.node(depth)?;
if eligible_class(value).is_none() {
return Ok(None);
}
let mut bytes: usize = 8;
match value {
Value::String(text) => bytes = bytes.saturating_add(text.len()),
Value::Array(values) => {
for value in values {
let Some(cost) = self.input(value, depth + 1)? else {
return Ok(None);
};
bytes = bytes.saturating_add(cost);
}
}
Value::Object(values) => {
for (key, value) in values {
let Some(cost) = self.input(value, depth + 1)? else {
return Ok(None);
};
bytes = bytes.saturating_add(key.len()).saturating_add(cost);
}
}
_ => {}
}
Ok(Some(bytes))
}
}
fn projection_digest(
scope: &str,
count: usize,
messages: &[ArchivedMessage],
) -> io::Result<String> {
let bytes = bounded_json(messages, MAX_SEGMENT_BYTES)?;
Ok(digest_parts(&[
scope.as_bytes(),
&(count as u64).to_be_bytes(),
&bytes,
]))
}
fn digest_parts(parts: &[&[u8]]) -> String {
let mut hash = Sha256::new();
for part in parts {
hash.update((part.len() as u64).to_be_bytes());
hash.update(part);
}
format!("{:x}", hash.finalize())
}
fn bounded_json<T: Serialize + ?Sized>(value: &T, limit: usize) -> io::Result<Vec<u8>> {
struct Sink {
bytes: Vec<u8>,
limit: usize,
}
impl Write for Sink {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
if bytes.len() > self.limit - self.bytes.len() {
return Err(over("archive serialized byte budget exceeded"));
}
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let mut sink = Sink {
bytes: Vec::new(),
limit,
};
serde_json::to_writer(&mut sink, value).map_err(json_error)?;
Ok(sink.bytes)
}
fn json_len(value: &Value) -> usize {
struct Count(usize);
impl Write for Count {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.0 += bytes.len();
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let mut count = Count(0);
let _ = serde_json::to_writer(&mut count, value);
count.0
}
fn before_results(messages: &[SharedMessage], start: usize, end: usize) -> usize {
let is_result = |index: usize| {
let message = &messages[index];
let blocks = message["content"].as_array();
message["role"] == "user"
&& blocks.is_some_and(|blocks| blocks.iter().any(|b| b["type"] == "tool_result"))
};
if end >= messages.len() {
return end;
}
let mut cut = end;
while cut > start + 1 && is_result(cut) {
cut -= 1;
}
if is_result(cut) {
end
} else {
cut
}
}
fn slot_name(slot: usize) -> String {
format!("{slot:03}.json")
}
fn slot_number(name: &str) -> Option<usize> {
let slot = name.strip_suffix(".json")?.parse().ok()?;
(slot < MAX_SEGMENTS && slot_name(slot) == name).then_some(slot)
}
fn archive_name(id: &str) -> String {
format!("{id}.json")
}
fn archive_id(name: &str) -> Option<&str> {
name.strip_suffix(".json")
.filter(|id| validate_id(id).is_ok())
}
fn shelf_name(number: u64) -> String {
format!("s{number:06}")
}
fn shelf_number(name: &str) -> Option<u64> {
let number = name.strip_prefix('s')?.parse().ok()?;
(number <= LAST_SHELF && shelf_name(number) == name).then_some(number)
}
fn says_forgotten(raw: &[u8], digest: &str) -> bool {
#[derive(Deserialize)]
struct Marked {
digest: String,
body: State,
}
#[derive(Deserialize)]
struct State {
state: String,
}
serde_json::from_slice::<Marked>(raw)
.is_ok_and(|marked| marked.digest == digest && marked.body.state == "tombstone")
}
fn names_digest(raw: &[u8], digest: &str) -> bool {
#[derive(Deserialize)]
struct Named {
digest: String,
}
serde_json::from_slice::<Named>(raw).is_ok_and(|named| named.digest == digest)
}
#[cfg(unix)]
fn temp_name() -> String {
format!("pending.{}.tmp", uuid::Uuid::new_v4().simple())
}
fn is_temp_name(name: &str) -> bool {
name.starts_with("pending.") && name.ends_with(".tmp")
}
fn sweep(dir: &PrivateDir, names: &[(String, bool, i64)]) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |since| since.as_nanos() as i128);
for (name, is_dir, modified) in names {
let stale = now - i128::from(*modified) > STALE_TEMP.as_nanos() as i128;
if is_temp_name(name) && !is_dir && stale {
if let Err(error) = dir.discard(name) {
tracing::warn!(file = %name, %error, "stale temporary archive file not removed");
}
}
}
}
fn prefix(text: &str, bytes: usize) -> &str {
let mut end = bytes.min(text.len());
while !text.is_char_boundary(end) {
end -= 1;
}
&text[..end]
}
fn validate_key(key: &str) -> io::Result<()> {
if key.is_empty() || key.len() > 4096 {
return Err(invalid("archive scope/logical id must be 1..4096 bytes"));
}
Ok(())
}
fn validate_id(id: &str) -> io::Result<()> {
if id.len() != 32
|| !id
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
Err(invalid("invalid opaque archive id"))
} else {
Ok(())
}
}
fn invalid(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message)
}
fn corrupt() -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, "invalid archive record")
}
fn not_found() -> io::Error {
io::Error::new(io::ErrorKind::NotFound, NOT_FOUND)
}
const FORGOTTEN: &str = "archive projection was forgotten";
fn forgotten() -> io::Error {
io::Error::new(io::ErrorKind::PermissionDenied, FORGOTTEN)
}
fn is_forgotten(error: &io::Error) -> bool {
error.kind() == io::ErrorKind::PermissionDenied
&& error.raw_os_error().is_none()
&& error.to_string() == FORGOTTEN
}
fn canceled() -> io::Error {
io::Error::new(
io::ErrorKind::Interrupted,
"archive operation canceled before it started",
)
}
fn json_error(_: serde_json::Error) -> io::Error {
over("archive JSON serialization failed or exceeded budget")
}
#[derive(Debug)]
struct OverBudget(&'static str);
impl std::fmt::Display for OverBudget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for OverBudget {}
fn over(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, OverBudget(message))
}
fn over_budget(error: &io::Error) -> bool {
error.get_ref().is_some_and(|inner| inner.is::<OverBudget>())
}
#[cfg(unix)]
struct PrivateDir(std::fs::File);
#[cfg(unix)]
impl PrivateDir {
fn check_base(base: &Path) -> io::Result<()> {
if !base.is_absolute() || base == Path::new("/") {
return Err(invalid("archive base must be an absolute non-root path"));
}
for component in base.components() {
match component {
std::path::Component::RootDir => {}
std::path::Component::Normal(name) => {
name.to_str().ok_or_else(|| invalid("archive path must be UTF-8"))?;
}
_ => return Err(invalid("archive base contains traversal")),
}
}
Ok(())
}
fn resolve(base: &Path) -> io::Result<(PathBuf, Vec<String>)> {
Self::check_base(base)?;
let (mut existing, mut missing) = (base.to_path_buf(), Vec::new());
let real = loop {
match std::fs::canonicalize(&existing) {
Ok(real) => break real,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
let name = existing.file_name().and_then(|name| name.to_str());
missing.push(name.ok_or(error)?.to_owned());
existing.pop();
}
Err(error) => return Err(error),
}
};
if real == Path::new("/") && missing.is_empty() {
return Err(invalid("archive base must be an absolute non-root path"));
}
missing.reverse();
Ok((real, missing))
}
fn at(path: &Path) -> io::Result<Self> {
use std::os::unix::fs::OpenOptionsExt;
let mut options = std::fs::OpenOptions::new();
options.read(true);
options.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC);
options.open(path).map(Self)
}
fn new(base: &Path, scope: &str) -> io::Result<Self> {
use crate::core::private_fs::DIR_MODE;
use std::os::unix::fs::PermissionsExt;
let (real, mut missing) = Self::resolve(base)?;
let mut above = real.clone();
if missing.is_empty() {
let name = real.file_name().and_then(|name| name.to_str());
missing.push(name.ok_or_else(|| invalid("archive path must be UTF-8"))?.to_owned());
above.pop();
}
let closed = |error: &io::Error| {
matches!(error.raw_os_error(), Some(libc::EACCES | libc::EPERM))
};
let mut dir = match Self::at(&above) {
Ok(above) => {
let last = missing.len() - 1;
let mut dir = above;
for (index, name) in missing.iter().enumerate() {
dir = dir.child(name, index == last)?;
}
dir
}
Err(error) if closed(&error) && above != real => {
let dir = Self::at(&real)?;
dir.0.set_permissions(std::fs::Permissions::from_mode(DIR_MODE))?;
dir
}
Err(error) => return Err(error),
};
dir = dir.child("context-archives", true)?;
dir.child(scope, true)
}
fn open_existing(base: &Path, scope: &str) -> io::Result<Self> {
Self::check_base(base)?;
let real = match std::fs::canonicalize(base) {
Ok(real) => real,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Err(match std::fs::symlink_metadata(base) {
Ok(_) => io::Error::other(format!(
"the archive base is a link to a folder that is not there: {error}"
)),
Err(_) => error,
});
}
Err(error) => return Err(error),
};
if real == Path::new("/") {
return Err(invalid("archive base must be an absolute non-root path"));
}
let mut dir = Self::at(&real)?;
for name in ["context-archives", scope] {
dir = Self(dir.open(&c_name(name)?, libc::O_RDONLY | libc::O_DIRECTORY)?);
}
Ok(dir)
}
fn fd(&self) -> libc::c_int {
use std::os::fd::AsRawFd;
self.0.as_raw_fd()
}
fn child(&self, name: &str, private: bool) -> io::Result<Self> {
use crate::core::private_fs::DIR_MODE;
use std::os::unix::fs::PermissionsExt;
let name = c_name(name)?;
let created =
unsafe { libc::mkdirat(self.fd(), name.as_ptr(), DIR_MODE as libc::mode_t) } == 0;
if !created {
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::EEXIST) {
return Err(error);
}
}
let file = self.open(&name, libc::O_RDONLY | libc::O_DIRECTORY)?;
if private || created {
file.set_permissions(std::fs::Permissions::from_mode(DIR_MODE))?;
}
self.sync()?;
Ok(Self(file))
}
fn open_child(&self, name: &str, readonly: bool) -> io::Result<Option<Self>> {
use crate::core::private_fs::DIR_MODE;
use std::os::unix::fs::PermissionsExt;
let file = match self.open(&c_name(name)?, libc::O_RDONLY | libc::O_DIRECTORY) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
if !readonly {
file.set_permissions(std::fs::Permissions::from_mode(DIR_MODE))?;
}
Ok(Some(Self(file)))
}
fn names(&self) -> io::Result<Vec<(String, bool, i64)>> {
use std::os::fd::IntoRawFd;
let fd = self
.open(c".", libc::O_RDONLY | libc::O_DIRECTORY)?
.into_raw_fd();
let stream = unsafe { libc::fdopendir(fd) };
if stream.is_null() {
let error = io::Error::last_os_error();
unsafe { libc::close(fd) };
return Err(error);
}
let names = self.read_names(stream);
unsafe { libc::closedir(stream) }; names
}
fn read_names(&self, stream: *mut libc::DIR) -> io::Result<Vec<(String, bool, i64)>> {
let mut names = Vec::new();
loop {
unsafe { *errno() = 0 };
let entry = unsafe { libc::readdir(stream) };
if entry.is_null() {
return match unsafe { *errno() } {
0 => Ok(names),
code => Err(io::Error::from_raw_os_error(code)),
};
}
let (raw, name) = unsafe {
let raw = std::ptr::addr_of!((*entry).d_name).cast::<libc::c_char>();
(raw, std::ffi::CStr::from_ptr(raw))
};
let Some(name) = name.to_str().ok().filter(|name| c_name(name).is_ok()) else {
continue;
};
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let found =
unsafe { libc::fstatat(self.fd(), raw, &mut stat, libc::AT_SYMLINK_NOFOLLOW) };
if found != 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::NotFound {
continue;
}
return Err(error);
}
let kind = stat.st_mode & libc::S_IFMT;
#[allow(clippy::useless_conversion)]
let modified = i64::from(stat.st_mtime)
.saturating_mul(1_000_000_000)
.saturating_add(i64::from(stat.st_mtime_nsec));
names.push((name.to_owned(), kind == libc::S_IFDIR, modified));
}
}
fn open(&self, name: &std::ffi::CStr, flags: i32) -> io::Result<std::fs::File> {
use std::os::fd::FromRawFd;
#[cfg(test)]
if let Some(error) = fault("open", name) {
return Err(error);
}
let fd = unsafe {
libc::openat(
self.fd(),
name.as_ptr(),
flags | libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK,
crate::core::private_fs::FILE_MODE,
)
};
if fd < 0 {
Err(io::Error::last_os_error())
} else {
Ok(unsafe { std::fs::File::from_raw_fd(fd) })
}
}
fn check_readonly(file: &std::fs::File) -> io::Result<()> {
Self::check_links(file, 1)
}
fn check_unlocked(file: &std::fs::File) -> io::Result<()> {
Self::check_links(file, 0)
}
fn check_links(file: &std::fs::File, fewest: u64) -> io::Result<()> {
use std::os::unix::fs::MetadataExt;
let meta = file.metadata()?;
let named = (fewest..=1).contains(&meta.nlink());
if !meta.is_file() || !named || meta.uid() != unsafe { libc::geteuid() } {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"archive requires owned regular single-link files",
));
}
Ok(())
}
fn check(file: &std::fs::File) -> io::Result<()> {
use std::os::unix::fs::PermissionsExt;
Self::check_readonly(file)?;
file.set_permissions(std::fs::Permissions::from_mode(
crate::core::private_fs::FILE_MODE,
))
}
fn not_a_segment(error: &io::Error) -> bool {
let of_the_file = [libc::ELOOP, libc::EACCES, libc::EPERM, libc::ENXIO];
error.raw_os_error().is_none_or(|code| of_the_file.contains(&code))
}
fn passing(error: &io::Error) -> bool {
let passing = [libc::EMFILE, libc::ENFILE, libc::ENOMEM, libc::EINTR, libc::EAGAIN];
error.raw_os_error().is_some_and(|code| passing.contains(&code))
}
fn identity(&self) -> io::Result<DirIdentity> {
use std::os::unix::fs::MetadataExt;
let meta = self.0.metadata()?;
Ok((meta.dev(), meta.ino()))
}
fn lock_file(&self, create: bool) -> io::Result<Option<std::fs::File>> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let refused = |what| io::Error::new(io::ErrorKind::PermissionDenied, LockRefused(what));
let name = c_name("lock")?;
let mut opened = match create {
true => self.open(&name, libc::O_RDWR | libc::O_CREAT),
false => self.open(&name, libc::O_RDONLY),
};
if create && opened.as_ref().is_err_and(|error| error.raw_os_error() == Some(libc::EACCES)) {
match self.open(&name, libc::O_RDONLY) {
Err(error) if error.kind() == io::ErrorKind::NotFound => return opened.map(Some),
again => opened = again,
}
}
let file = match opened {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(match error.raw_os_error() {
Some(libc::ELOOP | libc::EMLINK) => refused("is a link"),
Some(libc::EISDIR | libc::ENXIO) => refused("is not a regular file"),
Some(libc::EACCES) => refused("cannot be opened by its owner with its mode"),
_ => error,
});
}
};
let meta = file.metadata()?;
if !meta.is_file() {
return Err(refused("is not a regular file"));
}
if meta.uid() != unsafe { libc::geteuid() } {
return Err(refused("is owned by another user"));
}
let mode = meta.permissions().mode() & 0o7777;
if create && mode & 0o077 != 0 && meta.nlink() == 1 {
file.set_permissions(std::fs::Permissions::from_mode(mode & 0o700))?;
}
Ok(Some(file))
}
fn lock_existing(&self) -> io::Result<Option<FlockGuard>> {
let Some(file) = self.lock_file(false)? else {
return Ok(None);
};
if !fs4::fs_std::FileExt::try_lock_shared(&file)? {
return Err(io::Error::new(io::ErrorKind::WouldBlock, BUSY));
}
Ok(Some(FlockGuard(file)))
}
fn lock(&self) -> io::Result<FlockGuard> {
let file = self.lock_file(true)?.ok_or_else(not_found)?;
if !fs4::fs_std::FileExt::try_lock_exclusive(&file)? {
return Err(io::Error::new(io::ErrorKind::WouldBlock, BUSY));
}
Ok(FlockGuard(file))
}
fn read_with(
&self,
name: &str,
remaining: usize,
readonly: bool,
) -> io::Result<Option<Vec<u8>>> {
let file = match self.open(&c_name(name)?, libc::O_RDONLY) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
Self::read_open(file, remaining, readonly).map(Some)
}
fn read_open(file: std::fs::File, remaining: usize, readonly: bool) -> io::Result<Vec<u8>> {
if readonly {
Self::check_unlocked(&file)?;
} else {
Self::check(&file)?;
}
let limit = remaining.min(MAX_SEGMENT_BYTES);
if file.metadata()?.len() > limit as u64 {
return Err(corrupt());
}
let mut bytes = Vec::new();
file.take(limit as u64 + 1).read_to_end(&mut bytes)?;
if bytes.len() > limit {
return Err(corrupt());
}
Ok(bytes)
}
fn publish(&self, name: &str, bytes: &[u8], create_new: bool) -> io::Result<()> {
self.publish_dated(name, bytes, create_new, None)
}
fn publish_dated(
&self,
name: &str,
bytes: &[u8],
create_new: bool,
modified: Option<std::time::SystemTime>,
) -> io::Result<()> {
let target = c_name(name)?;
let tmp = c_name(&temp_name())?;
let mut file = self.open(&tmp, libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL)?;
let result = (|| {
file.write_all(bytes)?;
file.sync_all()?;
if create_new {
self.rename_new(&tmp, &target)?;
} else {
match self.open(&target, libc::O_RDONLY) {
Ok(existing) => Self::check(&existing)?,
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
self.rename(&tmp, &target)?;
}
if let Some(time) = modified {
#[cfg(test)]
if let Some(error) = fault("date", &target) {
return Err(error);
}
let times = std::fs::FileTimes::new().set_accessed(time).set_modified(time);
file.set_times(times)?;
file.sync_all()?;
}
self.sync()
})();
if result.is_err() {
unsafe {
libc::unlinkat(self.fd(), tmp.as_ptr(), 0);
}
}
result
}
fn rename_new(&self, from: &std::ffi::CStr, to: &std::ffi::CStr) -> io::Result<()> {
#[cfg(target_os = "linux")]
let code = unsafe {
libc::syscall(
libc::SYS_renameat2,
self.fd(),
from.as_ptr(),
self.fd(),
to.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
#[cfg(target_os = "macos")]
let code = unsafe {
libc::renameatx_np(
self.fd(),
from.as_ptr(),
self.fd(),
to.as_ptr(),
libc::RENAME_EXCL,
)
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
if code == 0 {
return Ok(());
}
let error = io::Error::last_os_error();
let unsupported = [libc::ENOSYS, libc::EINVAL, libc::ENOTSUP];
if !error.raw_os_error().is_some_and(|code| unsupported.contains(&code)) {
return Err(error);
}
}
self.rename_probed(from, to)
}
fn rename_probed(&self, from: &std::ffi::CStr, to: &std::ffi::CStr) -> io::Result<()> {
match self.open(to, libc::O_RDONLY) {
Ok(_) => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"archive file already exists",
)),
Err(error) if error.kind() == io::ErrorKind::NotFound => self.rename(from, to),
Err(error) => Err(error),
}
}
fn rename(&self, from: &std::ffi::CStr, to: &std::ffi::CStr) -> io::Result<()> {
#[cfg(test)]
if let Some(error) = fault("rename", to) {
return Err(error);
}
match unsafe { libc::renameat(self.fd(), from.as_ptr(), self.fd(), to.as_ptr()) } {
0 => Ok(()),
_ => Err(io::Error::last_os_error()),
}
}
fn discard(&self, name: &str) -> io::Result<()> {
let name = c_name(name)?;
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let found = unsafe {
libc::fstatat(self.fd(), name.as_ptr(), &mut stat, libc::AT_SYMLINK_NOFOLLOW)
};
if found != 0 {
return Err(io::Error::last_os_error());
}
let ours = stat.st_uid == unsafe { libc::geteuid() } && stat.st_nlink == 1;
if stat.st_mode & libc::S_IFMT != libc::S_IFREG || !ours {
return Ok(());
}
match unsafe { libc::unlinkat(self.fd(), name.as_ptr(), 0) } {
0 => Ok(()),
_ => Err(io::Error::last_os_error()),
}
}
fn sync(&self) -> io::Result<()> {
#[cfg(test)]
if let Some(error) = fault("sync", c"") {
return Err(error);
}
self.0.sync_all()
}
}
#[cfg(target_os = "linux")]
use libc::__errno_location as errno;
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
use libc::__error as errno;
#[cfg(any(target_os = "android", target_os = "netbsd", target_os = "openbsd"))]
use libc::__errno as errno;
#[cfg(all(test, unix))]
thread_local! {
static FAULTS: std::cell::RefCell<Vec<(&'static str, String, i32)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(all(test, unix))]
fn fault(call: &str, name: &std::ffi::CStr) -> Option<io::Error> {
FAULTS.with_borrow(|faults| {
let hit = |(failing, prefix, _): &&(&str, String, i32)| {
*failing == call && name.to_bytes().starts_with(prefix.as_bytes())
};
let code = faults.iter().find(hit).map(|(.., code)| *code);
code.map(io::Error::from_raw_os_error)
})
}
#[cfg(unix)]
fn c_name(name: &str) -> io::Result<std::ffi::CString> {
if name.is_empty() || matches!(name, "." | "..") || name.contains(['/', '\\']) {
return Err(invalid("invalid archive component"));
}
std::ffi::CString::new(name).map_err(|_| invalid("invalid archive component"))
}
#[cfg(not(unix))]
struct PrivateDir;
#[cfg(not(unix))]
impl PrivateDir {
fn unsupported<T>() -> io::Result<T> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"private context archive requires Unix",
))
}
fn new(_: &Path, _: &str) -> io::Result<Self> {
Self::unsupported()
}
fn not_a_segment(_: &io::Error) -> bool {
false
}
fn passing(_: &io::Error) -> bool {
false
}
fn identity(&self) -> io::Result<DirIdentity> {
Self::unsupported()
}
fn lock(&self) -> io::Result<FlockGuard> {
Self::unsupported()
}
fn open_existing(_: &Path, _: &str) -> io::Result<Self> {
Self::unsupported()
}
fn child(&self, _: &str, _: bool) -> io::Result<Self> {
Self::unsupported()
}
fn open_child(&self, _: &str, _: bool) -> io::Result<Option<Self>> {
Self::unsupported()
}
fn names(&self) -> io::Result<Vec<(String, bool, i64)>> {
Self::unsupported()
}
fn lock_existing(&self) -> io::Result<Option<FlockGuard>> {
Self::unsupported()
}
fn read_with(&self, _: &str, _: usize, _: bool) -> io::Result<Option<Vec<u8>>> {
Self::unsupported()
}
fn publish(&self, _: &str, _: &[u8], _: bool) -> io::Result<()> {
Self::unsupported()
}
fn publish_dated(
&self,
_: &str,
_: &[u8],
_: bool,
_: Option<std::time::SystemTime>,
) -> io::Result<()> {
Self::unsupported()
}
fn discard(&self, _: &str) -> io::Result<()> {
Self::unsupported()
}
fn sync(&self) -> io::Result<()> {
Self::unsupported()
}
}
#[cfg(test)]
mod projection_tests {
use super::*;
fn excluded_fixture(blob: &str) -> Vec<SharedMessage> {
vec![
Arc::new(json!({"role":"system","content":blob})),
Arc::new(json!({"role":"developer","content":blob})),
Arc::new(json!({"role":"user","_synaps_context":{"note":blob},"content":blob})),
Arc::new(json!({"role":"user","retention":"local_only","content":blob})),
Arc::new(json!({"role":"assistant","metadata":blob,"content":[
{"type":"thinking","thinking":blob},
{"type":"text","private":true,"text":blob},
{"type":"image","source":{"data":blob}},
{"type":"document","source":{"data":blob}},
{"type":"text","text":" exact evidence αβ\n","metadata":blob},
{"type":"tool_use","id":"cp","name":"context_checkpoint","input":{"note":blob}}
]})),
Arc::new(json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"r1","metadata":blob,"content":[
{"type":"image","source":{"data":blob}},
{"type":"text","sensitivity":"secret","text":blob},
{"type":"text","text":"exact tool evidence\n","metadata":blob}
]}
]})),
]
}
#[test]
fn excluded_aggregate_does_not_consume_eligible_budget_or_change_digest() {
let messages = excluded_fixture(&"x".repeat(2 * 1024 * 1024));
let source = messages.clone();
for redactor in [None, Some(Arc::new(str::to_owned) as Arc<ArchiveRedactor>)] {
let small =
project_archive("excluded", &excluded_fixture("x"), "note", redactor.clone())
.unwrap();
let large = project_archive("excluded", &messages, "note", redactor).unwrap();
assert!(small == large);
assert_eq!(large.source_message_count, 6);
assert_eq!(large.messages[0].source_index, 4);
assert_eq!(large.messages[0].block_indices, vec![4, 5]);
}
for (old, new) in source.iter().zip(&messages) {
assert!(Arc::ptr_eq(old, new));
}
}
#[test]
fn eligible_aggregate_refuses_before_any_redactor_call() {
use std::sync::atomic::{AtomicUsize, Ordering};
let calls = Arc::new(AtomicUsize::new(0));
let count = calls.clone();
let redactor: Arc<ArchiveRedactor> = Arc::new(move |_| {
count.fetch_add(1, Ordering::SeqCst);
"small".to_owned()
});
let piece = "x".repeat(MAX_INPUT_BYTES / 2);
for messages in [
vec![
Arc::new(json!({"role":"user","content":piece})),
Arc::new(json!({"role":"assistant","content":piece})),
],
vec![Arc::new(json!({"role":"user","content":[{
"type":"tool_result","tool_use_id":"r1","content":[
{"type":"text","text":piece}, {"type":"text","text":piece}
]
}]}))],
] {
let before = messages.clone();
let error = project_archive("overflow", &messages, "", Some(redactor.clone()))
.err()
.unwrap();
assert!(error.to_string().contains("input byte budget"));
assert_eq!(calls.load(Ordering::SeqCst), 0);
assert_eq!(before, messages);
}
}
#[test]
fn large_tool_results_exact_with_redactor_opaque_without_it() {
let text = "evidence αβ\n".repeat(250_000);
let make = |text: &str| {
vec![Arc::new(json!({"role":"user","content":[{
"type":"tool_result","tool_use_id":"r1","content":text
}]}))]
};
let p = project_archive("logs", &make(&text), "", Some(Arc::new(str::to_owned))).unwrap();
assert_eq!(p.messages[0].message["content"][0]["content"], text);
let huge = make(&"x".repeat(MAX_INPUT_BYTES + 1));
let p = project_archive("logs", &huge, "", None).unwrap();
assert_eq!(p.messages[0].message["content"][0]["content"], WITHHELD);
assert!(project_archive("logs", &huge, "", Some(Arc::new(str::to_owned))).is_err());
}
#[test]
fn whole_argument_withholding_discards_tentative_bytes_not_structural_guards() {
let huge = "x".repeat(MAX_INPUT_BYTES + 1);
let make = |input: Value| {
vec![Arc::new(json!({"role":"assistant","content":[{
"type":"tool_use","id":"r1","name":"read","input":input
}]}))]
};
for input in [
json!({"a":huge,"z":{"sensitivity":"secret","body":huge}}),
json!({"a":{"sensitivity":"secret","body":huge},"z":huge}),
] {
let p =
project_archive("input", &make(input), "", Some(Arc::new(str::to_owned))).unwrap();
assert_eq!(
p.messages[0].message["content"][0]["input"],
json!({"_archive_withheld":true})
);
}
let input = json!({"type":"image","data":"YWJj"});
let p = project_archive(
"input",
&make(input.clone()),
"",
Some(Arc::new(str::to_owned)),
)
.unwrap();
assert_eq!(p.messages[0].message["content"][0]["input"], input);
let large = make(json!({"type":"image","data":huge}));
assert!(project_archive("input", &large, "", Some(Arc::new(str::to_owned))).is_err());
assert!(project_archive("input", &large, "", None).is_ok());
}
#[test]
fn visited_structure_is_bounded_but_excluded_descendants_are_opaque() {
let mut deep = json!("leaf");
for _ in 0..40 {
deep = json!([deep]);
}
let make = |input: Value| {
vec![Arc::new(json!({"role":"assistant","content":[{
"type":"tool_use","id":"r1","name":"read","input":input
}]}))]
};
let identity = Some(Arc::new(str::to_owned) as Arc<ArchiveRedactor>);
assert!(project_archive("depth", &make(deep.clone()), "", identity.clone()).is_err());
assert!(project_archive("depth", &make(deep.clone()), "", None).is_ok());
assert!(project_archive(
"depth",
&make(json!({"private":true,"blob":deep})),
"",
identity.clone()
)
.is_ok());
assert!(project_archive(
"depth",
&[Arc::new(json!({"role":"user","content":deep}))],
"",
identity.clone()
)
.is_ok());
let wide = vec![Value::Null; 65536];
assert!(project_archive("nodes", &make(json!(wide)), "", identity.clone()).is_err());
let excluded = vec![Arc::new(json!({"role":"user","content":wide}))];
assert!(project_archive("nodes", &excluded, "", identity).is_err());
}
#[test]
fn withheld_after_redaction_text_is_not_scanned_without_redactor() {
let messages = vec![Arc::new(json!({"role":"user",
"disclosure":"model_visible_after_redaction", "content":"x".repeat(MAX_INPUT_BYTES+1)
}))];
let p = project_archive("withheld", &messages, "", None).unwrap();
assert_eq!(p.messages[0].message["content"], WITHHELD);
assert!(project_archive("withheld", &messages, "", Some(Arc::new(str::to_owned))).is_err());
}
#[test]
fn json_escape_scratch_can_exceed_segment_limit_before_redaction() {
let input = json!({"text":"\0".repeat(3 * 1024 * 1024)});
let messages = vec![Arc::new(json!({"role":"assistant","content":[{
"type":"tool_use","id":"r1","name":"read","input":input
}]}))];
let redactor: Arc<ArchiveRedactor> = Arc::new(|s| {
if s.starts_with('{') {
assert!(s.len() > MAX_SEGMENT_BYTES);
"{\"text\":\"screened\"}".to_owned()
} else {
s.to_owned()
}
});
let p = project_archive("escaping", &messages, "", Some(redactor)).unwrap();
assert_eq!(
p.messages[0].message["content"][0]["input"],
json!({"text":"screened"})
);
assert!(project_archive("escaping", &messages, "", Some(Arc::new(str::to_owned))).is_err());
}
fn compatibility_fixture() -> Vec<SharedMessage> {
vec![
Arc::new(json!({"role":"system","content":"omitted"})),
Arc::new(json!({"role":"user","content":" exact αβ\n"})),
Arc::new(json!({"role":"assistant","content":[
{"type":"thinking","thinking":"private"},
{"type":"text","text":"classified-value"},
{"type":"tool_use","id":"r1","name":"read","input":{"path":"src/lib.rs","n":7,"binary":{"type":"image","data":"YWJj"}}},
{"type":"tool_use","id":"c1","name":"context_checkpoint","input":{"note":"hidden"}},
{"type":"tool_use","id":"x1","name":"read","input":{"nested":{"disclosure":"local_only","body":"omitted"}}}
]})),
Arc::new(json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"r1","is_error":false,"content":[
{"type":"image","data":"omitted"},
{"type":"text","text":"exact result\n"},
{"type":"text","text":"classified-value","disclosure":"model_visible_after_redaction"}
]}
]})),
Arc::new(
json!({"role":"user","disclosure":"model_visible_after_redaction","content":"classified-value"}),
),
Arc::new(json!({"role":"user","_synaps_context":{},"content":"hidden"})),
]
}
#[test]
fn legacy_projection_golden() {
for (redactor, digest) in [
(
None,
"aeb544a71629f741def049575eefb1a765117d65aa914bdcc1657fa677622031",
),
(
Some(
Arc::new(|s: &str| s.replace("classified-value", "[redacted]"))
as Arc<ArchiveRedactor>,
),
"d70b321210ca3d2bc531b31a35cc02ee267eeb572e8a6dd88f1fb62725b082ec",
),
] {
let p = project_archive(
"compat-session",
&compatibility_fixture(),
"hidden note",
redactor,
)
.unwrap();
assert_eq!(p.digest, digest);
}
}
#[test]
fn pure_projection_keeps_legacy_logical_digest_and_never_opens_paths() {
let tmp = tempfile::tempdir().unwrap();
let logical = tmp
.path()
.join("not-created/session")
.to_str()
.unwrap()
.to_owned();
let messages = vec![
Arc::new(json!({"role":"system","content":"OMITTED"})),
Arc::new(json!({"role":"assistant","content":[
{"type":"thinking","thinking":"OMITTED"},
{"type":"text","text":" exact αβ evidence\n"}
]})),
];
let source = serde_json::to_vec(&messages).unwrap();
let first = project_archive(&logical, &messages, "hidden note", None).unwrap();
assert_eq!(first.logical_id, digest_parts(&[logical.as_bytes()]));
assert_eq!(
first.digest,
projection_digest(&first.logical_id, 2, &first.messages).unwrap()
);
assert_eq!(first.messages[0].source_index, 1);
assert_eq!(first.messages[0].block_indices, vec![1]);
assert_eq!(first.source_message_count, 2);
let mut changed = messages.clone();
changed[0] = Arc::new(json!({"role":"developer","content":"DIFFERENT OMITTED"}));
let second = project_archive(&logical, &changed, "changed note", None).unwrap();
assert_eq!(first.digest, second.digest);
assert_ne!(
first.digest,
project_archive("other-session", &messages, "hidden note", None)
.unwrap()
.digest
);
assert_eq!(serde_json::to_vec(&messages).unwrap(), source);
assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
}
#[test]
fn pure_projection_shares_disclosure_redaction_and_input_limits() {
let redactor: Arc<ArchiveRedactor> =
Arc::new(|s| s.replace("classified-value", "[redacted]"));
let messages = vec![
Arc::new(json!({"role":"assistant","content":[
{"type":"tool_use","id":"read_1","name":"read","input":{"path":"src/lib.rs"}},
{"type":"tool_use","id":"cp","name":"context_checkpoint","input":{"note":"NOTE_ONLY"}}
]})),
Arc::new(
json!({"role":"user","content":[{"type":"tool_result","tool_use_id":"read_1","content":"classified-value"}]}),
),
Arc::new(json!({"role":"user","sensitivity":"secret","content":"OMITTED"})),
Arc::new(json!({"role":"user","_synaps_context":{},"content":"NOTE_ONLY"})),
Arc::new(json!({"role":"user","content":crate::core::host_note::tool_in_flight_note_text(
"Canceled by user",
&["bash".into()],
)})),
];
let projected =
project_archive("session", &messages, "classified-value", Some(redactor)).unwrap();
assert_eq!(projected.note, "[redacted]");
let json = serde_json::to_string(&projected.messages).unwrap();
assert!(!json.contains("classified-value"));
assert!(!json.contains("NOTE_ONLY"));
assert!(!json.contains("partially applied"), "the tool note is not archived");
assert!(!json.contains("OMITTED"));
assert!(json.contains("src/lib.rs"));
assert_eq!(projected.messages.len(), 2);
let withheld = project_archive("session", &messages, "password=bad", None).unwrap();
assert_eq!(withheld.note, WITHHELD);
assert!(!serde_json::to_string(&withheld.messages)
.unwrap()
.contains("src/lib.rs"));
assert!(project_archive("", &[], "", None).is_err());
assert!(project_archive(&"l".repeat(4097), &[], "", None).is_err());
assert!(project_archive("s", &[], &"n".repeat(MAX_NOTE_BYTES + 1), None).is_err());
assert!(
project_archive("s", &vec![messages[0].clone(); MAX_MESSAGES + 1], "", None).is_err()
);
}
}
#[cfg(all(test, unix))]
#[serial_test::parallel(process_spawn)]
mod tests {
use super::*;
use std::os::unix::fs::{symlink, PermissionsExt};
fn msg(role: &str, content: Value) -> SharedMessage {
Arc::new(json!({"role":role,"content":content}))
}
fn setup() -> (tempfile::TempDir, ArchiveStore) {
let tmp = tempfile::tempdir().unwrap();
let store = setup_in(tmp.path(), "synthetic-project");
(tmp, store)
}
fn setup_in(base: &Path, project: &str) -> ArchiveStore {
ArchiveStore::new(base, project, "synthetic-logical").unwrap()
}
fn directory(tmp: &tempfile::TempDir, store: &ArchiveStore) -> std::path::PathBuf {
tmp.path().join("context-archives").join(&store.scope)
}
fn shelf_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
for shelf in std::fs::read_dir(dir).unwrap() {
let shelf = shelf.unwrap().path();
if std::fs::symlink_metadata(&shelf).unwrap().is_dir() {
let entries = std::fs::read_dir(&shelf).unwrap();
files.extend(entries.map(|entry| entry.unwrap().path()));
}
}
files.retain(|file| file.extension().is_some_and(|extension| extension == "json"));
files.sort();
files
}
fn only_archive_file(dir: &Path) -> PathBuf {
let files = shelf_files(dir);
assert_eq!(files.len(), 1);
assert_eq!(files[0].parent().unwrap(), dir.join("s000001"));
files[0].clone()
}
fn seal_legacy_slot(
store: &ArchiveStore,
slot: usize,
messages: &[SharedMessage],
note: &str,
) -> ArchiveRef {
let projection =
project_hashed(&store.logical, messages, note, None, Reduce::None).unwrap();
let record = Record {
version: 1,
scope: store.scope.clone(),
logical: store.logical.clone(),
id: uuid::Uuid::new_v4().simple().to_string(),
digest: projection.digest,
created_ms: None,
body: Body::Live {
source_message_count: projection.source_message_count,
messages: projection.messages,
note: projection.note,
},
};
let bytes = bounded_json(&record, MAX_SEGMENT_BYTES).unwrap();
let _lock = store.exclusive().unwrap();
store.fs.publish(&slot_name(slot), &bytes, false).unwrap();
record.reference().unwrap()
}
fn text(content: &str) -> Vec<SharedMessage> {
vec![msg("user", json!(content))]
}
fn mode(path: &Path) -> u32 {
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
fn identity(path: &Path) -> (u64, u64) {
use std::os::unix::fs::MetadataExt;
let meta = std::fs::symlink_metadata(path).unwrap();
(meta.dev(), meta.ino())
}
fn temporary_files(dir: &Path) -> Vec<PathBuf> {
let mut found = Vec::new();
let mut pending = vec![dir.to_path_buf()];
while let Some(directory) = pending.pop() {
for entry in std::fs::read_dir(&directory).unwrap() {
let path = entry.unwrap().path();
if is_temp_name(path.file_name().unwrap().to_str().unwrap()) {
found.push(path);
} else if std::fs::symlink_metadata(&path).unwrap().is_dir() {
pending.push(path);
}
}
}
found
}
fn written_ago(path: &Path, age: Duration) {
let file = std::fs::File::options().write(true).open(path).unwrap();
file.set_modified(std::time::SystemTime::now() - age).unwrap();
}
fn snapshot(dir: &Path) -> std::collections::BTreeMap<PathBuf, String> {
let mut files = std::collections::BTreeMap::new();
let mut pending = vec![dir.to_path_buf()];
while let Some(directory) = pending.pop() {
for entry in std::fs::read_dir(&directory).unwrap() {
let path = entry.unwrap().path();
if std::fs::symlink_metadata(&path).unwrap().is_dir() {
pending.push(path);
} else {
let hash = digest_parts(&[&std::fs::read(&path).unwrap()]);
let file = format!("{hash} {:?}", identity(&path));
files.insert(path, file);
}
}
}
files
}
#[test]
fn attachments_remain_outside_eligible_memory_archive() {
let (tmp, store) = setup();
let messages = vec![msg(
"user",
json!([
{"type":"text","text":"Compare the selected files"},
{"type":"image","source":{"type":"base64","media_type":"image/png","data":"ATTACHMENT_IMAGE_SECRET"}},
{"type":"document","title":"ATTACHMENT_FILENAME_SECRET","source":{"type":"base64","media_type":"application/pdf","data":"ATTACHMENT_PDF_SECRET"}},
{"type":"document","source":{"type":"text","media_type":"text/plain","data":"ATTACHMENT_TEXT_SECRET"}}
]),
)];
let reference = store.seal(&messages, "safe checkpoint").unwrap();
let fetched = store.fetch(&reference.id, 0, 10).unwrap();
let serialized = serde_json::to_string(&fetched).unwrap();
assert!(serialized.contains("Compare the selected files"));
assert!(!serialized.contains("ATTACHMENT_"));
let raw = std::fs::read_to_string(only_archive_file(&directory(&tmp, &store))).unwrap();
assert!(!raw.contains("ATTACHMENT_"));
assert!(messages[0].to_string().contains("ATTACHMENT_TEXT_SECRET"));
}
#[test]
fn exact_text_source_indices_note_and_restart() {
let (tmp, store) = setup();
let messages = vec![
msg("system", json!("FORBIDDEN SYSTEM")),
msg("user", json!(" exact\n αβ ")),
msg(
"assistant",
json!([
{"type":"thinking","thinking":"FORBIDDEN REASONING"},
{"type":"text","text":"evidence✓", "cache_control":{"type":"ephemeral"}},
{"type":"image","data":"FORBIDDEN IMAGE"}
]),
),
msg("developer", json!("FORBIDDEN DEVELOPER")),
];
let reference = store.seal(&messages, "hidden routing note").unwrap();
assert_eq!(reference.message_count, 2);
assert_eq!(reference.source_message_count, 4);
let reopened =
ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-logical").unwrap();
let fetched = reopened
.fetch_with_provenance(&reference.id, 0, 128)
.unwrap();
assert_eq!(fetched[0].message, messages[1]);
assert_eq!(fetched[0].source_index, 1);
assert_eq!(fetched[1].source_index, 2);
assert_eq!(fetched[1].block_indices, vec![1]);
assert_eq!(
reopened.fetch_note(&reference.id).unwrap(),
"hidden routing note"
);
assert!(reopened.search("hidden routing", 10).unwrap().is_empty());
assert_eq!(reopened.search("evidence", 10).unwrap()[0].id, reference.id);
assert!(reopened
.fetch(&reference.id, usize::MAX, 10)
.unwrap()
.is_empty());
let raw = std::fs::read_to_string(only_archive_file(&directory(&tmp, &store))).unwrap();
assert!(!raw.contains("FORBIDDEN"));
assert!(!raw.contains("cache_control"));
assert_eq!(messages[0]["content"], "FORBIDDEN SYSTEM");
}
#[test]
fn duplicate_retry_ignores_note_but_not_logical_id() {
let (tmp, store) = setup();
let messages = vec![msg("user", json!("same eligible words"))];
let first = store.seal(&messages, "first").unwrap();
assert_eq!(store.seal(&messages, "second").unwrap(), first);
assert_eq!(store.fetch_note(&first.id).unwrap(), "first");
let other = ArchiveStore::new(tmp.path(), "synthetic-project", "other-logical").unwrap();
let second = other.seal(&messages, "").unwrap();
assert_ne!(first.id, second.id);
assert_eq!(
project_search(tmp.path(), "synthetic-project", "", 10)
.unwrap()
.len(),
2
);
assert_eq!(
project_fetch(tmp.path(), "synthetic-project", &first.id, 0, 10).unwrap(),
messages
);
assert_eq!(other.fetch(&first.id, 0, 1).unwrap(), messages);
store.forget(&first.id).unwrap();
assert!(store.seal(&messages, "third").is_err());
assert_eq!(other.fetch(&second.id, 0, 1).unwrap(), messages);
assert_eq!(other.seal(&messages, "third").unwrap(), second);
}
#[test]
fn deletion_is_content_free_durable_and_cannot_resurrect() {
let (tmp, store) = setup();
let messages = vec![msg("user", json!("uniquely removable content"))];
let reference = store.seal(&messages, "removable note").unwrap();
store.forget(&reference.id).unwrap();
store.forget(&reference.id).unwrap();
let raw = std::fs::read_to_string(only_archive_file(&directory(&tmp, &store))).unwrap();
assert!(raw.contains("tombstone"));
assert!(!raw.contains("removable"));
assert!(!raw.contains("source_message_count"));
let store =
ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-logical").unwrap();
assert_eq!(
store.fetch(&reference.id, 0, 1).unwrap_err().kind(),
io::ErrorKind::NotFound
);
assert!(store.fetch_note(&reference.id).is_err());
assert!(store.search("", 10).unwrap().is_empty());
assert_eq!(
store.seal(&messages, "changed note").unwrap_err().kind(),
io::ErrorKind::PermissionDenied
);
}
#[test]
fn foreign_scope_copied_records_and_traversal_fail_closed() {
let (tmp, store) = setup();
let reference = store
.seal(&[msg("user", json!("scope evidence"))], "")
.unwrap();
let foreign =
ArchiveStore::new(tmp.path(), "foreign-project", "synthetic-logical").unwrap();
assert!(foreign.fetch(&reference.id, 0, 1).is_err());
for id in [
"../000.json",
"/tmp/file",
"",
"a/../b",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
] {
assert_eq!(
store.fetch(id, 0, 1).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
assert!(store.forget(id).is_err());
}
let source = only_archive_file(&directory(&tmp, &store));
let foreign_dir = directory(&tmp, &foreign);
std::fs::create_dir(foreign_dir.join("s000001")).unwrap();
for target in [
foreign_dir.join("s000001").join(source.file_name().unwrap()),
foreign_dir.join("000.json"),
] {
std::fs::copy(&source, &target).unwrap();
assert_eq!(
foreign.fetch(&reference.id, 0, 1).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert!(foreign.search("", 1).unwrap().is_empty());
std::fs::remove_file(&target).unwrap();
}
assert!(ArchiveStore::new(&tmp.path().join("../escape"), "p", "l").is_err());
}
#[test]
fn disclosure_and_raw_tool_data_are_withheld() {
let (tmp, store) = setup();
let mut messages = vec![
msg(
"assistant",
json!([{"type":"tool_use","id":"call_1","name":"read","input":{"path":"unclassified raw argument"}}]),
),
msg(
"user",
json!([{"type":"tool_result","tool_use_id":"call_1","content":"unclassified raw output"}]),
),
msg("user", json!("password=synthetic-credential")),
];
for class in [
"local_only",
"persist_never_transmit",
"never_persist",
"model_visible_after_consent",
"unknown",
] {
messages.push(Arc::new(
json!({"role":"user","content":"FORBIDDEN DISCLOSURE","disclosure":class}),
));
}
messages.push(Arc::new(
json!({"role":"user","content":"FORBIDDEN SENSITIVITY","sensitivity":"secret"}),
));
let reference = store.seal(&messages, "").unwrap();
assert_eq!(reference.message_count, 3);
let raw = std::fs::read_to_string(only_archive_file(&directory(&tmp, &store))).unwrap();
for forbidden in ["unclassified raw", "synthetic-credential", "FORBIDDEN"] {
assert!(!raw.contains(forbidden));
}
assert!(raw.contains("call_1"));
assert!(raw.contains("_archive_withheld"));
}
#[test]
fn derived_context_envelope_is_not_searchable_source() {
let (_tmp, store) = setup();
let first = store
.seal(
&[msg("assistant", json!("original evidence"))],
"NOTE_ONLY_SENTINEL",
)
.unwrap();
let second=store.seal(&[
Arc::new(json!({"role":"user","_synaps_context":{"schema":"synaps-context-window/1","archive":first.id},"content":"NOTE_ONLY_SENTINEL"})),
Arc::new(json!({"role":"assistant","content":[{"type":"tool_use","id":"checkpoint-note","name":"context_checkpoint","input":{"phase":"plan","note":"NOTE_ONLY_SENTINEL"}}]})),
msg("assistant",json!("second-window evidence")),
],"next note").unwrap();
assert!(store.search("NOTE_ONLY_SENTINEL", 10).unwrap().is_empty());
let fetched = store.fetch_with_provenance(&second.id, 0, 10).unwrap();
assert_eq!(fetched.len(), 2);
assert_eq!(fetched[0].source_index, 1);
assert_eq!(fetched[1].source_index, 2);
assert!(!serde_json::to_string(&fetched)
.unwrap()
.contains("NOTE_ONLY_SENTINEL"));
}
#[test]
fn checkpoint_tool_note_stays_hidden_even_with_redactor() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let reference = store
.seal(
&[msg(
"assistant",
json!([
{"type":"tool_use","id":"checkpoint-note","name":"context_checkpoint",
"input":{"phase":"plan","note":"NOTE_ARGUMENT_SENTINEL"}}
]),
)],
"NOTE_ARGUMENT_SENTINEL",
)
.unwrap();
assert!(store
.search("NOTE_ARGUMENT_SENTINEL", 10)
.unwrap()
.is_empty());
assert!(
!serde_json::to_string(&store.fetch(&reference.id, 0, 10).unwrap())
.unwrap()
.contains("NOTE_ARGUMENT_SENTINEL")
);
assert_eq!(
store.fetch_note(&reference.id).unwrap(),
"NOTE_ARGUMENT_SENTINEL"
);
}
#[test]
fn trusted_redactor_retains_exact_eligible_tool_evidence() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(|s| s.replace("classified-value", "[redacted]")));
let messages = vec![
msg(
"assistant",
json!([{"type":"tool_use","id":"call_1","name":"read","input":{"path":"src/lib.rs","line":7}}]),
),
msg(
"user",
json!([{"type":"tool_result","tool_use_id":"call_1","is_error":false,"content":"line 7\n exact evidence\nclassified-value"}]),
),
];
let reference = store.seal(&messages, "").unwrap();
let fetched = store.fetch(&reference.id, 0, 10).unwrap();
assert_eq!(fetched[0], messages[0]);
assert_eq!(
fetched[1]["content"][0]["content"],
"line 7\n exact evidence\n[redacted]"
);
}
#[test]
fn oversized_input_output_limits_and_depth_leave_source_intact() {
let (_tmp, store) = setup();
let huge = vec![msg("user", json!("x".repeat(MAX_INPUT_BYTES + 1)))];
assert!(store.seal(&huge, "").is_err());
assert_eq!(
huge[0]["content"].as_str().unwrap().len(),
MAX_INPUT_BYTES + 1
);
let file_too_large = vec![msg("user", json!("\n".repeat(MAX_SEGMENT_BYTES / 2)))];
assert!(store.seal(&file_too_large, "").is_err());
assert!(store
.seal(&vec![msg("user", json!("a")); MAX_MESSAGES + 1], "")
.is_err());
assert!(store.seal(&[], &"n".repeat(MAX_NOTE_BYTES + 1)).is_err());
let mut deep = json!("value");
for _ in 0..40 {
deep = json!([deep]);
}
let deep = vec![msg(
"assistant",
json!([{
"type":"tool_use", "id":"r1", "name":"read", "input":deep
}]),
)];
let store = store.with_redactor(Arc::new(str::to_owned));
assert!(store.seal(&deep, "").is_err());
assert!(store.search(&"q".repeat(513), 1).is_err());
assert!(store.search("", MAX_SEARCH_RESULTS + 1).is_err());
assert!(store
.fetch(&"a".repeat(32), 0, MAX_FETCH_MESSAGES + 1)
.is_err());
assert!(store.search("", 10).unwrap().is_empty());
}
#[test]
fn torn_final_fails_closed_stale_temporary_is_not_visible() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let reference = store.seal(&text("complete"), "").unwrap();
let file = only_archive_file(&dir);
let stale = dir.join("s000001").join("pending.tmp");
std::fs::write(&stale, b"unpublished partial").unwrap();
written_ago(&stale, STALE_TEMP * 2);
assert_eq!(store.search("", 2).unwrap().len(), 1);
let other = store.seal(&text("another"), "").unwrap();
assert!(!stale.exists());
std::fs::write(&file, b"{\"version\":1,").unwrap();
let torn = identity(&file);
for result in [
store.fetch(&reference.id, 0, 1).map(drop),
store.fetch_note(&reference.id).map(drop),
store.forget(&reference.id).map(drop),
] {
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
}
let again = store.seal(&text("complete"), "").unwrap();
assert_ne!(again.id, reference.id);
assert_eq!(store.fetch(&again.id, 0, 1).unwrap(), text("complete"));
assert_eq!(std::fs::read(&file).unwrap(), b"{\"version\":1,");
assert_eq!(identity(&file), torn);
assert_eq!(store.fetch(&other.id, 0, 1).unwrap(), text("another"));
assert!(store.seal(&text("next"), "").is_ok());
}
#[test]
fn private_modes_symlink_hardlink_fifo_and_lock_refusal() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let reference = store.seal(&text("private"), "").unwrap();
let file = only_archive_file(&dir);
for path in [
tmp.path().to_path_buf(),
tmp.path().join("context-archives"),
dir.clone(),
dir.join("s000001"),
] {
assert_eq!(mode(&path), 0o700);
}
for path in [&file, &dir.join("lock")] {
assert_eq!(mode(path), 0o600);
}
std::fs::set_permissions(dir.join("s000001"), std::fs::Permissions::from_mode(0o755))
.unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
let located = archive_file(tmp.path(), "synthetic-project", &reference.id).unwrap();
assert_eq!(located, Some(file.clone()));
assert_eq!((mode(&dir.join("s000001")), mode(&file)), (0o755, 0o644));
store.seal(&text("private"), "").unwrap();
assert_eq!((mode(&dir.join("s000001")), mode(&file)), (0o700, 0o600));
let victim = tmp.path().join("synthetic-victim");
std::fs::write(&victim, "do not change").unwrap();
std::fs::set_permissions(&victim, std::fs::Permissions::from_mode(0o644)).unwrap();
let refused = || {
let (_, unscanned) = store.search_scan("", 2).unwrap();
let unread = unscanned.unwrap().unread.unwrap();
let place = format!("first s000001/{}: ", archive_name(&reference.id));
assert!(unread.starts_with("1 place(s) could not be read, ") && unread.contains(&place));
assert!(store.fetch(&reference.id, 0, 1).is_err());
assert!(store.fetch_note(&reference.id).is_err());
assert!(store.forget(&reference.id).is_err());
let other = store.seal(&text("private"), "").unwrap();
assert_ne!(other.id, reference.id);
assert_eq!(store.fetch(&other.id, 0, 1).unwrap(), text("private"));
assert!(archive_file(tmp.path(), "synthetic-project", &reference.id).is_err());
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "do not change");
assert_eq!(mode(&victim), 0o644);
};
std::fs::remove_file(&file).unwrap();
symlink(&victim, &file).unwrap();
refused();
std::fs::remove_file(&file).unwrap();
std::fs::hard_link(&victim, &file).unwrap();
refused();
std::fs::remove_file(&file).unwrap();
let fifo = std::ffi::CString::new(file.to_str().unwrap()).unwrap();
assert_eq!(unsafe { libc::mkfifo(fifo.as_ptr(), 0o600) }, 0);
refused();
std::fs::remove_file(dir.join("lock")).unwrap();
symlink(&victim, dir.join("lock")).unwrap();
assert!(store.fs.lock().is_err());
}
#[test]
fn a_lock_file_with_two_names_or_read_only_is_used_and_a_link_is_refused_by_name() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let lock = dir.join("lock");
let mut other = setup_in(tmp.path(), "synthetic-project");
other.process_wait = Duration::ZERO;
store.seal(&text("first"), "").unwrap();
let legacy = seal_legacy_slot(&store, 0, &text("sealed long ago"), "");
let works = |words: &str| {
assert_eq!(store.fetch(&legacy.id, 0, 1).unwrap(), text("sealed long ago"), "{words}");
let sealed = store.seal(&text(words), "").unwrap();
assert_eq!(store.forget(&sealed.id).unwrap(), forgotten(&[&sealed.id]));
let held = store.fs.lock().unwrap();
assert!(store.fs.lock().is_err_and(|error| error.kind() == io::ErrorKind::WouldBlock));
let busy = other.seal(&text(words), "").unwrap_err();
assert_eq!(busy.kind(), io::ErrorKind::WouldBlock, "{words}: {busy}");
assert_eq!(busy.to_string(), BUSY);
drop(held);
assert!(other.seal(&text(&format!("{words}, after")), "").is_ok());
};
let second = tmp.path().join("a-backup-of-the-lock");
std::fs::hard_link(&lock, &second).unwrap();
works("with two names");
std::fs::set_permissions(&lock, std::fs::Permissions::from_mode(0o644)).unwrap();
works("loosened, with two names");
assert_eq!((mode(&lock), mode(&second)), (0o644, 0o644));
for links in [2, 1] {
std::fs::set_permissions(&lock, std::fs::Permissions::from_mode(0o400)).unwrap();
works(&format!("read-only, {links} name(s)"));
assert_eq!(mode(&lock), 0o400, "left as it is");
if links == 2 {
assert_eq!(identity(&lock), identity(&second), "not replaced");
std::fs::remove_file(&second).unwrap();
}
}
assert_eq!(std::fs::metadata(&lock).unwrap().len(), 0, "never written");
std::fs::set_permissions(&lock, std::fs::Permissions::from_mode(0o644)).unwrap();
works("loosened");
assert_eq!(mode(&lock), 0o600);
let victim = tmp.path().join("synthetic-victim");
std::fs::write(&victim, "do not change").unwrap();
std::fs::set_permissions(&victim, std::fs::Permissions::from_mode(0o644)).unwrap();
let sealed = store.seal(&text("to forget later"), "").unwrap();
let refused = |what: &str| {
let before = snapshot(&dir.join("s000001"));
let errors = [
store.seal(&text("refused"), "").unwrap_err(),
store.forget(&sealed.id).unwrap_err(),
store.fetch(&legacy.id, 0, 1).unwrap_err(),
];
for error in errors {
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{what}: {error}");
let message = error.to_string();
let named = format!("context archive lock file {} {what}", lock.display());
assert!(message.starts_with(&named), "{message}");
assert!(message.contains(&format!("rm '{}'", lock.display())), "{message}");
}
assert_eq!(snapshot(&dir.join("s000001")), before, "{what}");
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text("to forget later"));
};
std::fs::remove_file(&lock).unwrap();
symlink(&victim, &lock).unwrap();
refused("is a link");
assert!(std::fs::symlink_metadata(&lock).unwrap().file_type().is_symlink());
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "do not change");
assert_eq!(mode(&victim), 0o644);
std::fs::remove_file(&lock).unwrap();
std::fs::create_dir(&lock).unwrap();
refused("is not a regular file");
std::fs::remove_dir(&lock).unwrap();
let fifo = std::ffi::CString::new(lock.to_str().unwrap()).unwrap();
assert_eq!(unsafe { libc::mkfifo(fifo.as_ptr(), 0o600) }, 0);
refused("is not a regular file");
std::fs::remove_file(&lock).unwrap();
if unsafe { libc::geteuid() } != 0 {
std::fs::write(&lock, b"").unwrap();
std::fs::set_permissions(&lock, std::fs::Permissions::from_mode(0o000)).unwrap();
refused("cannot be opened by its owner with its mode");
std::fs::remove_file(&lock).unwrap();
}
assert_eq!(store.forget(&sealed.id).unwrap(), forgotten(&[&sealed.id]));
assert_eq!(mode(&lock), 0o600);
}
#[test]
fn lock_free_read_does_not_change_file_mode() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let reference = store.seal(&text("private"), "note").unwrap();
let (shelf, file) = (dir.join("s000001"), only_archive_file(&dir));
std::fs::set_permissions(&shelf, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(store.fetch(&reference.id, 0, 1).unwrap(), text("private"));
assert_eq!(store.fetch_note(&reference.id).unwrap(), "note");
assert_eq!(store.search("private", 1).unwrap().len(), 1);
assert_eq!((mode(&shelf), mode(&file)), (0o755, 0o644));
assert_eq!(store.seal(&text("private"), "").unwrap(), reference);
assert_eq!((mode(&shelf), mode(&file)), (0o700, 0o600));
}
type Stamp = (u32, (i64, i64), (i64, i64));
fn stamps(root: &Path) -> std::collections::BTreeMap<PathBuf, Stamp> {
use std::os::unix::fs::MetadataExt;
let mut found = std::collections::BTreeMap::new();
let mut pending = vec![root.to_path_buf()];
while let Some(path) = pending.pop() {
let meta = std::fs::symlink_metadata(&path).unwrap();
if meta.is_dir() {
let entries = std::fs::read_dir(&path).unwrap();
pending.extend(entries.map(|entry| entry.unwrap().path()));
}
let changed = (meta.ctime(), meta.ctime_nsec());
let modified = (meta.mtime(), meta.mtime_nsec());
found.insert(path, (meta.mode(), changed, modified));
}
found
}
fn read_everything(
base: &Path,
live: &[(&ArchiveRef, &str, &str)],
gone: &[String],
rows: usize,
) {
let open = || ArchiveStore::open_existing(base, "synthetic-project", "any-reader");
for (reference, words, note) in live {
let id = &reference.id;
assert_eq!(open().unwrap().unwrap().fetch(id, 0, 1).unwrap(), text(words));
assert_eq!(&open().unwrap().unwrap().fetch_note(id).unwrap(), note);
let found = project_fetch(base, "synthetic-project", id, 0, 1);
assert_eq!(found.unwrap(), text(words));
let located = archive_file(base, "synthetic-project", id).unwrap().unwrap();
assert!(std::fs::read_to_string(located).unwrap().contains(words));
let found = open().unwrap().unwrap().search(words, 8).unwrap();
assert_eq!(found.len(), 1, "{words}");
assert_eq!(found[0].id, *id);
}
for id in gone {
for result in [
open().unwrap().unwrap().fetch(id, 0, 1).map(drop),
open().unwrap().unwrap().fetch_note(id).map(drop),
project_fetch(base, "synthetic-project", id, 0, 1).map(drop),
] {
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
assert_eq!(error.to_string(), NOT_FOUND);
}
}
assert_eq!(open().unwrap().unwrap().search("", 8).unwrap().len(), rows);
assert_eq!(project_search(base, "synthetic-project", "", 8).unwrap().len(), rows);
assert_eq!(open().unwrap().unwrap().search_scan("", 8).unwrap().1, None);
}
#[test]
fn reads_write_nothing_and_work_where_nothing_can_be_written() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let shelved = store.seal(&text("read only"), "note").unwrap();
let forgotten = store.seal(&text("forgotten"), "").unwrap();
store.forget(&forgotten.id).unwrap();
let legacy = seal_legacy_slot(&store, 0, &text("sealed long ago"), "old note");
let old_forgotten = seal_legacy_slot(&store, 1, &text("forgotten long ago"), "");
store.forget(&old_forgotten.id).unwrap();
drop(store);
let live = [(&shelved, "read only", "note"), (&legacy, "sealed long ago", "old note")];
let gone = [forgotten.id.clone(), old_forgotten.id.clone(), "a".repeat(32)];
let context = tmp.path().join("context-archives");
let dirs = vec![tmp.path().to_path_buf(), context, dir.clone(), dir.join("s000001")];
let _reopen = Reopen(dirs.clone());
for lock in [true, false] {
if !lock {
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
std::fs::remove_file(dir.join("lock")).unwrap();
}
for path in &dirs {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
for path in shelf_files(&dir).into_iter().chain([dir.join("000.json")]) {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o644)).unwrap();
}
std::thread::sleep(Duration::from_millis(50));
let before = stamps(tmp.path());
assert_eq!(before.len(), if lock { 10 } else { 9 }, "4 directories, 5 files, lock");
read_everything(tmp.path(), &live, &gone, 2);
assert_eq!(stamps(tmp.path()), before, "lock file: {lock}");
for path in &dirs {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o500)).unwrap();
}
for path in shelf_files(&dir).into_iter().chain([dir.join("000.json")]) {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o400)).unwrap();
}
if lock {
let read_only = std::fs::Permissions::from_mode(0o400);
std::fs::set_permissions(dir.join("lock"), read_only).unwrap();
}
std::thread::sleep(Duration::from_millis(50));
let before = stamps(tmp.path());
read_everything(tmp.path(), &live, &gone, 2);
assert_eq!(stamps(tmp.path()), before, "read-only, lock file: {lock}");
}
ArchiveStore::new(tmp.path(), "synthetic-project", "any").unwrap();
assert_eq!((mode(tmp.path()), mode(&dir), mode(&dir.join("s000001"))), (0o700, 0o700, 0o500));
}
#[test]
fn a_read_of_a_project_without_archives_creates_nothing() {
let tmp = tempfile::tempdir().unwrap();
let id = "a".repeat(32);
let absent = |base: &Path, project: &str| {
assert!(ArchiveStore::open_existing(base, project, "reader").unwrap().is_none());
assert!(project_search(base, project, "", 8).unwrap().is_empty());
let error = project_fetch(base, project, &id, 0, 1).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
assert_eq!(error.to_string(), NOT_FOUND);
assert_eq!(archive_file(base, project, &id).unwrap(), None);
};
absent(&tmp.path().join("missing"), "synthetic-project");
absent(tmp.path(), "synthetic-project");
assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
let other = setup_in(tmp.path(), "another-project");
other.seal(&text("elsewhere"), "").unwrap();
std::thread::sleep(Duration::from_millis(50));
let before = stamps(tmp.path());
absent(tmp.path(), "synthetic-project");
assert_eq!(stamps(tmp.path()), before);
assert_eq!(std::fs::read_dir(tmp.path().join("context-archives")).unwrap().count(), 1);
for (project, id) in [("", id.as_str()), ("synthetic-project", "../x")] {
let error = project_fetch(tmp.path(), project, id, 0, 1).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
let context = tmp.path().join("context-archives");
let scope = digest_parts(&[b"synthetic-project"]);
symlink(context.join(&other.scope), context.join(&scope)).unwrap();
assert!(ArchiveStore::open_existing(tmp.path(), "synthetic-project", "reader").is_err());
assert!(project_search(tmp.path(), "synthetic-project", "", 8).is_err());
}
#[test]
fn a_base_reached_through_a_link_works_and_a_link_below_the_base_is_refused() {
let tmp = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(tmp.path()).unwrap();
let disk = root.join("another-disk");
std::fs::create_dir_all(disk.join("synaps")).unwrap();
std::fs::create_dir(root.join("home")).unwrap();
symlink(disk.join("synaps"), root.join("home/.synaps-cli")).unwrap();
symlink(&disk, root.join("mount")).unwrap();
let bases = [
(root.join("home/.synaps-cli"), disk.join("synaps")),
(root.join("mount/not/yet"), disk.join("not/yet")),
];
for (base, real) in &bases {
let store = setup_in(base, "synthetic-project");
let real_dir = real.join("context-archives").join(&store.scope);
let words = "through a link";
let sealed = store.seal_with_note(&text(words), "a note").unwrap().0;
let file = only_archive_file(&real_dir);
assert_eq!(file.file_name().unwrap().to_str(), Some(archive_name(&sealed.id).as_str()));
assert_eq!(mode(real), 0o700);
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text(words));
assert_eq!(store.search("through", 8).unwrap().len(), 1);
assert_eq!(project_fetch(base, "synthetic-project", &sealed.id, 0, 1).unwrap(), text(words));
assert_eq!(project_search(base, "synthetic-project", "through", 8).unwrap().len(), 1);
let located = archive_file(base, "synthetic-project", &sealed.id).unwrap().unwrap();
assert_eq!(std::fs::canonicalize(located).unwrap(), file);
let again = setup_in(base, "synthetic-project");
assert_eq!(again.seal(&text(words), "").unwrap(), sealed);
assert_eq!(again.forget(&sealed.id).unwrap(), forgotten(&[&sealed.id]));
assert!(is_tombstone_of(&store, &file, &digest_of(words)));
assert!(store.search("through", 8).unwrap().is_empty());
assert_eq!(shelf_files(&real_dir).len(), 1);
}
for link in [root.join("home/.synaps-cli"), root.join("mount")] {
assert!(std::fs::symlink_metadata(link).unwrap().file_type().is_symlink());
}
let outside = root.join("outside");
std::fs::create_dir(&outside).unwrap();
let empty = || std::fs::read_dir(&outside).unwrap().count() == 0;
let (base, real) = &bases[0];
let scope = digest_parts(&[b"linked-project"]);
symlink(&outside, real.join("context-archives").join(&scope)).unwrap();
assert!(ArchiveStore::new(base, "linked-project", "l").is_err());
assert!(ArchiveStore::open_existing(base, "linked-project", "l").is_err());
assert!(project_search(base, "linked-project", "", 8).is_err());
for base in [root.join("plain"), root.join("mount/linked-below")] {
std::fs::create_dir(&base).unwrap();
symlink(&outside, base.join("context-archives")).unwrap();
assert!(ArchiveStore::new(&base, "synthetic-project", "l").is_err());
assert!(ArchiveStore::open_existing(&base, "synthetic-project", "l").is_err());
}
let store = setup_in(base, "synthetic-project");
let real_dir = real.join("context-archives").join(&store.scope);
symlink(&outside, real_dir.join("s000002")).unwrap();
let sealed = store.seal(&text("past a shelf that is a link"), "").unwrap();
let file = real_dir.join("s000003").join(archive_name(&sealed.id));
assert!(file.is_file());
assert!(empty(), "nothing was written through a link");
if unsafe { libc::geteuid() } == 0 {
return; }
let parent = root.join("closed");
let base = parent.join("base");
std::fs::create_dir_all(&base).unwrap();
std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o100)).unwrap();
let result = (|| {
let store = ArchiveStore::new(&base, "synthetic-project", "synthetic-logical")?;
let sealed = store.seal(&text("under a closed parent"), "")?;
let read = project_fetch(&base, "synthetic-project", &sealed.id, 0, 1)?;
store.forget(&sealed.id)?;
Ok::<_, io::Error>(read)
})();
std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(result.unwrap(), text("under a closed parent"));
assert_eq!(mode(&base), 0o700);
}
#[test]
fn a_shelf_name_that_is_a_link_or_a_file_is_not_followed_and_does_not_stop_a_seal() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let reference = store.seal(&text("shelved"), "").unwrap();
let shelf = dir.join("s000001");
let outside = tmp.path().join("outside-shelf");
std::fs::rename(&shelf, &outside).unwrap();
let before = snapshot(&outside);
let unread = |result: io::Result<()>| {
let error = result.unwrap_err();
assert_ne!(error.kind(), io::ErrorKind::NotFound, "{error}");
assert!(error.to_string().contains("s000001"), "{error}");
assert!(error.to_string().contains("could not be read"), "{error}");
};
let refused = |id: &str| {
unread(store.fetch(id, 0, 1).map(drop));
unread(store.fetch_note(id).map(drop));
unread(store.forget(id).map(drop));
unread(archive_file(tmp.path(), "synthetic-project", id).map(drop));
let (_, unscanned) = store.search_scan("", MAX_SEARCH_RESULTS).unwrap();
let passed = unscanned.unwrap().unread.unwrap();
assert!(passed.starts_with("1 place(s) could not be read, first s000001: "), "{passed}");
};
symlink(&outside, &shelf).unwrap();
refused(&reference.id);
std::fs::remove_file(&shelf).unwrap();
std::fs::write(&shelf, b"not a directory").unwrap();
refused(&reference.id);
assert_eq!(snapshot(&outside), before);
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 2, "lock and s000001");
let new = store.seal(&text("something new"), "").unwrap();
assert!(dir.join("s000002").join(archive_name(&new.id)).is_file());
assert_eq!(std::fs::read(&shelf).unwrap(), b"not a directory");
std::fs::remove_file(&shelf).unwrap();
symlink(&outside, &shelf).unwrap();
assert_eq!(store.seal(&text("shelved"), "").unwrap(), reference);
assert!(dir.join("s000002").join(archive_name(&reference.id)).is_file());
assert_eq!(snapshot(&outside), before);
assert_eq!(store.fetch(&new.id, 0, 1).unwrap(), text("something new"));
assert_eq!(store.fetch(&reference.id, 0, 1).unwrap(), text("shelved"));
let located = archive_file(tmp.path(), "synthetic-project", &new.id).unwrap();
assert_eq!(located, Some(dir.join("s000002").join(archive_name(&new.id))));
refused(&"a".repeat(32));
std::fs::write(dir.join("s000003"), b"not a directory").unwrap();
let next = store.seal(&text("after a file"), "").unwrap();
assert!(dir.join("s000004").join(archive_name(&next.id)).is_file());
symlink(&outside, dir.join("s000005")).unwrap();
let last = store.seal(&text("after a link"), "").unwrap();
assert!(dir.join("s000006").join(archive_name(&last.id)).is_file());
assert_eq!(std::fs::read(dir.join("s000003")).unwrap(), b"not a directory");
assert_eq!(snapshot(&outside), before);
std::fs::remove_file(&shelf).unwrap();
std::fs::remove_file(dir.join("s000003")).unwrap();
std::fs::remove_file(dir.join("s000005")).unwrap();
assert_eq!(store.search("", MAX_SEARCH_RESULTS).unwrap().len(), 4);
}
struct Reopen(Vec<PathBuf>);
impl Drop for Reopen {
fn drop(&mut self) {
for dir in &self.0 {
let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
}
}
}
#[test]
fn a_shelf_that_cannot_be_opened_does_not_stop_a_seal() {
if unsafe { libc::geteuid() } == 0 {
return; }
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let seal = |index: usize| store.seal(&text(&format!("archive {index}")), "").unwrap();
let ids: Vec<_> = (0..=SHELF_FILES).map(|index| seal(index).id).collect();
let (older, newer) = (dir.join("s000001"), dir.join("s000002"));
let _reopen = Reopen(vec![older.clone(), newer.clone()]);
let close = |shelf: &Path, mode: u32| {
std::fs::set_permissions(shelf, std::fs::Permissions::from_mode(mode)).unwrap();
};
close(&older, 0o000);
let next = seal(1_000);
assert!(newer.join(archive_name(&next.id)).is_file());
assert_eq!(seal(0).id, ids[0]);
assert!(newer.join(archive_name(&ids[0])).is_file());
assert_eq!(store.fetch(&next.id, 0, 1).unwrap(), text("archive 1000"));
assert_eq!(store.fetch(&ids[0], 0, 1).unwrap(), text("archive 0"));
for id in [ids[5].clone(), "a".repeat(32)] {
for result in [store.fetch(&id, 0, 1).map(drop), store.forget(&id).map(drop)] {
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
assert!(error.to_string().contains("s000001"), "{error}");
assert!(error.to_string().contains("could not be read"), "{error}");
}
}
assert_eq!(mode(&older), 0o000);
close(&older, 0o700);
close(&newer, 0o000);
let later = seal(2_000);
assert!(dir.join("s000003").join(archive_name(&later.id)).is_file());
assert_eq!(store.fetch(&ids[5], 0, 1).unwrap(), text("archive 5"));
assert_eq!(mode(&newer), 0o000);
close(&newer, 0o700);
assert_eq!(std::fs::read_dir(&newer).unwrap().count(), 3);
assert_eq!(std::fs::read_dir(&older).unwrap().count(), SHELF_FILES);
for (id, index) in [(&ids[5], 5), (&ids[SHELF_FILES], SHELF_FILES), (&later.id, 2_000)] {
assert_eq!(store.fetch(id, 0, 1).unwrap(), text(&format!("archive {index}")));
}
}
#[test]
fn a_directory_with_an_absurd_shelf_number_is_not_a_shelf() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let seal = |index: usize| store.seal(&text(&format!("archive {index}")), "").unwrap();
let first = seal(0);
let file = only_archive_file(&dir);
let absurd = ["s18446744073709551615", "s1000000", "s0999999", "s99999999999999999999"];
for name in absurd {
std::fs::create_dir(dir.join(name)).unwrap();
std::fs::copy(&file, dir.join(name).join(archive_name(&first.id))).unwrap();
}
let before = snapshot(&dir);
assert_eq!(store.shelves().unwrap(), ["s000001"]);
let second = seal(1);
assert!(dir.join("s000001").join(archive_name(&second.id)).is_file());
assert_eq!(store.search("", 8).unwrap().len(), 2);
let after = snapshot(&dir);
assert!(before.iter().all(|(path, file)| after.get(path) == Some(file)));
assert_eq!(after.len(), before.len() + 1);
let last = dir.join("s999999");
std::fs::create_dir(&last).unwrap();
let ids: Vec<_> = (2..SHELF_FILES + 5).map(|index| seal(index).id).collect();
assert_eq!(std::fs::read_dir(&last).unwrap().count(), SHELF_FILES + 3);
assert_eq!(mode(&last), 0o700);
assert_eq!(store.shelves().unwrap(), ["s999999", "s000001"]);
for (id, index) in [(&ids[0], 2), (&ids[SHELF_FILES + 2], SHELF_FILES + 4)] {
assert_eq!(store.fetch(id, 0, 1).unwrap(), text(&format!("archive {index}")));
}
assert_eq!(store.seal(&text("archive 0"), "").unwrap(), first);
std::fs::rename(&last, tmp.path().join("moved-away")).unwrap();
std::fs::write(&last, b"not a directory").unwrap();
let fallback = seal(1_000);
assert!(dir.join("s000001").join(archive_name(&fallback.id)).is_file());
assert_eq!(std::fs::read(&last).unwrap(), b"not a directory");
}
fn record_bytes(
store: &ArchiveStore,
id: &str,
messages: &[SharedMessage],
change: impl FnOnce(&mut Record),
) -> Vec<u8> {
let projection = project_hashed(&store.logical, messages, "", None, Reduce::None).unwrap();
let mut record = Record {
version: 1,
scope: store.scope.clone(),
logical: store.logical.clone(),
id: id.to_owned(),
digest: projection.digest,
created_ms: None,
body: Body::Live {
source_message_count: projection.source_message_count,
messages: projection.messages,
note: projection.note,
},
};
change(&mut record);
bounded_json(&record, MAX_SEGMENT_BYTES).unwrap()
}
#[test]
fn a_file_under_the_id_that_is_not_the_segment_is_left_and_the_other_id_is_used() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let shelf = dir.join("s000001");
store.seal(&text("a shelf to begin with"), "").unwrap();
let digest = |words: &str| {
let projection = project_archive("synthetic-logical", &text(words), "", None);
projection.unwrap().digest
};
let other = |id: &str| record_bytes(&store, id, &text("another window"), |_| {});
let forged = |id: &str, digest: &str| {
record_bytes(&store, id, &text("another window"), |record| record.digest = digest.into())
};
for (case, words) in ["torn", "another projection", "forged digest"].into_iter().enumerate() {
let digest = digest(words);
let (first, second) = (&digest[..32], &digest[32..]);
let taken = shelf.join(archive_name(first));
let bytes = match case {
0 => b"{\"version\":1,".to_vec(),
1 => other(first),
_ => forged(first, &digest),
};
std::fs::write(&taken, &bytes).unwrap();
let before = snapshot(&dir);
let (sealed, note) = store.seal_with_note(&text(words), "first note").unwrap();
assert_eq!((sealed.id.as_str(), note.as_str()), (second, "first note"), "{words}");
assert_eq!(store.fetch(second, 0, 1).unwrap(), text(words), "{words}");
assert!(shelf.join(archive_name(second)).is_file(), "{words}");
let after = snapshot(&dir);
assert!(before.iter().all(|(path, file)| after.get(path) == Some(file)), "{words}");
assert_eq!(after.len(), before.len() + 1, "{words}");
let again = store.seal_with_note(&text(words), "second note").unwrap();
assert_eq!(again, (sealed.clone(), "first note".to_owned()), "{words}");
assert_eq!(snapshot(&dir), after, "{words}");
match store.fetch(first, 0, 1) {
Ok(found) => assert_eq!((case, found), (1, text("another window"))),
Err(error) => assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{words}"),
}
store.forget(second).unwrap();
let refused = store.seal(&text(words), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied, "{words}");
assert_eq!(std::fs::read(&taken).unwrap(), bytes, "{words}");
}
let digest = digest("no name left");
for id in [&digest[..32], &digest[32..]] {
std::fs::write(shelf.join(archive_name(id)), other(id)).unwrap();
}
let before = snapshot(&dir);
let refused = store.seal(&text("no name left"), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::AlreadyExists);
assert_eq!(snapshot(&dir), before);
assert!(store.seal(&text("any other window"), "").is_ok());
}
#[test]
fn a_tombstone_that_no_longer_validates_still_forbids_its_projection() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let words = "forgotten words";
let gone = seal_legacy_slot(&store, 0, &text(words), "");
store.forget(&gone.id).unwrap();
let digest = project_archive("synthetic-logical", &text(words), "", None);
let digest = digest.unwrap().digest;
let tombstone = std::fs::read_to_string(dir.join("000.json")).unwrap();
let altered = [
tombstone.replace("\"version\":1", "\"version\":2"),
tombstone.replacen('{', "{\"added\":true,", 1),
tombstone.replace(&store.scope, &"0".repeat(64)),
];
let refused = |kind: &str| {
let error = store.seal(&text(words), "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{kind}");
assert!(error.to_string().contains("was forgotten"), "{kind}");
};
let shelf = dir.join("s000001");
let places = [
dir.join("000.json"),
shelf.join(archive_name(&digest[..32])),
shelf.join(archive_name(&digest[32..])),
];
std::fs::remove_file(&places[0]).unwrap();
std::fs::remove_file(&places[1]).unwrap();
for (index, place) in places.iter().enumerate() {
for bytes in &altered {
assert!(store.record("000.json", bytes.as_bytes()).is_err());
std::fs::write(place, bytes).unwrap();
let before = snapshot(&dir);
refused(bytes);
assert_eq!(snapshot(&dir), before);
}
if index < 2 {
std::fs::remove_file(place).unwrap();
}
}
std::fs::write(&places[1], b"not a record").unwrap();
refused("with the first name taken");
assert!(store.seal(&text("other words"), "").is_ok());
std::fs::write(&places[2], tombstone.replace("tombstone", "tombstoned")).unwrap();
let error = store.seal(&text(words), "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "no name left: {error}");
std::fs::remove_file(&places[2]).unwrap();
let again = store.seal(&text(words), "").unwrap();
assert_eq!(again.id, digest[32..]);
}
struct Fault;
impl Fault {
fn on(call: &'static str, prefix: &str, code: i32) -> Self {
FAULTS.with_borrow_mut(|faults| faults.push((call, prefix.to_owned(), code)));
Fault
}
}
impl Drop for Fault {
fn drop(&mut self) {
FAULTS.with_borrow_mut(Vec::clear);
}
}
fn digest_of(words: &str) -> String {
let projection = project_archive("synthetic-logical", &text(words), "", None);
projection.unwrap().digest
}
#[test]
fn a_segment_under_its_second_id_is_found_when_the_first_name_is_free_again() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let shelf = dir.join("s000001");
store.seal(&text("a shelf to begin with"), "").unwrap();
let digest = digest_of("stored once");
let (first, second) = (&digest[..32], &digest[32..]);
let odd = shelf.join(archive_name(first));
std::fs::write(&odd, b"{\"version\":1,").unwrap();
let sealed = store.seal_with_note(&text("stored once"), "first note").unwrap();
assert_eq!(sealed.0.id, second);
std::fs::remove_file(&odd).unwrap();
let before = snapshot(&dir);
let again = store.seal_with_note(&text("stored once"), "second note").unwrap();
assert_eq!(again, sealed);
assert_eq!(snapshot(&dir), before);
assert_eq!(store.search("stored once", 8).unwrap().len(), 1);
store.forget(second).unwrap();
let forgotten = snapshot(&dir);
let refused = store.seal(&text("stored once"), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied);
assert!(refused.to_string().contains("was forgotten"));
assert_eq!(snapshot(&dir), forgotten);
assert!(!odd.exists());
}
#[test]
fn a_tombstone_under_either_name_refuses_whatever_the_other_holds() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let shelf = dir.join("s000001");
store.seal(&text("a shelf to begin with"), "").unwrap();
for (words, forgotten) in [("tombstone first", 0), ("tombstone second", 1)] {
let digest = digest_of(words);
let ids = [&digest[..32], &digest[32..]];
for (index, id) in ids.into_iter().enumerate() {
let bytes = record_bytes(&store, id, &text(words), |record| {
if index == forgotten {
record.body = Body::Tombstone;
}
});
std::fs::write(shelf.join(archive_name(id)), bytes).unwrap();
}
assert_eq!(store.fetch(ids[1 - forgotten], 0, 1).unwrap(), text(words));
let before = snapshot(&dir);
let refused = store.seal(&text(words), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied, "{words}");
assert_eq!(snapshot(&dir), before, "{words}");
}
}
#[test]
fn a_file_under_the_name_on_a_newer_shelf_does_not_hide_an_older_tombstone_or_copy() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let (older, newer) = (dir.join("s000001"), dir.join("s000002"));
store.seal(&text("a shelf to begin with"), "").unwrap();
std::fs::create_dir(&newer).unwrap();
std::fs::write(tmp.path().join("elsewhere"), b"not in the store").unwrap();
let odd = |name: &str, link: bool| {
if link {
symlink(tmp.path().join("elsewhere"), newer.join(name)).unwrap();
} else {
std::fs::write(newer.join(name), b"not a record").unwrap();
}
};
let refused = |words: &str| {
let before = snapshot(&dir);
let error = store.seal(&text(words), "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{words}: {error}");
assert_eq!(snapshot(&dir), before, "{words}");
assert!(store.search(words, 8).unwrap().is_empty(), "{words}");
};
let imported = ["0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba9876543210"];
for (link, imported) in [false, true].into_iter().zip(imported) {
let words = format!("forgotten behind an odd file, link {link}");
let sealed = store.seal(&text(&words), "").unwrap();
let name = archive_name(&sealed.id);
assert_eq!(sealed.id, digest_of(&words)[..32]);
std::fs::rename(newer.join(&name), older.join(&name)).unwrap();
let copy = record_bytes(&store, imported, &text(&words), |_| {});
std::fs::write(older.join(archive_name(imported)), copy).unwrap();
odd(&name, link);
let mut ids = store.forget(imported).unwrap().ids;
ids.sort_unstable();
let mut expected = [imported.to_owned(), sealed.id.clone()];
expected.sort_unstable();
assert_eq!(ids, expected);
assert!(is_tombstone_of(&store, &older.join(&name), &digest_of(&words)));
refused(&words);
for whole in [true, false] {
let words = format!("an older tombstone, link {link}, whole {whole}");
let digest = digest_of(&words);
let id = &digest[..32];
let bytes = record_bytes(&store, id, &text(&words), |record| {
record.body = Body::Tombstone;
if !whole {
record.version = 2;
}
});
std::fs::write(older.join(archive_name(id)), bytes).unwrap();
odd(&archive_name(id), link);
refused(&words);
}
let words = format!("an older copy, link {link}");
let sealed = store.seal_with_note(&text(&words), "first note").unwrap();
let name = archive_name(&sealed.0.id);
std::fs::rename(newer.join(&name), older.join(&name)).unwrap();
odd(&name, link);
let before = snapshot(&dir);
assert_eq!(store.seal_with_note(&text(&words), "a later note").unwrap(), sealed);
assert_eq!(snapshot(&dir), before);
}
let words = "only an odd file";
let digest = digest_of(words);
odd(&archive_name(&digest[..32]), false);
assert_eq!(store.seal(&text(words), "").unwrap().id, digest[32..]);
}
#[test]
fn a_failed_read_of_the_file_under_the_id_fails_the_seal_and_stores_no_second_copy() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let sealed = store.seal_with_note(&text("stored once"), "first note").unwrap();
let before = snapshot(&dir);
for code in [libc::EIO, libc::EMFILE, libc::ENFILE, libc::ENOMEM, libc::EINTR] {
let failing = Fault::on("open", &sealed.0.id, code);
let error = store.seal(&text("stored once"), "").unwrap_err();
assert_eq!(error.raw_os_error(), Some(code));
drop(failing);
assert_eq!(snapshot(&dir), before, "error {code}");
}
let again = store.seal_with_note(&text("stored once"), "a later note").unwrap();
assert_eq!(again, sealed);
assert_eq!(snapshot(&dir), before);
for code in [libc::ELOOP, libc::EACCES, libc::EPERM, libc::ENXIO] {
assert!(PrivateDir::not_a_segment(&io::Error::from_raw_os_error(code)));
}
assert!(PrivateDir::not_a_segment(&corrupt()));
if unsafe { libc::geteuid() } == 0 {
return; }
let file = only_archive_file(&dir);
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o000)).unwrap();
let other = store.seal(&text("stored once"), "").unwrap();
assert_eq!(other.id, digest_of("stored once")[32..]);
assert_eq!(mode(&file), 0o000);
}
fn is_tombstone_of(store: &ArchiveStore, file: &Path, digest: &str) -> bool {
let name = file.file_name().unwrap().to_str().unwrap();
let record = store.record(name, &std::fs::read(file).unwrap()).unwrap();
record.digest == digest && matches!(record.body, Body::Tombstone)
}
fn forgotten(ids: &[&str]) -> Forgotten {
Forgotten {
ids: ids.iter().map(|id| (*id).to_owned()).collect(),
unread: None,
}
}
#[test]
fn forget_reaches_every_copy_of_the_window() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let (older, newer) = (dir.join("s000001"), dir.join("s000002"));
let gone = |words: &str| {
assert!(store.search(words, 8).unwrap().is_empty(), "{words}");
let refused = store.seal(&text(words), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied, "{words}");
};
let words = "under both halves";
let digest = digest_of(words);
let (first, second) = (&digest[..32], &digest[32..]);
assert_eq!(store.seal(&text(words), "").unwrap().id, first);
let copy = record_bytes(&store, second, &text(words), |_| {});
std::fs::write(older.join(archive_name(second)), copy).unwrap();
assert_eq!(store.search(words, 8).unwrap().len(), 2);
assert_eq!(store.forget(second).unwrap(), forgotten(&[second, first]));
for id in [first, second] {
assert_eq!(store.fetch(id, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
assert!(is_tombstone_of(&store, &older.join(archive_name(id)), &digest));
}
gone(words);
let before = snapshot(&dir);
assert_eq!(store.forget(first).unwrap(), forgotten(&[]));
assert_eq!(snapshot(&dir), before);
let words = "one id on two shelves";
let twice = store.seal(&text(words), "").unwrap();
let name = archive_name(&twice.id);
std::fs::create_dir(&newer).unwrap();
std::fs::copy(older.join(&name), newer.join(&name)).unwrap();
assert_eq!(store.search(words, 8).unwrap().len(), 2);
assert_eq!(store.forget(&twice.id).unwrap(), forgotten(&[&twice.id, &twice.id]));
for shelf in [&older, &newer] {
assert!(is_tombstone_of(&store, &shelf.join(&name), &digest_of(words)));
}
gone(words);
let words = "in a slot and on a shelf";
let slot = seal_legacy_slot(&store, 0, &text(words), "");
let digest = digest_of(words);
let copy = record_bytes(&store, &digest[32..], &text(words), |_| {});
std::fs::write(older.join(archive_name(&digest[32..])), copy).unwrap();
let done = store.forget(&slot.id).unwrap();
assert_eq!(done, forgotten(&[&slot.id, &digest[32..]]));
assert!(is_tombstone_of(&store, &dir.join("000.json"), &digest));
gone(words);
assert!(temporary_files(&dir).is_empty());
}
#[test]
fn a_forget_leaves_a_tombstone_where_a_seal_looks() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let shelf = dir.join("s000001");
let imported = "0123456789abcdef0123456789abcdef";
let words = "only in a slot";
let digest = digest_of(words);
let slot = seal_legacy_slot(&store, 0, &text(words), "");
assert_eq!(store.forget(&slot.id).unwrap(), forgotten(&[&slot.id]));
let guard = shelf.join(archive_name(&digest[..32]));
assert!(is_tombstone_of(&store, &guard, &digest));
assert!(is_tombstone_of(&store, &dir.join("000.json"), &digest));
std::fs::write(dir.join("000.json"), b"{\"version\":1,").unwrap();
let refused = store.seal(&text(words), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied);
std::fs::remove_file(dir.join("000.json")).unwrap();
let words = "under an imported id";
let digest = digest_of(words);
let file = shelf.join(archive_name(imported));
std::fs::write(&file, record_bytes(&store, imported, &text(words), |_| {})).unwrap();
std::fs::write(shelf.join(archive_name(&digest[..32])), b"not a record").unwrap();
assert_eq!(store.fetch(imported, 0, 1).unwrap(), text(words));
let failing = Fault::on("rename", imported, libc::EIO);
assert_eq!(store.forget(imported).unwrap_err().raw_os_error(), Some(libc::EIO));
drop(failing);
let guard = shelf.join(archive_name(&digest[32..]));
assert!(is_tombstone_of(&store, &guard, &digest));
assert_eq!(store.fetch(imported, 0, 1).unwrap(), text(words));
let refused = store.seal(&text(words), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied);
let files = shelf_files(&dir).len();
let place = format!("1 place(s) could not be read, first s000001/{}: ", archive_name(&digest[..32]));
let done = store.forget(imported).unwrap();
assert_eq!(done.ids, [imported]);
assert!(done.unread.unwrap().starts_with(&place));
assert!(is_tombstone_of(&store, &file, &digest));
assert_eq!(shelf_files(&dir).len(), files);
assert_eq!(std::fs::read(shelf.join(archive_name(&digest[..32]))).unwrap(), b"not a record");
std::fs::remove_file(&guard).unwrap();
let done = store.forget(imported).unwrap();
assert!(done.ids.is_empty());
assert!(done.unread.unwrap().starts_with(&place));
assert!(is_tombstone_of(&store, &guard, &digest));
let native = store.seal(&text("under its own id"), "").unwrap();
let files = shelf_files(&dir).len();
assert_eq!(store.forget(&native.id).unwrap(), forgotten(&[&native.id]));
assert_eq!(shelf_files(&dir).len(), files);
assert!(temporary_files(&dir).is_empty());
}
#[test]
fn a_forget_says_which_places_it_could_not_read() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let (older, newer) = (dir.join("s000001"), dir.join("s000002"));
let words = "one id on two shelves";
let twice = store.seal(&text(words), "").unwrap();
let name = archive_name(&twice.id);
std::fs::create_dir(&newer).unwrap();
std::fs::copy(older.join(&name), newer.join(&name)).unwrap();
let failing = Fault::on("open", "s000001", libc::EIO);
let done = store.forget(&twice.id).unwrap();
drop(failing);
assert_eq!(done.ids, std::slice::from_ref(&twice.id));
let unread = done.unread.unwrap();
assert!(unread.starts_with("1 place(s) could not be read, first s000001: "), "{unread}");
assert!(is_tombstone_of(&store, &newer.join(&name), &digest_of(words)));
let rows = store.search(words, 8).unwrap();
assert_eq!(rows.len(), 1, "the copy that was not reached is still found");
assert_eq!(store.forget(&twice.id).unwrap(), forgotten(&[&twice.id]));
assert!(store.search(words, 8).unwrap().is_empty());
let words = "beside a file with two names";
let digest = digest_of(words);
let sealed = store.seal(&text(words), "").unwrap();
let linked = newer.join(archive_name(&digest[32..]));
std::fs::hard_link(newer.join(archive_name(&sealed.id)), tmp.path().join("a-second-name")).unwrap();
std::fs::write(&linked, b"x").unwrap();
std::fs::hard_link(&linked, tmp.path().join("another-second-name")).unwrap();
let error = store.forget(&sealed.id).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "its own file has two names");
std::fs::remove_file(tmp.path().join("a-second-name")).unwrap();
let done = store.forget(&sealed.id).unwrap();
assert_eq!(done.ids, std::slice::from_ref(&sealed.id));
let place = format!("first s000002/{}: ", archive_name(&digest[32..]));
assert!(done.unread.unwrap().contains(&place));
assert_eq!(std::fs::read(&linked).unwrap(), b"x");
}
#[test]
fn a_lookup_by_id_passes_over_a_file_that_is_not_a_record_to_an_older_shelf() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let (older, newer) = (dir.join("s000001"), dir.join("s000002"));
std::fs::create_dir(&older).unwrap();
std::fs::create_dir(&newer).unwrap();
std::fs::write(tmp.path().join("elsewhere"), b"not in the store").unwrap();
let base = tmp.path();
for link in [false, true] {
let words = format!("behind an odd file of its name, link {link}");
let digest = digest_of(&words);
let sealed = store.seal_with_note(&text(&words), "its note").unwrap().0;
let name = archive_name(&sealed.id);
assert_eq!(sealed.id, digest[..32]);
std::fs::rename(newer.join(&name), older.join(&name)).unwrap();
if link {
symlink(tmp.path().join("elsewhere"), newer.join(&name)).unwrap();
} else {
std::fs::write(newer.join(&name), b"not a record").unwrap();
}
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text(&words));
assert_eq!(store.fetch_note(&sealed.id).unwrap(), "its note");
let file = archive_file(base, "synthetic-project", &sealed.id).unwrap();
assert_eq!(file, Some(older.join(&name)));
let done = store.forget(&sealed.id).unwrap();
assert_eq!(done.ids, std::slice::from_ref(&sealed.id));
let unread = done.unread.unwrap();
let place = format!("1 place(s) could not be read, first s000002/{name}: ");
assert!(unread.starts_with(&place), "{unread}");
assert!(is_tombstone_of(&store, &older.join(&name), &digest));
let kind = std::fs::symlink_metadata(newer.join(&name)).unwrap().file_type();
assert_eq!(kind.is_symlink(), link, "the odd file is left as it is");
let error = store.fetch(&sealed.id, 0, 1).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
let before = snapshot(&dir);
let refused = store.seal(&text(&words), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(snapshot(&dir), before);
assert!(store.search(&words, 8).unwrap().is_empty());
}
let lone = "0123456789abcdef0123456789abcdef";
std::fs::write(newer.join(archive_name(lone)), b"not a record").unwrap();
std::fs::write(older.join(archive_name(lone)), b"{}").unwrap();
let named = format!(
"the newest file under this id is not its record: s000002/{}: invalid archive record",
archive_name(lone)
);
for error in [store.fetch(lone, 0, 1).unwrap_err(), store.forget(lone).unwrap_err()] {
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
assert_eq!(error.to_string(), named);
}
assert!(archive_file(base, "synthetic-project", lone).is_err());
let words = "behind a file that cannot be read now";
let sealed = store.seal(&text(words), "").unwrap();
let name = archive_name(&sealed.id);
std::fs::copy(newer.join(&name), older.join(&name)).unwrap();
let failing = Fault::on("open", &sealed.id, libc::EIO);
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap_err().raw_os_error(), Some(libc::EIO));
assert_eq!(store.forget(&sealed.id).unwrap_err().raw_os_error(), Some(libc::EIO));
drop(failing);
let words = "behind a damaged tombstone";
let digest = digest_of(words);
let sealed = store.seal(&text(words), "").unwrap();
let name = archive_name(&sealed.id);
std::fs::rename(newer.join(&name), older.join(&name)).unwrap();
let damaged = record_bytes(&store, &sealed.id, &text(words), |record| {
record.body = Body::Tombstone;
record.version = 2;
});
std::fs::write(newer.join(&name), damaged).unwrap();
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
assert_eq!(store.fetch_note(&sealed.id).unwrap_err().kind(), io::ErrorKind::NotFound);
assert!(archive_file(base, "synthetic-project", &sealed.id).is_err());
assert_eq!(store.forget(&sealed.id).unwrap(), forgotten(&[&sealed.id]));
assert!(is_tombstone_of(&store, &older.join(&name), &digest));
}
#[test]
fn a_forget_is_not_complete_over_a_damaged_copy_under_the_windows_name() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let shelf = dir.join("s000001");
let words = "a window with a damaged copy";
let digest = digest_of(words);
let (first, second) = (&digest[..32], &digest[32..]);
assert_eq!(store.seal(&text(words), "").unwrap().id, first);
let copy = record_bytes(&store, second, &text(words), |_| {});
let damaged = ©[..copy.len() - 2];
assert!(String::from_utf8_lossy(damaged).contains(words));
let file = shelf.join(archive_name(second));
std::fs::write(&file, damaged).unwrap();
let done = store.forget(first).unwrap();
assert_eq!(done.ids, [first]);
assert_ne!(done, forgotten(&[first]), "not a plain 'forgot'");
let unread = done.unread.unwrap();
let place = format!("1 place(s) could not be read, first s000001/{}: ", archive_name(second));
assert!(unread.starts_with(&place), "{unread}");
assert!(is_tombstone_of(&store, &shelf.join(archive_name(first)), &digest));
assert_eq!(std::fs::read(&file).unwrap(), damaged, "left as it is");
assert!(store.forget(first).unwrap().unread.is_some());
let other = record_bytes(&store, second, &text("another window"), |_| {});
std::fs::write(&file, other).unwrap();
assert_eq!(store.forget(first).unwrap(), forgotten(&[]));
let tombstone = record_bytes(&store, second, &text(words), |record| {
record.body = Body::Tombstone;
record.version = 2;
});
std::fs::write(&file, tombstone).unwrap();
assert_eq!(store.forget(first).unwrap(), forgotten(&[]));
}
const PASSING: [i32; 5] = [libc::EMFILE, libc::ENFILE, libc::ENOMEM, libc::EINTR, libc::EAGAIN];
#[test]
fn a_passing_error_on_a_shelf_or_a_slot_fails_the_seal_and_changes_nothing() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let slot = seal_legacy_slot(&store, 0, &text("in a slot"), "");
store.forget(&slot.id).unwrap();
std::fs::remove_file(only_archive_file(&dir)).unwrap();
let kept = store.seal(&text("kept"), "").unwrap();
let gone = store.seal(&text("forgotten"), "").unwrap();
store.forget(&gone.id).unwrap();
std::fs::create_dir(dir.join("s000002")).unwrap();
let before = snapshot(&dir);
let entries = || std::fs::read_dir(&dir).unwrap().count();
let listed = entries();
let refused = |words: &str, code: i32| {
let error = store.seal(&text(words), "").unwrap_err();
assert_eq!(error.raw_os_error(), Some(code), "{words}: {error}");
};
for code in PASSING {
assert!(PrivateDir::passing(&io::Error::from_raw_os_error(code)));
for place in ["s000001", "s000002", "s", "000.json"] {
let failing = Fault::on("open", place, code);
if place != "000.json" {
refused("kept", code);
refused("forgotten", code);
}
refused("in a slot", code);
refused("something new", code);
drop(failing);
assert_eq!(snapshot(&dir), before, "{place}, error {code}");
assert_eq!(entries(), listed, "{place}, error {code}: a shelf was started");
}
}
assert_eq!(store.seal(&text("kept"), "").unwrap(), kept);
for words in ["forgotten", "in a slot"] {
let error = store.seal(&text(words), "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{words}");
}
assert_eq!(snapshot(&dir), before);
}
#[test]
fn an_io_error_on_a_shelf_or_a_slot_is_passed_over() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
seal_legacy_slot(&store, 0, &text("in a slot"), "");
let kept = store.seal(&text("kept"), "").unwrap();
for code in [libc::EIO, libc::EACCES, libc::ENOTDIR, libc::ELOOP, libc::ESTALE] {
assert!(!PrivateDir::passing(&io::Error::from_raw_os_error(code)));
}
std::fs::create_dir(dir.join("s000002")).unwrap();
let failing = Fault::on("open", "s000001", libc::EIO);
let new = store.seal(&text("something new"), "").unwrap();
assert!(dir.join("s000002").join(archive_name(&new.id)).is_file());
assert_eq!(store.seal(&text("kept"), "").unwrap(), kept);
assert!(dir.join("s000002").join(archive_name(&kept.id)).is_file());
drop(failing);
let failing = Fault::on("open", "s000002", libc::EIO);
let next = store.seal(&text("after the newest"), "").unwrap();
assert!(dir.join("s000003").join(archive_name(&next.id)).is_file());
drop(failing);
let _failing = Fault::on("open", "000.json", libc::EIO);
let again = store.seal(&text("in a slot"), "").unwrap();
assert!(dir.join("s000003").join(archive_name(&again.id)).is_file());
}
#[test]
fn a_failed_attempt_leaves_at_most_one_empty_shelf_and_the_next_seal_uses_it() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
store.seal(&text("a lock to begin with"), "").unwrap();
std::fs::write(dir.join("s000002"), b"not a directory").unwrap();
let shelves = || {
let mut names: Vec<_> = std::fs::read_dir(&dir).unwrap().collect();
names.retain(|entry| entry.as_ref().unwrap().file_name() != "lock");
names.len()
};
let failing = Fault::on("open", "s000003", libc::EMFILE);
for _ in 0..3 {
let error = store.seal(&text("after the trouble"), "").unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::EMFILE));
assert_eq!(shelves(), 3, "s000001, the file s000002 and the empty s000003");
}
assert_eq!(std::fs::read_dir(dir.join("s000003")).unwrap().count(), 0);
drop(failing);
let sealed = store.seal(&text("after the trouble"), "").unwrap();
assert!(dir.join("s000003").join(archive_name(&sealed.id)).is_file());
assert_eq!(shelves(), 3);
}
#[test]
fn multi_mib_context_roundtrips_without_truncation() {
let (_tmp, store) = setup();
let text = "eligible exact evidence\n".repeat(400_000);
assert!(text.len() > 8 * 1024 * 1024);
let messages = vec![msg("user", json!(text))];
let reference = store.seal(&messages, "large source window").unwrap();
assert_eq!(store.fetch(&reference.id, 0, 1).unwrap(), messages);
assert_eq!(store.seal(&messages, "retry").unwrap(), reference);
}
#[test]
fn private_reasoning_disguised_as_assistant_text_is_omitted() {
let (_tmp, store) = setup();
let messages = vec![
Arc::new(json!({"role":"assistant","channel":"analysis","content":"private analysis"})),
Arc::new(
json!({"role":"assistant","content_class":"private_reasoning","content":"private reasoning"}),
),
Arc::new(json!({"role":"assistant","private":true,"content":"private text"})),
Arc::new(json!({"role":"assistant","channel":"final","content":"public evidence"})),
];
let reference = store.seal(&messages, "").unwrap();
assert_eq!(reference.message_count, 1);
let fetched = store.fetch_with_provenance(&reference.id, 0, 1).unwrap();
assert_eq!(fetched[0].source_index, 3);
assert_eq!(fetched[0].message["content"], "public evidence");
}
#[test]
fn after_redaction_requires_host_redactor_including_parent_class() {
let (_tmp, store) = setup();
let messages = vec![Arc::new(json!({
"role":"assistant", "disclosure":"model_visible_after_redaction",
"content":[{"type":"text", "text":"sensitive host classified material"}]
}))];
let reference = store.seal(&messages, "").unwrap();
let fetched = store.fetch(&reference.id, 0, 1).unwrap();
assert_eq!(fetched[0]["content"][0]["text"], WITHHELD);
}
#[test]
fn bounded_search_utf8() {
let (_tmp, store) = setup();
for index in 0..MAX_SEARCH_RESULTS + 8 {
let words = format!("{index} {}", "é".repeat(200));
store.seal(&text(&words), "").unwrap();
}
let descriptors = store.search("", MAX_SEARCH_RESULTS).unwrap();
assert_eq!(descriptors.len(), MAX_SEARCH_RESULTS);
assert!(descriptors.iter().all(|d| d.snippet.len() <= 256));
}
#[test]
fn seal_129_succeed() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let seal = |index: usize| store.seal(&text(&format!("archive {index}")), "").unwrap();
let mut ids = vec![seal(0).id];
let first = only_archive_file(&dir);
let bytes = std::fs::read(&first).unwrap();
ids.extend((1..=SHELF_FILES).map(|index| seal(index).id));
assert_eq!(ids.len(), 129);
assert_eq!(first, dir.join("s000001").join(archive_name(&ids[0])));
for (index, id) in ids.iter().enumerate() {
assert_eq!(
store.fetch(id, 0, 1).unwrap(),
text(&format!("archive {index}"))
);
}
assert_eq!(std::fs::read(&first).unwrap(), bytes);
let files = shelf_files(&dir);
assert_eq!(files.len(), 129);
assert_eq!(files[128], dir.join("s000002").join(archive_name(&ids[128])));
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 3, "lock and two shelves");
store.forget(&ids[0]).unwrap();
let next = seal(129);
assert_eq!(shelf_files(&dir).len(), 130);
assert!(dir.join("s000002").join(archive_name(&next.id)).is_file());
}
#[test]
#[ignore = "seals 10,000 archives (30,000 fsyncs): 4 s on tmpfs, far longer on a real disk"]
fn seal_10000_succeed() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let seal = |index: usize| store.seal(&text(&format!("archive {index}")), "").unwrap();
let ids: Vec<_> = (0..10_000).map(|index| seal(index).id).collect();
let shelves = store.shelves().unwrap();
assert_eq!(shelves.len(), 79);
assert_eq!(shelves[0], "s000079");
assert_eq!(shelf_files(&dir).len(), 10_000);
for index in [0, 9_999] {
assert_eq!(
store.fetch(&ids[index], 0, 1).unwrap(),
text(&format!("archive {index}"))
);
}
let started = Instant::now();
store.fetch(&ids[0], 0, 1).unwrap();
eprintln!("fetch of the oldest of 10,000: {:?}", started.elapsed());
}
#[test]
fn no_seal_replaces_a_file() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
seal_legacy_slot(&store, 0, &text("legacy kept"), "");
let forgotten = seal_legacy_slot(&store, 1, &text("legacy forgotten"), "");
store.forget(&forgotten.id).unwrap();
let mut before = snapshot(&dir);
assert_eq!(before.len(), 4, "lock, two legacy slots and the forget's tombstone on a shelf");
for index in 0..300 {
store.seal(&text(&format!("archive {index}")), "").unwrap();
let after = snapshot(&dir);
for (path, hash) in &before {
assert_eq!(after.get(path), Some(hash), "seal {index} touched {path:?}");
}
assert_eq!(after.len(), before.len() + 1);
before = after;
}
store.seal(&text("archive 7"), "changed note").unwrap();
assert!(store.seal(&text("legacy forgotten"), "").is_err());
assert_eq!(snapshot(&dir), before);
}
#[test]
#[ignore = "writes and re-reads about 280 MiB: about 55 s in a debug build"]
fn byte_cap_is_gone() {
let (tmp, store) = setup();
let large = |index: usize| {
let filler = "x".repeat(15 * 1024 * 1024 + 512 * 1024);
text(&format!("archive-{index:02} {filler}"))
};
let ids: Vec<_> = (0..18)
.map(|index| store.seal(&large(index), "").unwrap().id)
.collect();
let files = shelf_files(&directory(&tmp, &store));
let stored: u64 = files.iter().map(|f| f.metadata().unwrap().len()).sum();
assert!(stored > SEARCH_SCAN_BYTES as u64 + MAX_SEGMENT_BYTES as u64);
for (index, id) in ids.iter().enumerate() {
let fetched = store.fetch(id, 0, 1).unwrap();
let content = fetched[0]["content"].as_str().unwrap();
assert!(content.starts_with(&format!("archive-{index:02} ")));
assert_eq!(content.len(), large(index)[0]["content"].as_str().unwrap().len());
}
let (rows, stopped) = store.search_scan("archive-00", 1).unwrap();
assert!(rows.is_empty());
assert_eq!(stopped, Some(Unscanned { stopped_after: Some(17), unread: None }));
let (rows, stopped) = store.search_scan("archive-17", 1).unwrap();
assert_eq!((rows[0].id.as_str(), stopped), (ids[17].as_str(), None));
}
#[test]
fn legacy_slots_read_by_new_code() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let a = seal_legacy_slot(&store, 0, &text("legacy alpha"), "note a");
let b = seal_legacy_slot(&store, 1, &text("legacy beta"), "note b");
let c = seal_legacy_slot(&store, 2, &text("legacy gamma"), "note c");
store.forget(&c.id).unwrap();
let slots = ["000.json", "001.json", "002.json"].map(|name| dir.join(name));
assert!(std::fs::read_to_string(&slots[2]).unwrap().contains("tombstone"));
let kept = std::fs::read(&slots[1]).unwrap();
assert_eq!(store.fetch(&a.id, 0, 1).unwrap(), text("legacy alpha"));
assert_eq!(store.fetch_note(&b.id).unwrap(), "note b");
assert_eq!(
store.fetch(&c.id, 0, 1).unwrap_err().kind(),
io::ErrorKind::NotFound
);
let found = |query: &str| -> Vec<String> {
let rows = store.search(query, 10).unwrap();
rows.into_iter().map(|row| row.id).collect()
};
assert_eq!(found("legacy"), [b.id.as_str(), a.id.as_str()]);
assert_eq!(
archive_file(tmp.path(), "synthetic-project", &b.id).unwrap(),
Some(slots[1].clone())
);
store.forget(&a.id).unwrap();
store.forget(&a.id).unwrap();
assert!(std::fs::read_to_string(&slots[0]).unwrap().contains("tombstone"));
assert_eq!(found(""), [b.id.as_str()]);
let shelf = dir.join("s000001");
let guards = ["legacy alpha", "legacy gamma"].map(|words| {
let digest = digest_of(words);
let guard = shelf.join(archive_name(&digest[..32]));
assert!(is_tombstone_of(&store, &guard, &digest), "{words}");
guard
});
let mut guards = guards.to_vec();
guards.sort();
assert_eq!(shelf_files(&dir), guards);
let new = store.seal(&text("after shelves"), "").unwrap();
assert!(shelf.join(archive_name(&new.id)).is_file());
assert_eq!(shelf_files(&dir).len(), 3);
assert_eq!(found(""), [new.id.as_str(), b.id.as_str()]);
assert_eq!(std::fs::read(&slots[1]).unwrap(), kept);
assert!(slots.iter().all(|slot| slot.is_file()));
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 5);
}
#[test]
fn legacy_full_store_accepts_seal() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let legacy: Vec<_> = (0..MAX_SEGMENTS)
.map(|slot| seal_legacy_slot(&store, slot, &text(&format!("legacy {slot}")), ""))
.collect();
let before = snapshot(&dir);
let new = store.seal(&text("the 129th"), "").unwrap();
assert_eq!(store.fetch(&new.id, 0, 1).unwrap(), text("the 129th"));
assert_eq!(
only_archive_file(&dir),
dir.join("s000001").join(archive_name(&new.id))
);
for slot in [0, MAX_SEGMENTS - 1] {
assert_eq!(
store.fetch(&legacy[slot].id, 0, 1).unwrap(),
text(&format!("legacy {slot}"))
);
}
let after = snapshot(&dir);
assert!(before.iter().all(|(path, hash)| after.get(path) == Some(hash)));
let rows = store.search("", 2).unwrap();
assert_eq!(rows[0].id, new.id);
assert_eq!(rows[1].id, legacy[MAX_SEGMENTS - 1].id);
}
#[test]
fn legacy_duplicate_and_tombstone_still_honoured() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let live = seal_legacy_slot(&store, 0, &text("sealed long ago"), "first note");
let gone = seal_legacy_slot(&store, 1, &text("forgotten long ago"), "");
store.forget(&gone.id).unwrap();
let (again, note) = store
.seal_with_note(&text("sealed long ago"), "second note")
.unwrap();
assert_eq!((again, note.as_str()), (live, "first note"));
assert_eq!(
store.seal(&text("forgotten long ago"), "").unwrap_err().kind(),
io::ErrorKind::PermissionDenied
);
let guard = only_archive_file(&dir);
assert!(is_tombstone_of(&store, &guard, &digest_of("forgotten long ago")));
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 4, "lock, two slots, one shelf");
}
#[test]
fn legacy_fetch_waits_for_the_lock() {
let (_tmp, mut store) = setup();
let legacy = seal_legacy_slot(&store, 0, &text("sealed long ago"), "old note");
let shelved = store.seal(&text("sealed today"), "").unwrap();
let hold = Duration::from_millis(300);
let started = Instant::now();
let foreign = store.fs.lock().unwrap();
let holder = std::thread::spawn(move || {
std::thread::sleep(hold);
drop(foreign);
});
assert_eq!(store.fetch(&legacy.id, 0, 1).unwrap(), text("sealed long ago"));
assert!(started.elapsed() >= hold);
holder.join().unwrap();
store.process_wait = Duration::from_millis(100);
let _foreign = store.fs.lock().unwrap();
let started = Instant::now();
for result in [
store.fetch(&legacy.id, 0, 1).map(drop),
store.fetch_note(&legacy.id).map(drop),
store.fetch(&"a".repeat(32), 0, 1).map(drop),
store.search("", 2).map(drop),
] {
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
assert_eq!(error.to_string(), BUSY);
}
assert!(started.elapsed() >= store.process_wait * 4);
assert_eq!(store.fetch(&shelved.id, 0, 1).unwrap(), text("sealed today"));
assert_eq!(store.search("", 1).unwrap()[0].id, shelved.id);
}
#[test]
fn duplicate_seal_returns_same_id_across_shelves() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let seal = |index: usize, note: &str| {
let words = format!("archive {index}");
store.seal_with_note(&text(&words), note).unwrap()
};
let first = seal(0, "first note");
for index in 1..=SHELF_FILES {
seal(index, "");
}
let before = snapshot(&dir);
assert_eq!(shelf_files(&dir).len(), 129);
assert_eq!(seal(0, "another note"), first);
assert_eq!(seal(SHELF_FILES, "another note").1, "");
assert_eq!(snapshot(&dir), before);
assert_eq!(first.0.id.len(), 32);
let digest = project_archive("synthetic-logical", &text("archive 0"), "", None)
.unwrap()
.digest;
assert_eq!(first.0.id, digest[..32]);
}
#[test]
fn forgotten_projection_cannot_be_resealed() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let reference = store.seal(&text("forget me"), "note").unwrap();
store.forget(&reference.id).unwrap();
let before = snapshot(&dir);
for note in ["note", "another note"] {
assert_eq!(
store.seal(&text("forget me"), note).unwrap_err().kind(),
io::ErrorKind::PermissionDenied
);
}
assert_eq!(snapshot(&dir), before);
let other = ArchiveStore::new(tmp.path(), "synthetic-project", "other-logical").unwrap();
assert!(other.seal(&text("forget me"), "").is_ok());
assert!(store.seal(&text("remember me"), "").is_ok());
}
#[test]
fn forget_is_in_place_and_idempotent() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let kept = store.seal(&text("keep me"), "").unwrap();
let reference = store.seal(&text("forget me"), "note").unwrap();
let file = dir.join("s000001").join(archive_name(&reference.id));
let neighbour = dir.join("s000001").join(archive_name(&kept.id));
let untouched = snapshot(&dir)[&neighbour].clone();
store.forget(&reference.id).unwrap();
assert_eq!(shelf_files(&dir).len(), 2, "same name, same shelf");
let tombstone = std::fs::read_to_string(&file).unwrap();
assert!(tombstone.contains("tombstone") && !tombstone.contains("forget me"));
let written = file.metadata().unwrap().modified().unwrap();
store.forget(&reference.id).unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), tombstone);
assert_eq!(file.metadata().unwrap().modified().unwrap(), written);
assert!(temporary_files(&dir).is_empty());
assert_eq!(std::fs::read_dir(dir.join("s000001")).unwrap().count(), 2);
assert_eq!(snapshot(&dir)[&neighbour], untouched);
assert_eq!(store.fetch(&kept.id, 0, 1).unwrap(), text("keep me"));
assert_eq!(
store.forget(&"a".repeat(32)).unwrap_err().kind(),
io::ErrorKind::NotFound
);
}
#[test]
fn fetch_reads_one_file() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let ids: Vec<_> = (0..300)
.map(|index| store.seal(&text(&format!("archive {index}")), "note").unwrap().id)
.collect();
let target = dir.join("s000001").join(archive_name(&ids[5]));
for file in shelf_files(&dir) {
if file != target {
std::fs::write(file, b"corrupt").unwrap();
}
}
assert_eq!(store.fetch(&ids[5], 0, 1).unwrap(), text("archive 5"));
assert_eq!(store.fetch_note(&ids[5]).unwrap(), "note");
assert_eq!(store.seal(&text("archive 5"), "").unwrap().id, ids[5]);
assert!(store.seal(&text("archive 300"), "").is_ok());
store.forget(&ids[5]).unwrap();
assert_eq!(
store.fetch(&ids[6], 0, 1).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn search_is_newest_first_and_budgeted() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let epoch = std::time::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
let seal = |index: usize| {
let words = match index {
0 => "needle oldest-only".to_owned(),
_ => format!("needle {index}"),
};
let id = store.seal(&text(&words), "").unwrap().id;
let shelf = dir.join(shelf_name((index / SHELF_FILES + 1) as u64));
let file = std::fs::File::options()
.write(true)
.open(shelf.join(archive_name(&id)))
.unwrap();
file.set_modified(epoch + Duration::from_secs(index as u64))
.unwrap();
id
};
let mut ids: Vec<_> = (0..100).map(&seal).collect();
let (rows, stopped) = store.search_scan("oldest-only", 32).unwrap();
assert_eq!((rows.len(), rows[0].id.as_str(), stopped), (1, ids[0].as_str(), None));
ids.extend((100..300).map(&seal));
let rows = store.search("needle", 32).unwrap();
let newest: Vec<_> = rows.iter().map(|row| row.id.as_str()).collect();
let expected: Vec<_> = ids.iter().rev().take(32).map(String::as_str).collect();
assert_eq!(newest, expected);
assert_eq!(store.search_scan("needle", 32).unwrap().1, None);
ids.extend((300..SEARCH_SCAN_FILES).map(&seal));
let (rows, stopped) = store.search_scan("oldest-only", 32).unwrap();
assert_eq!((rows.len(), stopped), (1, None));
ids.push(seal(SEARCH_SCAN_FILES));
let (rows, stopped) = store.search_scan("oldest-only", 32).unwrap();
assert!(rows.is_empty());
let budget = Unscanned { stopped_after: Some(SEARCH_SCAN_FILES), unread: None };
assert_eq!(stopped, Some(budget));
assert_eq!(store.search("needle", 1).unwrap()[0].id, ids[SEARCH_SCAN_FILES]);
assert_eq!(store.fetch(&ids[0], 0, 1).unwrap(), text("needle oldest-only"));
}
#[test]
fn search_reads_files_with_the_same_modification_time_in_name_order() {
let (tmp, store) = setup();
let shelf = directory(&tmp, &store).join("s000001");
let ids: Vec<_> = (0..8)
.map(|index| store.seal(&text(&format!("tied {index}")), "").unwrap().id)
.collect();
let epoch = std::time::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
let written = |id: &str, seconds: u64| {
let path = shelf.join(archive_name(id));
let file = std::fs::File::options().write(true).open(path).unwrap();
file.set_modified(epoch + Duration::from_secs(seconds)).unwrap();
};
for id in &ids[2..] {
written(id, 100);
}
written(&ids[0], 50);
written(&ids[1], 200);
let mut tied = ids[2..].to_vec();
tied.sort();
assert_ne!(tied, ids[2..], "the order of sealing is not the order of names");
let mut expected = vec![ids[1].clone()];
expected.extend(tied);
expected.push(ids[0].clone());
let order = |limit: usize| -> Vec<String> {
let rows = store.search("tied", limit).unwrap();
rows.into_iter().map(|row| row.id).collect()
};
assert_eq!(order(8), expected);
for limit in 1..8 {
assert_eq!(order(limit), expected[..limit]);
assert_eq!(order(limit), expected[..limit]);
}
let late = store.seal(&text("tied late"), "").unwrap().id;
written(&late, 100);
expected.insert(1 + expected[1..7].partition_point(|id| *id < late), late);
assert_eq!(order(9), expected);
}
#[test]
fn search_skips_a_corrupt_file() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let ids: Vec<_> = ["first", "second", "third"]
.iter()
.map(|words| store.seal(&text(words), "").unwrap().id)
.collect();
let legacy = seal_legacy_slot(&store, 0, &text("legacy first"), "");
seal_legacy_slot(&store, 1, &text("legacy second"), "");
std::fs::write(dir.join("s000001").join(archive_name(&ids[1])), b"corrupt").unwrap();
std::fs::write(dir.join("001.json"), b"{}").unwrap();
let huge = std::fs::File::create(dir.join("s000001").join(archive_name(&"f".repeat(32))));
huge.unwrap().set_len(MAX_SEGMENT_BYTES as u64 + 1).unwrap();
std::fs::write(dir.join("s000001").join("notes.json"), b"stray").unwrap();
std::fs::write(dir.join("s0000001"), b"stray").unwrap();
let (rows, unscanned) = store.search_scan("", 10).unwrap();
let mut found: Vec<_> = rows.into_iter().map(|row| row.id).collect();
found.sort();
let mut expected = vec![ids[0].clone(), ids[2].clone(), legacy.id];
expected.sort();
assert_eq!(found, expected);
let unscanned = unscanned.unwrap();
assert_eq!(unscanned.stopped_after, None);
let unread = unscanned.unread.unwrap();
assert!(unread.starts_with("3 place(s) could not be read, first s000001/"), "{unread}");
assert_eq!(store.search("second", 10).unwrap(), []);
}
#[test]
fn a_search_passes_over_what_it_cannot_read_and_says_so() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let old = store.seal(&text("on the oldest shelf"), "").unwrap();
let slot = seal_legacy_slot(&store, 0, &text("in a slot"), "");
let mut all = vec![old.id.clone(), slot.id.clone()];
let searched = |all: &[String], limit: usize| {
let (rows, unscanned) = store.search_scan("", limit).unwrap();
let mut found: Vec<_> = rows.into_iter().map(|row| row.id).collect();
found.sort();
let mut all = all.to_vec();
all.sort();
if limit >= all.len() {
assert_eq!(found, all);
} else {
assert_eq!(found.len(), limit);
}
match unscanned {
Some(unscanned) => {
assert_eq!(unscanned.stopped_after, None);
unscanned.unread.unwrap()
}
None => String::new(),
}
};
assert_eq!(searched(&all, 8), "");
std::fs::write(dir.join("s000002"), b"not a directory").unwrap();
let unread = searched(&all, 8);
assert!(unread.starts_with("1 place(s) could not be read, first s000002: "), "{unread}");
let new = store.seal(&text("on the newest shelf"), "").unwrap();
let newest = dir.join("s000003");
assert!(newest.join(archive_name(&new.id)).is_file());
all.push(new.id.clone());
std::fs::remove_file(dir.join("s000002")).unwrap();
symlink(&newest, dir.join("s000002")).unwrap();
let unread = searched(&all, 8);
assert!(unread.starts_with("1 place(s) could not be read, first s000002: "), "{unread}");
let victim = tmp.path().join("victim");
std::fs::write(&victim, b"do not change").unwrap();
let odd = |letter: &str| newest.join(archive_name(&letter.repeat(32)));
symlink(&victim, odd("a")).unwrap();
let fifo = std::ffi::CString::new(odd("b").to_str().unwrap()).unwrap();
assert_eq!(unsafe { libc::mkfifo(fifo.as_ptr(), 0o600) }, 0);
std::fs::write(odd("c"), record_bytes(&store, &"c".repeat(32), &text("two names"), |_| {})).unwrap();
std::fs::hard_link(odd("c"), tmp.path().join("a-second-name")).unwrap();
std::fs::write(odd("d"), b"corrupt").unwrap();
let unread = searched(&all, 8);
assert!(unread.starts_with("5 place(s) could not be read, first s000003/"), "{unread}");
symlink(&victim, dir.join("001.json")).unwrap();
let unread = searched(&all, 8);
assert!(unread.starts_with("6 place(s) could not be read, first s000003/"), "{unread}");
let rest = [old.id.clone(), slot.id.clone()];
let failing = Fault::on("open", "s000003", libc::EMFILE);
let unread = searched(&rest, 8);
assert!(unread.starts_with("3 place(s) could not be read, first s000003: "), "{unread}");
let unread = searched(&rest, 1);
assert!(unread.starts_with("2 place(s) could not be read, first s000003: "), "{unread}");
drop(failing);
if unsafe { libc::geteuid() } != 0 {
let _reopen = Reopen(vec![newest.clone()]);
std::fs::set_permissions(&newest, std::fs::Permissions::from_mode(0o000)).unwrap();
let unread = searched(&rest, 8);
assert!(unread.starts_with("3 place(s) could not be read, first s000003: "), "{unread}");
}
assert_eq!(std::fs::read(&victim).unwrap(), b"do not change");
assert!(std::fs::symlink_metadata(odd("a")).unwrap().file_type().is_symlink());
assert_eq!(std::fs::read(odd("d")).unwrap(), b"corrupt");
assert_eq!(store.search("", 8).unwrap().len(), 3);
}
#[test]
fn file_name_must_match_record_id() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let a = store.seal(&text("the original"), "").unwrap();
let b = "b".repeat(32);
let copy = dir.join("s000001").join(archive_name(&b));
std::fs::copy(only_archive_file(&dir), ©).unwrap();
for result in [
store.fetch(&b, 0, 1).map(drop),
store.fetch_note(&b).map(drop),
store.forget(&b).map(drop),
archive_file(tmp.path(), "synthetic-project", &b).map(drop),
] {
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
}
let original = dir.join("s000001").join(archive_name(&a.id));
assert_eq!(std::fs::read(©).unwrap(), std::fs::read(original).unwrap());
let rows = store.search("", 10).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].id, a.id);
assert_eq!(store.fetch(&a.id, 0, 1).unwrap(), text("the original"));
}
#[test]
fn shelf_gap_is_tolerated() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let seal = |index: usize| store.seal(&text(&format!("archive {index}")), "").unwrap().id;
let ids: Vec<_> = (0..2 * SHELF_FILES + 1).map(seal).collect();
assert_eq!(store.shelves().unwrap(), ["s000003", "s000002", "s000001"]);
std::fs::remove_dir_all(dir.join("s000002")).unwrap();
assert_eq!(store.fetch(&ids[256], 0, 1).unwrap(), text("archive 256"));
assert_eq!(store.fetch(&ids[0], 0, 1).unwrap(), text("archive 0"));
assert_eq!(
store.fetch(&ids[200], 0, 1).unwrap_err().kind(),
io::ErrorKind::NotFound
);
let next = seal(1_000);
assert!(dir.join("s000003").join(archive_name(&next)).is_file());
assert_eq!(store.shelves().unwrap(), ["s000003", "s000001"]);
assert_eq!(store.search("", 1).unwrap().len(), 1);
}
#[test]
fn crash_leftovers() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let first = store.seal(&text("before the crash"), "").unwrap();
let empty = dir.join("s000002");
std::fs::create_dir(&empty).unwrap();
let stale = [empty.join(temp_name()), empty.join("pending.tmp")];
let fresh = [empty.join(temp_name()), empty.join(temp_name())];
for path in stale.iter().chain(&fresh) {
std::fs::write(path, b"unpublished partial").unwrap();
}
let minute = Duration::from_secs(60);
written_ago(&stale[0], STALE_TEMP + minute);
written_ago(&stale[1], STALE_TEMP * 24);
written_ago(&fresh[1], STALE_TEMP - minute);
assert_eq!(store.search("", 10).unwrap().len(), 1);
assert_eq!(temporary_files(&dir).len(), 4);
let next = store.seal(&text("after the crash"), "").unwrap();
assert!(empty.join(archive_name(&next.id)).is_file());
assert!(stale.iter().all(|path| !path.exists()));
for path in &fresh {
assert_eq!(std::fs::read(path).unwrap(), b"unpublished partial");
}
assert_eq!(temporary_files(&dir).len(), 2);
assert_eq!(store.fetch(&first.id, 0, 1).unwrap(), text("before the crash"));
assert_eq!(store.fetch(&next.id, 0, 1).unwrap(), text("after the crash"));
assert_eq!(store.search("", 10).unwrap().len(), 2);
store.forget(&next.id).unwrap();
assert_eq!(temporary_files(&dir).len(), 2);
}
#[test]
fn a_temporary_name_that_is_a_directory_a_fifo_or_a_link_is_left_alone() {
use std::os::unix::fs::FileTypeExt;
fn aged(path: &Path) {
let then = std::time::SystemTime::now() - STALE_TEMP * 2;
let since = then.duration_since(std::time::UNIX_EPOCH).unwrap();
let mut time: libc::timespec = unsafe { std::mem::zeroed() };
time.tv_sec = since.as_secs() as libc::time_t;
let name = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let (times, flags) = ([time; 2], libc::AT_SYMLINK_NOFOLLOW);
let set = unsafe { libc::utimensat(libc::AT_FDCWD, name.as_ptr(), times.as_ptr(), flags) };
assert_eq!(set, 0);
}
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let first = store.seal(&text("first"), "").unwrap();
let legacy = seal_legacy_slot(&store, 0, &text("legacy"), "");
let victim = tmp.path().join("synthetic-victim");
std::fs::write(&victim, "do not change").unwrap();
written_ago(&victim, STALE_TEMP * 2);
let unchanged = snapshot(tmp.path());
let places = [dir.join("s000001").join("pending.tmp"), dir.join("pending.tmp")];
let kind = |path: &Path| std::fs::symlink_metadata(path).unwrap().file_type();
for shape in ["directory", "FIFO", "symlink", "hard link"] {
for path in &places {
match shape {
"directory" => std::fs::create_dir(path).unwrap(),
"FIFO" => {
let name = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
assert_eq!(unsafe { libc::mkfifo(name.as_ptr(), 0o600) }, 0);
}
"symlink" => symlink(&victim, path).unwrap(),
_ => std::fs::hard_link(&victim, path).unwrap(),
}
aged(path);
}
let words = format!("sealed beside a {shape}");
let sealed = store.seal(&text(&words), "").unwrap();
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text(&words), "{shape}");
assert_eq!(store.fetch(&first.id, 0, 1).unwrap(), text("first"), "{shape}");
assert_eq!(store.search(&words, 2).unwrap().len(), 1, "{shape}");
store.forget(&sealed.id).unwrap();
let old = seal_legacy_slot(&store, 1, &text(&words), "");
store.forget(&old.id).unwrap();
assert_eq!(store.fetch(&legacy.id, 0, 1).unwrap(), text("legacy"), "{shape}");
std::fs::remove_file(dir.join("001.json")).unwrap();
for path in &places {
let found = kind(path);
match shape {
"directory" => assert!(found.is_dir()),
"FIFO" => assert!(found.is_fifo()),
"symlink" => assert!(found.is_symlink()),
_ => assert_eq!(identity(path), identity(&victim)),
}
if found.is_dir() {
std::fs::remove_dir(path).unwrap();
} else {
std::fs::remove_file(path).unwrap();
}
}
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "do not change");
}
let after = snapshot(tmp.path());
assert!(unchanged.iter().all(|(path, file)| after.get(path) == Some(file)));
assert_eq!(after.len(), unchanged.len() + 4);
assert!(temporary_files(&dir).is_empty());
}
#[test]
fn publish_create_new_never_replaces() {
let (tmp, store) = setup();
let shelf_path = directory(&tmp, &store).join("s000001");
let shelf = store.fs.child("s000001", true).unwrap();
let name = archive_name(&"a".repeat(32));
let path = shelf_path.join(&name);
let victim = tmp.path().join("synthetic-victim");
std::fs::write(&victim, "do not change").unwrap();
shelf.publish(&name, b"first", true).unwrap();
let first = identity(&path);
let refused = |bytes: &[u8]| {
let error = shelf.publish(&name, bytes, true).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
assert_eq!(std::fs::read_dir(&shelf_path).unwrap().count(), 1, "no temporary file");
};
for bytes in [&b"first"[..], b"second"] {
refused(bytes);
assert_eq!(std::fs::read(&path).unwrap(), b"first");
assert_eq!(identity(&path), first, "replaced by a file with the same bytes");
}
std::fs::remove_file(&path).unwrap();
for target in [victim.clone(), tmp.path().join("missing")] {
symlink(&target, &path).unwrap();
let link = identity(&path);
refused(b"through the link");
assert_eq!(identity(&path), link);
assert!(!tmp.path().join("missing").exists());
std::fs::remove_file(&path).unwrap();
}
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "do not change");
std::fs::create_dir(&path).unwrap();
refused(b"over the directory");
assert!(path.is_dir());
std::fs::remove_dir(&path).unwrap();
let from = c_name(&temp_name()).unwrap();
let to = c_name(&name).unwrap();
let temporary = shelf_path.join(from.to_str().unwrap());
std::fs::write(&temporary, b"probed").unwrap();
let written = identity(&temporary);
shelf.rename_probed(&from, &to).unwrap();
assert_eq!((identity(&path), temporary.exists()), (written, false));
std::fs::write(&temporary, b"probed again").unwrap();
let error = shelf.rename_probed(&from, &to).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
assert_eq!(identity(&path), written);
assert_eq!(std::fs::read(&temporary).unwrap(), b"probed again");
shelf.publish(&name, b"tombstone", false).unwrap();
assert_ne!(identity(&path), written);
assert_eq!(std::fs::read(&path).unwrap(), b"tombstone");
}
#[test]
fn archive_file_returns_path_and_creates_nothing() {
let tmp = tempfile::tempdir().unwrap();
let unknown = "a".repeat(32);
for base in [tmp.path().join("missing"), tmp.path().to_path_buf()] {
assert_eq!(archive_file(&base, "synthetic-project", &unknown).unwrap(), None);
}
assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
let store =
ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-logical").unwrap();
let dir = directory(&tmp, &store);
let reference = store.seal(&text("where am I"), "").unwrap();
let file = only_archive_file(&dir);
let locate = |project: &str, id: &str| archive_file(tmp.path(), project, id);
let held = store.fs.lock().unwrap();
assert_eq!(locate("synthetic-project", &reference.id).unwrap(), Some(file.clone()));
drop(held);
std::fs::remove_file(dir.join("lock")).unwrap();
let before = snapshot(tmp.path());
assert_eq!(locate("synthetic-project", &reference.id).unwrap(), Some(file.clone()));
assert_eq!(locate("synthetic-project", &unknown).unwrap(), None);
assert_eq!(locate("another-project", &reference.id).unwrap(), None);
assert_eq!(
locate("synthetic-project", "../s000001/x").unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
assert_eq!(snapshot(tmp.path()), before);
assert_eq!(std::fs::read_dir(tmp.path().join("context-archives")).unwrap().count(), 1);
store.forget(&reference.id).unwrap();
assert_eq!(locate("synthetic-project", &reference.id).unwrap(), Some(file));
}
fn exact(store: &ArchiveStore, messages: &[SharedMessage]) -> Vec<ArchivedMessage> {
let mut expected = Vec::new();
for (index, message) in messages.iter().enumerate() {
let one = std::slice::from_ref(message);
let redactor = store.redactor.clone();
let alone = project_hashed(&store.logical, one, "", redactor, Reduce::None).unwrap();
expected.extend(alone.messages.into_iter().map(|mut archived| {
archived.source_index = index;
archived
}));
}
expected
}
fn reassemble(
store: &ArchiveStore,
window: &SealedWindow,
len: usize,
) -> Vec<ArchivedMessage> {
let mut messages = Vec::new();
let mut next = 0;
for part in &window.parts {
let ArchiveRef {
id,
message_count,
source_message_count,
} = &part.archive;
assert_eq!(part.first_source_index, next);
assert!(*source_message_count <= MAX_MESSAGES);
next += source_message_count;
let mut start = 0;
while start < *message_count {
let page = store
.fetch_with_provenance(id, start, MAX_FETCH_MESSAGES)
.unwrap();
assert!(!page.is_empty());
start += page.len();
messages.extend(page.into_iter().map(|mut archived| {
assert!(archived.source_index < *source_message_count);
archived.source_index += part.first_source_index;
archived
}));
}
assert_eq!(start, *message_count);
}
assert_eq!(next, len, "the parts cover the window");
messages
}
fn assert_whole(read: &[ArchivedMessage], messages: &[SharedMessage]) {
assert_eq!(read.len(), messages.len());
for (index, archived) in read.iter().enumerate() {
assert_eq!(archived.source_index, index);
assert!(archived.message == messages[index], "message {index} differs");
}
}
fn lines(len: usize) -> Vec<SharedMessage> {
(0..len)
.map(|index| msg("user", json!(format!("line {index} of {len}"))))
.collect()
}
fn deep_input() -> Value {
let mut deep = json!("leaf");
for _ in 0..40 {
deep = json!([deep]);
}
deep
}
const OVER_LIMIT: &str = "over the per-archive limit]";
#[test]
fn window_within_caps_is_one_part_identical_to_seal() {
let messages = vec![
msg("system", json!("not eligible")),
msg("user", json!("a question")),
msg("assistant", json!([{"type":"text","text":"an answer"}])),
];
let (strict_tmp, strict) = setup();
let (tmp, store) = setup();
let (reference, note) = strict.seal_with_note(&messages, "the note").unwrap();
let window = store.seal_window(&messages, "the note").unwrap();
let part = ArchivePart {
archive: reference.clone(),
first_source_index: 0,
reduced_messages: 0,
};
let expected = SealedWindow {
parts: vec![part],
note,
};
assert_eq!(window, expected);
let undated = |dir: PathBuf| {
let mut record: Value = serde_json::from_slice(&std::fs::read(dir).unwrap()).unwrap();
assert!(record["created_ms"].take().is_u64());
record
};
assert_eq!(
undated(only_archive_file(&directory(&tmp, &store))),
undated(only_archive_file(&directory(&strict_tmp, &strict)))
);
assert_eq!(strict.seal_window(&messages, "another note").unwrap(), window);
assert_eq!(
store.seal_with_note(&messages, "another note").unwrap(),
(reference, "the note".to_owned())
);
let empty = store.seal_window(&[], "").unwrap();
assert_eq!(empty.parts.len(), 1);
assert_eq!(empty.parts[0].archive, store.seal(&[], "").unwrap());
}
#[test]
fn window_over_message_cap_splits() {
let (tmp, store) = setup();
for (len, parts) in [(MAX_MESSAGES + 1, 2), (10_000, 3)] {
let messages: Vec<_> = (0..len)
.map(|index| {
let role = ["user", "assistant", "user", "system", "assistant"][index % 7 % 5];
msg(role, json!(format!("window of {len}, line {index}")))
})
.collect();
assert!(store.seal(&messages, "").is_err());
let window = store.seal_window(&messages, "").unwrap();
assert_eq!(window.parts.len(), parts);
assert!(window.parts.iter().all(|part| part.reduced_messages == 0));
let read = reassemble(&store, &window, len);
assert_eq!(read.len(), len - (len + 3) / 7);
assert!(read == exact(&store, &messages));
let starts: Vec<_> = window.parts.iter().map(|part| part.first_source_index).collect();
let full: Vec<_> = (0..parts).map(|part| part * MAX_MESSAGES).collect();
assert_eq!(starts, full);
}
assert_eq!(shelf_files(&directory(&tmp, &store)).len(), 5);
}
#[test]
fn window_over_byte_cap_splits() {
let (_tmp, store) = setup();
let messages: Vec<_> = (0..34)
.map(|index| msg("user", json!(format!("{index:02} {}", "x".repeat(512 * 1024)))))
.collect();
let error = store.seal(&messages, "").unwrap_err();
assert!(error.to_string().contains("input byte budget"));
let window = store.seal_window(&messages, "").unwrap();
assert_eq!(window.parts.len(), 2);
assert_whole(&reassemble(&store, &window, messages.len()), &messages);
}
#[test]
fn window_over_node_cap_splits() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let input: serde_json::Map<String, Value> = (0..40)
.map(|key| (format!("k{key:02}"), json!(key)))
.collect();
let messages: Vec<_> = (0..3000)
.map(|index| {
let id = format!("call-{index}");
msg(
"assistant",
json!([{"type":"tool_use","id":id,"name":"plot","input":input}]),
)
})
.collect();
let error = store.seal(&messages, "").unwrap_err();
assert!(error.to_string().contains("structural budget"));
let window = store.seal_window(&messages, "").unwrap();
let starts: Vec<_> = window.parts.iter().map(|part| part.first_source_index).collect();
assert_eq!(starts, [0, 1365, 2730]);
assert!(window.parts.iter().all(|part| part.reduced_messages == 0));
let read = reassemble(&store, &window, messages.len());
assert_eq!(read[2999].message, messages[2999]);
assert!(read == exact(&store, &messages));
}
#[test]
fn serialized_overflow_after_projection_splits() {
let (_tmp, store) = setup();
let messages: Vec<_> = (0..4)
.map(|index| msg("user", json!(format!("{index}{}", "\u{1}".repeat(768 * 1024)))))
.collect();
let error = store.seal(&messages, "").unwrap_err();
assert!(error.to_string().contains("serialization failed or exceeded budget"));
let window = store.seal_window(&messages, "").unwrap();
assert_eq!(window.parts.len(), 2);
assert_whole(&reassemble(&store, &window, messages.len()), &messages);
}
#[test]
fn single_deep_input_is_stored_as_its_json_text_not_left_out() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let messages = vec![
msg("user", json!("please run both")),
msg(
"assistant",
json!([
{"type":"text","text":"running both"},
{"type":"tool_use","id":"t1","name":"nest","input":deep_input()},
{"type":"tool_use","id":"t2","name":"read","input":{"path":"src/lib.rs"}}
]),
),
msg(
"user",
json!([
{"type":"tool_result","tool_use_id":"t1","content":"nested"},
{"type":"tool_result","tool_use_id":"t2","content":"fn main() {}"}
]),
),
msg("assistant", json!("both ran")),
];
assert!(store.seal(&messages, "").is_err());
let window = store.seal_window(&messages, "").unwrap();
let reduced: Vec<_> = window
.parts
.iter()
.map(|part| (part.first_source_index, part.reduced_messages))
.collect();
assert_eq!(reduced, [(0, 0), (1, 1), (2, 0)]);
let read = reassemble(&store, &window, messages.len());
for index in [0, 2, 3] {
assert_eq!(read[index].source_index, index);
assert_eq!(read[index].message, messages[index]);
}
let as_text = serde_json::to_string(&deep_input()).unwrap();
let expected = json!({
"role":"assistant",
"_archive_reduced":"this message alone is over the per-archive limit; it is stored \
with 1 tool input(s) as JSON text, each marked in place",
"content":[
{"type":"text","text":"running both"},
{"type":"tool_use","id":"t1","name":"nest","input":{"_archive_json":as_text}},
{"type":"tool_use","id":"t2","name":"read","input":{"path":"src/lib.rs"}}
]
});
assert_eq!(*read[1].message, expected);
assert_eq!(read[1].block_indices, [0, 1, 2]);
let stored = read[1].message["content"][1]["input"][INPUT_JSON_KEY].as_str().unwrap();
assert_eq!(serde_json::from_str::<Value>(stored).unwrap(), deep_input());
let found = store.search("running both", 1).unwrap();
assert!(found[0].snippet.starts_with("{\"_archive_reduced\":\"this message alone"));
}
fn sealed_calls(store: &ArchiveStore, text: &str, inputs: [&Value; 3]) -> SharedMessage {
let mut blocks = vec![json!({"type":"text","text":text})];
for (index, input) in inputs.into_iter().enumerate() {
let id = format!("t{}", index + 1);
blocks.push(json!({"type":"tool_use","id":id,"name":"plot","input":input}));
}
let window = store.seal_window(&[msg("assistant", json!(blocks))], "").unwrap();
assert_eq!(window.parts.len(), 1);
assert_eq!(window.parts[0].reduced_messages, 1);
store.fetch(&window.parts[0].archive.id, 0, 1).unwrap().remove(0)
}
#[test]
fn inputs_with_too_many_nodes_together_are_all_stored_as_text() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let wide = json!(vec![0; 30_000]);
let long = "exact words ".repeat(1000);
let stored = sealed_calls(&store, &long, [&wide; 3]);
assert_eq!(stored["content"][0]["text"], long);
let as_text = serde_json::to_string(&wide).unwrap();
for block in 1..=3 {
assert_eq!(stored["content"][block]["input"], json!({"_archive_json":as_text}));
}
assert_eq!(
stored[REDUCED_KEY],
"this message alone is over the per-archive limit; it is stored with 3 tool input(s) \
as JSON text, each marked in place"
);
}
#[test]
fn inputs_too_large_together_are_all_omitted_and_text_stays_exact() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let large = json!("x".repeat(6 * 1024 * 1024));
let long = "exact words ".repeat(1000);
let stored = sealed_calls(&store, &long, [&large; 3]);
assert_eq!(stored["content"][0]["text"], long);
let bytes = serde_json::to_string(&large).unwrap().len();
let omitted = format!("[archive: tool input of {bytes} bytes omitted, {OVER_LIMIT}");
for block in 1..=3 {
assert_eq!(stored["content"][block]["input"], json!({"_archive_omitted":omitted}));
}
assert!(stored[REDUCED_KEY].as_str().unwrap().ends_with("with 3 omission(s), each marked in place"));
}
#[test]
fn an_input_over_the_byte_budget_is_left_out_and_a_withheld_one_stays_withheld() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let nested = |leaf: Value| (0..40).fold(leaf, |deep, _| json!([deep]));
let large = nested(json!("x".repeat(MAX_INPUT_BYTES + 1)));
let credential = nested(json!("the password is example-only"));
let private = nested(json!({"private":true,"value":"kept out"}));
let stored = sealed_calls(&store, "three calls", [&large, &credential, &private]);
let bytes = serde_json::to_string(&large).unwrap().len();
let omitted = format!("[archive: tool input of {bytes} bytes omitted, {OVER_LIMIT}");
assert_eq!(stored["content"][1]["input"], json!({"_archive_omitted":omitted}));
for block in [2, 3] {
assert_eq!(stored["content"][block]["input"], json!({"_archive_withheld":true}));
}
assert!(stored[REDUCED_KEY].as_str().unwrap().ends_with("with 1 omission(s), each marked in place"));
let deep = deep_input();
let small = json!({"path":"src/lib.rs"});
let stored = sealed_calls(&store, "three calls", [&large, &deep, &small]);
assert_eq!(stored["content"][1]["input"], json!({"_archive_omitted":omitted}));
let as_text = serde_json::to_string(&deep).unwrap();
assert_eq!(stored["content"][2]["input"], json!({"_archive_json":as_text}));
assert_eq!(stored["content"][3]["input"], small);
assert_eq!(
stored[REDUCED_KEY],
"this message alone is over the per-archive limit; it is stored with 1 omission(s) \
and 1 tool input(s) as JSON text, each marked in place"
);
}
#[test]
fn single_huge_message_is_reduced_with_markers() {
let (_tmp, store) = setup();
let huge = format!("a{}", "é".repeat(MAX_INPUT_BYTES / 2));
assert_eq!(huge.len(), MAX_INPUT_BYTES + 1);
let messages = vec![
msg("user", json!("before")),
msg("user", json!(huge)),
msg("assistant", json!("after")),
];
assert!(store.seal(&messages, "").is_err());
let window = store.seal_window(&messages, "").unwrap();
let reduced: Vec<_> = window.parts.iter().map(|part| part.reduced_messages).collect();
assert_eq!(reduced, [0, 1, 0]);
let read = reassemble(&store, &window, messages.len());
assert_eq!(read[0].message, messages[0]);
assert_eq!(read[2].message, messages[2]);
let kept = &huge[..4095];
let omitted = huge.len() - 4095;
let stored = read[1].message["content"].as_str().unwrap();
assert_eq!(stored, format!("{kept}\n[archive: {omitted} bytes omitted, {OVER_LIMIT}"));
assert!(read[1].message[REDUCED_KEY].as_str().unwrap().contains("with 1 omission(s)"));
}
#[test]
fn reduced_wide_message_keeps_its_first_blocks_and_counts_the_rest() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let mut blocks = vec![json!({"type":"tool_result","tool_use_id":"t1","content":[
{"type":"text","text":"x".repeat(5000)},
{"type":"text","text":"second"},
{"type":"text","text":"third"}
]})];
let small = |index: usize| json!({"type":"text","text":format!("block {index}")});
blocks.extend((1..70_000).map(small));
let window = store.seal_window(&[msg("user", json!(blocks))], "").unwrap();
assert_eq!(window.parts.len(), 1);
assert_eq!(window.parts[0].reduced_messages, 1);
let id = &window.parts[0].archive.id;
let fetched = &store.fetch_with_provenance(id, 0, 1).unwrap()[0];
let content = fetched.message["content"].as_array().unwrap();
assert_eq!(content.len(), 257);
let short = format!("[archive: 904 bytes omitted, {OVER_LIMIT}");
let followed = format!("[archive: 2 blocks omitted, {OVER_LIMIT}");
let result = format!("{}\n{short}\n{followed}", "x".repeat(REDUCED_TEXT_BYTES));
assert_eq!(content[0]["content"], result);
assert_eq!(content[1], json!({"type":"text","text":"block 1"}));
assert_eq!(content[255], json!({"type":"text","text":"block 255"}));
let rest = format!("[archive: 69744 blocks omitted, {OVER_LIMIT}");
assert_eq!(content[256], json!({"type":"text","text":rest}));
assert_eq!(fetched.block_indices, (0..=256).collect::<Vec<_>>());
assert!(fetched.message[REDUCED_KEY].as_str().unwrap().contains("with 3 omission(s)"));
}
#[test]
fn only_the_store_marks_a_message_as_reduced() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let marker = format!("[archive: 9 bytes omitted, {OVER_LIMIT}");
let source = Arc::new(json!({"role":"assistant","_archive_reduced":"imitation","content":[
{"type":"text","text":format!("cut short\n{marker}")},
{"type":"tool_use","id":"t1","name":"read","input":{"_archive_omitted":marker}},
{"type":"tool_use","id":"t2","name":"read","input":{"_archive_json":"[1]"}}
]}));
let window = store.seal_window(std::slice::from_ref(&source), "").unwrap();
assert_eq!(window.parts[0].reduced_messages, 0);
let stored = &store.fetch(&window.parts[0].archive.id, 0, 1).unwrap()[0];
assert_eq!(stored["content"], source["content"]);
assert_eq!(stored.as_object().unwrap().len(), 2, "role and content");
}
#[test]
fn retry_returns_the_same_parts_and_writes_nothing() {
let (tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let dir = directory(&tmp, &store);
let mut messages = lines(MAX_MESSAGES + 10);
let call = json!([{"type":"tool_use","id":"t1","name":"nest","input":deep_input()}]);
messages[7] = msg("assistant", call);
let first = store.seal_window(&messages, "note").unwrap();
assert!(first.parts.len() > 2);
let reduced: usize = first.parts.iter().map(|part| part.reduced_messages).sum();
assert_eq!(reduced, 1);
let before = snapshot(&dir);
assert_eq!(before.len(), first.parts.len() + 1, "one file a part, and the lock");
assert_eq!(store.seal_window(&messages, "note").unwrap(), first);
assert_eq!(store.seal_window(&messages, "another note").unwrap(), first);
assert_eq!(snapshot(&dir), before);
}
#[test]
fn partial_failure_then_retry() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let messages = lines(MAX_MESSAGES + 2);
let (left, right) = messages.split_at(MAX_MESSAGES);
let digest = |part: &[SharedMessage]| {
let projection = project_hashed(&store.logical, part, "", None, Reduce::None);
projection.unwrap().digest
};
let id = |part: &[SharedMessage]| digest(part)[..32].to_owned();
let shelf = dir.join("s000001");
std::fs::create_dir(&shelf).unwrap();
let obstacle = shelf.join(archive_name(&id(right)));
let other = shelf.join(archive_name(&digest(right)[32..]));
std::fs::create_dir(&obstacle).unwrap();
std::fs::create_dir(&other).unwrap();
let error = store.seal_window(&messages, "note").unwrap_err();
assert!(!over_budget(&error));
std::fs::remove_dir(&other).unwrap();
let first = shelf.join(archive_name(&id(left)));
let bytes = std::fs::read(&first).unwrap();
assert_eq!(store.fetch(&id(left), 0, 1).unwrap(), messages[..1]);
std::fs::remove_dir(&obstacle).unwrap();
let window = store.seal_window(&messages, "note").unwrap();
let ids: Vec<_> = window.parts.iter().map(|part| part.archive.id.clone()).collect();
assert_eq!(ids, [id(left), id(right)]);
assert_eq!(std::fs::read(&first).unwrap(), bytes);
assert_eq!(window.note, "note");
assert!(reassemble(&store, &window, messages.len()) == exact(&store, &messages));
}
#[test]
fn note_is_stored_on_the_last_part_only() {
let (_tmp, store) = setup();
let messages = lines(2 * MAX_MESSAGES + 1);
let window = store.seal_window(&messages, "the working note").unwrap();
assert_eq!(window.note, "the working note");
let notes: Vec<_> = window
.parts
.iter()
.map(|part| store.fetch_note(&part.archive.id).unwrap())
.collect();
assert_eq!(notes, ["", "", "the working note"]);
let again = store.seal_window(&messages, "a later note").unwrap();
assert_eq!(again.note, "the working note");
}
#[test]
fn identical_halves_share_one_id() {
let (tmp, store) = setup();
let messages = vec![msg("user", json!("again")); 2 * MAX_MESSAGES];
let window = store.seal_window(&messages, "the working note").unwrap();
assert_eq!(window.parts.len(), 2);
assert_eq!(window.parts[0].archive, window.parts[1].archive);
assert_eq!(window.parts[0].first_source_index, 0);
assert_eq!(window.parts[1].first_source_index, MAX_MESSAGES);
assert_eq!(shelf_files(&directory(&tmp, &store)).len(), 1);
assert!(reassemble(&store, &window, messages.len()) == exact(&store, &messages));
assert_eq!(store.fetch_note(&window.parts[1].archive.id).unwrap(), "");
assert_eq!(window.note, "the working note");
}
#[test]
fn oversized_note_is_not_treated_as_splittable() {
let (tmp, store) = setup();
let window = lines(MAX_MESSAGES + 1);
let note = "n".repeat(9 * 1024);
for messages in [&window[..1], &window[..]] {
let error = store.seal_window(messages, ¬e).unwrap_err();
assert_eq!(error.to_string(), "archive message/note budget exceeded");
assert!(!over_budget(&error));
}
let tripling = ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-logical")
.unwrap()
.with_redactor(Arc::new(|text: &str| text.repeat(3)));
let error = tripling.seal_window(&window, &"n".repeat(4096)).unwrap_err();
assert_eq!(error.to_string(), "redacted note exceeds archive budget");
assert!(shelf_files(&directory(&tmp, &store)).is_empty());
}
#[test]
fn strict_seal_still_refuses() {
let (tmp, store) = setup();
let messages = lines(MAX_MESSAGES + 1);
for error in [
store.seal(&messages, "").unwrap_err(),
store.seal_with_note(&messages, "").unwrap_err(),
project_archive("synthetic-logical", &messages, "", None).err().unwrap(),
] {
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert_eq!(error.to_string(), "archive message/note budget exceeded");
assert!(over_budget(&error));
}
assert!(shelf_files(&directory(&tmp, &store)).is_empty());
assert_eq!(store.seal_window(&messages, "").unwrap().parts.len(), 2);
}
#[test]
fn a_part_does_not_end_between_a_call_and_its_results_where_it_can() {
let call = msg("assistant", json!([{"type":"tool_use","id":"t","name":"read","input":{}}]));
let result = msg("user", json!([{"type":"tool_result","tool_use_id":"t","content":"ok"}]));
let words = msg("user", json!("words"));
let window = [words.clone(), call.clone(), result.clone(), words.clone()];
assert_eq!(before_results(&window, 0, 2), 1, "moved back to before the call");
assert_eq!(before_results(&window, 0, 3), 3, "the next part starts with words");
assert_eq!(before_results(&window, 1, 2), 2, "it cannot: the part would be empty");
assert_eq!(before_results(&window, 0, 4), 4, "the end of the window");
let results = [words.clone(), call.clone(), result.clone(), result.clone(), words.clone()];
assert_eq!(before_results(&results, 0, 3), 1, "back over every result of the call");
assert_eq!(before_results(&results, 1, 3), 3, "nothing but results back to the start");
let (_tmp, store) = setup();
let mut messages = numbered(0..MAX_MESSAGES + 5);
messages[MAX_MESSAGES - 1] = call;
messages[MAX_MESSAGES] = result;
let window = store.seal_window(&messages, "").unwrap();
let starts: Vec<_> = window.parts.iter().map(|part| part.first_source_index).collect();
assert_eq!(starts, [0, MAX_MESSAGES - 1]);
}
fn numbered(range: std::ops::Range<usize>) -> Vec<SharedMessage> {
range.map(|index| msg("user", json!(format!("message {index}")))).collect()
}
fn starts_and_ids(window: &SealedWindow) -> (Vec<usize>, Vec<String>) {
let parts = window.parts.iter();
parts.map(|part| (part.first_source_index, part.archive.id.clone())).unzip()
}
#[test]
fn parts_do_not_change_when_the_window_grows() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let mut messages = numbered(0..2 * MAX_MESSAGES + 10);
let (starts, first) = starts_and_ids(&store.seal_window(&messages, "note").unwrap());
assert_eq!(starts, [0, MAX_MESSAGES, 2 * MAX_MESSAGES]);
assert_eq!(shelf_files(&dir).len(), 3);
messages.extend(numbered(messages.len()..messages.len() + 1));
let (starts, second) = starts_and_ids(&store.seal_window(&messages, "note").unwrap());
assert_eq!(starts, [0, MAX_MESSAGES, 2 * MAX_MESSAGES]);
assert_eq!(second[..2], first[..2]);
assert_ne!(second[2], first[2]);
assert_eq!(shelf_files(&dir).len(), 4, "one file more: the new last part");
messages.extend(numbered(messages.len()..3 * MAX_MESSAGES + 5));
let window = store.seal_window(&messages, "note").unwrap();
let (starts, third) = starts_and_ids(&window);
assert_eq!(starts, [0, MAX_MESSAGES, 2 * MAX_MESSAGES, 3 * MAX_MESSAGES]);
assert_eq!(third[..2], first[..2]);
assert_eq!(shelf_files(&dir).len(), 6, "the part that is full now, and the last");
assert_whole(&reassemble(&store, &window, messages.len()), &messages);
assert_eq!(window.note, "note");
}
#[test]
fn a_part_cut_for_its_written_size_does_not_change_when_the_window_grows() {
let (_tmp, store) = setup();
let large = |index: usize| msg("user", json!(format!("{index}{}", "\u{1}".repeat(768 * 1024))));
let mut cuts = Vec::new();
for len in [4, 6] {
let messages: Vec<_> = (0..len).map(large).collect();
let window = store.seal_window(&messages, "").unwrap();
let (starts, ids) = starts_and_ids(&window);
assert_eq!(starts[..2], [0, 2], "{len} messages");
cuts.push(ids[0].clone());
}
cuts.dedup();
assert_eq!(cuts.len(), 1, "the first part is the same segment every time");
}
#[test]
fn an_oversized_message_costs_two_parts_more() {
let (_tmp, store) = setup();
let store = store.with_redactor(Arc::new(str::to_owned));
let deep = || msg("assistant", json!([{"type":"tool_use","id":"t1","name":"nest","input":deep_input()}]));
let cut = |len: usize, oversized: &[usize]| {
let mut messages = numbered(0..len);
for &index in oversized {
messages[index] = deep();
}
let window = store.seal_window(&messages, "").unwrap();
let parts = window.parts.iter();
parts.map(|part| (part.first_source_index, part.reduced_messages)).collect::<Vec<_>>()
};
let after = 8 + MAX_MESSAGES;
assert_eq!(cut(MAX_MESSAGES + 10, &[7]), [(0, 0), (7, 1), (8, 0), (after, 0)]);
let seven = [(0, 0), (1000, 1), (1001, 0), (2000, 1), (2001, 0), (3000, 1), (3001, 0)];
assert_eq!(cut(4000, &[1000, 2000, 3000]), seven);
assert_eq!(cut(4, &[0, 1, 3]), [(0, 1), (1, 1), (2, 0), (3, 1)]);
}
fn now_ms() -> u64 {
let since = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
since.unwrap().as_millis() as u64
}
fn json_of(path: &Path) -> Value {
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap()
}
fn top_level(record: &Value) -> Vec<&str> {
record.as_object().unwrap().keys().map(String::as_str).collect()
}
#[test]
fn a_seal_records_the_time_of_the_write_and_a_later_seal_leaves_it() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let messages = text("sealed once");
let before = now_ms();
let sealed = store.seal(&messages, "note").unwrap();
let after = now_ms();
let file = only_archive_file(&dir);
let record = json_of(&file);
let fields = ["body", "created_ms", "digest", "id", "logical", "scope", "version"];
assert_eq!(top_level(&record), fields);
let created = record["created_ms"].as_u64().unwrap();
assert!((before..=after).contains(&created), "{before} <= {created} <= {after}");
let raw = std::fs::read_to_string(&file).unwrap();
let digest = record["digest"].as_str().unwrap();
let expected = format!("\"digest\":\"{digest}\",\"created_ms\":{created},\"body\":{{");
assert!(raw.contains(&expected));
let projection = project_archive("synthetic-logical", &messages, "note", None).unwrap();
assert_eq!(record["digest"], projection.digest.as_str());
assert_eq!(sealed.id, projection.digest[..32]);
let snapshot_before = snapshot(&dir);
std::thread::sleep(Duration::from_millis(5));
assert_eq!(store.seal(&messages, "another note").unwrap(), sealed);
assert_eq!(store.seal_window(&messages, "").unwrap().parts[0].archive, sealed);
assert_eq!(snapshot(&dir), snapshot_before);
assert_eq!(json_of(&file)["created_ms"], created);
}
#[test]
fn a_projection_of_another_chat_or_with_a_wrong_digest_is_refused_and_nothing_is_written() {
let tmp = tempfile::tempdir().unwrap();
let state = || {
let mut paths = Vec::new();
let mut pending = vec![tmp.path().to_path_buf()];
while let Some(directory) = pending.pop() {
for entry in std::fs::read_dir(&directory).unwrap() {
let path = entry.unwrap().path();
if std::fs::symlink_metadata(&path).unwrap().is_dir() {
pending.push(path.clone());
}
paths.push(path);
}
}
paths.sort();
(paths, snapshot(tmp.path()))
};
let store = setup_in(tmp.path(), "synthetic-project");
let words = "handed to the store";
let project = |logical: &str, words: &str| {
project_hashed(logical, &text(words), "", None, Reduce::None).unwrap()
};
let good = || project(&store.logical, words);
let with_digest = |digest: &str| ArchiveProjection {
digest: digest.to_owned(),
..good()
};
let other_chat = digest_parts(&[b"another-chat"]);
assert_ne!(other_chat, store.logical);
let half_way = format!("{}é{}", "a".repeat(31), "a".repeat(31));
assert!(half_way.len() == 64 && !half_way.is_char_boundary(32));
let refused = [
("another chat", project(&other_chat, words)),
("renamed", ArchiveProjection {
logical_id: store.logical.clone(),
..project(&other_chat, words)
}),
("another digest", with_digest(&project(&store.logical, "other words").digest)),
("other messages", ArchiveProjection {
messages: project(&store.logical, "other words").messages,
..good()
}),
("another count", ArchiveProjection {
source_message_count: 2,
..good()
}),
("upper case", with_digest(&good().digest.to_uppercase())),
("ten characters", with_digest("0123456789")),
("empty", with_digest("")),
("not hex", with_digest(&"z".repeat(64))),
("not ASCII", with_digest(&"é".repeat(32))),
("not ASCII, cut at the half", with_digest(&half_way)),
];
for sealed_before in [false, true] {
if sealed_before {
store.seal(&text("a shelf to begin with"), "").unwrap();
}
let before = state();
for (what, projection) in &refused {
for written in [None, Some(Written { created_ms: Some(1), modified: None })] {
let error = store.seal_projection(projection.clone(), written).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{what}: {error}");
assert_eq!(state(), before, "{what}");
}
}
}
let (sealed, _) = store.seal_projection(good(), None).unwrap();
assert_eq!(sealed.id, good().digest[..32]);
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text(words));
assert_eq!(store.seal(&text(words), "").unwrap(), sealed);
}
#[test]
fn a_supplied_created_ms_and_file_time_are_stored_as_given() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let project = |words: &str| {
project_hashed(&store.logical, &text(words), "", None, Reduce::None).unwrap()
};
let created_ms = 1_000_000_000_123;
let modified = std::time::UNIX_EPOCH + Duration::from_secs(1_000_000_500);
let given = Written {
created_ms: Some(created_ms),
modified: Some(modified),
};
let started = std::time::SystemTime::now() - Duration::from_secs(5);
let (sealed, _) = store.seal_projection(project("from another store"), Some(given)).unwrap();
let file = dir.join("s000001").join(archive_name(&sealed.id));
let metadata = std::fs::metadata(&file).unwrap();
assert_eq!(metadata.modified().unwrap(), modified);
assert_eq!(metadata.accessed().unwrap(), modified);
assert_eq!(json_of(&file)["created_ms"], created_ms);
for directory in [dir.clone(), dir.join("s000001")] {
let changed = std::fs::metadata(&directory).unwrap().modified().unwrap();
assert!(changed >= started, "{directory:?} was given the time of a file");
}
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text("from another store"));
let other = Written {
created_ms: Some(5),
modified: None,
};
let before = snapshot(&dir);
let again = store.seal_projection(project("from another store"), Some(other)).unwrap();
assert_eq!(again.0, sealed);
assert_eq!(store.seal(&text("from another store"), "").unwrap(), sealed);
assert_eq!(snapshot(&dir), before);
assert_eq!(std::fs::metadata(&file).unwrap().modified().unwrap(), modified);
let undated = Written {
created_ms: None,
modified: None,
};
let (plain, _) = store.seal_projection(project("never had a time"), Some(undated)).unwrap();
let file = dir.join("s000001").join(archive_name(&plain.id));
let fields = ["body", "digest", "id", "logical", "scope", "version"];
assert_eq!(top_level(&json_of(&file)), fields);
assert!(std::fs::metadata(&file).unwrap().modified().unwrap() >= started);
assert_eq!(store.fetch(&plain.id, 0, 1).unwrap(), text("never had a time"));
}
#[test]
fn created_ms_is_optional_and_no_other_field_is_accepted() {
let (tmp, store) = setup();
let shelf = directory(&tmp, &store).join("s000001");
std::fs::create_dir(&shelf).unwrap();
let id = "a".repeat(32);
let file = shelf.join(archive_name(&id));
let words = text("with or without");
let without = record_bytes(&store, &id, &words, |_| {});
assert!(!String::from_utf8_lossy(&without).contains("created_ms"));
let with = |value: Value| {
let mut record: Value = serde_json::from_slice(&without).unwrap();
record["created_ms"] = value;
serde_json::to_vec(&record).unwrap()
};
for valid in [without.clone(), with(json!(0)), with(json!(1_757_000_000_000u64)), with(json!(u64::MAX))] {
std::fs::write(&file, valid).unwrap();
assert_eq!(store.fetch(&id, 0, 1).unwrap(), words);
assert_eq!(store.search("without", 4).unwrap().len(), 1);
}
let mut extra: Value = serde_json::from_slice(&without).unwrap();
extra["created_at"] = json!(5);
let mut nested: Value = serde_json::from_slice(&without).unwrap();
nested["body"]["created_ms"] = json!(5);
let invalid = [
with(Value::Null),
with(json!(-1)),
with(json!(1.5)),
with(json!("1757000000000")),
with(json!(true)),
serde_json::to_vec(&extra).unwrap(),
serde_json::to_vec(&nested).unwrap(),
];
for (index, bytes) in invalid.into_iter().enumerate() {
std::fs::write(&file, bytes).unwrap();
let error = store.fetch(&id, 0, 1).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidData, "case {index}: {error}");
}
}
#[test]
fn a_tombstone_carries_no_created_ms() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let sealed = store.seal(&text("to be forgotten"), "").unwrap();
let file = only_archive_file(&dir);
assert!(json_of(&file)["created_ms"].is_u64());
store.forget(&sealed.id).unwrap();
let tombstone = json_of(&file);
let fields = ["body", "digest", "id", "logical", "scope", "version"];
assert_eq!(top_level(&tombstone), fields);
assert_eq!(tombstone["body"], json!({"state":"tombstone"}));
let slot = seal_legacy_slot(&store, 0, &text("forgotten in a slot"), "");
store.forget(&slot.id).unwrap();
for path in shelf_files(&dir).into_iter().chain([dir.join(slot_name(0))]) {
let record = json_of(&path);
assert_eq!(top_level(&record), fields, "{path:?}");
assert_eq!(record["body"]["state"], "tombstone");
}
assert_eq!(shelf_files(&dir).len(), 2);
}
#[test]
fn a_tombstone_with_created_ms_is_not_a_record_and_still_blocks_a_seal() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let words = text("forgotten somewhere else");
let digest = project_hashed(&store.logical, &words, "", None, Reduce::None).unwrap().digest;
let id = &digest[..32];
let shelf = dir.join("s000001");
std::fs::create_dir(&shelf).unwrap();
let file = shelf.join(archive_name(id));
let dated = |created_ms| {
record_bytes(&store, id, &words, |record| {
record.body = Body::Tombstone;
record.created_ms = created_ms;
})
};
std::fs::write(&file, dated(None)).unwrap();
assert_eq!(store.fetch(id, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
let invalid = dated(Some(1_757_000_000_000));
std::fs::write(&file, &invalid).unwrap();
assert_eq!(store.fetch(id, 0, 1).unwrap_err().kind(), io::ErrorKind::InvalidData);
let error = store.seal(&words, "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
assert_eq!(shelf_files(&dir), std::slice::from_ref(&file));
assert_eq!(std::fs::read(&file).unwrap(), invalid);
}
fn legacy_store() -> (tempfile::TempDir, ArchiveStore, ArchiveRef) {
let (tmp, store) = setup();
let live = seal_legacy_slot(&store, 0, &text("legacy live"), "first note");
let gone = seal_legacy_slot(&store, 2, &text("legacy forgotten"), "");
store.forget(&gone.id).unwrap();
(tmp, store, live)
}
fn assert_slot_1_is_passed_over(
tmp: &tempfile::TempDir,
store: &ArchiveStore,
live: &ArchiveRef,
words: &str,
kind: io::ErrorKind,
) {
let new = store.seal(&text(words), "").unwrap();
assert_eq!(store.fetch(&new.id, 0, 1).unwrap(), text(words), "{words}");
let again = store.seal_with_note(&text("legacy live"), "second note");
assert_eq!(again.unwrap(), (live.clone(), "first note".to_owned()), "{words}");
let forgotten = store.seal(&text("legacy forgotten"), "").unwrap_err();
assert!(forgotten.to_string().contains("was forgotten"), "{words}");
assert_eq!(store.fetch(&live.id, 0, 1).unwrap(), text("legacy live"), "{words}");
assert_eq!(store.fetch_note(&live.id).unwrap(), "first note", "{words}");
let slot = directory(tmp, store).join("000.json");
let located = archive_file(tmp.path(), "synthetic-project", &live.id);
assert_eq!(located.unwrap(), Some(slot), "{words}");
let rows = store.search("legacy", 2).unwrap();
assert_eq!(rows.len(), 1, "{words}");
assert_eq!(rows[0].id, live.id, "{words}");
let missing = "a".repeat(32);
for result in [
store.fetch(&missing, 0, 1).map(drop),
store.fetch_note(&missing).map(drop),
store.forget(&missing).map(drop),
archive_file(tmp.path(), "synthetic-project", &missing).map(drop),
] {
let error = result.unwrap_err();
assert_eq!(error.kind(), kind, "{words}: {error}");
assert!(error.to_string().contains("first 001.json"), "{words}: {error}");
assert!(error.to_string().contains("could not be read"), "{words}: {error}");
}
}
fn assert_forgets_beside_slot_1(tmp: &tempfile::TempDir, store: &ArchiveStore, live: &ArchiveRef) {
let dir = directory(tmp, store);
let slot = dir.join("000.json");
let guard = dir.join("s000001").join(archive_name(&digest_of("legacy live")[..32]));
let others = || -> Vec<_> {
let mut files = shelf_files(&dir);
files.retain(|file| *file != guard);
files.push(dir.join("002.json"));
files.into_iter().map(|path| (identity(&path), path)).collect()
};
let (before, entries) = (others(), std::fs::read_dir(&dir).unwrap().count());
let live_file = identity(&slot);
assert!(!guard.exists());
let done = store.forget(&live.id).unwrap();
assert_eq!(done.ids, std::slice::from_ref(&live.id));
let unread = done.unread.unwrap();
assert!(unread.starts_with("1 place(s) could not be read, first 001.json: "), "{unread}");
assert_ne!(identity(&slot), live_file);
let tombstone = (identity(&slot), identity(&guard));
assert!(store.forget(&live.id).unwrap().ids.is_empty());
assert_eq!((identity(&slot), identity(&guard)), tombstone, "forgotten twice, written once");
assert!(std::fs::read_to_string(&slot).unwrap().contains("tombstone"));
assert!(is_tombstone_of(store, &guard, &digest_of("legacy live")));
let error = store.fetch(&live.id, 0, 1).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
assert_eq!(error.to_string(), not_found().to_string());
let refused = store.seal(&text("legacy live"), "").unwrap_err();
assert!(refused.to_string().contains("was forgotten"));
assert_eq!(others(), before);
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), entries);
assert!(temporary_files(&dir).is_empty());
}
#[test]
fn a_legacy_slot_that_is_not_a_valid_record_hides_only_itself() {
let (tmp, store, live) = legacy_store();
let dir = directory(&tmp, &store);
let slot = dir.join("001.json");
let foreign =
ArchiveStore::new(tmp.path(), "foreign-project", "synthetic-logical").unwrap();
seal_legacy_slot(&foreign, 0, &text("foreign words"), "");
let foreign = std::fs::read(directory(&tmp, &foreign).join("000.json")).unwrap();
let altered = std::fs::read_to_string(dir.join("000.json")).unwrap();
let altered = altered.replace("legacy live", "legacy LIVE");
let cases = [
("bad JSON", b"{\"version\":1,".to_vec()),
("empty", Vec::new()),
("wrong scope", foreign),
("digest mismatch", altered.into_bytes()),
];
let invalid = io::ErrorKind::InvalidData;
for (kind, bytes) in cases {
std::fs::write(&slot, &bytes).unwrap();
assert_slot_1_is_passed_over(&tmp, &store, &live, kind, invalid);
assert_eq!(std::fs::read(&slot).unwrap(), bytes, "{kind}");
}
let large = MAX_SEGMENT_BYTES as u64 + 1;
std::fs::File::create(&slot).unwrap().set_len(large).unwrap();
assert_slot_1_is_passed_over(&tmp, &store, &live, "over the size bound", invalid);
assert_eq!(std::fs::metadata(&slot).unwrap().len(), large);
assert_eq!(shelf_files(&dir).len(), 6);
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 5, "lock, 3 slots, 1 shelf");
assert_forgets_beside_slot_1(&tmp, &store, &live);
assert_eq!(std::fs::metadata(&slot).unwrap().len(), large);
}
#[test]
fn a_legacy_slot_that_is_not_a_private_file_hides_only_itself() {
use std::os::unix::fs::FileTypeExt;
let (tmp, store, live) = legacy_store();
let slot = directory(&tmp, &store).join("001.json");
let kind = || std::fs::symlink_metadata(&slot).unwrap().file_type();
let victim = tmp.path().join("synthetic-victim");
std::fs::write(&victim, "do not change").unwrap();
std::fs::set_permissions(&victim, std::fs::Permissions::from_mode(0o644)).unwrap();
let passed_over = |words: &str, error: io::ErrorKind| {
assert_slot_1_is_passed_over(&tmp, &store, &live, words, error);
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "do not change");
assert_eq!(mode(&victim), 0o644, "{words}");
};
let denied = io::ErrorKind::PermissionDenied;
symlink(&victim, &slot).unwrap();
let looped = store.fs.read_with("001.json", MAX_SEGMENT_BYTES, true).unwrap_err().kind();
passed_over("past a symlink", looped);
assert!(kind().is_symlink());
std::fs::remove_file(&slot).unwrap();
std::fs::hard_link(&victim, &slot).unwrap();
passed_over("past a hard link", denied);
std::fs::remove_file(&slot).unwrap();
let fifo = std::ffi::CString::new(slot.to_str().unwrap()).unwrap();
assert_eq!(unsafe { libc::mkfifo(fifo.as_ptr(), 0o600) }, 0);
passed_over("past a FIFO", denied);
assert!(kind().is_fifo());
std::fs::remove_file(&slot).unwrap();
std::fs::create_dir(&slot).unwrap();
passed_over("past a directory", denied);
assert!(kind().is_dir());
assert_forgets_beside_slot_1(&tmp, &store, &live);
assert!(kind().is_dir());
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "do not change");
}
#[test]
fn a_legacy_tombstone_that_cannot_be_read_no_longer_forbids_its_projection() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let slot = dir.join("000.json");
let gone = seal_legacy_slot(&store, 0, &text("forgotten words"), "");
store.forget(&gone.id).unwrap();
let refused = store.seal(&text("forgotten words"), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied);
let tombstone = std::fs::read(&slot).unwrap();
let torn = &tombstone[..tombstone.len() / 2];
std::fs::write(&slot, torn).unwrap();
let refused = store.seal(&text("forgotten words"), "").unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied);
std::fs::remove_file(only_archive_file(&dir)).unwrap();
let again = store.seal(&text("forgotten words"), "").unwrap();
assert_eq!(store.fetch(&again.id, 0, 1).unwrap(), text("forgotten words"));
assert_eq!(only_archive_file(&dir), dir.join("s000001").join(archive_name(&again.id)));
assert_eq!(std::fs::read(&slot).unwrap(), torn);
}
#[test]
fn a_failed_directory_read_is_an_error_not_a_shorter_listing() {
use std::os::fd::{AsRawFd, IntoRawFd};
let (tmp, store) = setup();
store.seal(&text("listed"), "").unwrap();
let shelf = store.fs.open_child("s000001", true).unwrap().unwrap();
assert_eq!(shelf.names().unwrap().len(), 1);
let handle = shelf.open(c".", libc::O_RDONLY | libc::O_DIRECTORY);
let fd = handle.unwrap().into_raw_fd();
let stream = unsafe { libc::fdopendir(fd) };
assert!(!stream.is_null());
let file = std::fs::File::open(only_archive_file(&directory(&tmp, &store))).unwrap();
assert_eq!(unsafe { libc::dup2(file.as_raw_fd(), fd) }, fd);
let listed = shelf.read_names(stream);
unsafe { libc::closedir(stream) };
let error = listed.unwrap_err();
#[cfg(target_os = "linux")]
assert_eq!(error.raw_os_error(), Some(libc::ENOTDIR));
assert_ne!(error.raw_os_error(), Some(0));
}
#[test]
fn a_listing_is_not_failed_by_an_error_from_before_it() {
let (_tmp, store) = setup();
store.seal(&text("listed"), "").unwrap();
let shelf = store.fs.open_child("s000001", true).unwrap().unwrap();
assert_eq!(unsafe { libc::close(-1) }, -1);
assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EBADF));
assert_eq!(shelf.names().unwrap().len(), 1);
let empty = store.fs.child("s000002", true).unwrap();
assert_eq!(unsafe { libc::close(-1) }, -1);
assert!(empty.names().unwrap().is_empty());
}
#[test]
fn a_directory_that_cannot_be_opened_again_is_an_error_not_an_empty_listing() {
if unsafe { libc::geteuid() } == 0 {
return; }
let (tmp, store) = setup();
store.seal(&text("listed"), "").unwrap();
let path = directory(&tmp, &store).join("s000001");
let shelf = store.fs.open_child("s000001", true).unwrap().unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
let listed = shelf.names();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(listed.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
assert_eq!(shelf.names().unwrap().len(), 1);
}
#[test]
fn names_that_are_not_store_names_are_ignored_whatever_they_look_like() {
use std::os::unix::ffi::OsStringExt;
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let first = store.seal(&text("first"), "").unwrap();
let legacy = seal_legacy_slot(&store, 0, &text("legacy"), "");
let id = &first.id;
let odd: Vec<std::ffi::OsString> = vec![
"x".repeat(255).into(),
format!("{}.json", "9".repeat(250)).into(),
std::ffi::OsString::from_vec(b"\xff\xfe.json".to_vec()),
"back\\slash.json".into(),
"new\nline.json".into(),
"s00001".into(),
"s0000001".into(),
"S000001".into(),
"s000001.json".into(),
"0.json".into(),
"128.json".into(),
"0000.json".into(),
format!("{}.json", &id[1..]).into(),
format!("{}.json", id.to_uppercase()).into(),
format!("{id}.json.tmp").into(),
format!("{id}.JSON").into(),
format!(".{id}.json").into(),
".json".into(),
"lock.json".into(),
];
let mut strays = Vec::new();
for parent in [dir.clone(), dir.join("s000001")] {
for name in &odd {
let path = parent.join(name);
if std::fs::write(&path, b"stray").is_ok() {
strays.push(path);
}
}
}
#[cfg(target_os = "linux")]
assert_eq!(strays.len(), 2 * odd.len());
assert!(strays.len() >= 2 * 10);
let second = store.seal(&text("second"), "").unwrap();
assert!(dir.join("s000001").join(archive_name(&second.id)).is_file());
assert_eq!(store.seal(&text("first"), "").unwrap(), first);
assert_eq!(store.fetch(&first.id, 0, 1).unwrap(), text("first"));
assert_eq!(store.fetch(&legacy.id, 0, 1).unwrap(), text("legacy"));
let mut found: Vec<_> = store.search("", 32).unwrap().into_iter().map(|row| row.id).collect();
found.sort();
let mut expected = vec![first.id.clone(), second.id.clone(), legacy.id.clone()];
expected.sort();
assert_eq!(found, expected);
store.forget(&second.id).unwrap();
assert_eq!(store.shelves().unwrap(), ["s000001"]);
for stray in strays {
assert_eq!(std::fs::read(&stray).unwrap(), b"stray", "{stray:?}");
}
}
#[test]
fn a_failed_check_has_a_name_and_a_read_does_not_say_it() {
let (_tmp, store) = setup();
let id = "0123456789abcdef0123456789abcdef";
let name = archive_name(id);
let bytes = |change: &dyn Fn(&mut Record)| {
record_bytes(&store, id, &[msg("user", json!("a")), msg("user", json!("b"))], change)
};
let swap = |record: &mut Record| {
if let Body::Live { messages, .. } = &mut record.body {
messages.swap(0, 1);
}
};
let many = |record: &mut Record| {
if let Body::Live { source_message_count, .. } = &mut record.body {
*source_message_count = MAX_MESSAGES + 1;
}
};
let long = |record: &mut Record| {
if let Body::Live { note, .. } = &mut record.body {
*note = "n".repeat(MAX_NOTE_BYTES + 1);
}
};
let cases: Vec<(Vec<u8>, &str)> = vec![
(bytes(&|record| record.version = 2), "version 2 is not supported"),
(bytes(&|record| record.scope = "0".repeat(64)), "scope is not this project's"),
(bytes(&|record| record.id = id.to_uppercase()), "invalid id"),
(bytes(&|record| record.logical = "z".repeat(64)), "invalid logical"),
(bytes(&|record| record.digest.truncate(63)), "invalid digest"),
(bytes(&|record| record.id = "f".repeat(32)), "file name is not the record's id"),
(bytes(&many), "over a per-archive limit"),
(bytes(&long), "over a per-archive limit"),
(bytes(&swap), "source_index out of order"),
(
bytes(&|record| {
record.body = Body::Tombstone;
record.created_ms = Some(1);
}),
"a tombstone carries no created_ms",
),
(b"{\"version\":1".to_vec(), "not an archive record: the JSON ends early (column 12)"),
(b"not json".to_vec(), "not an archive record: not JSON (column 2)"),
];
for (raw, reason) in cases {
let fault = ArchiveStore::parsed(&store.scope, &name, &raw).err().expect(reason);
assert_eq!(fault.to_string(), reason);
let error = store.record(&name, &raw).err().expect(reason);
assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{reason}");
assert_eq!(error.to_string(), "invalid archive record", "{reason}");
}
let other = digest_of("something else");
let raw = bytes(&|record| record.digest = other.clone());
let reason = ArchiveStore::parsed(&store.scope, &name, &raw).err().unwrap().to_string();
let prefix = format!("digest does not match content (record {other}, content ");
assert!(reason.starts_with(&prefix), "{reason}");
assert_eq!(reason.len(), prefix.len() + 64 + 1);
assert_eq!(store.record(&name, &raw).err().unwrap().to_string(), "invalid archive record");
let mut unknown: Value = serde_json::from_slice(&bytes(&|_| {})).unwrap();
unknown["private words"] = json!("more private words");
let raw = serde_json::to_vec(&unknown).unwrap();
let reason = ArchiveStore::parsed(&store.scope, &name, &raw).err().unwrap().to_string();
assert!(reason.starts_with("not an archive record: an unknown field (column "), "{reason}");
assert!(!reason.contains("private"), "{reason}");
let mut wrong: Value = serde_json::from_slice(&bytes(&|_| {})).unwrap();
wrong["version"] = json!("private words");
let raw = serde_json::to_vec(&wrong).unwrap();
let reason = ArchiveStore::parsed(&store.scope, &name, &raw).err().unwrap().to_string();
assert!(reason.starts_with("not an archive record: a field of the wrong type"), "{reason}");
assert!(!reason.contains("private"), "{reason}");
assert!(ArchiveStore::parsed(&store.scope, &name, &bytes(&|_| {})).is_ok());
assert!(ArchiveStore::parsed(&store.scope, "000.json", &bytes(&|_| {})).is_ok());
}
#[test]
fn a_projection_no_read_would_accept_is_refused_before_it_is_written() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
store.seal(&text("a shelf to begin with"), "").unwrap();
let project = |words: &[&str]| {
let messages: Vec<_> = words.iter().map(|words| msg("user", json!(words))).collect();
project_hashed(&store.logical, &messages, "", None, Reduce::None).unwrap()
};
let mut long_note = project(&["a long note"]);
long_note.note = "n".repeat(9_000);
let mut falling = project(&["first", "second"]);
falling.messages.swap(0, 1);
falling.digest = projection_digest(&store.logical, 2, &falling.messages).unwrap();
let mut beyond = project(&["only"]);
beyond.messages[0].source_index = 1;
beyond.digest = projection_digest(&store.logical, 1, &beyond.messages).unwrap();
let before = snapshot(&dir);
let mut names: Vec<_> = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().path()).collect();
names.sort();
for (projection, check) in [
(long_note, "over a per-archive limit"),
(falling, "source_index out of order"),
(beyond, "source_index out of order"),
] {
store.admit(&projection).expect("admit does not check this");
let error = store.seal_projection(projection, None).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{check}: {error}");
assert!(error.to_string().ends_with(check), "{check}: {error}");
assert_eq!(snapshot(&dir), before, "{check}");
let mut after: Vec<_> =
std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().path()).collect();
after.sort();
assert_eq!(after, names, "{check}");
}
let sealed = store.seal(&text("ordinary"), "a note").unwrap();
assert_eq!(sealed.id, digest_of("ordinary")[..32]);
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text("ordinary"));
assert_eq!(store.fetch_note(&sealed.id).unwrap(), "a note");
}
fn random_id() -> String {
uuid::Uuid::new_v4().simple().to_string()
}
fn import(store: &ArchiveStore, line: &[u8]) -> Result<Imported, ImportRefused> {
store.import(line, false, false, &mut ImportSeen::default()).unwrap()
}
fn reason(store: &ArchiveStore, line: &[u8]) -> String {
import(store, line).expect_err("refused").reason
}
fn tombstone(record: &mut Record) {
record.body = Body::Tombstone;
}
fn raw_line(store: &ArchiveStore, id: &str, messages: Vec<Value>, note: &str) -> Vec<u8> {
let messages: Vec<_> = messages
.into_iter()
.enumerate()
.map(|(source_index, message)| ArchivedMessage {
source_index,
block_indices: Vec::new(),
message: Arc::new(message),
})
.collect();
let record = Record {
version: 1,
scope: store.scope.clone(),
logical: store.logical.clone(),
id: id.to_owned(),
digest: projection_digest(&store.logical, messages.len(), &messages).unwrap(),
created_ms: None,
body: Body::Live {
source_message_count: messages.len(),
messages,
note: note.to_owned(),
},
};
serde_json::to_vec(&record).unwrap()
}
fn entries(dir: &Path) -> Vec<String> {
let mut names: Vec<_> = std::fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.collect();
names.sort();
names
}
fn at_ms(ms: u64) -> std::time::SystemTime {
std::time::UNIX_EPOCH + Duration::from_millis(ms)
}
#[test]
fn t_a1_an_imported_record_is_the_file_it_was() {
let (tmp, first) = setup();
let sealed = first.seal_with_note(&text("round trip words"), "a hidden note").unwrap();
let source = only_archive_file(&directory(&tmp, &first));
let line = std::fs::read(&source).unwrap();
let created_ms = json_of(&source)["created_ms"].as_u64().unwrap();
let other = tempfile::tempdir().unwrap();
let second = ArchiveStore::for_scope(other.path(), &first.scope).unwrap();
assert_eq!(second.scope(), first.scope);
assert_eq!(record_scope(&line).unwrap(), first.scope);
let done = import(&second, &line).unwrap();
assert_eq!(
done,
Imported {
outcome: ImportOutcome::Stored,
id: sealed.0.id.clone(),
live: true,
message_count: 1,
shelf: Some("s000001".to_owned()),
same_content_as: None,
unread: None,
}
);
let copy = only_archive_file(&other.path().join("context-archives").join(&first.scope));
let metadata = std::fs::metadata(©).unwrap();
assert_eq!(metadata.modified().unwrap(), at_ms(created_ms));
assert_eq!(metadata.accessed().unwrap(), at_ms(created_ms));
assert_eq!(copy.file_name(), source.file_name());
assert_eq!(std::fs::read(©).unwrap(), line);
assert_eq!(mode(©), 0o600);
let id = &sealed.0.id;
assert_eq!(second.fetch(id, 0, 8).unwrap(), first.fetch(id, 0, 8).unwrap());
assert_eq!(second.fetch_note(id).unwrap(), "a hidden note");
assert_eq!(second.search("round", 8).unwrap(), first.search("round", 8).unwrap());
assert_eq!(second.search("round", 8).unwrap().len(), 1);
let day = 24 * 60 * 60 * 1000;
let ids = [random_id(), random_id(), random_id()];
for index in [1, 2, 0] {
let words = format!("daily entry {index}");
let line = record_bytes(&first, &ids[index], &text(&words), |record| {
record.created_ms = Some(1_600_000_000_000 + index as u64 * day);
});
assert_eq!(import(&second, &line).unwrap().outcome, ImportOutcome::Stored);
}
let found = second.search("daily entry", 8).unwrap();
let found: Vec<_> = found.iter().map(|row| row.id.as_str()).collect();
assert_eq!(found, [&ids[2], &ids[1], &ids[0]], "newest first");
}
#[test]
fn t_a2_a_foreign_id_is_kept_and_no_time_is_invented() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let id = random_id();
let digest = digest_of("kept under its own id");
assert!(id != digest[..32] && id != digest[32..]);
let line = record_bytes(&store, &id, &text("kept under its own id"), |_| {});
let started = std::time::SystemTime::now() - Duration::from_secs(5);
let done = import(&store, &line).unwrap();
assert_eq!((done.outcome, done.id.as_str()), (ImportOutcome::Stored, id.as_str()));
let file = dir.join("s000001").join(archive_name(&id));
assert_eq!(shelf_files(&dir), std::slice::from_ref(&file));
assert!(std::fs::metadata(&file).unwrap().modified().unwrap() >= started);
assert_eq!(std::fs::read(&file).unwrap(), line);
let fields = ["body", "digest", "id", "logical", "scope", "version"];
assert_eq!(top_level(&json_of(&file)), fields);
assert_eq!(store.fetch(&id, 0, 8).unwrap(), text("kept under its own id"));
let found = store.search("its own id", 8).unwrap();
assert_eq!(found.iter().map(|row| row.id.as_str()).collect::<Vec<_>>(), [id.as_str()]);
let project = ArchiveStore::for_scope(tmp.path(), &store.scope).unwrap();
assert_eq!(project.fetch(&id, 0, 8).unwrap(), text("kept under its own id"));
let error = project.seal(&text("a project store has no chat"), "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert_eq!(shelf_files(&dir), [file]);
let sealed = store.seal(&text("kept under its own id"), "").unwrap();
assert_eq!(sealed.id, digest[..32]);
let other = record_bytes(&store, &random_id(), &text("kept under its own id"), |_| {});
let done = import(&store, &other).unwrap();
assert_eq!(done.outcome, ImportOutcome::Stored);
assert_eq!(done.same_content_as, Some(sealed.id));
}
#[test]
fn t_a3_the_same_line_twice_changes_nothing() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let id = random_id();
let line = record_bytes(&store, &id, &text("imported twice"), |record| {
record.created_ms = Some(1_600_000_000_000);
});
assert_eq!(import(&store, &line).unwrap().outcome, ImportOutcome::Stored);
let file = dir.join("s000001").join(archive_name(&id));
let state = || (snapshot(&dir), std::fs::metadata(&file).unwrap().modified().unwrap());
let before = state();
assert_eq!(before.1, at_ms(1_600_000_000_000));
let again = import(&store, &line).unwrap();
assert_eq!(again.outcome, ImportOutcome::Present { note_differs: false });
assert_eq!(again.shelf.as_deref(), Some("s000001"));
assert_eq!(state(), before);
let noted = record_bytes(&store, &id, &text("imported twice"), |record| {
record.created_ms = Some(5);
if let Body::Live { note, .. } = &mut record.body {
*note = "another note".to_owned();
}
});
let again = import(&store, ¬ed).unwrap();
assert_eq!(again.outcome, ImportOutcome::Present { note_differs: true });
assert_eq!(state(), before);
assert_eq!(store.fetch_note(&id).unwrap(), "");
}
#[test]
fn t_a4_a_digest_that_is_not_the_contents_is_refused() {
let (tmp, chat) = setup();
let line = record_bytes(&chat, &random_id(), &text("one byte of this changes"), |_| {});
let text_of = String::from_utf8(line.clone()).unwrap();
let changed = text_of.replace("one byte of this changes", "one byte of this chanGes");
assert_ne!(changed, text_of);
let other = tempfile::tempdir().unwrap();
let store = ArchiveStore::for_scope(other.path(), &chat.scope).unwrap();
let dir = other.path().join("context-archives").join(&chat.scope);
let refused = import(&store, changed.as_bytes()).unwrap_err();
let recorded = digest_of("one byte of this changes");
let content = digest_of("one byte of this chanGes");
assert_eq!(
refused.reason,
format!("digest does not match content (record {recorded}, content {content})")
);
assert!(refused.id.is_some());
assert_eq!(entries(&dir), Vec::<String>::new());
assert_eq!(import(&store, &line).unwrap().outcome, ImportOutcome::Stored);
drop(tmp);
}
#[test]
fn t_a5_each_refusal_has_its_reason_and_writes_nothing() {
let (_tmp, chat) = setup();
let other = tempfile::tempdir().unwrap();
let store = ArchiveStore::for_scope(other.path(), &chat.scope).unwrap();
let dir = other.path().join("context-archives").join(&chat.scope);
let id = random_id();
let two = [msg("user", json!("a")), msg("user", json!("b"))];
let line = |change: &dyn Fn(&mut Record)| record_bytes(&chat, &id, &two, change);
let with = |key: &str, value: Value| {
let mut record: Value = serde_json::from_slice(&line(&|_| {})).unwrap();
record[key] = value;
serde_json::to_vec(&record).unwrap()
};
let note = |length: usize| {
line(&move |record: &mut Record| {
if let Body::Live { note, .. } = &mut record.body {
*note = "n".repeat(length);
}
})
};
let many = vec![json!({"role": "user", "content": "m"}); MAX_MESSAGES + 1];
let mut huge: Value = serde_json::from_slice(&line(&|_| {})).unwrap();
huge["body"]["messages"][0]["message"]["content"] = json!("x".repeat(MAX_SEGMENT_BYTES));
let huge = serde_json::to_vec(&huge).unwrap();
let cases: Vec<(&str, Vec<u8>, &str)> = vec![
("version", line(&|record| record.version = 2), "version 2 is not supported"),
("unknown field", with("extra", json!(1)), "not an archive record: an unknown field"),
("a missing field", b"{\"version\":1}".to_vec(), "not an archive record: a field is"),
("not JSON", b"not json".to_vec(), "not an archive record: not JSON"),
("uppercase id", line(&|record| record.id = id.to_uppercase()), "invalid id"),
("logical", line(&|record| record.logical = "z".repeat(64)), "invalid logical"),
("digest", line(&|record| record.digest.truncate(60)), "invalid digest"),
(
"an uppercase digest on a tombstone",
line(&|record| {
record.digest = record.digest.to_uppercase();
record.body = Body::Tombstone;
}),
"invalid digest",
),
("scope shape", line(&|record| record.scope = "Z".repeat(64)), "invalid scope"),
(
"another project",
line(&|record| record.scope = "0".repeat(64)),
"scope is not this project's",
),
("4,097 messages", raw_line(&chat, &id, many, ""), "over a per-archive limit"),
("a note of 8,193 bytes", note(MAX_NOTE_BYTES + 1), "over a per-archive limit"),
(
"source_index out of order",
line(&|record| {
if let Body::Live { messages, .. } = &mut record.body {
messages.swap(0, 1);
}
}),
"source_index out of order",
),
(
"a message that is a string",
raw_line(&chat, &id, vec![json!("just text")], ""),
"message is not an object",
),
(
"a tombstone with created_ms",
line(&|record| {
record.body = Body::Tombstone;
record.created_ms = Some(1);
}),
"a tombstone carries no created_ms",
),
(
"over 16 MiB",
huge,
"over the 16 MiB archive size",
),
(
"a note tombstone line",
b"{\"tombstone\":\"mem-0123\",\"timestamp_ms\":1}".to_vec(),
"not an archive record: an unknown field",
),
];
for (what, line, expected) in &cases {
let reason = reason(&store, line);
assert!(reason.starts_with(expected), "{what}: {reason}");
let mut seen = ImportSeen::default();
let dry = store.import(line, false, true, &mut seen).unwrap().unwrap_err();
assert_eq!(dry.reason, reason, "{what}");
assert_eq!(entries(&dir), Vec::<String>::new(), "{what}");
}
assert_eq!(import(&store, ¬e(MAX_NOTE_BYTES)).unwrap().outcome, ImportOutcome::Stored);
let system = random_id();
let line = raw_line(&chat, &system, vec![json!({"role": "system", "content": "s"})], "");
assert_eq!(import(&store, &line).unwrap().outcome, ImportOutcome::Stored);
assert_eq!(store.fetch(&system, 0, 1).unwrap(), [msg("system", json!("s"))]);
let routed = record_scope(b"{\"tombstone\":\"mem-0123\",\"timestamp_ms\":1}").unwrap_err();
assert_eq!(routed.reason, "not an archive record: a field is missing (column 41)");
let routed = record_scope(&with("scope", json!("../../etc"))).unwrap_err();
assert_eq!(routed.reason, "invalid scope");
for scope in ["../../etc", "", &"A".repeat(64)] {
assert!(ArchiveStore::for_scope(other.path(), scope).is_err());
assert!(ArchiveStore::existing_scope(other.path(), scope).is_err());
}
assert_eq!(project_scope("synthetic-project").unwrap(), chat.scope);
}
#[test]
fn t_a6_an_id_that_is_taken_is_never_written_over() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let id = random_id();
assert_eq!(
import(&store, &record_bytes(&store, &id, &text("first content"), |_| {}))
.unwrap()
.outcome,
ImportOutcome::Stored
);
let before = snapshot(&dir);
for change in [(|_: &mut Record| {}) as fn(&mut Record), tombstone] {
let other = record_bytes(&store, &id, &text("second content"), change);
let refused = import(&store, &other).unwrap_err();
assert_eq!(refused.reason, "id already holds different content");
assert_eq!(refused.id.as_deref(), Some(id.as_str()));
assert_eq!(snapshot(&dir), before);
}
assert_eq!(store.fetch(&id, 0, 1).unwrap(), text("first content"));
let odd = random_id();
let file = dir.join("s000001").join(archive_name(&odd));
std::fs::write(&file, b"{\"cut\": \"short").unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap();
let before = snapshot(&dir);
let line = record_bytes(&store, &odd, &text("under a damaged name"), |_| {});
for dry_run in [true, false] {
let stopped = store.import(&line, false, dry_run, &mut ImportSeen::default());
let error = stopped.expect_err("the run stops");
assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{error}");
assert_eq!(snapshot(&dir), before);
}
std::fs::write(dir.join("s000002"), b"not a directory").unwrap();
let line = record_bytes(&store, &random_id(), &text("behind an odd shelf"), |_| {});
let stopped = store.import(&line, false, false, &mut ImportSeen::default());
assert!(stopped.is_err());
assert_eq!(shelf_files(&dir).len(), 2);
}
#[test]
fn t_a7_tombstones_win_in_both_directions() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let id = random_id();
let live = record_bytes(&store, &id, &text("to be forgotten"), |record| {
record.created_ms = Some(1_600_000_000_000);
});
let dead = record_bytes(&store, &id, &text("to be forgotten"), tombstone);
assert_eq!(import(&store, &live).unwrap().outcome, ImportOutcome::Stored);
let done = import(&store, &dead).unwrap();
assert_eq!((done.outcome, done.live, done.message_count), (ImportOutcome::Forgotten, false, 0));
assert_eq!(done.unread, None);
let file = dir.join("s000001").join(archive_name(&id));
assert!(is_tombstone_of(&store, &file, &digest_of("to be forgotten")));
let fields = ["body", "digest", "id", "logical", "scope", "version"];
assert_eq!(top_level(&json_of(&file)), fields);
assert_eq!(store.fetch(&id, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
let before = snapshot(&dir);
let kept = import(&store, &live).unwrap();
assert_eq!((kept.outcome, kept.live), (ImportOutcome::KeptForgotten, true));
let again = import(&store, &dead).unwrap();
assert_eq!(again.outcome, ImportOutcome::Present { note_differs: false });
assert_eq!(snapshot(&dir), before);
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let done = import(&store, &dead).unwrap();
assert_eq!((done.outcome, done.shelf.as_deref()), (ImportOutcome::Stored, Some("s000001")));
let file = dir.join("s000001").join(archive_name(&id));
assert_eq!(std::fs::read(&file).unwrap(), dead);
assert_eq!(store.fetch(&id, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
assert_eq!(import(&store, &live).unwrap().outcome, ImportOutcome::KeptForgotten);
let sealed = store.seal(&text("sealed and forgotten"), "").unwrap();
store.forget(&sealed.id).unwrap();
let before = snapshot(&dir);
let line = record_bytes(&store, &random_id(), &text("sealed and forgotten"), |_| {});
assert_eq!(reason(&store, &line), "this content was forgotten in this store");
assert_eq!(snapshot(&dir), before);
let sealed = store.seal(&text("live under its own id"), "").unwrap();
let before = snapshot(&dir);
let line = record_bytes(&store, &random_id(), &text("live under its own id"), tombstone);
let refused = reason(&store, &line);
assert!(refused.contains(&sealed.id) && refused.starts_with("a tombstone of content"));
assert_eq!(snapshot(&dir), before);
assert_eq!(store.fetch(&sealed.id, 0, 1).unwrap(), text("live under its own id"));
}
fn a_run(chat: &ArchiveStore, scope: &str) -> Vec<Vec<u8>> {
let ids: Vec<_> = (0..7).map(|_| random_id()).collect();
let line = |id: &String, words: &str, change: fn(&mut Record)| {
record_bytes(chat, id, &text(words), |record| {
scope.clone_into(&mut record.scope);
change(record);
})
};
let live = |_: &mut Record| {};
vec![
line(&ids[0], "first", live),
line(&ids[0], "first", live),
line(&ids[0], "other content under the first id", live),
line(&ids[1], "second", live),
line(&ids[1], "second", tombstone),
line(&ids[1], "second", live),
line(&ids[2], "third", tombstone),
line(&ids[2], "third", live),
line(&ids[2], "third", tombstone),
line(&ids[3], "fourth", tombstone),
line(&ids[4], "fourth", live),
line(&ids[5], "fifth", live),
line(&ids[6], "fifth", tombstone),
b"not a record".to_vec(),
]
}
#[test]
fn t_a8_rehoming_and_a_dry_run_that_writes_nothing() {
let (tmp, chat) = setup();
let target = project_scope("another-project").unwrap();
assert_ne!(target, chat.scope);
let line = record_bytes(&chat, &random_id(), &text("moved between projects"), |_| {});
let id = serde_json::from_slice::<Value>(&line).unwrap()["id"].as_str().unwrap().to_owned();
let store = ArchiveStore::for_scope(tmp.path(), &target).unwrap();
let mut seen = ImportSeen::default();
let refused = store.import(&line, false, false, &mut seen).unwrap().unwrap_err();
assert_eq!(refused.reason, "scope is not this project's");
let done = store.import(&line, true, false, &mut seen).unwrap().unwrap();
assert_eq!(done.outcome, ImportOutcome::Stored);
let dir = tmp.path().join("context-archives").join(&target);
let file = only_archive_file(&dir);
let mut expected: Value = serde_json::from_slice(&line).unwrap();
expected["scope"] = json!(target);
assert_eq!(json_of(&file), expected, "only the scope changed");
assert_eq!(store.fetch(&id, 0, 1).unwrap(), text("moved between projects"));
assert_eq!(shelf_files(&directory(&tmp, &chat)), Vec::<PathBuf>::new());
let base = tmp.path().join("no such base");
let lines = a_run(&chat, &chat.scope);
assert!(ArchiveStore::existing_scope(&base, &chat.scope).unwrap().is_none());
let mut seen = ImportSeen::default();
let dry: Vec<_> = lines
.iter()
.map(|line| ArchiveStore::import_dry_absent(&chat.scope, line, false, &mut seen).unwrap())
.collect();
assert!(!base.exists());
let store = ArchiveStore::for_scope(&base, &chat.scope).unwrap();
let mut unused = ImportSeen::default();
let real: Vec<_> =
lines.iter().map(|line| store.import(line, false, false, &mut unused).unwrap()).collect();
assert_eq!(dry, real);
let outcomes: Vec<_> = real
.iter()
.map(|done| match done {
Ok(done) => format!("{:?}", done.outcome),
Err(refused) => refused.reason.clone(),
})
.collect();
assert_eq!(
outcomes[..9],
[
"Stored",
"Present { note_differs: false }",
"id already holds different content",
"Stored",
"Forgotten",
"KeptForgotten",
"Stored",
"KeptForgotten",
"Present { note_differs: false }",
]
);
assert_eq!(outcomes[9], "Stored");
assert_eq!(outcomes[10], "this content was forgotten in this store");
assert_eq!(outcomes[11], "Stored");
assert!(outcomes[12].starts_with("a tombstone of content that is live in this store as"));
assert!(outcomes[13].starts_with("not an archive record"));
let dir = base.join("context-archives").join(&chat.scope);
std::fs::remove_file(dir.join("lock")).unwrap();
let open = std::fs::Permissions::from_mode(0o755);
let files = shelf_files(&dir);
for path in [dir.clone(), dir.join("s000001")] {
std::fs::set_permissions(&path, open.clone()).unwrap();
}
for file in &files {
std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o640)).unwrap();
}
let before = (snapshot(&dir), entries(&dir));
let existing = ArchiveStore::existing_scope(&base, &chat.scope).unwrap().unwrap();
let mut seen = ImportSeen::default();
let dry: Vec<_> =
lines.iter().map(|line| existing.import(line, false, true, &mut seen).unwrap()).collect();
assert_eq!((snapshot(&dir), entries(&dir)), before);
assert!(!dir.join("lock").exists());
assert_eq!(mode(&dir), 0o755);
assert_eq!(mode(&dir.join("s000001")), 0o755);
for file in &files {
assert_eq!(mode(file), 0o640);
}
for file in &files {
std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o600)).unwrap();
}
let real: Vec<_> =
lines.iter().map(|line| store.import(line, false, false, &mut unused).unwrap()).collect();
assert_eq!(dry, real);
assert!(real[..2].iter().all(|done| {
done.as_ref().unwrap().outcome == ImportOutcome::Present { note_differs: false }
}));
}
#[test]
fn t_a_shelves_an_import_goes_on_in_the_next_shelf() {
let (tmp, chat) = setup();
let dir = directory(&tmp, &chat);
let store = ArchiveStore::for_scope(tmp.path(), &chat.scope).unwrap();
let total = SHELF_FILES + 2;
let ids: Vec<_> = (0..total).map(|_| random_id()).collect();
let lines: Vec<_> = ids
.iter()
.enumerate()
.map(|(index, id)| {
record_bytes(&chat, id, &text(&format!("archive {index}")), |record| {
record.created_ms = Some(1_600_000_000_000 + index as u64);
})
})
.collect();
let mut seen = ImportSeen::default();
let run = |dry_run: bool, seen: &mut ImportSeen| -> Vec<String> {
let mut shelves = Vec::new();
for line in &lines {
let done = store.import(line, false, dry_run, seen).unwrap().unwrap();
shelves.push(done.shelf.unwrap());
}
shelves
};
let before = (snapshot(&dir), entries(&dir));
let dry = run(true, &mut seen);
assert_eq!((snapshot(&dir), entries(&dir)), before);
let real = run(false, &mut seen);
assert_eq!(dry, real);
assert!(real[..SHELF_FILES].iter().all(|shelf| shelf == "s000001"));
assert_eq!(real[SHELF_FILES..], ["s000002", "s000002"]);
let on = |shelf: &str| {
let mut names = entries(&dir.join(shelf));
names.retain(|name| name.ends_with(".json"));
names
};
let mut first: Vec<_> = ids[..SHELF_FILES].iter().map(|id| archive_name(id)).collect();
let mut second: Vec<_> = ids[SHELF_FILES..].iter().map(|id| archive_name(id)).collect();
first.sort();
second.sort();
assert_eq!(on("s000001"), first);
assert_eq!(on("s000002"), second);
for (index, id) in ids.iter().enumerate() {
assert_eq!(store.fetch(id, 0, 1).unwrap(), text(&format!("archive {index}")));
}
assert!(run(false, &mut seen).iter().zip(&real).all(|(again, first)| again == first));
let more = record_bytes(&chat, &random_id(), &text("one more"), |_| {});
let mut seen = ImportSeen::default();
let planned = store.import(&more, false, true, &mut seen).unwrap().unwrap();
assert_eq!(planned.shelf.as_deref(), Some("s000002"));
let sealed = chat.seal(&text("sealed after the import"), "").unwrap();
assert!(dir.join("s000002").join(archive_name(&sealed.id)).is_file());
assert_eq!(on("s000002").len(), 3);
}
#[test]
fn t_a9_a_forgotten_import_cannot_be_sealed_again() {
let words = "imported and then forgotten";
let digest = digest_of(words);
let refused = |store: &ArchiveStore| {
let error = store.seal(&text(words), "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
assert!(error.to_string().contains("was forgotten"), "{error}");
};
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let id = random_id();
let line = record_bytes(&store, &id, &text(words), |_| {});
assert_eq!(import(&store, &line).unwrap().outcome, ImportOutcome::Stored);
std::fs::create_dir(dir.join("s000002")).unwrap();
std::fs::set_permissions(dir.join("s000002"), std::fs::Permissions::from_mode(0o700))
.unwrap();
assert_eq!(store.forget(&id).unwrap(), forgotten(&[&id]));
let guard = dir.join("s000002").join(archive_name(&digest[..32]));
let file = dir.join("s000001").join(archive_name(&id));
assert_eq!(shelf_files(&dir), [file.clone(), guard.clone()]);
assert!(is_tombstone_of(&store, &guard, &digest));
assert!(is_tombstone_of(&store, &file, &digest));
refused(&store);
assert_eq!(import(&store, &line).unwrap().outcome, ImportOutcome::KeptForgotten);
let other = record_bytes(&store, &random_id(), &text(words), |_| {});
assert_eq!(reason(&store, &other), "this content was forgotten in this store");
assert_eq!(shelf_files(&dir).len(), 2);
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let dead = record_bytes(&store, &id, &text(words), tombstone);
let done = import(&store, &dead).unwrap();
assert_eq!((done.outcome, done.shelf.as_deref()), (ImportOutcome::Stored, Some("s000001")));
let guard = dir.join("s000001").join(archive_name(&digest[..32]));
let file = dir.join("s000001").join(archive_name(&id));
let mut both = [file.clone(), guard.clone()];
both.sort();
assert_eq!(shelf_files(&dir), both);
assert!(is_tombstone_of(&store, &guard, &digest));
assert_eq!(std::fs::read(&file).unwrap(), dead);
refused(&store);
assert_eq!(reason(&store, &other), "this content was forgotten in this store");
assert_eq!(import(&store, &dead).unwrap().outcome, ImportOutcome::Present { note_differs: false });
let again = record_bytes(&store, &random_id(), &text(words), tombstone);
assert_eq!(import(&store, &again).unwrap().outcome, ImportOutcome::Stored);
assert_eq!(shelf_files(&dir).len(), 3);
let (tmp, store) = setup();
let own = record_bytes(&store, &digest[32..], &text(words), tombstone);
assert_eq!(import(&store, &own).unwrap().outcome, ImportOutcome::Stored);
assert_eq!(shelf_files(&directory(&tmp, &store)).len(), 1);
refused(&store);
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
store.seal(&text("a shelf to begin with"), "").unwrap();
let file = dir.join("s000001").join(archive_name(&id));
std::fs::write(&file, &dead).unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap();
assert_eq!(store.seal(&text(words), "").unwrap().id, digest[..32], "not seen by a seal");
let (tmp, store) = (drop(tmp), setup()).1;
let dir = directory(&tmp, &store);
store.seal(&text("a shelf to begin with"), "").unwrap();
let file = dir.join("s000001").join(archive_name(&id));
std::fs::write(&file, &dead).unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap();
assert_eq!(store.forget(&id).unwrap(), forgotten(&[]));
assert!(is_tombstone_of(&store, &dir.join("s000001").join(archive_name(&digest[..32])), &digest));
refused(&store);
let (tmp, store) = setup();
let sealed = store.seal(&text(words), "").unwrap();
store.forget(&sealed.id).unwrap();
assert_eq!(shelf_files(&directory(&tmp, &store)).len(), 1);
refused(&store);
}
#[test]
fn t_a9_forgetting_either_of_a_native_and_an_imported_copy_forgets_both() {
let words = "sealed here and imported too";
for forget_the_import in [true, false] {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let sealed = store.seal(&text(words), "").unwrap();
for index in 0..SHELF_FILES {
store.seal(&text(&format!("filler {index}")), "").unwrap();
}
let id = random_id();
let done = import(&store, &record_bytes(&store, &id, &text(words), |_| {})).unwrap();
assert_eq!(done.same_content_as.as_deref(), Some(sealed.id.as_str()));
assert_eq!(done.shelf.as_deref(), Some("s000002"));
let before = shelf_files(&dir);
let asked = if forget_the_import { &id } else { &sealed.id };
let done = store.forget(asked).unwrap();
let mut ids = done.ids.clone();
ids.sort();
let mut expected = [id.clone(), sealed.id.clone()];
expected.sort();
assert_eq!(ids, expected, "forgetting {asked}");
assert_eq!(done.ids[0], *asked);
assert_eq!(done.unread, None);
assert_eq!(shelf_files(&dir), before, "no third file");
let digest = digest_of(words);
assert!(is_tombstone_of(&store, &dir.join("s000001").join(archive_name(&sealed.id)), &digest));
assert!(is_tombstone_of(&store, &dir.join("s000002").join(archive_name(&id)), &digest));
for gone in [&id, &sealed.id] {
assert_eq!(store.fetch(gone, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
}
assert!(store.search("imported too", 8).unwrap().is_empty());
assert_eq!(
store.seal(&text(words), "").unwrap_err().kind(),
io::ErrorKind::PermissionDenied
);
assert_eq!(store.fetch(&store.seal(&text("filler 0"), "").unwrap().id, 0, 1).unwrap(), text("filler 0"));
assert_eq!(store.forget(asked).unwrap(), forgotten(&[]));
}
}
#[test]
fn a_forget_says_when_a_file_under_another_id_names_its_content_and_is_no_record() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let sealed = store.seal(&text("with a damaged twin"), "").unwrap();
let shelf = dir.join("s000001");
let private = std::fs::Permissions::from_mode(0o600);
let stray = shelf.join(archive_name(&random_id()));
std::fs::write(&stray, b"not json at all").unwrap();
std::fs::set_permissions(&stray, private.clone()).unwrap();
let twin = shelf.join(archive_name(&random_id()));
let damaged = format!("{{\"digest\":\"{}\",\"body\":\"cut\"}}", digest_of("with a damaged twin"));
std::fs::write(&twin, damaged).unwrap();
std::fs::set_permissions(&twin, private).unwrap();
let done = store.forget(&sealed.id).unwrap();
assert_eq!(done.ids, std::slice::from_ref(&sealed.id));
let unread = done.unread.expect("not complete");
let name = twin.file_name().unwrap().to_str().unwrap();
assert!(unread.starts_with(&format!("1 place(s) could not be read, first s000001/{name}: ")), "{unread}");
assert!(unread.ends_with(&format!(" (in {})", dir.display())), "{unread}");
assert_eq!(std::fs::read(&stray).unwrap(), b"not json at all");
}
#[test]
fn a_tombstone_line_is_refused_while_its_content_is_live_under_another_imported_id() {
let words = "live under an imported id";
let (a, b) = (random_id(), random_id());
let refusal = format!(
"a tombstone of content that is live in this store as {a}: forget that archive \
here, then import again"
);
for dry_first in [false, true] {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let live = record_bytes(&store, &a, &text(words), |_| {});
let dead = record_bytes(&store, &b, &text(words), tombstone);
let mut seen = ImportSeen::default();
if dry_first {
let planned = store.import(&live, false, true, &mut seen).unwrap().unwrap();
assert_eq!(planned.outcome, ImportOutcome::Stored);
let refused = store.import(&dead, false, true, &mut seen).unwrap().unwrap_err();
assert_eq!(refused.reason, refusal);
assert_eq!(shelf_files(&dir), Vec::<PathBuf>::new());
}
assert_eq!(import(&store, &live).unwrap().outcome, ImportOutcome::Stored);
let before = snapshot(&dir);
for dry_run in [true, false] {
let mut seen = ImportSeen::default();
let refused = store.import(&dead, false, dry_run, &mut seen).unwrap().unwrap_err();
assert_eq!((refused.reason.as_str(), refused.id.as_deref()), (&*refusal, Some(&*b)));
assert_eq!(snapshot(&dir), before);
}
assert_eq!(store.fetch(&a, 0, 1).unwrap(), text(words));
std::fs::create_dir(dir.join("s000002")).unwrap();
assert_eq!(reason(&store, &dead), refusal);
assert_eq!(store.fetch(&a, 0, 1).unwrap(), text(words));
}
let (_tmp, store) = setup();
let live = record_bytes(&store, &a, &text(words), |_| {});
let dead = record_bytes(&store, &b, &text(words), tombstone);
for dry_run in [true, false] {
let mut seen = ImportSeen::default();
let stored = store.import(&dead, false, dry_run, &mut seen).unwrap().unwrap();
assert_eq!(stored.outcome, ImportOutcome::Stored);
let refused = match dry_run {
true => store.import(&live, false, true, &mut seen).unwrap().unwrap_err(),
false => import(&store, &live).unwrap_err(),
};
assert_eq!(refused.reason, "this content was forgotten in this store");
}
assert_eq!(store.fetch(&a, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[test]
fn a_forget_is_not_complete_over_a_file_under_another_id_it_failed_to_read() {
let words = "a twin that is not read";
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let sealed = store.seal(&text(words), "").unwrap();
let twin = random_id();
let line = record_bytes(&store, &twin, &text(words), |_| {});
assert_eq!(import(&store, &line).unwrap().outcome, ImportOutcome::Stored);
let name = archive_name(&twin);
let dead = record_bytes(&store, &random_id(), &text("only the twin"), tombstone);
let lone_id = random_id();
let lone = record_bytes(&store, &lone_id, &text("only the twin"), |_| {});
assert_eq!(import(&store, &lone).unwrap().outcome, ImportOutcome::Stored);
for code in [libc::EIO, libc::EMFILE, libc::ENOMEM] {
let failing = Fault::on("open", &twin, code);
let done = store.forget(&sealed.id).unwrap();
let unread = done.unread.expect("not complete");
let start = format!("1 place(s) could not be read, first s000001/{name}: ");
assert!(unread.starts_with(&start), "{unread}");
drop(failing);
assert_eq!(store.fetch(&twin, 0, 1).unwrap(), text(words), "error {code}");
let failing = Fault::on("open", &lone_id, code);
let before = snapshot(&dir);
let stopped = store.import(&dead, false, false, &mut ImportSeen::default());
let error = stopped.expect_err("the run stops").to_string();
assert!(error.starts_with("a tombstone is not imported while"), "{error}");
drop(failing);
assert_eq!(snapshot(&dir), before);
}
let done = store.forget(&sealed.id).unwrap();
assert_eq!((done.ids, done.unread), (vec![twin.clone()], None));
assert_eq!(store.fetch(&twin, 0, 1).unwrap_err().kind(), io::ErrorKind::NotFound);
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let sealed = store.seal(&text(words), "").unwrap();
let link = dir.join("s000001").join(archive_name(&random_id()));
std::os::unix::fs::symlink(only_archive_file(&dir), &link).unwrap();
let closed = dir.join("s000001").join(archive_name(&random_id()));
if unsafe { libc::geteuid() } != 0 {
std::fs::write(&closed, record_bytes(&store, &random_id(), &text("x"), |_| {})).unwrap();
std::fs::set_permissions(&closed, std::fs::Permissions::from_mode(0o000)).unwrap();
}
let done = store.forget(&sealed.id).unwrap();
assert_eq!((done.ids, done.unread), (vec![sealed.id.clone()], None));
}
#[test]
fn a_created_ms_that_has_not_come_is_refused() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let hour = 60 * 60 * 1000;
let at = |ahead_ms: u64| {
record_bytes(&store, &random_id(), &text("dated ahead"), |record| {
record.created_ms = Some(now_ms() + ahead_ms);
})
};
for line in [at(25 * hour), at(u64::MAX / 2), at(366 * 24 * hour)] {
let refused = import(&store, &line).unwrap_err();
assert_eq!(refused.reason, "created_ms is more than a day after this machine's clock");
let mut seen = ImportSeen::default();
let dry = store.import(&line, false, true, &mut seen).unwrap().unwrap_err();
assert_eq!(dry, refused);
}
assert_eq!(shelf_files(&dir), Vec::<PathBuf>::new());
assert_eq!(import(&store, &at(23 * hour)).unwrap().outcome, ImportOutcome::Stored);
}
#[test]
fn an_imported_file_gets_its_old_time_after_its_name() {
let (tmp, store) = setup();
let dir = directory(&tmp, &store);
let id = random_id();
let line = record_bytes(&store, &id, &text("dated after the rename"), |record| {
record.created_ms = Some(1_600_000_000_000);
});
let started = std::time::SystemTime::now() - Duration::from_secs(5);
let stopping = Fault::on("date", &id, libc::EIO);
let stopped = store.import(&line, false, false, &mut ImportSeen::default());
assert_eq!(stopped.expect_err("stopped").raw_os_error(), Some(libc::EIO));
drop(stopping);
let file = dir.join("s000001").join(archive_name(&id));
assert_eq!(shelf_files(&dir), std::slice::from_ref(&file));
assert_eq!(entries(&dir.join("s000001")), [archive_name(&id)]);
assert!(std::fs::metadata(&file).unwrap().modified().unwrap() >= started);
assert_eq!(std::fs::read(&file).unwrap(), line);
assert_eq!(store.fetch(&id, 0, 1).unwrap(), text("dated after the rename"));
let again = import(&store, &line).unwrap();
assert_eq!(again.outcome, ImportOutcome::Present { note_differs: false });
let other = random_id();
let line = record_bytes(&store, &other, &text("dated and not stopped"), |record| {
record.created_ms = Some(1_600_000_000_000);
});
assert_eq!(import(&store, &line).unwrap().outcome, ImportOutcome::Stored);
let file = dir.join("s000001").join(archive_name(&other));
assert_eq!(std::fs::metadata(&file).unwrap().modified().unwrap(), at_ms(1_600_000_000_000));
}
#[test]
fn an_import_that_finds_its_record_confirms_it_on_disk() {
let (_tmp, store) = setup();
let id = random_id();
let live = record_bytes(&store, &id, &text("found and confirmed"), |_| {});
let gone = random_id();
let dead = record_bytes(&store, &gone, &text("kept forgotten"), tombstone);
let kept = record_bytes(&store, &gone, &text("kept forgotten"), |_| {});
assert_eq!(import(&store, &live).unwrap().outcome, ImportOutcome::Stored);
assert_eq!(import(&store, &dead).unwrap().outcome, ImportOutcome::Stored);
let present = ImportOutcome::Present { note_differs: false };
for (line, outcome) in [(&live, present), (&kept, ImportOutcome::KeptForgotten)] {
let failing = Fault::on("sync", "", libc::EIO);
let mut seen = ImportSeen::default();
let stopped = store.import(line, false, false, &mut seen);
assert_eq!(stopped.expect_err("synced").raw_os_error(), Some(libc::EIO));
let dry = store.import(line, false, true, &mut seen).unwrap().unwrap();
assert_eq!(dry.outcome, outcome);
drop(failing);
assert_eq!(import(&store, line).unwrap().outcome, outcome);
}
}
#[test]
fn a_base_that_links_to_a_folder_that_is_away_is_an_error_not_an_empty_store() {
let tmp = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(tmp.path()).unwrap();
let disk = root.join("disk");
let base = root.join("base");
std::fs::create_dir(&disk).unwrap();
symlink(&disk, &base).unwrap();
let store = setup_in(&base, "synthetic-project");
let sealed = store.seal(&text("on the other disk"), "").unwrap();
drop(store);
std::fs::rename(&disk, root.join("unmounted")).unwrap();
let away = ArchiveStore::open_existing(&base, "synthetic-project", "l").err().expect("away");
assert_ne!(away.kind(), io::ErrorKind::NotFound);
assert!(away.to_string().starts_with("the archive base is a link to a folder that is not there"));
assert!(ArchiveStore::existing_scope(&base, &project_scope("synthetic-project").unwrap()).is_err());
assert!(project_search(&base, "synthetic-project", "", 8).is_err());
let fetched = project_fetch(&base, "synthetic-project", &sealed.id, 0, 1).unwrap_err();
assert_ne!(fetched.kind(), io::ErrorKind::NotFound);
assert!(archive_file(&base, "synthetic-project", &sealed.id).is_err());
std::fs::rename(root.join("unmounted"), &disk).unwrap();
assert_eq!(project_search(&base, "synthetic-project", "", 8).unwrap().len(), 1);
for missing in [root.join("no-base"), root.join("no-folder/base")] {
assert!(ArchiveStore::open_existing(&missing, "synthetic-project", "l").unwrap().is_none());
assert!(project_search(&missing, "synthetic-project", "", 8).unwrap().is_empty());
}
}
}
#[cfg(all(test, unix))]
mod lock_tests {
use super::*;
use std::time::{Duration, Instant};
fn msg(role: &str, content: Value) -> SharedMessage {
Arc::new(json!({"role":role,"content":content}))
}
fn setup() -> (tempfile::TempDir, ArchiveStore) {
let tmp = tempfile::tempdir().unwrap();
let store =
ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-logical").unwrap();
(tmp, store)
}
fn wait_until(condition: impl Fn() -> bool) {
let started = Instant::now();
while !condition() {
assert!(started.elapsed() < Duration::from_secs(30), "timed out");
std::thread::sleep(Duration::from_millis(1));
}
}
fn gate_of(store: &ArchiveStore) -> Arc<Gate> {
Gate::for_directory(store.fs.identity().unwrap())
}
#[test]
fn a_descriptor_inherited_by_a_forked_child_does_not_keep_the_archive_locked() {
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::process::ExitStatus;
use std::thread::JoinHandle;
struct Child {
release: Option<io::PipeWriter>,
spawner: Option<JoinHandle<io::Result<ExitStatus>>>,
}
impl Child {
fn release_and_reap(&mut self) -> Option<std::thread::Result<io::Result<ExitStatus>>> {
if let Some(mut release) = self.release.take() {
let _ = release.write_all(b"r");
}
self.spawner.take().map(JoinHandle::join)
}
}
impl Drop for Child {
fn drop(&mut self) {
let _ = self.release_and_reap();
}
}
let (_tmp, mut store) = setup();
store.process_wait = Duration::ZERO;
let reference = store
.seal(&[msg("user", json!("evidence"))], "note")
.unwrap();
let (mut forked, signal) = io::pipe().unwrap();
let (hold, release) = io::pipe().unwrap();
let (signal_fd, hold_fd) = (signal.as_raw_fd(), hold.as_raw_fd());
let held = store.fs.lock().unwrap();
let mut child = Child {
release: Some(release),
spawner: Some(std::thread::spawn(move || {
let mut command = std::process::Command::new("true");
unsafe {
command.pre_exec(move || {
let interrupted =
|| io::Error::last_os_error().raw_os_error() == Some(libc::EINTR);
loop {
match libc::write(signal_fd, b"f".as_ptr().cast(), 1) {
1 => break,
-1 if interrupted() => {}
_ => libc::_exit(1),
}
}
let mut byte = 0u8;
while libc::read(hold_fd, (&mut byte as *mut u8).cast(), 1) < 0
&& interrupted()
{}
Ok(())
});
}
let status = command.status();
drop((signal, hold));
status
})),
};
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let mut ready = libc::pollfd {
fd: forked.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let left = deadline.saturating_duration_since(Instant::now());
let timeout = left.as_millis().try_into().unwrap_or(i32::MAX);
match unsafe { libc::poll(&mut ready, 1, timeout) } {
1 => break,
0 => panic!("the forked child never signaled"),
_ => assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EINTR)),
}
}
forked.read_exact(&mut [0; 1]).unwrap();
drop(held);
let sealed = store.seal(&[msg("user", json!("more evidence"))], "");
let status = child.release_and_reap().unwrap().unwrap().unwrap();
assert!(status.success());
assert_ne!(sealed.unwrap(), reference);
}
#[test]
fn an_expired_waiter_is_not_admitted_when_the_gate_frees_late() {
let gate = Gate::for_directory((u64::MAX, u64::MAX));
let holder = Gate::enter(gate.clone(), Duration::ZERO, None, BUSY).unwrap();
let wait = Duration::from_millis(50);
let waiter = {
let gate = gate.clone();
std::thread::spawn(move || Gate::enter(gate, wait, None, BUSY).map(drop))
};
wait_until(|| gate.waited.load(Ordering::Acquire) == 1);
{
let mut held = gate.held.lock().unwrap();
*held = false;
std::thread::sleep(wait * 2);
}
let error = waiter.join().unwrap().unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
assert_eq!(error.to_string(), BUSY);
drop(holder);
Gate::enter(gate, Duration::ZERO, None, BUSY).unwrap();
}
#[test]
fn concurrent_operations_in_one_process_wait_instead_of_reporting_busy() {
let tmp = tempfile::tempdir().unwrap();
let workers = 8;
let barrier = Arc::new(std::sync::Barrier::new(workers));
let handles: Vec<_> = (0..workers)
.map(|worker| {
let base = tmp.path().to_path_buf();
let barrier = barrier.clone();
std::thread::spawn(move || {
let logical = format!("synthetic-logical-{worker}");
let store = ArchiveStore::new(&base, "synthetic-project", &logical)?;
barrier.wait();
for round in 0..8 {
let text = format!("worker {worker} round {round}");
let reference = store.seal(&[msg("user", json!(text))], "note")?;
assert_eq!(store.fetch(&reference.id, 0, 1)?.len(), 1);
assert_eq!(store.fetch_note(&reference.id)?, "note");
store.search("worker", 4)?;
}
Ok::<_, io::Error>(())
})
})
.collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
for result in results {
result.unwrap();
}
let store = ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-reader").unwrap();
let dir = tmp.path().join("context-archives").join(&store.scope);
let segments: usize = std::fs::read_dir(&dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| path.is_dir())
.map(|shelf| std::fs::read_dir(shelf).unwrap().count())
.sum();
assert_eq!(segments, workers * 8);
}
#[test]
fn a_lock_held_outside_this_process_fails_closed_at_once_when_the_wait_is_zero() {
let (_tmp, mut store) = setup();
store.process_wait = Duration::ZERO;
let reference = store
.seal(&[msg("user", json!("evidence"))], "note")
.unwrap();
let foreign = store.fs.lock().unwrap();
let started = Instant::now();
let results = [
store
.seal(&[msg("user", json!("other evidence"))], "")
.map(drop),
store.forget(&reference.id).map(drop),
];
for result in results {
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
}
assert!(started.elapsed() < PROCESS_LOCK_WAIT);
drop(foreign);
assert_eq!(store.fetch(&reference.id, 0, 1).unwrap().len(), 1);
}
fn other(tmp: &tempfile::TempDir) -> ArchiveStore {
ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-other").unwrap()
}
#[test]
fn an_operation_queues_behind_the_one_in_progress_in_this_process() {
let (tmp, store) = setup();
let held = store.exclusive().unwrap();
let queued = other(&tmp);
let waiter =
std::thread::spawn(move || queued.seal(&[msg("user", json!("queued evidence"))], ""));
let gate = gate_of(&store);
wait_until(|| gate.waited.load(Ordering::Acquire) == 1);
assert!(!waiter.is_finished());
drop(held);
let reference = waiter.join().unwrap().unwrap();
assert_eq!(store.fetch(&reference.id, 0, 1).unwrap().len(), 1);
}
#[test]
fn a_stuck_operation_in_this_process_fails_closed_after_the_bounded_wait() {
let (tmp, store) = setup();
let held = store.exclusive().unwrap();
let mut queued = other(&tmp);
queued.wait = Duration::from_millis(50);
let started = Instant::now();
let evidence = [msg("user", json!("queued evidence"))];
let error = queued.seal(&evidence, "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
assert_eq!(error.to_string(), BUSY);
assert!(started.elapsed() >= queued.wait);
drop(held);
assert!(queued.search("", 1).unwrap().is_empty());
queued.seal(&evidence, "").unwrap();
}
#[test]
fn an_aborted_operation_never_starts_even_while_queued() {
let (tmp, store) = setup();
let abort = Arc::new(AtomicBool::new(true));
let error = other(&tmp)
.with_abort(abort.clone())
.seal(&[msg("user", json!("abandoned before start"))], "")
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::Interrupted);
abort.store(false, Ordering::Release);
let held = store.exclusive().unwrap();
let queued = other(&tmp).with_abort(abort.clone());
let waiter = std::thread::spawn(move || {
queued.seal(&[msg("user", json!("abandoned while queued"))], "")
});
let gate = gate_of(&store);
wait_until(|| gate.waited.load(Ordering::Acquire) == 1);
assert!(!waiter.is_finished());
abort.store(true, Ordering::Release);
let error = waiter.join().unwrap().unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::Interrupted);
drop(held);
assert!(store.search("", 1).unwrap().is_empty());
}
#[test]
fn seal_with_note_returns_the_note_stored_with_the_segment() {
let (_tmp, store) = setup();
let messages = [msg("user", json!("evidence"))];
let (first, note) = store.seal_with_note(&messages, "first note").unwrap();
assert_eq!(note, "first note");
let (again, note) = store.seal_with_note(&messages, "second note").unwrap();
assert_eq!(again, first);
assert_eq!(note, "first note");
assert_eq!(store.fetch_note(&first.id).unwrap(), "first note");
store.forget(&first.id).unwrap();
let error = store.seal_with_note(&messages, "").unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
}
fn project_entries(tmp: &tempfile::TempDir, store: &ArchiveStore) -> Vec<String> {
let dir = tmp.path().join("context-archives").join(&store.scope);
let entries = std::fs::read_dir(dir).unwrap();
let mut names: Vec<_> = entries
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.collect();
names.sort();
names
}
fn foreign_lock_for(store: &ArchiveStore, hold: Duration) -> std::thread::JoinHandle<()> {
let foreign = store.fs.lock().unwrap();
std::thread::spawn(move || {
std::thread::sleep(hold);
drop(foreign);
})
}
struct Raise<'a>(&'a AtomicBool);
impl Drop for Raise<'_> {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
fn evidence() -> [SharedMessage; 1] {
[msg("user", json!("evidence"))]
}
fn assert_reads(reader: &ArchiveStore, reference: &ArchiveRef) {
assert_eq!(reader.fetch(&reference.id, 0, 8).unwrap(), evidence());
assert_eq!(reader.fetch_note(&reference.id).unwrap(), "note");
let rows = reader.search("evidence", 8).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].id, reference.id);
}
fn assert_busy(result: io::Result<()>) {
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
assert_eq!(error.to_string(), BUSY);
}
#[test]
fn seal_waits_for_a_foreign_lock_then_succeeds() {
let (_tmp, store) = setup();
let hold = Duration::from_millis(300);
let started = Instant::now();
let holder = foreign_lock_for(&store, hold);
let reference = store.seal(&evidence(), "note").unwrap();
assert!(started.elapsed() >= hold);
holder.join().unwrap();
assert_reads(&store, &reference);
}
#[test]
fn forget_waits_for_a_foreign_lock_then_succeeds() {
let (_tmp, store) = setup();
let reference = store.seal(&evidence(), "note").unwrap();
let hold = Duration::from_millis(300);
let started = Instant::now();
let holder = foreign_lock_for(&store, hold);
store.forget(&reference.id).unwrap();
assert!(started.elapsed() >= hold);
holder.join().unwrap();
let error = store.fetch(&reference.id, 0, 1).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
}
#[test]
fn seal_reports_busy_after_the_wait() {
let (tmp, mut store) = setup();
assert_eq!(store.process_wait, PROCESS_LOCK_WAIT);
assert_eq!(PROCESS_LOCK_WAIT, Duration::from_secs(5));
store.process_wait = Duration::from_millis(200);
let foreign = store.fs.lock().unwrap();
let started = Instant::now();
assert_busy(store.seal(&evidence(), "note").map(drop));
assert!(started.elapsed() >= store.process_wait);
assert_eq!(project_entries(&tmp, &store), ["lock"]);
drop(foreign);
let reference = store.seal(&evidence(), "note").unwrap();
let foreign = store.fs.lock().unwrap();
let started = Instant::now();
assert_busy(store.forget(&reference.id).map(drop));
assert!(started.elapsed() >= store.process_wait);
drop(foreign);
assert_reads(&store, &reference);
}
#[test]
fn abandon_while_polling_returns_interrupted() {
let (tmp, store) = setup();
let foreign = store.fs.lock().unwrap();
let abort = Arc::new(AtomicBool::new(false));
let mut abandoned = other(&tmp).with_abort(abort.clone());
abandoned.process_wait = Duration::from_secs(30);
let started = Instant::now();
let waiter = std::thread::spawn(move || abandoned.seal(&evidence(), ""));
let gate = gate_of(&store);
wait_until(|| *gate.held.lock().unwrap());
abort.store(true, Ordering::Release);
let error = waiter.join().unwrap().unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::Interrupted);
assert!(started.elapsed() < Duration::from_secs(30));
assert_eq!(project_entries(&tmp, &store), ["lock"]);
assert!(!*gate.held.lock().unwrap());
drop(foreign);
let reference = store.seal(&evidence(), "note").unwrap();
assert_reads(&store, &reference);
}
#[test]
fn fetch_and_search_ignore_a_foreign_lock() {
let (_tmp, mut store) = setup();
let reference = store.seal(&evidence(), "note").unwrap();
store.process_wait = Duration::ZERO;
let _foreign = store.fs.lock().unwrap();
assert_reads(&store, &reference);
assert_busy(store.forget(&reference.id).map(drop));
}
#[test]
fn fetch_and_search_ignore_the_in_process_gate() {
let (tmp, store) = setup();
let reference = store.seal(&evidence(), "note").unwrap();
let _held = store.exclusive().unwrap();
let mut reader = other(&tmp);
reader.wait = Duration::ZERO;
assert_reads(&reader, &reference);
assert_busy(reader.forget(&reference.id).map(drop));
}
#[test]
fn missing_id_without_legacy_files_is_not_found_without_locking() {
let (tmp, store) = setup();
let mut reader = other(&tmp);
(reader.wait, reader.process_wait) = (Duration::ZERO, Duration::ZERO);
let missing = "a".repeat(32);
let absent = |id: &str| {
let _held = store.exclusive().unwrap();
for result in [reader.fetch(id, 0, 1).map(drop), reader.fetch_note(id).map(drop)] {
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
assert!(reader.search("", 8).unwrap().is_empty());
};
absent(&missing);
let reference = store.seal(&evidence(), "note").unwrap();
store.forget(&reference.id).unwrap();
absent(&missing);
absent(&reference.id);
}
#[test]
fn reader_racing_forget_sees_live_or_not_found() {
let (tmp, store) = setup();
let reader = other(&tmp);
for round in 0..100 {
let words = format!("evidence {round};");
let messages = [msg("user", json!(words)), msg("assistant", json!("reply"))];
let id = store.seal(&messages, "note").unwrap().id;
let live = |kind: usize| {
let found = match kind {
8 => reader.fetch_note(&id).map(|note| assert_eq!(note, "note")),
9 => reader.search(&words, 2).and_then(|rows| match rows.as_slice() {
[] => Err(not_found()),
[row] => {
assert_eq!((row.id.as_str(), row.message_count), (id.as_str(), 2));
Ok(())
}
rows => panic!("one segment, {} rows", rows.len()),
}),
_ => reader
.fetch(&id, 0, MAX_FETCH_MESSAGES)
.map(|found| assert_eq!(found, messages)),
};
match found {
Ok(()) => true,
Err(error) => {
assert_eq!(error.kind(), io::ErrorKind::NotFound, "{error}");
false
}
}
};
let forgotten = AtomicBool::new(false);
let start = std::sync::Barrier::new(12);
std::thread::scope(|scope| {
scope.spawn(|| {
start.wait();
loop {
let after = forgotten.load(Ordering::Acquire);
let located = archive_file(tmp.path(), "synthetic-project", &id);
assert!(located.unwrap().is_some());
if after {
break;
}
}
});
for kind in 0..10 {
let (live, forgotten, start) = (&live, &forgotten, &start);
scope.spawn(move || {
start.wait();
let mut gone = false;
loop {
let after = forgotten.load(Ordering::Acquire);
if live(kind) {
assert!(!gone && !after, "read after it was forgotten");
} else if after {
break;
} else {
gone = true;
}
}
});
}
start.wait();
let _returned = Raise(&forgotten);
store.forget(&id).unwrap();
});
}
}
#[test]
fn reader_holding_the_old_file_reads_the_whole_live_record() {
let (tmp, store) = setup();
let reference = store.seal(&evidence(), "note").unwrap();
let name = archive_name(&reference.id);
let project = tmp.path().join("context-archives").join(&store.scope);
let path = project.join("s000001").join(&name);
let original = std::fs::read(&path).unwrap();
let shelf = store.fs.open_child("s000001", true).unwrap().unwrap();
let open = || shelf.open(&c_name(&name).unwrap(), libc::O_RDONLY).unwrap();
let (unlocked, locked) = (open(), open());
store.forget(&reference.id).unwrap();
let bytes = PrivateDir::read_open(unlocked, MAX_SEGMENT_BYTES, true).unwrap();
assert_eq!(bytes, original);
let Body::Live { messages, note, .. } = store.record(&name, &bytes).unwrap().body else {
panic!("not the live record");
};
let messages: Vec<_> = messages.into_iter().map(|m| m.message).collect();
assert_eq!((messages.as_slice(), note.as_str()), (&evidence()[..], "note"));
let error = PrivateDir::read_open(locked, MAX_SEGMENT_BYTES, false).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
assert_ne!(std::fs::read(&path).unwrap(), original);
let error = store.fetch(&reference.id, 0, 1).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
let located = archive_file(tmp.path(), "synthetic-project", &reference.id);
assert_eq!(located.unwrap(), Some(path));
}
#[test]
fn searcher_racing_seals_never_errors() {
let (tmp, store) = setup();
let reader = other(&tmp);
let words = |index: usize| format!("racing seal {index};");
let window = |index: usize| [msg("user", json!(words(index)))];
let ids: Vec<String> = (0..300)
.map(|index| {
let projection = project_archive("synthetic-logical", &window(index), "", None);
projection.unwrap().digest[..32].to_owned()
})
.collect();
let sealed = std::sync::atomic::AtomicUsize::new(0);
let done = AtomicBool::new(false);
let (searching, fetching) = (AtomicBool::new(false), AtomicBool::new(false));
std::thread::scope(|scope| {
scope.spawn(|| loop {
let finished = done.load(Ordering::Acquire);
let count = sealed.load(Ordering::Acquire);
if let Some(newest) = count.checked_sub(1) {
let (rows, unread) = reader.search_scan(&words(newest), 2).unwrap();
let found: Vec<_> = rows.iter().map(|row| row.id.as_str()).collect();
assert_eq!((found, unread), (vec![ids[newest].as_str()], None));
}
let rows = reader.search("racing seal", MAX_SEARCH_RESULTS).unwrap();
assert!(rows.len() >= count.min(MAX_SEARCH_RESULTS));
for (index, row) in rows.iter().enumerate() {
assert!(ids.contains(&row.id));
assert_eq!((row.message_count, row.source_message_count), (1, 1));
assert!(rows[..index].iter().all(|earlier| earlier.id != row.id));
}
searching.store(true, Ordering::Release);
if finished {
break;
}
});
let fetcher = scope.spawn(|| {
let mut next = 0;
while next < ids.len() {
let finished = done.load(Ordering::Acquire);
match reader.fetch(&ids[next], 0, 2) {
Ok(found) => {
assert_eq!(found, window(next));
next += 1;
}
Err(error) => {
assert_eq!(error.kind(), io::ErrorKind::NotFound, "{error}");
fetching.store(true, Ordering::Release);
if finished {
break;
}
}
}
}
next
});
let stop = Raise(&done);
wait_until(|| searching.load(Ordering::Acquire) && fetching.load(Ordering::Acquire));
for (index, id) in ids.iter().enumerate() {
assert_eq!(&store.seal(&window(index), "").unwrap().id, id);
sealed.store(index + 1, Ordering::Release);
}
drop(stop);
assert_eq!(fetcher.join().unwrap(), ids.len());
});
assert_eq!(store.shelves().unwrap().len(), 3);
}
fn shelved(tmp: &tempfile::TempDir, store: &ArchiveStore) -> Vec<String> {
let project = tmp.path().join("context-archives").join(&store.scope);
let mut names = Vec::new();
for shelf in store.shelves().unwrap() {
let entries = std::fs::read_dir(project.join(shelf)).unwrap();
names.extend(entries.map(|entry| entry.unwrap().file_name().into_string().unwrap()));
}
names
}
#[test]
fn writers_inside_publish_at_once_without_the_lock() {
const WRITERS: usize = 4;
const ROUNDS: usize = 150;
let (tmp, store) = setup();
let shelf = store.fs.child("s000001", true).unwrap();
let bytes = |writer: usize, round: usize| {
format!("writer {writer} round {round};").repeat(4096).into_bytes()
};
let own = |writer: usize, round: usize| format!("{:032x}.json", writer * ROUNDS + round);
let wanted = |round: usize| format!("{:032x}.json", WRITERS * ROUNDS + round);
let start = std::sync::Barrier::new(WRITERS);
let won: Vec<Vec<usize>> = std::thread::scope(|scope| {
let writers: Vec<_> = (0..WRITERS)
.map(|writer| {
let (shelf, start) = (&shelf, &start);
scope.spawn(move || {
start.wait();
let mut won = Vec::new();
for round in 0..ROUNDS {
let stored = shelf.publish(&own(writer, round), &bytes(writer, round), true);
stored.unwrap();
match shelf.publish(&wanted(round), &bytes(writer, round), true) {
Ok(()) => won.push(round),
Err(error) => assert_eq!(error.kind(), io::ErrorKind::AlreadyExists),
}
}
won
})
})
.collect();
writers.into_iter().map(|writer| writer.join().unwrap()).collect()
});
let read = |name: &str| shelf.read_with(name, MAX_SEGMENT_BYTES, true).unwrap().unwrap();
for round in 0..ROUNDS {
for writer in 0..WRITERS {
assert!(read(&own(writer, round)) == bytes(writer, round), "{writer} {round}");
}
let winners: Vec<_> = (0..WRITERS).filter(|writer| won[*writer].contains(&round)).collect();
assert_eq!(winners.len(), 1, "round {round}");
assert!(read(&wanted(round)) == bytes(winners[0], round), "round {round}");
}
assert_eq!(shelved(&tmp, &store).len(), (WRITERS + 1) * ROUNDS);
}
#[test]
fn seals_at_once_without_the_lock_each_store_their_window() {
const WRITERS: usize = 4;
let (tmp, store) = setup();
let window = |words: String| [msg("user", json!(words))];
let project = |messages: &[SharedMessage], note: &str| {
project_hashed(&store.logical, messages, note, None, Reduce::None).unwrap()
};
let seal_all = |same: bool, rounds: usize| -> Vec<Vec<(ArchiveRef, String)>> {
let start = std::sync::Barrier::new(WRITERS);
std::thread::scope(|scope| {
let writers: Vec<_> = (0..WRITERS)
.map(|writer| {
let (store, start) = (&store, &start);
scope.spawn(move || {
start.wait();
let note = format!("note of writer {writer}");
let name = if same { "any".to_owned() } else { format!("writer {writer}") };
let seal = |round| {
let window = window(format!("{name} window {round};"));
store.store(project(&window, ¬e), Written::now())
};
(0..rounds).map(|round| seal(round).unwrap()).collect()
})
})
.collect();
writers.into_iter().map(|writer| writer.join().unwrap()).collect()
})
};
let same = seal_all(true, 30);
for (round, (reference, note)) in same[0].iter().enumerate() {
let words = format!("any window {round};");
assert_eq!(store.fetch(&reference.id, 0, 2).unwrap(), window(words));
assert_eq!(&store.fetch_note(&reference.id).unwrap(), note);
assert!(note.starts_with("note of writer "));
for other in &same[1..] {
assert_eq!(other[round], (reference.clone(), note.clone()));
}
}
assert_eq!(shelved(&tmp, &store).len(), 30);
let own = seal_all(false, 80);
for (writer, sealed) in own.iter().enumerate() {
for (round, (reference, note)) in sealed.iter().enumerate() {
let words = format!("writer {writer} window {round};");
assert_eq!(store.fetch(&reference.id, 0, 2).unwrap(), window(words));
assert_eq!(note, &format!("note of writer {writer}"));
}
}
let mut names = shelved(&tmp, &store);
assert_eq!(names.len(), 30 + WRITERS * 80);
names.sort();
names.dedup();
assert_eq!(names.len(), 30 + WRITERS * 80);
assert!(names.iter().all(|name| archive_id(name).is_some()));
assert!(store.shelves().unwrap().len() >= 3);
let (reference, note) = store.seal_with_note(&window("any window 0;".into()), "").unwrap();
assert_eq!((&reference, ¬e), (&same[0][0].0, &same[0][0].1));
}
#[test]
fn concurrent_seals_across_shelf_boundaries_fill_each_shelf_exactly() {
const WRITERS: usize = 4;
const EACH: usize = 80;
let (tmp, store) = setup();
let window = |writer: usize, round: usize| {
[msg("user", json!(format!("writer {writer} window {round};")))]
};
let start = std::sync::Barrier::new(WRITERS);
let sealed: Vec<Vec<String>> = std::thread::scope(|scope| {
let writers: Vec<_> = (0..WRITERS)
.map(|writer| {
let (tmp, start) = (&tmp, &start);
scope.spawn(move || {
let name = format!("writer-{writer}");
let store = ArchiveStore::new(tmp.path(), "synthetic-project", &name);
let store = store.unwrap();
start.wait();
let seal = |round| store.seal(&window(writer, round), "note").unwrap().id;
(0..EACH).map(seal).collect()
})
})
.collect();
writers.into_iter().map(|writer| writer.join().unwrap()).collect()
});
assert_eq!(store.shelves().unwrap(), ["s000003", "s000002", "s000001"]);
let project = tmp.path().join("context-archives").join(&store.scope);
let held = |shelf: &str| std::fs::read_dir(project.join(shelf)).unwrap().count();
let rest = WRITERS * EACH - 2 * SHELF_FILES;
assert_eq!(
(held("s000001"), held("s000002"), held("s000003")),
(SHELF_FILES, SHELF_FILES, rest)
);
for (writer, ids) in sealed.iter().enumerate() {
for (round, id) in ids.iter().enumerate() {
assert_eq!(store.fetch(id, 0, 2).unwrap(), window(writer, round));
}
}
let mut names = shelved(&tmp, &store);
names.sort();
names.dedup();
assert_eq!(names.len(), WRITERS * EACH);
assert!(names.iter().all(|name| archive_id(name).is_some()));
}
const SEAL_CHILD: &str = "SYNTHETIC_ARCHIVE_SEAL_CHILD";
const CHILD_SEALS: usize = 100;
#[test]
fn two_processes_seal_child() {
let Some(base) = std::env::var_os(SEAL_CHILD).map(PathBuf::from) else {
return;
};
let name = format!("child-{}", std::process::id());
let store = ArchiveStore::new(&base, "synthetic-project", &name).unwrap();
std::fs::write(base.join(format!("ready-{}", std::process::id())), b"").unwrap();
wait_until(|| base.join("go").exists());
for round in 0..CHILD_SEALS {
let words = format!("{name} round {round};");
store.seal(&[msg("user", json!(words))], "note").unwrap();
}
}
#[test]
#[serial_test::parallel(process_spawn)]
fn two_processes_seal_concurrently() {
let tmp = tempfile::tempdir().unwrap();
let (_, module) = module_path!().split_once("::").unwrap();
let child = format!("{module}::two_processes_seal_child");
let mut children: Vec<_> = (0..2)
.map(|_| {
std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", &child, "--nocapture"])
.env(SEAL_CHILD, tmp.path())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.spawn()
.unwrap()
})
.collect();
let ready = |child: &std::process::Child| format!("ready-{}", child.id());
wait_until(|| children.iter().all(|child| tmp.path().join(ready(child)).exists()));
std::fs::write(tmp.path().join("go"), b"").unwrap();
for child in &mut children {
assert!(child.wait().unwrap().success());
}
let store = ArchiveStore::new(tmp.path(), "synthetic-project", "synthetic-reader").unwrap();
let project = tmp.path().join("context-archives").join(&store.scope);
let held = |shelf: &str| std::fs::read_dir(project.join(shelf)).unwrap().count();
assert_eq!(
(held("s000001"), held("s000002")),
(SHELF_FILES, 2 * CHILD_SEALS - SHELF_FILES)
);
assert_eq!(store.shelves().unwrap().len(), 2);
for child in &children {
let name = format!("child-{}", child.id());
for round in 0..CHILD_SEALS {
let window = [msg("user", json!(format!("{name} round {round};")))];
let projection = project_archive(&name, &window, "", None).unwrap();
let found = store.fetch(&projection.digest[..32], 0, 2);
assert_eq!(found.unwrap(), window, "{name} round {round}");
}
for round in [0, CHILD_SEALS - 1] {
let words = format!("{name} round {round};");
assert_eq!(store.search(&words, 2).unwrap().len(), 1, "{words}");
}
}
}
}