use crate::context_index::IndexedContext;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::io::Write;
use std::path::{Path, PathBuf};
pub(crate) const SCHEMA: u32 = 3;
#[derive(Debug, serde::Deserialize)]
pub(crate) struct Stored {
pub(crate) schema: u32,
pub(crate) canonicalization: String,
pub(crate) files: BTreeMap<String, String>,
pub(crate) records: Vec<IndexedContext>,
}
#[derive(serde::Serialize)]
struct Entry<'a> {
schema: u32,
canonicalization: &'a str,
files: &'a BTreeMap<String, String>,
records: &'a [IndexedContext],
}
#[derive(Debug)]
pub(crate) enum Load {
Stored(Stored),
Missing,
Corrupt,
Io(std::io::Error),
}
pub(crate) struct Store {
root: PathBuf,
}
impl Store {
pub(crate) fn default_root() -> PathBuf {
match std::env::var_os("XDG_CACHE_HOME") {
Some(dir) if !dir.is_empty() => PathBuf::from(dir).join("echoes"),
_ => {
let home = std::env::var_os("HOME").map_or_else(PathBuf::new, PathBuf::from);
home.join(".cache").join("echoes")
}
}
}
pub(crate) fn open(root: &Path) -> std::io::Result<Self> {
std::fs::create_dir_all(root)?;
Ok(Self {
root: root.to_path_buf(),
})
}
pub(crate) fn load(&self, dir: &str, name: &str) -> Load {
let path = self.root.join(dir).join(format!("{name}.json"));
match std::fs::read(&path) {
Ok(bytes) => match serde_json::from_slice::<Stored>(&bytes) {
Ok(stored) => Load::Stored(stored),
Err(_) => Load::Corrupt,
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Load::Missing,
Err(err) => Load::Io(err),
}
}
pub(crate) fn save(
&self,
dir: &str,
name: &str,
canonicalization: &str,
files: &BTreeMap<String, String>,
records: &[IndexedContext],
) -> std::io::Result<()> {
let dir = self.root.join(dir);
std::fs::create_dir_all(&dir)?;
let payload = Entry {
schema: SCHEMA,
canonicalization,
files,
records,
};
let target = dir.join(format!("{name}.json"));
let mut temp = tempfile::NamedTempFile::new_in(dir)?;
{
let mut writer = std::io::BufWriter::new(temp.as_file_mut());
serde_json::to_writer(&mut writer, &payload)?;
writer.flush()?;
}
temp.persist(target).map_err(|error| error.error)?;
Ok(())
}
}
const SENTINEL: &str = r#"
pub mod alpha {
use core::mem;
pub struct Beta {
pub gamma: u32,
}
impl Beta {
pub fn delta(&self, input: u32) -> u32 {
let value = input + 1u32;
match value {
0 => self.gamma,
_ => mem::swap(value, 2u32),
}
}
}
pub fn epsilon(value: u32) -> u32 {
let zeta: u8 = 0;
if value > 1u32 { value - 1u32 } else { u32::from(zeta) }
}
pub fn reflexive(value: u32) -> bool {
value == value
}
pub fn xor(value: u32) -> u32 {
value ^ value
}
pub fn xor_literal(value: u32) -> u32 {
value ^ 7 ^ value
}
pub fn wrapping(value: u32, other: u32) -> u32 {
other.wrapping_add(value)
}
pub fn selected(flag: bool, value: u32, other: u32) -> u32 {
match flag {
false => other,
true => value,
}
}
pub fn joined() -> &'static str {
concat!("al", "pha")
}
pub fn unset(flag: bool) -> bool {
flag == false
}
pub fn both(flag: bool, other: bool) -> bool {
if other { flag } else { false }
}
pub fn accumulated(mut total: u32, step: u32) -> u32 {
total += step;
total
}
pub fn counted() -> u32 {
let start: u32 = 1;
start
}
pub fn scaled(value: u32, other: u32) -> u32 {
other.saturating_mul(value) * value
}
pub fn padded(value: u32) -> u32 {
(value | 0) * 1
}
pub fn armed(flag: bool, other: bool) -> u32 {
if flag { if other { 1u32 } else { 0u32 } } else { 0u32 }
}
pub fn guarded(flag: bool, value: u32) -> u32 {
if flag { return value; }
0u32
}
pub fn slot(slot: Option<u32>) -> u32 {
let Some(value) = slot else { return 0u32; };
match value { 0 => 1u32, 1 => 1u32, _ => value }
}
pub fn label() -> String {
String::from("sentinel")
}
pub fn widened(value: u32, flag: bool) -> u64 {
u64::from(value) + flag as u64
}
pub fn picked<'a, T>(values: &'a [T]) -> Option<&'a T> where T: Ord + Copy {
values.iter().max()
}
pub fn copied(value: u32, other: u32) -> bool {
&value == &other
}
pub fn checked(value: u32) -> Option<u32> {
if value == 0 { return None; }
Some(value)
}
pub fn skipped(values: &[u32], quiet: bool) {
if quiet { return; }
for value in values { if *value == 0 { continue; } epsilon(*value); }
epsilon(0u32);
}
pub fn ranked(value: Option<u32>, digit: u8) -> u32 {
let first = if let Some(found) = value { found } else { 0 };
match digit { 2 => first, 0 => 1, 1 => first, _ => 9 }
}
pub fn trimmed(values: &mut Vec<u32>) {
let _ = values.pop();
}
pub fn bounded(value: u32) -> bool {
value >= 1 && value <= 9
}
pub fn chosen(value: u32, other: u32, flag: bool) -> u32 {
let bit: u32 = if flag { 1 } else { 0 };
bit + std::cmp::max(value, other)
}
pub fn summed(values: &[u32]) -> u32 {
let mut sum = 0;
for value in values.iter() { sum += value; }
if values.len() == 0 { 1 } else { sum }
}
pub fn worded(value: u32) -> String {
println!("{} {}", "value", value);
if value == 0 { String::from("") } else { format!("set") }
}
pub fn boxed(value: u32) -> Box<dyn core::fmt::Debug + Send + Sync> {
Box::new(value)
}
pub struct Held<'a>(pub &'a u32);
impl<'a> Held<'a> {
pub fn get(&self) -> u32 { *self.0 }
}
pub fn bucket(id: u32) -> u32 {
(id / 16 % 8) as u32 + (b'a' as u32) - u8::MAX as u32
}
pub fn small(value: u32) -> bool {
matches!(value, 1 | 3..=5) || (7..=9).contains(&value)
}
pub fn coded(flag: bool) -> u8 {
if flag { return 2 + 3; }
if flag { 1 } else { 0 }
}
pub fn shifted(value: u32) -> u32 {
let width = 2u32 * 4u32;
return value << width;
}
}
mod custom {
macro_rules! vec {
(1) => { 10u32 };
(1,) => { 20u32 };
}
pub(crate) use vec;
}
fn tail_scope() -> u32 {
let value = std::cell::RefCell::new(7u32);
return *value.borrow();
}
fn main() {
let beta = alpha::Beta { gamma: 3u32 };
beta.delta(1u32);
alpha::epsilon(2u32);
custom::vec!(1,);
}
"#;
pub(crate) fn canonicalization_fingerprint() -> String {
let file = syn::parse_file(SENTINEL).expect("the sentinel source parses");
let tokens = syn_canon::canonicalize(file).to_string();
format!(
"{}:{}",
blake3::hash(tokens.as_bytes()).to_hex(),
env!("CARGO_PKG_VERSION")
)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::borrow::ToOwned;
fn records(n: usize) -> Vec<IndexedContext> {
(0..n)
.map(|i| IndexedContext {
form: format!("form-{i}"),
tokens: i + 1,
site: crate::ContextSite {
file: "src/lib.rs".to_owned(),
line: u32::try_from(i).unwrap_or(u32::MAX) + 1,
column: 1,
end: u32::try_from(i).unwrap_or(u32::MAX) + 2,
end_column: 2,
item: format!("pkg::f{i}"),
kind: crate::ContextKind::FunctionBody,
},
module: format!("pkg::f{i}"),
crate_name: "pkg".to_owned(),
crate_root: "/tmp/pkg/src/lib.rs".to_owned(),
access: crate::ContextAccess::PublicFunction,
conditions: Vec::new(),
extraction_notes: Vec::new(),
api_path: None,
})
.collect()
}
fn map(n: usize) -> BTreeMap<String, String> {
(0..n)
.map(|i| (format!("src/f{i}.rs"), format!("hash-{i}")))
.collect()
}
#[test]
fn save_then_load_round_trips() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let records = records(3);
store
.save(
"public/crates.io",
"alpha-1.0.0",
&canonicalization_fingerprint(),
&map(3),
&records,
)
.unwrap();
let load = store.load("public/crates.io", "alpha-1.0.0");
assert!(
matches!(&load, Load::Stored(stored) if stored.schema == SCHEMA
&& stored.canonicalization == canonicalization_fingerprint()
&& stored.files == map(3)
&& stored.records.len() == 3),
"{load:?}"
);
assert!(matches!(
store.load("public/crates.io", "alpha-1.0.1"),
Load::Missing
));
let raw = dir.path().join("public/crates.io/alpha-1.0.0.json");
assert!(raw.is_file());
let temp = dir.path().join("public/crates.io/.alpha-1.0.0.json.tmp");
assert!(!temp.exists(), "temp file must not survive a save");
}
#[test]
fn corrupt_entry_reports_corrupt_and_rebuilds() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
std::fs::create_dir_all(dir.path().join("private/path-a")).unwrap();
std::fs::write(dir.path().join("private/path-a/beta.json"), "not json").unwrap();
let records = records(1);
store
.save(
"private/path-a",
"beta",
&canonicalization_fingerprint(),
&map(1),
&records,
)
.unwrap();
assert!(matches!(
store.load("private/path-a", "beta"),
Load::Stored(stored) if stored.schema == SCHEMA
));
}
#[test]
fn truncated_entry_reports_corrupt() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let records = records(1);
store
.save(
"private/path-a",
"beta",
&canonicalization_fingerprint(),
&map(1),
&records,
)
.unwrap();
let path = dir.path().join("private/path-a/beta.json");
let bytes = std::fs::read(&path).unwrap();
std::fs::write(&path, &bytes[..bytes.len() / 2]).unwrap();
assert!(matches!(
store.load("private/path-a", "beta"),
Load::Corrupt
));
}
#[test]
fn save_replaces_an_existing_entry() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let one = records(1);
store
.save("public/crates.io", "alpha-1.0.0", "c1", &map(1), &one)
.unwrap();
let two = records(2);
store
.save("public/crates.io", "alpha-1.0.0", "c2", &map(2), &two)
.unwrap();
let load = store.load("public/crates.io", "alpha-1.0.0");
assert!(
matches!(&load, Load::Stored(stored) if stored.canonicalization == "c2"
&& stored.records.len() == 2),
"{load:?}"
);
}
#[test]
fn a_directory_where_the_entry_lives_reports_io() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
std::fs::create_dir_all(dir.path().join("public/crates.io/alpha.json")).unwrap();
assert!(matches!(
store.load("public/crates.io", "alpha"),
Load::Io(err) if matches!(err.kind(), std::io::ErrorKind::IsADirectory)
));
}
#[test]
fn the_sentinel_source_parses() {
assert!(syn::parse_file(SENTINEL).is_ok());
}
#[test]
fn default_root_honors_the_xdg_cache_home() {
if std::env::var_os("ECHOES_COV_XDG_PROBE").is_some() {
let home = std::env::var_os("XDG_CACHE_HOME").expect("the probe run sets the variable");
assert_eq!(Store::default_root(), PathBuf::from(home).join("echoes"));
return;
}
let dir = tempfile::tempdir().unwrap();
let output =
std::process::Command::new(std::env::current_exe().expect("the test binary path"))
.env("ECHOES_COV_XDG_PROBE", "1")
.env("XDG_CACHE_HOME", dir.path())
.arg("context_cache::tests::default_root_honors_the_xdg_cache_home")
.arg("--exact")
.output()
.expect("the test binary runs itself");
assert!(
output.status.success(),
"the probe run failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn default_root_ignores_an_empty_xdg_cache_home() {
if std::env::var_os("ECHOES_COV_XDG_EMPTY_PROBE").is_some() {
let home = std::env::var_os("HOME").expect("the probe run sets the variable");
assert_eq!(
Store::default_root(),
PathBuf::from(home).join(".cache").join("echoes")
);
return;
}
let home = tempfile::tempdir().unwrap();
let output =
std::process::Command::new(std::env::current_exe().expect("the test binary path"))
.env("ECHOES_COV_XDG_EMPTY_PROBE", "1")
.env("XDG_CACHE_HOME", "")
.env("HOME", home.path())
.arg("context_cache::tests::default_root_ignores_an_empty_xdg_cache_home")
.arg("--exact")
.output()
.expect("the test binary runs itself");
let text = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(output.status.success(), "the probe run failed: {text}");
}
}