1use std::{
30 cell::{Cell, RefCell},
31 collections::{BTreeMap, BTreeSet},
32 rc::Rc,
33};
34
35use geop_core_math::geop_error::{GeopError, GeopResult};
36
37use crate::{cache::Cache, operation_registry::OperationRegistry, program::Program};
38
39pub trait Workspace {
41 fn program(&self, path: &str) -> GeopResult<Rc<Program>>;
45
46 fn read(&self, path: &str) -> GeopResult<Rc<str>>;
49
50 fn list(&self) -> Vec<String>;
52
53 fn resolve(&self, from: &str, name: &str) -> String;
55
56 fn relative(&self, from: &str, path: &str) -> String;
58
59 fn revision(&self, path: &str) -> u64;
62
63 fn cache(&self) -> Option<&Cache> {
67 None
68 }
69
70 fn digest(&self, path: &str) -> Option<u128> {
74 self.read(path)
75 .ok()
76 .map(|text| crate::kept::digest_text(&text))
77 }
78}
79
80#[derive(Clone, Debug, Default, PartialEq)]
85pub struct Location {
86 pub file: String,
87 pub within: Vec<String>,
88}
89
90pub fn is_program(path: &str) -> bool {
93 path.ends_with(".geop")
94}
95
96pub const BYTES_PREFIX: &str = "data:application/octet-stream;base64,";
99
100pub fn content(bytes: Vec<u8>) -> String {
107 match String::from_utf8(bytes) {
108 Ok(text) => text,
109 Err(e) => format!("{BYTES_PREFIX}{}", base64(e.as_bytes())),
110 }
111}
112
113pub fn bytes(content: &str) -> GeopResult<std::borrow::Cow<'_, [u8]>> {
115 match content.strip_prefix(BYTES_PREFIX) {
116 Some(encoded) => Ok(std::borrow::Cow::Owned(from_base64(encoded)?)),
117 None => Ok(std::borrow::Cow::Borrowed(content.as_bytes())),
118 }
119}
120
121pub fn text(content: &str) -> std::borrow::Cow<'_, str> {
124 match bytes(content) {
125 Ok(std::borrow::Cow::Owned(bytes)) => {
126 std::borrow::Cow::Owned(String::from_utf8_lossy(&bytes).into_owned())
127 }
128 _ => std::borrow::Cow::Borrowed(content),
129 }
130}
131
132const BASE64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
134
135pub fn base64(bytes: &[u8]) -> String {
137 let mut text = String::with_capacity(bytes.len().div_ceil(3) * 4);
138 for chunk in bytes.chunks(3) {
139 let b = [0, 1, 2].map(|k| chunk.get(k).copied().unwrap_or(0) as u32);
140 let n = (b[0] << 16) | (b[1] << 8) | b[2];
141 for k in 0..4 {
142 text.push(match BASE64.get((n >> (18 - 6 * k) & 63) as usize) {
143 Some(&digit) if k <= chunk.len() => digit as char,
144 _ => '=',
145 });
146 }
147 }
148 text
149}
150
151pub fn from_base64(text: &str) -> GeopResult<Vec<u8>> {
153 let digits = text.trim_end_matches('=').as_bytes();
154 let mut bytes = Vec::with_capacity(digits.len() * 3 / 4);
155 let (mut n, mut bits) = (0u32, 0);
156 for &c in digits {
157 let Some(value) = BASE64.iter().position(|&d| d == c) else {
158 return Err(GeopError::new(format!(
159 "{:?} is no base64 digit",
160 c as char
161 )));
162 };
163 n = (n << 6) | value as u32;
164 bits += 6;
165 if bits >= 8 {
166 bits -= 8;
167 bytes.push((n >> bits) as u8);
168 n &= (1 << bits) - 1;
169 }
170 }
171 Ok(bytes)
172}
173
174pub trait FileStore {
176 fn read(&self, path: &str) -> GeopResult<String>;
178 fn list(&self) -> Vec<String>;
180}
181
182pub trait FileStoreMut: FileStore {
184 fn write(&mut self, path: &str, text: Option<String>);
186}
187
188impl FileStore for BTreeMap<String, String> {
190 fn read(&self, path: &str) -> GeopResult<String> {
191 self.get(path)
192 .cloned()
193 .ok_or_else(|| GeopError::new(format!("there is no file {path:?}")))
194 }
195
196 fn list(&self) -> Vec<String> {
197 self.keys().cloned().collect()
198 }
199}
200
201impl FileStoreMut for BTreeMap<String, String> {
202 fn write(&mut self, path: &str, text: Option<String>) {
203 match text {
204 Some(text) => self.insert(path.to_string(), text),
205 None => self.remove(path),
206 };
207 }
208}
209
210impl FileStore for Box<dyn FileStoreMut> {
213 fn read(&self, path: &str) -> GeopResult<String> {
214 (**self).read(path)
215 }
216
217 fn list(&self) -> Vec<String> {
218 (**self).list()
219 }
220}
221
222impl FileStoreMut for Box<dyn FileStoreMut> {
223 fn write(&mut self, path: &str, text: Option<String>) {
224 (**self).write(path, text)
225 }
226}
227
228pub struct Files<F: 'static = BTreeMap<String, String>> {
236 files: RefCell<F>,
237 registry: Rc<OperationRegistry>,
238 programs: RefCell<BTreeMap<String, Rc<Program>>>,
240 texts: RefCell<BTreeMap<String, Rc<str>>>,
242 digests: RefCell<BTreeMap<String, u128>>,
244 revision: Cell<u64>,
246 revisions: RefCell<BTreeMap<String, u64>>,
248 cache: Cache,
250}
251
252impl<F: FileStore + 'static> Files<F> {
253 pub fn new(files: F, registry: Rc<OperationRegistry>) -> Rc<Self> {
255 Self::with_cache(files, registry, Cache::new())
256 }
257
258 pub fn with_cache(files: F, registry: Rc<OperationRegistry>, cache: Cache) -> Rc<Self> {
260 Rc::new(Self {
261 files: RefCell::new(files),
262 registry,
263 programs: RefCell::new(BTreeMap::new()),
264 texts: RefCell::new(BTreeMap::new()),
265 digests: RefCell::new(BTreeMap::new()),
266 revision: Cell::new(0),
267 revisions: RefCell::new(BTreeMap::new()),
268 cache,
269 })
270 }
271
272 pub fn files(&self) -> std::cell::Ref<'_, F> {
274 self.files.borrow()
275 }
276
277 pub fn registry(&self) -> &Rc<OperationRegistry> {
279 &self.registry
280 }
281
282 fn forget(&self, changed: &BTreeSet<String>) {
285 (self.programs.borrow_mut()).retain(|path, _| !changed.contains(path));
286 (self.texts.borrow_mut()).retain(|path, _| !changed.contains(path));
287 (self.digests.borrow_mut()).retain(|path, _| !changed.contains(path));
288 }
289
290 fn read_as(&self, path: &str, text: &str) {
292 let digest = crate::kept::digest_text(text);
293 (self.digests.borrow_mut()).insert(path.to_string(), digest);
294 }
295}
296
297impl<F: FileStoreMut + 'static> Files<F> {
298 pub fn write(&self, path: &str, text: Option<String>) -> bool {
302 if self.files.borrow().read(path).ok() == text {
303 return false;
304 }
305 self.files.borrow_mut().write(path, text);
306 let path = resolve("", path);
307 self.forget(&BTreeSet::from([path.clone()]));
308 self.revision.set(self.revision.get() + 1);
309 (self.revisions.borrow_mut()).insert(path, self.revision.get());
310 true
311 }
312}
313
314impl<F: FileStore + 'static> Workspace for Files<F> {
315 fn program(&self, path: &str) -> GeopResult<Rc<Program>> {
316 if let Some(program) = self.programs.borrow().get(path) {
317 return Ok(program.clone());
318 }
319 let text = self.files.borrow().read(path)?;
320 self.read_as(path, &text);
321 let program = (Program::from_json(&text))
322 .and_then(|program| program.validate(&self.registry).map(|()| program))
323 .map_err(|e| e.with_context(format!("reading {path}")))?;
324 let program = Rc::new(program);
325 (self.programs.borrow_mut()).insert(path.to_string(), program.clone());
326 Ok(program)
327 }
328
329 fn read(&self, path: &str) -> GeopResult<Rc<str>> {
330 if let Some(text) = self.texts.borrow().get(path) {
331 return Ok(text.clone());
332 }
333 let text: Rc<str> = self.files.borrow().read(path)?.into();
334 self.read_as(path, &text);
335 (self.texts.borrow_mut()).insert(path.to_string(), text.clone());
336 Ok(text)
337 }
338
339 fn list(&self) -> Vec<String> {
340 (self.files.borrow().list().into_iter())
341 .map(|f| resolve("", &f))
342 .collect()
343 }
344
345 fn resolve(&self, from: &str, name: &str) -> String {
346 resolve(from, name)
347 }
348
349 fn relative(&self, from: &str, path: &str) -> String {
350 relative(from, path)
351 }
352
353 fn revision(&self, path: &str) -> u64 {
354 self.revisions
355 .borrow()
356 .get(path)
357 .copied()
358 .unwrap_or_default()
359 }
360
361 fn cache(&self) -> Option<&Cache> {
362 Some(&self.cache)
363 }
364
365 fn digest(&self, path: &str) -> Option<u128> {
366 if let Some(digest) = self.digests.borrow().get(path) {
367 return Some(*digest);
368 }
369 self.read(path).ok()?;
370 self.digests.borrow().get(path).copied()
371 }
372}
373
374pub fn is_shared(reference: &str) -> bool {
379 reference.split_once(':').is_some_and(|(scheme, _)| {
380 scheme.len() >= 2 && scheme.chars().all(|c| c.is_ascii_alphabetic())
381 })
382}
383
384pub fn resolve(from: &str, reference: &str) -> String {
389 if is_shared(reference) {
390 return reference.to_string();
391 }
392 let reference = reference.replace('\\', "/");
393 let from = from.replace('\\', "/");
394 let joined = match (reference.starts_with('/'), from.rsplit_once('/')) {
395 (true, _) | (false, None) => reference,
396 (false, Some((folder, _))) => format!("{folder}/{reference}"),
397 };
398 let absolute = joined.starts_with('/');
399 let mut parts: Vec<&str> = Vec::new();
400 for part in joined.split('/') {
401 match part {
402 "" | "." => {}
403 ".." if parts.last().is_some_and(|p| *p != "..") => {
404 parts.pop();
405 }
406 ".." if absolute => {}
407 part => parts.push(part),
408 }
409 }
410 let path = parts.join("/");
411 if absolute { format!("/{path}") } else { path }
412}
413
414pub fn relative(from: &str, to: &str) -> String {
418 if is_shared(to) {
419 return to.to_string();
420 }
421 let mut folders: Vec<&str> = from.split('/').collect();
422 folders.pop();
423 let target: Vec<&str> = to.split('/').collect();
424 let common = folders
425 .iter()
426 .zip(&target)
427 .take_while(|(a, b)| a == b)
428 .count()
429 .min(target.len().saturating_sub(1));
430 let up = std::iter::repeat_n("..", folders.len() - common);
431 up.chain(target.iter().skip(common).copied())
432 .collect::<Vec<_>>()
433 .join("/")
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
444 fn files_hold_bytes_as_text() {
445 assert_eq!(content(b"solid cube".to_vec()), "solid cube");
446 assert_eq!(&*bytes("solid cube").unwrap(), b"solid cube");
447 assert_eq!(base64(b"foobar"), "Zm9vYmFy");
448 assert_eq!(base64(b"fo"), "Zm8=");
449 assert_eq!(
451 content(vec![0xff, 0xf8, 0xf1]),
452 format!("{BYTES_PREFIX}//jx")
453 );
454 for n in 0..7 {
455 let binary: Vec<u8> = (0..n).map(|k| 0xff - 7 * k as u8).collect();
456 let held = content(binary.clone());
457 if n > 0 {
458 assert!(held.starts_with(BYTES_PREFIX), "{held}");
459 }
460 assert_eq!(&*bytes(&held).unwrap(), &binary[..]);
461 }
462 let latin1 = content(b"caf\xe9".to_vec());
463 assert_eq!(text(&latin1), "caf\u{fffd}");
464 assert!(bytes(&format!("{BYTES_PREFIX}!!")).is_err());
465 }
466
467 #[test]
468 fn references_resolve_relative_to_their_file() {
469 assert_eq!(resolve("asm.geop", "bolt.geop"), "bolt.geop");
470 assert_eq!(resolve("a/asm.geop", "bolt.geop"), "a/bolt.geop");
471 assert_eq!(resolve("a/asm.geop", "../b/bolt.geop"), "b/bolt.geop");
472 assert_eq!(resolve("a/asm.geop", "./x/../bolt.geop"), "a/bolt.geop");
473 assert_eq!(
474 resolve("/home/a/asm.geop", "bolt.geop"),
475 "/home/a/bolt.geop"
476 );
477 assert_eq!(resolve("a/asm.geop", "/b/bolt.geop"), "/b/bolt.geop");
478 assert_eq!(resolve("asm.geop", "../bolt.geop"), "../bolt.geop");
479 assert_eq!(resolve("a/b/asm.geop", "std:nut.geop"), "std:nut.geop");
480 assert_eq!(resolve("asm.geop", "C:/b/bolt.geop"), "C:/b/bolt.geop");
481 }
482
483 #[test]
484 fn shared_files_are_named_alike_from_anywhere() {
485 assert!(is_shared("std:nut.geop"));
486 assert!(!is_shared("C:/parts/nut.geop"));
487 assert!(!is_shared("parts/nut.geop"));
488 assert_eq!(relative("a/b/asm.geop", "std:nut.geop"), "std:nut.geop");
489 }
490
491 #[test]
492 fn relative_names_resolve_back() {
493 for (from, to) in [
494 ("asm.geop", "bolt.geop"),
495 ("a/asm.geop", "a/bolt.geop"),
496 ("a/asm.geop", "b/c/bolt.geop"),
497 ("a/b/asm.geop", "bolt.geop"),
498 ] {
499 let name = relative(from, to);
500 assert_eq!(resolve(from, &name), to, "{from} -> {to} named {name}");
501 }
502 assert_eq!(relative("a/asm.geop", "b/bolt.geop"), "../b/bolt.geop");
503 }
504}