use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::{Arc, Mutex};
pub(crate) const MAX_SOURCES: usize = 64;
type PermissionKey = (String, String, String);
thread_local! {
static PERMITTED: std::cell::RefCell<Option<std::collections::HashMap<PermissionKey, bool>>> =
const { std::cell::RefCell::new(None) };
}
pub fn with_permission_memo<T>(operation: impl FnOnce() -> T) -> T {
struct Restore(Option<std::collections::HashMap<PermissionKey, bool>>);
impl Drop for Restore {
fn drop(&mut self) {
let previous = self.0.take();
PERMITTED.with(|memo| *memo.borrow_mut() = previous);
}
}
let _restore =
Restore(PERMITTED.with(|memo| memo.borrow_mut().replace(std::collections::HashMap::new())));
operation()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchiveAuthority {
version: u8,
project: String,
path: String,
tool: String,
admitted_digest: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
more: Vec<AdditionalSource>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
incomplete: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct AdditionalSource {
path: String,
tool: String,
admitted_digest: String,
}
#[derive(Debug, Default)]
pub struct CaptureSlot {
authority: Mutex<Option<ArchiveAuthority>>,
unbound: std::sync::atomic::AtomicBool,
}
const SLOT_WAIT: std::time::Duration = std::time::Duration::from_millis(250);
impl CaptureSlot {
fn lock_within(
&self,
budget: std::time::Duration,
) -> Option<std::sync::MutexGuard<'_, Option<ArchiveAuthority>>> {
let deadline = std::time::Instant::now() + budget;
loop {
match self.authority.try_lock() {
Ok(guard) => return Some(guard),
Err(std::sync::TryLockError::Poisoned(poisoned)) => {
return Some(poisoned.into_inner());
}
Err(std::sync::TryLockError::WouldBlock)
if std::time::Instant::now() < deadline =>
{
std::thread::sleep(std::time::Duration::from_micros(200));
}
Err(std::sync::TryLockError::WouldBlock) => {
self.unbound
.store(true, std::sync::atomic::Ordering::SeqCst);
return None;
}
}
}
}
fn publish_within(&self, authority: ArchiveAuthority, budget: std::time::Duration) {
if let Some(mut slot) = self.lock_within(budget) {
match slot.as_mut() {
Some(existing) => existing.join(authority),
None => *slot = Some(authority),
}
}
}
fn finish_within(&self, budget: std::time::Duration) -> Option<ArchiveAuthority> {
let mut authority = self.lock_within(budget).and_then(|mut slot| slot.take());
if self.unbound.load(std::sync::atomic::Ordering::SeqCst) {
match authority.as_mut() {
Some(authority) => authority.incomplete = true,
None => authority = Some(ArchiveAuthority::unbound()),
}
}
authority
}
}
pub(crate) type Capture = Arc<CaptureSlot>;
tokio::task_local! {
pub static CAPTURE: Option<Capture>;
}
pub fn current_capture() -> Option<Capture> {
CAPTURE.try_with(Clone::clone).ok().flatten()
}
pub fn without_capture<T>(operation: impl FnOnce() -> T) -> T {
CAPTURE.sync_scope(None, operation)
}
pub fn mark_unbound() {
if let Some(capture) = current_capture() {
capture
.unbound
.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
pub fn publish_read(project: &Path, canonical_path: &str, tool: &str, admitted: &str) {
crate::core::context_admission::capture::note_source(project, canonical_path);
if current_capture().is_none() {
return;
}
if let Some(authority) = ArchiveAuthority::file(project, canonical_path, tool, admitted) {
authority.publish();
}
}
pub async fn capture<T>(
operation: impl std::future::Future<Output = T>,
) -> (T, Option<ArchiveAuthority>) {
let capture = Arc::new(CaptureSlot::default());
let result = CAPTURE.scope(Some(capture.clone()), operation).await;
(result, capture.finish_within(SLOT_WAIT))
}
pub fn capture_sync<T>(operation: impl FnOnce() -> T) -> (T, Option<ArchiveAuthority>) {
let capture = Arc::new(CaptureSlot::default());
let result = CAPTURE.sync_scope(Some(capture.clone()), operation);
(result, capture.finish_within(SLOT_WAIT))
}
impl ArchiveAuthority {
pub fn file(project: &Path, path: &str, tool: &str, admitted: &str) -> Option<Self> {
if !crate::core::context_admission::stored::source_authority_required() {
return None;
}
let authority = Self::bind(project, path, tool, admitted);
if authority.is_none() {
mark_unbound();
}
authority
}
pub(crate) fn admitted_source(
project: &Path,
path: &str,
tool: &str,
admitted_digest: &str,
) -> Option<Self> {
if !crate::core::context_admission::stored::source_authority_required() {
return None;
}
let authority = Self::bind_digest(project, path, tool, admitted_digest);
if authority.is_none() {
mark_unbound();
}
authority
}
fn bind_digest(project: &Path, path: &str, tool: &str, admitted_digest: &str) -> Option<Self> {
let well_formed = admitted_digest.len() == 64
&& admitted_digest
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase());
if !well_formed {
return None;
}
let project = canonical_archive_path(project)?;
let path = canonical_archive_path(&project.join(path))?;
if !path.starts_with(&project) {
return None;
}
Some(Self {
version: 1,
project: project.to_str()?.to_owned(),
path: path.to_str()?.to_owned(),
tool: tool.to_owned(),
admitted_digest: admitted_digest.to_owned(),
more: Vec::new(),
incomplete: false,
})
}
fn unbound() -> Self {
Self {
version: 1,
project: String::new(),
path: String::new(),
tool: String::new(),
admitted_digest: String::new(),
more: Vec::new(),
incomplete: true,
}
}
pub fn is_incomplete(&self) -> bool {
self.incomplete
}
fn bind(project: &Path, path: &str, tool: &str, admitted: &str) -> Option<Self> {
let project = canonical_archive_path(project)?;
let path = canonical_archive_path(Path::new(path))?;
if !path.starts_with(&project) {
return None;
}
Some(Self {
version: 1,
project: project.to_str()?.to_owned(),
path: path.to_str()?.to_owned(),
tool: tool.to_owned(),
admitted_digest: blake3::hash(admitted.as_bytes()).to_hex().to_string(),
more: Vec::new(),
incomplete: false,
})
}
pub fn publish(self) {
if let Some(capture) = current_capture() {
capture.publish_within(self, SLOT_WAIT);
}
}
pub fn join(&mut self, other: Self) {
if other.project != self.project {
self.incomplete = true;
return;
}
self.incomplete |= other.incomplete;
let first = AdditionalSource {
path: other.path,
tool: other.tool,
admitted_digest: other.admitted_digest,
};
for source in std::iter::once(first).chain(other.more) {
let primary = source.path == self.path
&& source.tool == self.tool
&& source.admitted_digest == self.admitted_digest;
if primary || self.more.contains(&source) {
continue;
}
if self.more.len() + 1 >= MAX_SOURCES {
self.incomplete = true;
return;
}
self.more.push(source);
}
}
pub(super) fn project(&self) -> &str {
&self.project
}
pub(super) fn sources(&self) -> Option<Vec<(&str, &str, &str)>> {
if self.incomplete {
return None;
}
Some(
std::iter::once((&self.path, &self.tool, &self.admitted_digest))
.chain(
self.more
.iter()
.map(|source| (&source.path, &source.tool, &source.admitted_digest)),
)
.map(|(path, tool, digest)| (path.as_str(), tool.as_str(), digest.as_str()))
.collect(),
)
}
pub(super) fn absorb(&mut self, later: Self) {
if later.project != self.project {
self.incomplete = true;
return;
}
self.incomplete |= later.incomplete;
let first = AdditionalSource {
path: later.path,
tool: later.tool,
admitted_digest: later.admitted_digest,
};
for source in std::iter::once(first).chain(later.more) {
if source.path == self.path && source.tool == self.tool {
self.admitted_digest = source.admitted_digest;
continue;
}
if let Some(known) = self
.more
.iter_mut()
.find(|known| known.path == source.path && known.tool == source.tool)
{
known.admitted_digest = source.admitted_digest;
continue;
}
if self.more.len() + 1 >= MAX_SOURCES {
self.incomplete = true;
return;
}
self.more.push(source);
}
}
pub(crate) fn id(&self, content: &str) -> Option<String> {
let mut hash = blake3::Hasher::new();
hash.update(b"leanctx-source-archive-v1\0");
hash.update(&serde_json::to_vec(self).ok()?);
hash.update(b"\0");
hash.update(content.as_bytes());
Some(hash.finalize().to_hex().to_string())
}
pub fn admitted(&self) -> bool {
self.admitted_budgeted(&mut crate::core::limits::max_read_bytes())
}
#[must_use]
pub(crate) fn still_admitted(&self) -> bool {
crate::core::policy::runtime::with_source_view(|| self.admitted()).unwrap_or(false)
}
#[must_use]
pub(crate) fn still_permitted(&self) -> bool {
crate::core::policy::runtime::with_source_view(|| self.permitted()).unwrap_or(false)
}
fn permitted(&self) -> bool {
if self.incomplete || !self.matches_project() {
return false;
}
std::iter::once((&self.path, &self.tool))
.chain(self.more.iter().map(|source| (&source.path, &source.tool)))
.all(|(path, tool)| {
let key = (self.project.clone(), path.clone(), tool.clone());
if let Some(known) = PERMITTED.with(|memo| {
memo.borrow()
.as_ref()
.and_then(|memo| memo.get(&key).copied())
}) {
return known;
}
let mut remaining = crate::core::limits::max_read_bytes();
let permitted = crate::core::rooted_read::read_file_for_tool_rooted_admitted(
path,
&self.project,
tool,
&mut remaining,
)
.is_ok_and(|read| read.complete && read.storable);
PERMITTED.with(|memo| {
if let Some(memo) = memo.borrow_mut().as_mut() {
memo.insert(key, permitted);
}
});
permitted
})
}
#[must_use]
pub(crate) fn identity_digest(&self) -> Option<String> {
if self.incomplete {
return None;
}
let mut sources: Vec<(&str, &str, &str)> =
std::iter::once((&self.path, &self.tool, &self.admitted_digest))
.chain(
self.more
.iter()
.map(|source| (&source.path, &source.tool, &source.admitted_digest)),
)
.map(|(path, tool, digest)| (path.as_str(), tool.as_str(), digest.as_str()))
.collect();
sources.sort_unstable();
let mut hash = blake3::Hasher::new();
hash.update(b"leanctx-source-lineage-v1\0");
hash.update(&serde_json::to_vec(&(self.version, &self.project, sources)).ok()?);
Some(hash.finalize().to_hex().to_string())
}
pub(super) fn matches_project(&self) -> bool {
if self.version != 1 {
return false;
}
let Some(project) = crate::core::policy::runtime::REQUEST_PROJECT
.try_with(|slot| slot.borrow().clone())
.ok()
.flatten()
.and_then(|path| crate::core::pathutil::canonicalize_secure(&path).ok())
else {
return false;
};
project == Path::new(&self.project)
}
pub(super) fn admitted_budgeted(&self, remaining: &mut usize) -> bool {
if self.incomplete || !self.matches_project() {
return false;
}
let primary = (&self.path, &self.tool, &self.admitted_digest);
std::iter::once(primary)
.chain(
self.more
.iter()
.map(|source| (&source.path, &source.tool, &source.admitted_digest)),
)
.all(|(path, tool, digest)| {
*remaining != 0
&& crate::core::rooted_read::read_file_for_tool_rooted_admitted(
path,
&self.project,
tool,
remaining,
)
.is_ok_and(|read| {
read.complete
&& read.storable
&& blake3::hash(read.content.as_bytes()).to_hex().as_str()
== digest.as_str()
})
})
}
}
fn canonical_archive_path(path: &Path) -> Option<std::path::PathBuf> {
if !path.is_absolute() {
return None;
}
crate::core::pathutil::canonicalize_secure(path)
.or_else(|error| {
let simplified = crate::core::pathutil::strip_verbatim(path.to_path_buf());
if simplified.as_path() == path {
Err(error)
} else {
crate::core::pathutil::canonicalize_secure(&simplified)
}
})
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn source(path: &str) -> ArchiveAuthority {
ArchiveAuthority {
version: 1,
project: "/project".to_owned(),
path: path.to_owned(),
tool: "ctx_read".to_owned(),
admitted_digest: blake3::hash(path.as_bytes()).to_hex().to_string(),
more: Vec::new(),
incomplete: false,
}
}
use std::time::Duration;
fn hold(slot: &Arc<CaptureSlot>) -> (std::sync::mpsc::Sender<()>, std::thread::JoinHandle<()>) {
let (release, wait) = std::sync::mpsc::channel::<()>();
let (held, is_held) = std::sync::mpsc::channel::<()>();
let slot = slot.clone();
let holder = std::thread::spawn(move || {
let guard = slot.lock_within(Duration::from_secs(5)).expect("free slot");
held.send(()).expect("signal");
let _ = wait.recv();
drop(guard);
});
is_held.recv().expect("the holder owns the slot");
(release, holder)
}
#[test]
fn slot_contention_is_bounded_and_never_loses_a_source_silently() {
let slot = Arc::new(CaptureSlot::default());
let (release, holder) = hold(&slot);
let started = std::time::Instant::now();
slot.publish_within(source("/project/a.rs"), Duration::from_millis(20));
assert!(started.elapsed() < Duration::from_secs(2), "bounded wait");
drop(release);
holder.join().expect("holder");
let captured = slot
.finish_within(Duration::from_secs(5))
.expect("an unknown acquisition is reported, not dropped");
assert!(captured.is_incomplete(), "exceeded budget → incomplete");
assert!(!captured.admitted(), "an incomplete authority never admits");
let slot = Arc::new(CaptureSlot::default());
let (release, holder) = hold(&slot);
let publisher = {
let slot = slot.clone();
std::thread::spawn(move || {
slot.publish_within(source("/project/a.rs"), Duration::from_secs(5));
slot.publish_within(source("/project/b.rs"), Duration::from_secs(5));
})
};
std::thread::sleep(Duration::from_millis(20));
drop(release);
holder.join().expect("holder");
publisher.join().expect("publisher");
let captured = slot
.finish_within(Duration::from_secs(5))
.expect("captured");
assert_eq!(captured.more.len(), 1, "the second source joined the first");
assert!(!captured.is_incomplete());
}
}