1use super::*;
2use crate::{Error, discover};
3use onestore::Transaction;
4use std::{
5 collections::{BTreeMap, BTreeSet},
6 sync::Mutex,
7};
8
9fn mapping(name: &str, shape: u16, art: &str, mapped: u64) -> TagMapping {
10 TagMapping {
11 name: name.into(),
12 shape,
13 art: art_name(art.as_bytes(), "png"),
14 mapped,
15 }
16}
17
18#[test]
19fn merging_keeps_every_tag_and_each_tags_later_mapping() {
20 let ours = vec![
21 mapping("Launch", 13, "rocket", 10),
22 mapping("Bug", 17, "bug", 5),
23 ];
24 let theirs = vec![
25 mapping("Launch", 13, "flame", 20),
26 mapping("Launch", 36, "circle", 1),
27 mapping("Bug", 17, "beetle", 5),
28 ];
29 let mut here = ours.clone();
30 merge(&mut here, theirs.clone());
31 let mut there = theirs;
32 merge(&mut there, ours);
33 let sorted = |mut mappings: Vec<TagMapping>| {
34 mappings.sort_by(|a, b| (&a.name, a.shape).cmp(&(&b.name, b.shape)));
35 mappings
36 };
37 assert_eq!(sorted(here.clone()), sorted(there));
38 let art = |name: &str, shape| {
39 here.iter()
40 .find(|mapping| mapping.name == name && mapping.shape == shape)
41 .map(|mapping| mapping.art.clone())
42 };
43 assert_eq!(here.len(), 3);
44 assert_eq!(art("Launch", 13), Some(art_name(b"flame", "png")));
45 assert_eq!(art("Launch", 36), Some(art_name(b"circle", "png")));
46 // Mapped at once, the greater name wins on either side.
47 let tied = [art_name(b"bug", "png"), art_name(b"beetle", "png")];
48 assert_eq!(art("Bug", 17).as_ref(), tied.iter().max());
49 let before = here.clone();
50 merge(&mut here, before.clone());
51 assert_eq!(here, before);
52}
53
54/// A notebook folder in memory, as a share would hold it.
55#[derive(Default)]
56struct Folder {
57 files: Mutex<BTreeMap<String, Vec<u8>>>,
58 folders: Mutex<BTreeSet<String>>,
59 hidden: Mutex<BTreeSet<String>>,
60 /// Pictures renamed into place.
61 kept: Mutex<Vec<String>>,
62 /// A mapping another writer, having read before this one wrote, puts in place just after.
63 race: Mutex<Option<Vec<u8>>>,
64}
65
66fn missing() -> Error {
67 io::Error::from(io::ErrorKind::NotFound).into()
68}
69
70impl Folder {
71 fn parent_exists(&self, path: &str) -> bool {
72 path.rsplit_once('/')
73 .is_none_or(|(parent, _)| self.folders.lock().unwrap().contains(parent))
74 }
75}
76
77impl Storage for Folder {
78 fn discover(&self, _: &mut discover::Cache, _: discover::Limits) -> Result<discover::Folder> {
79 unimplemented!()
80 }
81
82 fn location(&self) -> String {
83 "memory".into()
84 }
85
86 fn entries(&self, _: &str) -> io::Result<Vec<discover::Entry>> {
87 unimplemented!()
88 }
89
90 fn stamp(&self, _: &str) -> io::Result<onestore::Stamp> {
91 unimplemented!()
92 }
93
94 fn confirm(
95 &self,
96 _: &str,
97 _: &onestore::Stamp,
98 ) -> std::result::Result<(), onestore::CommitError> {
99 unimplemented!()
100 }
101
102 fn exists(&self, path: &str) -> bool {
103 self.files.lock().unwrap().contains_key(path) || self.folders.lock().unwrap().contains(path)
104 }
105
106 fn read(&self, _: &str) -> Result<Vec<u8>> {
107 unimplemented!()
108 }
109
110 fn read_file(&self, path: &str, limit: usize) -> Result<Vec<u8>> {
111 let bytes = self
112 .files
113 .lock()
114 .unwrap()
115 .get(path)
116 .cloned()
117 .ok_or_else(missing)?;
118 assert!(bytes.len() <= limit);
119 Ok(bytes)
120 }
121
122 fn create(&self, path: &str, bytes: &[u8]) -> Result<()> {
123 if !self.parent_exists(path) {
124 return Err(missing());
125 }
126 let mut files = self.files.lock().unwrap();
127 if files.contains_key(path) {
128 return Err(io::Error::from(io::ErrorKind::AlreadyExists).into());
129 }
130 files.insert(path.into(), bytes.to_vec());
131 Ok(())
132 }
133
134 fn create_directory(&self, path: &str) -> Result<()> {
135 if !self.folders.lock().unwrap().insert(path.into()) {
136 return Err(io::Error::from(io::ErrorKind::AlreadyExists).into());
137 }
138 Ok(())
139 }
140
141 fn hide(&self, path: &str) -> Result<()> {
142 assert!(self.exists(path));
143 self.hidden.lock().unwrap().insert(path.into());
144 Ok(())
145 }
146
147 fn rename(&self, from: &str, to: &str) -> Result<()> {
148 let mut files = self.files.lock().unwrap();
149 if files.contains_key(to) {
150 return Err(io::Error::from(io::ErrorKind::AlreadyExists).into());
151 }
152 let bytes = files.remove(from).ok_or_else(missing)?;
153 files.insert(to.into(), bytes);
154 self.kept.lock().unwrap().push(to.into());
155 Ok(())
156 }
157
158 fn rename_root(&self, _: &str, _: &[String]) -> Result<String> {
159 unreachable!()
160 }
161
162 fn replace(&self, from: &str, to: &str) -> Result<()> {
163 let mut files = self.files.lock().unwrap();
164 let bytes = files.remove(from).ok_or_else(missing)?;
165 files.insert(to.into(), bytes);
166 if let Some(theirs) = self.race.lock().unwrap().take() {
167 files.insert(to.into(), theirs);
168 }
169 Ok(())
170 }
171
172 fn delete(&self, path: &str) -> Result<()> {
173 self.files
174 .lock()
175 .unwrap()
176 .remove(path)
177 .ok_or_else(missing)?;
178 Ok(())
179 }
180
181 fn place(&self, _: &str, _: [u8; 16], _: &str) -> Result<()> {
182 unimplemented!()
183 }
184
185 fn commit(&self, _: &str, _: &Transaction) -> Result<()> {
186 unimplemented!()
187 }
188
189 fn supersede(&self, _: &str, _: &onestore::Stamp, _: &str) -> Result<()> {
190 unimplemented!()
191 }
192}
193
194#[test]
195fn mapping_hides_the_folder_and_keeps_each_picture_once() {
196 let folder = Folder::default();
197 assert!(mappings(&folder).unwrap().is_empty());
198 let mapped = map(&folder, "Launch", 13, b"rocket", "png").unwrap();
199 assert_eq!(mapped.len(), 1);
200 assert_eq!(
201 *folder.hidden.lock().unwrap(),
202 BTreeSet::from([FOLDER.to_owned()])
203 );
204 let rocket = art_name(b"rocket", "png");
205 assert_eq!(art(&folder, &rocket).unwrap(), b"rocket");
206 // A second tag with the same picture, and a drawing.
207 map(&folder, "Ship it", 127, b"rocket", "png").unwrap();
208 let mapped = map(&folder, "Idea", 21, b"<svg/>", "svg").unwrap();
209 assert_eq!(
210 mapped
211 .iter()
212 .map(|mapping| (mapping.name.as_str(), mapping.shape))
213 .collect::<Vec<_>>(),
214 [("Launch", 13), ("Ship it", 127), ("Idea", 21)]
215 );
216 assert_eq!(
217 *folder.kept.lock().unwrap(),
218 [
219 format!("{ART}/{rocket}"),
220 format!("{ART}/{}", art_name(b"<svg/>", "svg"))
221 ]
222 );
223 // Only the pictures and the mapping are left: no written file stays behind.
224 let files: Vec<_> = folder.files.lock().unwrap().keys().cloned().collect();
225 assert_eq!(files.len(), 3);
226 assert!(files.iter().all(|file| !file.ends_with(".tmp")));
227 // The same tag mapped again takes the new picture.
228 let mapped = map(&folder, "Launch", 13, b"flame", "png").unwrap();
229 assert_eq!(mapped[0].art, art_name(b"flame", "png"));
230 assert_eq!(mappings(&folder).unwrap(), mapped);
231}
232
233/// Another writer read the mapping before this one replaced it and replaced it just after,
234/// without this one's tag: this one reads it back and merges again.
235#[test]
236fn a_writer_overtaken_merges_again() {
237 let folder = Folder::default();
238 map(&folder, "Theirs before", 13, b"one", "png").unwrap();
239 let theirs = vec![
240 mappings(&folder).unwrap()[0].clone(),
241 mapping("Theirs", 17, "two", crate::now()),
242 ];
243 *folder.race.lock().unwrap() = Some(serde_json::to_vec(&theirs).unwrap());
244 let mapped = map(&folder, "Ours", 21, b"three", "png").unwrap();
245 let names: BTreeSet<_> = mapped.iter().map(|mapping| mapping.name.as_str()).collect();
246 assert_eq!(names, BTreeSet::from(["Theirs before", "Theirs", "Ours"]));
247 assert!(folder.race.lock().unwrap().is_none());
248}
249
250#[test]
251fn unreadable_mappings_and_pictures_are_passed_over() {
252 let folder = Folder::default();
253 map(&folder, "Launch", 13, b"rocket", "png").unwrap();
254 let rocket = art_name(b"rocket", "png");
255 let entry = |art: &str| serde_json::json!({"name": "Odd", "shape": 1, "art": art, "mapped": 1});
256 let stored = serde_json::json!([
257 mappings(&folder).unwrap()[0],
258 entry("../../Open Notebook.onetoc2"),
259 entry(&rocket.replace(".png", ".exe")),
260 {"name": "No art"},
261 7,
262 ]);
263 folder
264 .files
265 .lock()
266 .unwrap()
267 .insert(MAPPING.into(), stored.to_string().into_bytes());
268 assert_eq!(mappings(&folder).unwrap().len(), 1);
269 folder
270 .files
271 .lock()
272 .unwrap()
273 .insert(MAPPING.into(), b"{not json".to_vec());
274 assert!(mappings(&folder).unwrap().is_empty());
275 let kind = |result: Result<Vec<u8>>| match result {
276 Err(Error::Io(error)) => error.kind(),
277 other => panic!("{other:?}"),
278 };
279 assert_eq!(
280 kind(art(&folder, "../tags.json")),
281 io::ErrorKind::InvalidInput
282 );
283 folder
284 .files
285 .lock()
286 .unwrap()
287 .insert(format!("{ART}/{rocket}"), b"rock".to_vec());
288 assert_eq!(kind(art(&folder, &rocket)), io::ErrorKind::InvalidData);
289 assert!(map(&folder, "Tag", 13, b"x", "gif").is_err());
290}
291
292/// On this computer the folder takes the hidden attribute Windows or macOS keeps, and the
293/// notebook's catalog never lists it.
294#[test]
295fn a_notebook_folder_keeps_the_art_hidden() {
296 let temporary = tempfile::tempdir().unwrap();
297 let root = temporary.path().join("Notebook");
298 let page = onestore::PageCreation::new(None, Some(""), "Author").unwrap();
299 let mut notebook =
300 crate::session::Notebook::create(&root, temporary.path().join("cache"), 0x00f0_c090, &page)
301 .unwrap();
302 let mapped = notebook
303 .map_tag_art("Launch", 13, b"rocket", "png")
304 .unwrap();
305 assert_eq!(notebook.tag_art().unwrap(), mapped);
306 assert_eq!(notebook.tag_art_file(&mapped[0].art).unwrap(), b"rocket");
307 let folder = root.join(FOLDER);
308 assert!(folder.join("tags.json").is_file());
309 #[cfg(target_vendor = "apple")]
310 {
311 use nix::sys::stat::{FileFlag, stat};
312 let flags = FileFlag::from_bits_retain(stat(&folder).unwrap().st_flags);
313 assert!(flags.contains(FileFlag::UF_HIDDEN));
314 }
315 #[cfg(windows)]
316 {
317 use std::os::windows::fs::MetadataExt;
318 // FILE_ATTRIBUTE_HIDDEN.
319 assert_ne!(folder.metadata().unwrap().file_attributes() & 2, 0);
320 }
321 notebook.refresh().unwrap();
322 assert!(notebook.catalog().groups.is_empty());
323 assert_eq!(notebook.catalog().sections.len(), 1);
324}
325
326/// Themes go in the hidden folder beside the tag art, and a writer another overtook merges
327/// again until its change holds; entries it can't read are passed over, built-ins refused.
328#[test]
329fn themes_merge_into_the_hidden_folder() {
330 use themes::{Assignment, Scope, Themes};
331 let folder = Folder::default();
332 let assign = |scope, theme: &str, assigned| Themes {
333 assignments: vec![Assignment {
334 scope,
335 theme: Some(theme.into()),
336 assigned,
337 }],
338 ..Default::default()
339 };
340 let theirs = assign(Scope::Notebook, "modern", 1);
341 *folder.race.lock().unwrap() = Some(serde_json::to_vec(&theirs).unwrap());
342 let kept = themes::write(&folder, assign(Scope::section([7; 16]), "editorial", 2)).unwrap();
343 assert!(folder.hidden.lock().unwrap().contains(".snowbound"));
344 assert_eq!(kept.assignments.len(), 2);
345 assert_eq!(kept, themes::read(&folder).unwrap());
346 assert_eq!(kept.effective(Some([7; 16]), None).unwrap().id, "editorial");
347 folder.files.lock().unwrap().insert(
348 ".snowbound/themes.json".into(),
349 br#"{"themes": [{"id": 3}], "assignments": [{"scope": "notebook", "theme": "manuscript", "assigned": 4}, "junk"]}"#.to_vec(),
350 );
351 let read = themes::read(&folder).unwrap();
352 assert!(read.themes.is_empty());
353 assert_eq!(read.effective(None, None).unwrap().id, "manuscript");
354 let built_in = Themes {
355 themes: vec![themes::built_in().remove(0)],
356 ..Default::default()
357 };
358 assert!(themes::write(&folder, built_in).is_err());
359}
360
361/// The presence room's secret is made once, in the hidden folder, and every reader after
362/// takes it; a writer that finds one made meanwhile takes that one.
363#[test]
364fn the_presence_room_is_made_once() {
365 let folder = Folder::default();
366 let secret = room(&folder).unwrap();
367 assert_eq!(room(&folder).unwrap(), secret);
368 assert!(folder.hidden.lock().unwrap().contains(FOLDER));
369 let files: Vec<_> = folder.files.lock().unwrap().keys().cloned().collect();
370 assert_eq!(files, [ROOM]);
371 let theirs = br#"{"room":"000102030405060708090a0b0c0d0e0f"}"#;
372 let other = Folder::default();
373 other.folders.lock().unwrap().insert(FOLDER.into());
374 other
375 .files
376 .lock()
377 .unwrap()
378 .insert(ROOM.into(), theirs.to_vec());
379 assert_eq!(room(&other).unwrap(), std::array::from_fn(|at| at as u8));
380}