lean_ctx/core/ccr/
storage.rs1use crate::core::context_admission::stored::source_authority_required;
14use crate::core::policy::{content, runtime};
15use std::path::Path;
16
17fn limit() -> usize {
18 crate::core::limits::max_read_bytes().min(content::MAX_PROTECTED_CONTENT_BYTES)
19}
20
21fn community<T>(operation: impl FnOnce() -> Option<T>) -> Option<T> {
22 runtime::with_source_view(|| {
23 if source_authority_required() {
24 return None;
25 }
26 operation()
27 })
28 .ok()
29 .flatten()
30}
31
32fn store_path(path: &Path) -> Option<()> {
33 let directory = crate::core::paths::state_dir_read_only().ok()?.join("tee");
34 if path.parent()? != directory
35 || crate::core::pathutil::is_symlink_or_reparse(
36 &std::fs::symlink_metadata(&directory).ok()?,
37 )
38 {
39 return None;
40 }
41 Some(())
42}
43
44pub fn read(path: &Path) -> Option<String> {
47 read_detailed(path).ok()
48}
49
50pub fn read_detailed(path: &Path) -> Result<String, &'static str> {
51 runtime::with_source_view(|| {
52 if source_authority_required() {
55 return Err(
56 "stored output has no source authority; repeat the original authorized operation",
57 );
58 }
59 const UNAVAILABLE: &str =
60 "stored output is unavailable or is not a regular UTF-8 text file";
61 const TOO_LARGE: &str = "stored output exceeds the configured recovery byte limit";
62 store_path(path).ok_or(UNAVAILABLE)?;
63 let mut options = std::fs::OpenOptions::new();
64 options.read(true);
65 #[cfg(unix)]
66 {
67 use std::os::unix::fs::OpenOptionsExt;
68 options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC);
69 }
70 #[cfg(windows)]
71 {
72 use std::os::windows::fs::OpenOptionsExt;
73 options.custom_flags(0x0020_0000); }
75 let file = options.open(path).map_err(|_| UNAVAILABLE)?;
76 let metadata = file.metadata().map_err(|_| UNAVAILABLE)?;
77 if !metadata.is_file() || crate::core::pathutil::is_symlink_or_reparse(&metadata) {
78 return Err(UNAVAILABLE);
79 }
80 if metadata.len() > limit() as u64 {
81 return Err(TOO_LARGE);
82 }
83 #[cfg(unix)]
84 {
85 use std::os::unix::fs::MetadataExt;
86 if metadata.nlink() != 1 {
87 return Err(UNAVAILABLE);
88 }
89 }
90 use std::io::Read;
91 let mut text = String::new();
92 file.take(limit() as u64 + 1)
93 .read_to_string(&mut text)
94 .map_err(|_| UNAVAILABLE)?;
95 if text.len() > limit() {
96 return Err(TOO_LARGE);
97 }
98 if text.contains('\0') {
99 return Err(UNAVAILABLE);
100 }
101 let checked = content::protect_active(&text)
102 .map_err(|_| "stored output is withheld by current content rules")?;
103 crate::core::context_admission::recovery::admit_recovered(&checked, "tee")
106 .map_err(|_| "stored output is withheld by the context gateway")
107 })
108 .map_err(|_| "current policy cannot be verified; retry the authorized operation")?
109}
110
111pub fn write(path: &Path, text: &str) -> Option<()> {
114 community(|| {
115 if text.len() > limit() || text.contains('\0') {
116 return None;
117 }
118 let checked = content::protect_active(text).ok()?;
119 let admitted = crate::core::context_admission::recovery::admit_for_storage(&checked)?;
122 let masked = crate::core::redaction::redact_text(&admitted);
123 let (redacted, _) = crate::core::secret_detection::scan_and_redact_from_config(&masked);
124 if redacted.len() > limit() {
125 return None;
126 }
127 let directory = crate::core::paths::state_dir_read_only().ok()?.join("tee");
128 if path.parent()? != directory {
129 return None;
130 }
131 std::fs::create_dir_all(&directory).ok()?;
132 store_path(path)?;
133 if std::fs::symlink_metadata(path).is_ok_and(|meta| meta.file_type().is_file())
140 && std::fs::read(path).is_ok_and(|existing| existing == redacted.as_bytes())
141 {
142 return std::fs::File::options()
143 .write(true)
144 .open(path)
145 .and_then(|file| file.set_modified(std::time::SystemTime::now()))
146 .ok();
147 }
148 #[cfg(unix)]
149 let permissions = {
150 use std::os::unix::fs::PermissionsExt;
151 Some(std::fs::Permissions::from_mode(0o600))
152 };
153 #[cfg(not(unix))]
154 let permissions = None;
155 crate::core::atomic_fs::try_atomic_write(path, redacted.as_bytes(), permissions.as_ref())
156 .ok()
157 })
158}