1use std::io::{Cursor, Read};
21use std::path::{Path, PathBuf};
22
23use kernel::{Address, AxCode, AxError, B3Hash, Locator};
24use same_file::Handle;
25
26use super::chosen_path::{self, Located, ReadBound};
27
28#[derive(Clone)]
32pub struct BoundReader {
33 pub(in crate::tools) city_root: PathBuf,
34 pub(in crate::tools) bound: ReadBound,
35 pub(in crate::tools) block_store: PathBuf,
36}
37
38impl BoundReader {
39 #[must_use]
43 pub fn new(city_root: &Path, bound: ReadBound, block_store: &Path) -> BoundReader {
44 BoundReader {
45 city_root: city_root.to_path_buf(),
46 bound,
47 block_store: block_store.to_path_buf(),
48 }
49 }
50
51 pub fn open(&self, asked: &str, action: &'static str) -> Result<Opened, AxError> {
65 if let Some(found) = super::read::locator::open_locator(asked, self, action) {
66 let (locator, bytes) = found?;
67 let named = match locator {
68 Locator::Cas { hash, .. } => Named::Block(hash),
69 Locator::File { address, .. } => Named::File(address),
70 };
71 return Ok(Opened {
72 named,
73 source: Source::Held(Cursor::new(bytes)),
74 });
75 }
76 let spelled = chosen_path::within_city(&self.city_root, asked, action)?;
77 let addr = chosen_path::admit(&spelled, action, &*self.bound)?;
78 let real = match chosen_path::land(&self.city_root, &addr, action, &*self.bound)? {
79 Located::Present(real) if real.is_file() => real,
80 Located::Present(_) => {
81 return Err(AxError::failure(
82 AxCode::InvalidArgs,
83 action,
84 format!("{asked} is not a file"),
85 )
86 .with_recovery("name one file; `search` finds files by name"));
87 }
88 Located::Absent(_) => return Err(not_there(asked, action)),
89 };
90 let opened = std::fs::File::open(&real)
91 .and_then(Handle::from_file)
92 .map_err(|err| unopened(asked, action, &err))?;
93 chosen_path::still_judged(asked, &real, &opened, action)?;
94 Ok(Opened {
95 named: Named::File(addr),
96 source: Source::Disk(opened),
97 })
98 }
99}
100
101fn not_there(asked: &str, action: &'static str) -> AxError {
103 AxError::failure(AxCode::InvalidArgs, action, format!("{asked} is not there"))
104 .with_recovery("name a file that exists; `search` finds files by name")
105}
106
107fn unopened(asked: &str, action: &'static str, err: &std::io::Error) -> AxError {
111 if err.kind() == std::io::ErrorKind::NotFound {
112 return not_there(asked, action);
113 }
114 AxError::failure(AxCode::StorageFatal, action, format!("{asked}: {err}"))
115 .with_recovery("the User has to make the file readable")
116}
117
118pub struct Opened {
120 named: Named,
121 source: Source,
122}
123
124enum Source {
126 Disk(Handle),
128 Held(Cursor<Vec<u8>>),
130}
131
132impl Opened {
133 #[must_use]
136 pub fn named(&self) -> &Named {
137 &self.named
138 }
139}
140
141impl Read for Opened {
142 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
143 match &mut self.source {
144 Source::Disk(handle) => handle.as_file_mut().read(buf),
145 Source::Held(bytes) => bytes.read(buf),
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum Named {
155 File(Address),
156 Block(B3Hash),
157}
158
159#[cfg(test)]
160#[allow(
161 clippy::unwrap_used,
162 clippy::expect_used,
163 clippy::panic,
164 clippy::indexing_slicing,
165 reason = "test code"
166)]
167mod tests {
168 use std::sync::{Arc, Mutex};
169
170 use kernel::{ReadVerdict, Tool as _, ToolCall, ToolName};
171
172 use super::*;
173
174 fn vault_closed() -> ReadBound {
177 Arc::new(|addr: &Address| {
178 if addr.as_str().starts_with("vault") {
179 ReadVerdict::Confidential
180 } else {
181 ReadVerdict::Open
182 }
183 })
184 }
185
186 fn origin(building: &str) -> storage::BlockOrigin {
187 storage::BlockOrigin {
188 run: kernel::RunId::from_bytes([7; 16]),
189 building: Address::parse(building).unwrap(),
190 }
191 }
192
193 fn refused(opened: Result<Opened, AxError>) -> AxError {
194 match opened {
195 Ok(opened) => panic!("{asked:?} was let through", asked = opened.named),
196 Err(err) => err,
197 }
198 }
199
200 #[test]
204 fn the_door_refuses_what_read_refuses_with_the_same_code() {
205 let city = tempfile::tempdir().unwrap();
206 let store = kernel::layout::CityLayout::new(city.path()).cas();
207 storage::Cas::open(&store).unwrap();
208 let door = BoundReader::new(city.path(), vault_closed(), &store);
209 let read = crate::tools::ReadTool::new(
210 city.path(),
211 Arc::new(Mutex::new(crate::catalog::Catalog::new())),
212 vault_closed(),
213 &store,
214 )
215 .unwrap();
216 let stray = format!("cas:b3-{}", B3Hash::digest(b"never stored"));
217 let asked = [
218 "vault/room/shot.png",
219 "lab/.sprawling/RULES.toml",
220 stray.as_str(),
221 ];
222 let codes: Vec<(AxCode, AxCode, String)> = asked
223 .iter()
224 .map(|asked| {
225 let by_door = refused(door.open(asked, "ocr"));
226 let call = ToolCall {
227 id: "call-1".to_owned(),
228 name: ToolName::parse("read").unwrap(),
229 args: kernel::Payload::new(
230 serde_json::json!({ "path": asked })
231 .as_object()
232 .cloned()
233 .unwrap(),
234 )
235 .unwrap(),
236 };
237 let by_read = read.invoke(&call).unwrap_err();
238 (
239 *by_door.code(),
240 *by_read.code(),
241 by_door.action().to_owned(),
242 )
243 })
244 .collect();
245 let owed = vec![(AxCode::GateDenied, AxCode::GateDenied, "ocr".to_owned()); 3];
246 assert_eq!(codes, owed);
247 }
248
249 #[test]
252 fn a_file_and_a_block_come_back_as_their_bytes_and_what_they_were() {
253 let city = tempfile::tempdir().unwrap();
254 let store = kernel::layout::CityLayout::new(city.path()).cas();
255 let hash = storage::Cas::open(&store)
256 .unwrap()
257 .put_for(b"block bytes", &origin("hall"))
258 .unwrap();
259 std::fs::create_dir_all(city.path().join("hall").join("room")).unwrap();
260 std::fs::write(
261 city.path().join("hall").join("room").join("shot.png"),
262 b"file bytes",
263 )
264 .unwrap();
265 let door = BoundReader::new(city.path(), vault_closed(), &store);
266 let read_back = |asked: &str| {
267 door.open(asked, "ocr").map(|mut opened| {
268 let mut bytes = Vec::new();
269 opened.read_to_end(&mut bytes).unwrap();
270 (opened.named().clone(), bytes)
271 })
272 };
273 assert_eq!(
274 vec![
275 read_back("hall/room/shot.png"),
276 read_back(&format!("cas:b3-{hash}")),
277 ],
278 vec![
279 Ok((
280 Named::File(Address::parse("hall/room/shot.png").unwrap()),
281 b"file bytes".to_vec()
282 )),
283 Ok((Named::Block(hash), b"block bytes".to_vec())),
284 ]
285 );
286 }
287
288 #[test]
291 fn a_file_that_is_not_there_or_is_a_directory_is_the_callers_mistake() {
292 let city = tempfile::tempdir().unwrap();
293 std::fs::create_dir_all(city.path().join("lab").join("room")).unwrap();
294 let store = kernel::layout::CityLayout::new(city.path()).cas();
295 let door = BoundReader::new(city.path(), vault_closed(), &store);
296 let codes: Vec<AxCode> = ["lab/room/absent.png", "lab/room"]
297 .into_iter()
298 .map(|asked| *refused(door.open(asked, "ocr")).code())
299 .collect();
300 assert_eq!(codes, vec![AxCode::InvalidArgs, AxCode::InvalidArgs]);
301 }
302}