use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result};
use rusqlite::{Connection, OptionalExtension, Row};
use serde_json::{Value, json};
fn database_path(state_root: &Path) -> PathBuf {
state_root.join("cache").join("token-v4.sqlite3")
}
fn configured_connection(path: &Path) -> Result<Connection> {
let connection = Connection::open(path)
.with_context(|| format!("failed to open packed cache {}", path.display()))?;
connection.busy_timeout(Duration::from_secs(5))?;
Ok(connection)
}
pub(crate) fn sqlite_u64(row: &Row<'_>, index: usize) -> rusqlite::Result<u64> {
let value = row.get::<_, i64>(index)?;
u64::try_from(value).map_err(|_| rusqlite::Error::IntegralValueOutOfRange(index, value))
}
pub(crate) fn sqlite_i64(value: usize, field: &str) -> Result<i64> {
i64::try_from(value).with_context(|| format!("{field} exceeds SQLite's signed INTEGER range"))
}
fn sidecar_bytes(path: &Path, suffix: &str) -> u64 {
fs::metadata(format!("{}{suffix}", path.display()))
.map(|metadata| metadata.len())
.unwrap_or_default()
}
fn quarantined_stats(path: &Path) -> (usize, u64) {
let Some(parent) = path.parent() else {
return (0, 0);
};
let prefix = format!(
"{}.corrupt-",
path.file_name()
.and_then(|value| value.to_str())
.unwrap_or("token-v4.sqlite3")
);
fs::read_dir(parent)
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|entry| {
let name = entry.file_name().to_string_lossy().to_string();
let Some(suffix) = name.strip_prefix(&prefix) else {
return false;
};
let timestamp = suffix
.strip_suffix("-wal")
.or_else(|| suffix.strip_suffix("-shm"))
.unwrap_or(suffix);
!timestamp.is_empty() && timestamp.bytes().all(|byte| byte.is_ascii_digit())
})
.filter_map(|entry| fs::metadata(entry.path()).ok())
.fold((0usize, 0u64), |(count, bytes), metadata| {
(count + 1, bytes.saturating_add(metadata.len()))
})
}
pub fn status(state_root: &Path) -> Result<Value> {
let path = database_path(state_root);
let (quarantined_files, quarantined_bytes) = quarantined_stats(&path);
if !path.exists() {
return Ok(
json!({"schema_version": 1, "command": "cache status", "status": if quarantined_files > 0 { "quarantined_uncached" } else { "absent" }, "entries": 0, "payload_bytes": 0, "database_bytes": 0, "wal_bytes": 0, "shm_bytes": 0, "persistent_bytes": 0, "transient_bytes": 0, "allocated_bytes": 0, "quarantined_files": quarantined_files, "quarantined_bytes": quarantined_bytes, "repair_command": "git slop find", "cleanup_command": "git slop cache prune --max-entries 0 --max-bytes 0 --compact --yes"}),
);
}
let connection = match configured_connection(&path) {
Ok(connection) => connection,
Err(error) => {
return Ok(json!({
"schema_version": 1, "command": "cache status", "status": "unavailable",
"entries": 0, "payload_bytes": 0,
"database_bytes": fs::metadata(&path).map(|value| value.len()).unwrap_or_default(),
"wal_bytes": sidecar_bytes(&path, "-wal"), "shm_bytes": sidecar_bytes(&path, "-shm"),
"integrity": "not_checked", "diagnostic": format!("cache could not be opened: {}", error.root_cause())
}));
}
};
let table_exists: Option<String> = connection
.query_row(
"SELECT name FROM sqlite_master WHERE type='table' AND name='token_cache'",
[],
|row| row.get(0),
)
.optional()?;
if table_exists.is_none() {
return Ok(json!({
"schema_version": 1, "command": "cache status", "status": "invalid_schema",
"entries": 0, "payload_bytes": 0,
"database_bytes": fs::metadata(&path).map(|value| value.len()).unwrap_or_default(),
"wal_bytes": sidecar_bytes(&path, "-wal"), "shm_bytes": sidecar_bytes(&path, "-shm"),
"integrity": "token_cache_table_missing"
}));
}
let (entries, bytes, oldest, newest): (u64, u64, Option<i64>, Option<i64>) = connection.query_row(
"SELECT COUNT(*), COALESCE(SUM(payload_bytes), 0), MIN(accessed_at), MAX(accessed_at) FROM token_cache",
[],
|row| Ok((sqlite_u64(row, 0)?, sqlite_u64(row, 1)?, row.get(2)?, row.get(3)?)),
)?;
let integrity: String = connection.query_row("PRAGMA quick_check(1)", [], |row| row.get(0))?;
let page_count = connection.query_row("PRAGMA page_count", [], |row| sqlite_u64(row, 0))?;
let free_pages = connection.query_row("PRAGMA freelist_count", [], |row| sqlite_u64(row, 0))?;
let page_size = connection.query_row("PRAGMA page_size", [], |row| sqlite_u64(row, 0))?;
let database_bytes = fs::metadata(&path)
.map(|metadata| metadata.len())
.unwrap_or_default();
let wal_bytes = sidecar_bytes(&path, "-wal");
let shm_bytes = sidecar_bytes(&path, "-shm");
Ok(json!({
"schema_version": 1, "command": "cache status", "status": if integrity == "ok" { "ready" } else { "corrupt" },
"entries": entries, "payload_bytes": bytes,
"database_bytes": database_bytes, "wal_bytes": wal_bytes, "shm_bytes": shm_bytes,
"persistent_bytes": database_bytes, "transient_bytes": wal_bytes.saturating_add(shm_bytes),
"allocated_bytes": database_bytes.saturating_add(wal_bytes).saturating_add(shm_bytes),
"quarantined_files": quarantined_files, "quarantined_bytes": quarantined_bytes,
"page_count": page_count, "free_pages": free_pages, "page_size": page_size,
"reclaimable_bytes": free_pages.saturating_mul(page_size), "integrity": integrity,
"oldest_accessed_at_unix": oldest, "newest_accessed_at_unix": newest
}))
}
pub fn prune(
state_root: &Path,
max_entries: usize,
max_bytes: u64,
dry_run: bool,
compact: bool,
) -> Result<Value> {
let path = database_path(state_root);
let before = status(state_root)?;
if !path.exists() {
return Ok(
json!({"schema_version": 1, "command": "cache prune", "dry_run": dry_run, "before": before, "removed_entries": 0, "removed_payload_bytes": 0, "after": before}),
);
}
let mut connection = configured_connection(&path)?;
let candidates = {
let mut statement = connection.prepare(
"SELECT cache_key, payload_bytes FROM token_cache ORDER BY accessed_at, cache_key",
)?;
statement
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, sqlite_u64(row, 1)?))
})?
.collect::<rusqlite::Result<Vec<_>>>()?
};
let mut entries = before["entries"].as_u64().unwrap_or_default() as usize;
let mut bytes = before["payload_bytes"].as_u64().unwrap_or_default();
let mut selected = Vec::new();
for (key, candidate_bytes) in candidates {
if entries <= max_entries && bytes <= max_bytes {
break;
}
entries = entries.saturating_sub(1);
bytes = bytes.saturating_sub(candidate_bytes);
selected.push((key, candidate_bytes));
}
if !dry_run {
let transaction = connection.transaction()?;
for (key, _) in &selected {
transaction.execute("DELETE FROM token_cache WHERE cache_key = ?1", [key])?;
}
transaction.commit()?;
connection.execute_batch("PRAGMA wal_checkpoint(TRUNCATE); PRAGMA optimize;")?;
if compact {
connection.execute_batch("VACUUM;")?;
}
}
let removed_bytes = selected.iter().map(|(_, bytes)| *bytes).sum::<u64>();
if !dry_run {
drop(connection);
}
let after = if dry_run {
json!({"entries": entries, "payload_bytes": bytes, "projected": true})
} else {
status(state_root)?
};
Ok(json!({
"schema_version": 1, "command": "cache prune", "dry_run": dry_run,
"limits": {"max_entries": max_entries, "max_bytes": max_bytes}, "compact": compact,
"before": before, "removed_entries": selected.len(),
"removed_payload_bytes": removed_bytes,
"physical_reclaimed_bytes": before["allocated_bytes"].as_u64().unwrap_or_default().saturating_sub(after["allocated_bytes"].as_u64().unwrap_or_default()),
"after": after
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sqlite_unsigned_adapter_rejects_negative_integers() {
let connection = Connection::open_in_memory().unwrap();
let value = connection
.query_row("SELECT 42", [], |row| sqlite_u64(row, 0))
.unwrap();
assert_eq!(value, 42);
let error = connection
.query_row("SELECT -1", [], |row| sqlite_u64(row, 0))
.unwrap_err();
assert!(matches!(
error,
rusqlite::Error::IntegralValueOutOfRange(0, -1)
));
}
#[test]
fn sqlite_size_adapter_rejects_values_outside_signed_range() {
assert_eq!(sqlite_i64(42, "fixture").unwrap(), 42);
if usize::BITS > i64::BITS {
assert!(sqlite_i64(usize::MAX, "fixture").is_err());
}
}
}