1use alloc::collections::BTreeMap;
43#[cfg(feature = "std")]
44use alloc::collections::BTreeSet;
45
46use shuyi_core::ContentHash;
47
48use crate::error::ContentError;
49use crate::prelude::*;
50
51mod fingerprint;
52#[cfg(test)]
54pub(crate) use fingerprint::tests::fixed as fixed_fingerprint_bundle;
55
56#[cfg(feature = "std")]
57use include_dir::Dir;
58use once_cell::race::OnceBox;
59
60#[derive(Debug, Clone, Copy)]
62#[non_exhaustive]
63pub enum Files {
64 #[cfg(feature = "std")]
66 Dir(&'static Dir<'static>),
67 Table(&'static [(&'static str, &'static [u8])]),
71 Owned(&'static OwnedFiles),
74}
75
76type TableFile = (&'static str, &'static [u8]);
78
79#[derive(Debug)]
84pub struct OwnedFiles {
85 #[cfg(feature = "std")]
87 table: std::sync::RwLock<Vec<TableFile>>,
88 #[cfg(not(feature = "std"))]
90 table: Vec<TableFile>,
91}
92
93impl OwnedFiles {
94 fn new(table: Vec<TableFile>) -> Self {
96 Self {
97 #[cfg(feature = "std")]
98 table: std::sync::RwLock::new(table),
99 #[cfg(not(feature = "std"))]
100 table,
101 }
102 }
103
104 fn with<T>(&self, read: impl FnOnce(&[TableFile]) -> T) -> T {
106 #[cfg(feature = "std")]
107 let guard = self
108 .table
109 .read()
110 .unwrap_or_else(std::sync::PoisonError::into_inner);
111 #[cfg(feature = "std")]
112 let table: &[TableFile] = &guard;
113 #[cfg(not(feature = "std"))]
114 let table: &[TableFile] = &self.table;
115 read(table)
116 }
117
118 #[cfg(feature = "std")]
121 fn replace(&self, path: &str, bytes: Vec<u8>) {
122 let bytes: &'static [u8] = Box::leak(bytes.into_boxed_slice());
123 let mut table = self
124 .table
125 .write()
126 .unwrap_or_else(std::sync::PoisonError::into_inner);
127 match table.binary_search_by(|(p, _)| (*p).cmp(path)) {
128 Ok(at) => table[at].1 = bytes,
129 Err(at) => {
130 let path: &'static str = Box::leak(path.to_owned().into_boxed_str());
131 table.insert(at, (path, bytes));
132 }
133 }
134 }
135}
136
137impl Files {
138 fn get(self, path: &str) -> Option<&'static [u8]> {
140 match self {
141 #[cfg(feature = "std")]
142 Self::Dir(dir) => dir.get_file(path).map(include_dir::File::contents),
143 Self::Table(files) => table_get(files, path),
144 Self::Owned(files) => files.with(|table| table_get(table, path)),
145 }
146 }
147
148 fn list(self, dir: &str, deep: bool, paths: &mut Vec<&'static str>) {
151 match self {
152 #[cfg(feature = "std")]
153 Self::Dir(root) => {
154 let root = if dir.is_empty() {
155 Some(root)
156 } else {
157 root.get_dir(dir)
158 };
159 let mut pending: Vec<&'static Dir<'static>> = root.into_iter().collect();
160 while let Some(dir) = pending.pop() {
161 paths.extend(dir.files().filter_map(|f| f.path().to_str()));
162 if deep {
163 pending.extend(dir.dirs());
164 }
165 }
166 }
167 Self::Table(files) => table_list(files, dir, deep, paths),
168 Self::Owned(files) => files.with(|table| table_list(table, dir, deep, paths)),
169 }
170 }
171}
172
173fn table_get(files: &[TableFile], path: &str) -> Option<&'static [u8]> {
175 let at = files.binary_search_by(|(p, _)| (*p).cmp(path)).ok()?;
176 Some(files[at].1)
177}
178
179fn table_list(files: &[TableFile], dir: &str, deep: bool, paths: &mut Vec<&'static str>) {
182 paths.extend(files.iter().map(|(path, _)| *path).filter(|path| {
183 let rest = if dir.is_empty() {
184 Some(*path)
185 } else {
186 path.strip_prefix(dir)
187 .and_then(|rest| rest.strip_prefix('/'))
188 };
189 rest.is_some_and(|rest| deep || !rest.contains('/'))
190 }));
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
196#[non_exhaustive]
197pub enum BadPath {
198 #[error("the path is empty")]
200 Empty,
201 #[error("the path is absolute; it must be relative to the layer's root")]
203 Absolute,
204 #[error("the path has a backslash; paths are `/`-separated")]
206 Backslash,
207 #[error("the path goes up a folder (`..`)")]
209 Parent,
210 #[error("the path has an empty or `.` part")]
213 NotNormal,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
219#[error("layer `{layer}`: `{path}`: {problem}")]
220#[non_exhaustive]
221pub struct LayerError {
222 pub layer: String,
224 pub path: String,
226 pub problem: BadPath,
228}
229
230fn check_path(path: &str) -> Result<(), BadPath> {
232 if path.is_empty() {
233 return Err(BadPath::Empty);
234 }
235 if path.contains('\\') {
236 return Err(BadPath::Backslash);
237 }
238 let first = path.split('/').next().unwrap_or(path);
239 let drive = matches!(first.as_bytes(), [letter, b':'] if letter.is_ascii_alphabetic());
240 if path.starts_with('/') || drive {
241 return Err(BadPath::Absolute);
242 }
243 for part in path.split('/') {
244 match part {
245 ".." => return Err(BadPath::Parent),
246 "" | "." => return Err(BadPath::NotNormal),
247 _ => {}
248 }
249 }
250 Ok(())
251}
252
253#[derive(Debug, Clone, Copy)]
255pub struct Layer {
256 pub name: &'static str,
259 pub files: Files,
261 pub private: bool,
263 pub test: bool,
265 pub left_out: &'static [&'static str],
267 pub stands_for: Option<ContentHash>,
271}
272
273impl Layer {
274 #[cfg(feature = "std")]
276 pub const fn dir(name: &'static str, dir: &'static Dir<'static>) -> Self {
277 Self::of(name, Files::Dir(dir))
278 }
279
280 pub const fn table(
282 name: &'static str,
283 files: &'static [(&'static str, &'static [u8])],
284 ) -> Self {
285 Self::of(name, Files::Table(files))
286 }
287
288 pub fn owned(name: String, files: BTreeMap<String, Vec<u8>>) -> Result<Self, LayerError> {
312 for path in files.keys() {
313 check_path(path).map_err(|problem| LayerError {
314 layer: name.clone(),
315 path: path.clone(),
316 problem,
317 })?;
318 }
319 let table: Vec<TableFile> = files
321 .into_iter()
322 .map(|(path, bytes)| {
323 let path: &'static str = Box::leak(path.into_boxed_str());
324 let bytes: &'static [u8] = Box::leak(bytes.into_boxed_slice());
325 (path, bytes)
326 })
327 .collect();
328 let files: &'static OwnedFiles = Box::leak(Box::new(OwnedFiles::new(table)));
329 Ok(Self::of(
330 Box::leak(name.into_boxed_str()),
331 Files::Owned(files),
332 ))
333 }
334
335 #[cfg(feature = "std")]
343 #[must_use]
344 pub fn replace(&self, path: &str, bytes: Vec<u8>) -> bool {
345 let Files::Owned(files) = self.files else {
346 return false;
347 };
348 if check_path(path).is_err() {
349 return false;
350 }
351 files.replace(path, bytes);
352 true
353 }
354
355 const fn of(name: &'static str, files: Files) -> Self {
357 Self {
358 name,
359 files,
360 private: false,
361 test: false,
362 left_out: &[],
363 stands_for: None,
364 }
365 }
366
367 #[must_use]
369 pub const fn private(self) -> Self {
370 Self {
371 private: true,
372 ..self
373 }
374 }
375
376 #[must_use]
378 pub const fn test(self) -> Self {
379 Self { test: true, ..self }
380 }
381
382 #[must_use]
386 pub const fn leaving_out(self, paths: &'static [&'static str]) -> Self {
387 Self {
388 left_out: paths,
389 ..self
390 }
391 }
392
393 #[must_use]
398 pub const fn standing_for(self, content: ContentHash) -> Self {
399 Self {
400 stands_for: Some(content),
401 ..self
402 }
403 }
404
405 pub fn target(&self) -> Option<&'static str> {
409 let (base, target) = self.name.rsplit_once(TARGET_MARK)?;
410 (!base.is_empty() && !target.is_empty()).then_some(target)
411 }
412}
413
414pub const TARGET_MARK: char = '@';
417
418pub fn target_layer_name(layer: &str, target: &str) -> String {
421 format!("{layer}{TARGET_MARK}{target}")
422}
423
424#[derive(Debug, Clone)]
426pub struct Bundle {
427 layers: Vec<Layer>,
429 shipped: usize,
431}
432
433impl Bundle {
434 pub fn new(mut layers: Vec<Layer>) -> Self {
437 layers.sort_by_key(|layer| layer.test);
439 let shipped = layers.iter().filter(|layer| !layer.test).count();
440 Self { layers, shipped }
441 }
442
443 pub fn for_target(layers: Vec<Layer>, per_target: &[Layer], target: Option<&str>) -> Self {
471 let Some(target) = target else {
472 return Self::new(layers);
473 };
474 let mut laid = Vec::with_capacity(layers.len() + per_target.len());
475 for layer in layers {
476 let name = target_layer_name(layer.name, target);
477 laid.extend(per_target.iter().filter(|over| over.name == name));
478 laid.push(layer);
479 }
480 Self::new(laid)
481 }
482
483 pub fn layer_of(&self, path: &str) -> Option<&Layer> {
487 find(self.read(), path).map(|(layer, _)| layer)
488 }
489
490 pub fn layers(&self) -> &[Layer] {
492 &self.layers
493 }
494
495 fn shipped(&self) -> &[Layer] {
497 &self.layers[..self.shipped]
498 }
499
500 fn tests(&self) -> &[Layer] {
502 &self.layers[self.shipped..]
503 }
504
505 fn read(&self) -> &[Layer] {
508 if story_only_now() {
509 self.shipped()
510 } else {
511 &self.layers
512 }
513 }
514
515 fn home(&self) -> Option<&Layer> {
518 self.layers
519 .iter()
520 .find(|layer| !layer.private && !layer.test)
521 }
522
523 pub fn file(&self, path: &str) -> Option<&'static str> {
527 core::str::from_utf8(self.bytes(path)?).ok()
528 }
529
530 pub fn bytes(&self, path: &str) -> Option<&'static [u8]> {
533 let bytes = find(self.read(), path).map(|(_, bytes)| bytes);
534 if let Some(watcher) = WATCHER.get() {
535 watcher(path, bytes.is_some());
536 }
537 #[cfg(feature = "std")]
538 note_read(path, bytes.is_some());
539 bytes
540 }
541
542 pub fn has(&self, path: &str) -> bool {
546 find(self.read(), path).is_some()
547 }
548
549 #[cfg(feature = "std")]
570 #[must_use]
571 pub fn replace(&self, path: &str, bytes: Vec<u8>) -> bool {
572 let mut disk = self
573 .layers
574 .iter()
575 .filter(|layer| matches!(layer.files, Files::Owned(_)));
576 let first = disk.clone().next();
577 disk.find(|layer| layer.files.get(path).is_some())
578 .or(first)
579 .is_some_and(|layer| layer.replace(path, bytes))
580 }
581
582 pub fn left_out(&self, path: &str) -> bool {
586 self.read()
587 .iter()
588 .any(|layer| layer.left_out.binary_search(&path).is_ok())
589 }
590
591 pub fn test_part(&self, path: &str) -> Option<(String, &'static str)> {
597 if story_only_now() {
598 return None;
599 }
600 find(self.shipped(), path)?;
601 let (layer, bytes) = find(self.tests(), path)?;
602 let display = format!("{}/{path}", layer.name);
603 Some((display, core::str::from_utf8(bytes).ok()?))
604 }
605
606 pub fn files_in(&self, dir: &str) -> Vec<&'static str> {
611 list(self.read(), dir, false)
612 }
613
614 pub fn files_under(&self, dir: &str) -> Vec<&'static str> {
619 list(self.read(), dir, true)
620 }
621
622 pub fn load_each<T, E: From<NotText>>(
647 &self,
648 dir: &str,
649 ext: &str,
650 mut parse: impl FnMut(&LoadFile<'_>) -> Result<T, Vec<E>>,
651 ) -> Result<BTreeMap<String, T>, Vec<E>> {
652 let mut loaded = BTreeMap::new();
653 let mut errors = Vec::new();
654 for path in self.files_in(dir) {
655 let name = path.rsplit('/').next().unwrap_or(path);
656 let Some(stem) = name
657 .strip_suffix(ext)
658 .and_then(|stem| stem.strip_suffix('.'))
659 else {
660 continue;
661 };
662 let shown = self.display_path(path);
663 let Some(text) = self.file(path) else {
664 errors.push(NotText { path: shown }.into());
665 continue;
666 };
667 match parse(&LoadFile {
668 stem,
669 path: &shown,
670 text,
671 }) {
672 Ok(value) => {
673 loaded.insert(stem.to_owned(), value);
674 }
675 Err(more) => errors.extend(more),
676 }
677 }
678 if errors.is_empty() {
679 Ok(loaded)
680 } else {
681 Err(errors)
682 }
683 }
684
685 pub fn display_path(&self, path: &str) -> String {
689 let layer = find(self.read(), path).map(|(layer, _)| layer);
690 match layer.or_else(|| self.home()) {
691 Some(layer) => format!("{}/{path}", layer.name),
692 None => path.to_owned(),
693 }
694 }
695
696 pub fn is_private(&self, shown: &str) -> bool {
699 self.layers
700 .iter()
701 .any(|layer| layer.private && shown_under(shown, layer.name))
702 }
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707pub struct LoadFile<'a> {
708 pub stem: &'a str,
710 pub path: &'a str,
712 pub text: &'static str,
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
719pub struct NotText {
720 pub path: String,
722}
723
724impl From<NotText> for ContentError {
725 fn from(not_text: NotText) -> Self {
726 Self::new(not_text.path, "file is not valid UTF-8").with_code(crate::error::codes::NOT_TEXT)
727 }
728}
729
730pub fn shown_under(shown: &str, name: &str) -> bool {
734 shown
735 .strip_prefix(name)
736 .is_some_and(|rest| rest.starts_with('/'))
737}
738
739fn find<'a>(layers: &'a [Layer], path: &str) -> Option<(&'a Layer, &'static [u8])> {
742 layers
743 .iter()
744 .find_map(|layer| Some((layer, layer.files.get(path)?)))
745}
746
747fn list(layers: &[Layer], dir: &str, deep: bool) -> Vec<&'static str> {
750 let mut paths = Vec::new();
751 for layer in layers {
752 layer.files.list(dir, deep, &mut paths);
753 }
754 paths.sort_unstable();
755 paths.dedup();
756 paths
757}
758
759static INSTALLED: OnceBox<Bundle> = OnceBox::new();
761
762pub fn install(bundle: Bundle) -> Result<(), Bundle> {
765 INSTALLED.set(Box::new(bundle)).map_err(|bundle| *bundle)
766}
767
768pub fn installed() -> Option<&'static Bundle> {
771 #[cfg(feature = "std")]
772 if let Some(bundle) = READING.get() {
773 return Some(bundle);
774 }
775 INSTALLED.get()
776}
777
778pub fn get_or_install(make: impl FnOnce() -> Bundle) -> &'static Bundle {
783 #[cfg(feature = "std")]
784 if let Some(bundle) = READING.get() {
785 return bundle;
786 }
787 INSTALLED.get_or_init(|| Box::new(make()))
788}
789
790pub type Watcher = fn(path: &str, found: bool);
793
794static WATCHER: OnceBox<Watcher> = OnceBox::new();
796
797pub fn watch(watcher: Watcher) -> Result<(), Watcher> {
802 WATCHER.set(Box::new(watcher)).map_err(|watcher| *watcher)
803}
804
805#[cfg(feature = "std")]
807#[derive(Debug, Default)]
808pub(crate) struct Reads {
809 pub(crate) found: BTreeSet<String>,
811 pub(crate) asked: BTreeSet<String>,
813}
814
815#[cfg(feature = "std")]
816std::thread_local! {
817 static READS: core::cell::RefCell<Option<Reads>> = const { core::cell::RefCell::new(None) };
819}
820
821#[cfg(feature = "std")]
823fn note_read(path: &str, found: bool) {
824 READS.with_borrow_mut(|reads| {
825 if let Some(reads) = reads {
826 reads.asked.insert(path.to_owned());
827 if found {
828 reads.found.insert(path.to_owned());
829 }
830 }
831 });
832}
833
834#[cfg(feature = "std")]
837pub(crate) fn reads_of<T>(f: impl FnOnce() -> T) -> (T, Reads) {
838 struct Restore(Option<Reads>);
840 impl Drop for Restore {
841 fn drop(&mut self) {
842 READS.set(self.0.take());
843 }
844 }
845 let mut outer = Restore(READS.replace(Some(Reads::default())));
846 let value = f();
847 let reads = READS.take().unwrap_or_default();
848 if let Some(outer) = &mut outer.0 {
849 outer.found.extend(reads.found.iter().cloned());
850 outer.asked.extend(reads.asked.iter().cloned());
851 }
852 (value, reads)
853}
854
855#[cfg(feature = "std")]
861pub fn recording<T>(f: impl FnOnce() -> T) -> (T, BTreeSet<String>) {
862 let (value, reads) = reads_of(f);
863 (value, reads.found)
864}
865
866#[cfg(feature = "std")]
867std::thread_local! {
868 static READING: core::cell::Cell<Option<&'static Bundle>> =
870 const { core::cell::Cell::new(None) };
871}
872
873#[cfg(feature = "std")]
878pub fn reading<T>(bundle: &'static Bundle, f: impl FnOnce() -> T) -> T {
879 struct Restore(Option<&'static Bundle>);
881 impl Drop for Restore {
882 fn drop(&mut self) {
883 READING.set(self.0);
884 }
885 }
886 let _restore = Restore(READING.replace(Some(bundle)));
887 f()
888}
889
890#[cfg(feature = "std")]
891std::thread_local! {
892 static STORY_ONLY: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
894}
895
896#[cfg(not(feature = "std"))]
898static STORY_ONLY: Flag = Flag::new();
899
900#[cfg(any(not(feature = "std"), test))]
904struct Flag(core::sync::atomic::AtomicBool);
905
906#[cfg(any(not(feature = "std"), test))]
907impl Flag {
908 const fn new() -> Self {
910 Self(core::sync::atomic::AtomicBool::new(false))
911 }
912
913 fn get(&self) -> bool {
915 self.0.load(core::sync::atomic::Ordering::Relaxed)
916 }
917
918 fn replace(&self, on: bool) -> bool {
920 self.0.swap(on, core::sync::atomic::Ordering::Relaxed)
921 }
922}
923
924fn story_only_now() -> bool {
926 STORY_ONLY.get()
927}
928
929fn set_story_only(on: bool) -> bool {
931 STORY_ONLY.replace(on)
932}
933
934pub fn story_only<T>(f: impl FnOnce() -> T) -> T {
938 struct Restore(bool);
940 impl Drop for Restore {
941 fn drop(&mut self) {
942 set_story_only(self.0);
943 }
944 }
945 let _restore = Restore(set_story_only(true));
946 f()
947}
948
949#[cfg(test)]
950mod tests {
951 use include_dir::{DirEntry, File};
952
953 use super::*;
954
955 const fn entry(path: &'static str, contents: &'static str) -> DirEntry<'static> {
956 DirEntry::File(File::new(path, contents.as_bytes()))
957 }
958
959 static PRIVATE_FIXTURE: Dir<'static> = Dir::new(
961 "",
962 &[
963 DirEntry::Dir(Dir::new("data", &[entry("data/a.ron", "private a")])),
964 DirEntry::Dir(Dir::new(
965 "portraits",
966 &[entry("portraits/new.portrait", "private only")],
967 )),
968 DirEntry::Dir(Dir::new(
969 "deep",
970 &[DirEntry::Dir(Dir::new(
971 "deep/x",
972 &[entry("deep/x/y.txt", "private y")],
973 ))],
974 )),
975 ],
976 );
977
978 static PUBLIC_FIXTURE: Dir<'static> = Dir::new(
980 "",
981 &[
982 entry("top.txt", "public top"),
983 DirEntry::Dir(Dir::new(
984 "data",
985 &[
986 entry("data/a.ron", "public a"),
987 entry("data/b.ron", "public b"),
988 ],
989 )),
990 DirEntry::Dir(Dir::new(
991 "deep",
992 &[DirEntry::Dir(Dir::new(
993 "deep/x",
994 &[entry("deep/x/z.txt", "public z")],
995 ))],
996 )),
997 ],
998 );
999
1000 static PUBLIC_TABLE: &[(&str, &[u8])] = &[
1002 ("data/a.ron", b"public a"),
1003 ("data/b.ron", b"public b"),
1004 ("deep/x/z.txt", b"public z"),
1005 ("top.txt", b"public top"),
1006 ];
1007
1008 static PRIVATE_TEST_FIXTURE: Dir<'static> = Dir::new(
1010 "",
1011 &[DirEntry::Dir(Dir::new(
1012 "portraits",
1013 &[entry("portraits/t.png", "bought t")],
1014 ))],
1015 );
1016
1017 static TEST_FIXTURE: Dir<'static> = Dir::new(
1019 "",
1020 &[
1021 DirEntry::Dir(Dir::new(
1022 "data",
1023 &[entry("data/b.ron", "test b"), entry("data/t.ron", "test t")],
1024 )),
1025 DirEntry::Dir(Dir::new(
1026 "portraits",
1027 &[entry("portraits/t.png", "placeholder t")],
1028 )),
1029 ],
1030 );
1031
1032 const PRIVATE: Layer = Layer::dir("bought", &PRIVATE_FIXTURE).private();
1033 const PUBLIC: Layer = Layer::dir("base", &PUBLIC_FIXTURE);
1034 const PRIVATE_TEST: Layer = Layer::dir("bought-tests", &PRIVATE_TEST_FIXTURE)
1035 .private()
1036 .test();
1037 const TEST: Layer = Layer::dir("tests", &TEST_FIXTURE).test();
1038
1039 fn merged() -> Bundle {
1041 Bundle::new(vec![PRIVATE, PUBLIC])
1042 }
1043
1044 fn with_tests() -> Bundle {
1046 Bundle::new(vec![PRIVATE, PUBLIC, PRIVATE_TEST, TEST])
1047 }
1048
1049 fn names(layers: &[Layer]) -> Vec<&'static str> {
1050 layers.iter().map(|layer| layer.name).collect()
1051 }
1052
1053 #[test]
1054 fn the_test_layers_are_the_last_ones() {
1055 let bundle = with_tests();
1056 assert_eq!(names(bundle.shipped()), ["bought", "base"]);
1057 assert_eq!(names(bundle.tests()), ["bought-tests", "tests"]);
1058 let given = Bundle::new(vec![PRIVATE_TEST, PRIVATE, TEST, PUBLIC]);
1060 assert_eq!(
1061 names(given.layers()),
1062 ["bought", "base", "bought-tests", "tests"]
1063 );
1064 assert_eq!(names(merged().shipped()), names(merged().layers()));
1065 let bundle = merged();
1066 let got = bundle.tests();
1067 assert!(got.is_empty(), "{got:?}");
1068 }
1069
1070 static GAME: &[(&str, &[u8])] = &[("a.png", b"game a"), ("b.png", b"game b")];
1072 static MODDED: &[(&str, &[u8])] = &[("a.png", b"mod a")];
1073 static PLAYDATE: &[(&str, &[u8])] = &[("b.png", b"playdate b"), ("c.png", b"playdate c")];
1074 static GBA: &[(&str, &[u8])] = &[("b.png", b"gba b")];
1075
1076 fn per_target() -> [Layer; 4] {
1077 [
1078 Layer::table("assets@gba", GBA),
1079 Layer::table("assets@playdate", PLAYDATE),
1080 Layer::table("mods@playdate", MODDED),
1081 Layer::table("nothing@playdate", MODDED),
1082 ]
1083 }
1084
1085 #[test]
1086 fn a_targets_layer_is_laid_right_above_its_own() {
1087 let layers = || vec![Layer::table("mods", MODDED), Layer::table("assets", GAME)];
1088 let bundle = Bundle::for_target(layers(), &per_target(), Some("playdate"));
1089 assert_eq!(
1090 names(bundle.layers()),
1091 ["mods@playdate", "mods", "assets@playdate", "assets"]
1092 );
1093 assert_eq!(bundle.file("b.png"), Some("playdate b"));
1094 assert_eq!(bundle.display_path("b.png"), "assets@playdate/b.png");
1095 assert_eq!(bundle.file("c.png"), Some("playdate c"));
1096 assert_eq!(bundle.display_path("a.png"), "mods@playdate/a.png");
1098 let layer = |path| bundle.layer_of(path).map(Layer::target);
1099 assert_eq!(layer("b.png"), Some(Some("playdate")));
1100 assert_eq!(layer("x.png"), None);
1101 let gba = Bundle::for_target(layers(), &per_target(), Some("gba"));
1104 assert_eq!(names(gba.layers()), ["mods", "assets@gba", "assets"]);
1105 assert_eq!(gba.file("b.png"), Some("gba b"));
1106 let none = Bundle::for_target(layers(), &per_target(), None);
1107 assert_eq!(names(none.layers()), ["mods", "assets"]);
1108 let web = Bundle::for_target(layers(), &per_target(), Some("web"));
1109 assert_eq!(names(web.layers()), ["mods", "assets"]);
1110 assert_eq!(web.fingerprint(), none.fingerprint());
1111 assert_ne!(bundle.fingerprint(), none.fingerprint());
1112 }
1113
1114 #[test]
1115 fn a_layer_is_for_the_target_after_its_last_mark() {
1116 let target = |name| Layer::table(name, GAME).target();
1117 assert_eq!(target("assets@playdate"), Some("playdate"));
1118 assert_eq!(target("a@b@steam-deck"), Some("steam-deck"));
1119 assert_eq!(target("assets"), None);
1120 assert_eq!(target("@playdate"), None);
1121 assert_eq!(target("assets@"), None);
1122 assert_eq!(target_layer_name("files", "playdate"), "files@playdate");
1123 }
1124
1125 #[test]
1126 fn a_test_file_is_added_after_the_shipped_files() {
1127 let bundle = with_tests();
1128 assert_eq!(bundle.file("data/t.ron"), Some("test t"));
1129 assert_eq!(bundle.display_path("data/t.ron"), "tests/data/t.ron");
1130 assert_eq!(story_only(|| bundle.file("data/t.ron")), None);
1131 assert_eq!(bundle.file("data/b.ron"), Some("public b"));
1133 assert_eq!(
1134 bundle.files_in("data"),
1135 ["data/a.ron", "data/b.ron", "data/t.ron"]
1136 );
1137 }
1138
1139 #[test]
1140 fn a_test_part_is_the_test_layers_own_file_where_a_shipped_one_has_one_too() {
1141 let bundle = with_tests();
1142 assert_eq!(
1143 bundle.test_part("data/b.ron"),
1144 Some(("tests/data/b.ron".to_owned(), "test b"))
1145 );
1146 assert_eq!(bundle.test_part("data/t.ron"), None);
1148 assert_eq!(bundle.test_part("data/a.ron"), None);
1150 assert_eq!(bundle.test_part("data/nope.ron"), None);
1151 assert_eq!(merged().test_part("data/b.ron"), None);
1153 assert_eq!(story_only(|| bundle.test_part("data/b.ron")), None);
1154 }
1155
1156 #[test]
1157 fn a_private_test_file_replaces_the_placeholder_and_is_private() {
1158 let bundle = with_tests();
1159 assert_eq!(bundle.file("portraits/t.png"), Some("bought t"));
1160 let shown = |path| bundle.display_path(path);
1161 assert_eq!(shown("portraits/t.png"), "bought-tests/portraits/t.png");
1162 assert!(bundle.is_private(&shown("portraits/t.png")));
1163 assert!(bundle.is_private(&shown("data/a.ron")));
1164 assert!(bundle.is_private(&shown("portraits/new.portrait")));
1165 assert!(!bundle.is_private(&shown("data/b.ron")));
1166 assert!(!bundle.is_private(&shown("data/t.ron")));
1167 assert!(!bundle.is_private(&shown("data/nope.ron")));
1168 let placeholders = Bundle::new(vec![PRIVATE, PUBLIC, TEST]);
1170 assert_eq!(placeholders.file("portraits/t.png"), Some("placeholder t"));
1171 let shown = placeholders.display_path("portraits/t.png");
1172 assert!(!placeholders.is_private(&shown));
1173 assert!(bundle.is_private("bought/x.png"));
1175 assert!(bundle.is_private("bought-tests/x.png"));
1176 assert!(!bundle.is_private("boughts/x.png"));
1177 assert!(!bundle.is_private("bought"));
1178 assert!(!bundle.is_private("base/x.png"));
1179 assert!(!bundle.is_private("x/bought/x.png"));
1180 }
1181
1182 #[test]
1183 fn a_private_file_replaces_the_public_file_at_the_same_path() {
1184 assert_eq!(merged().file("data/a.ron"), Some("private a"));
1185 assert_eq!(merged().display_path("data/a.ron"), "bought/data/a.ron");
1186 let public = Bundle::new(vec![PUBLIC]);
1188 assert_eq!(public.file("data/a.ron"), Some("public a"));
1189 }
1190
1191 #[test]
1192 fn a_private_only_file_is_added() {
1193 let path = "portraits/new.portrait";
1194 assert_eq!(merged().file(path), Some("private only"));
1195 assert_eq!(merged().files_in("portraits"), [path]);
1196 let public = Bundle::new(vec![PUBLIC]);
1197 assert_eq!(public.file(path), None);
1198 let listed = public.files_in("portraits");
1199 assert!(listed.is_empty(), "{listed:?}");
1200 }
1201
1202 #[test]
1203 fn a_public_only_file_is_still_read() {
1204 assert_eq!(merged().file("data/b.ron"), Some("public b"));
1205 assert_eq!(merged().display_path("data/b.ron"), "base/data/b.ron");
1206 }
1207
1208 #[test]
1209 fn a_file_in_no_layer_is_missing_and_shown_under_the_first_public_layer() {
1210 assert_eq!(merged().bytes("data/nope.ron"), None);
1211 assert_eq!(merged().display_path("data/nope.ron"), "base/data/nope.ron");
1212 let private = Bundle::new(vec![PRIVATE, TEST]);
1214 assert_eq!(private.display_path("data/nope.ron"), "data/nope.ron");
1215 assert_eq!(Bundle::new(vec![]).display_path("x"), "x");
1216 }
1217
1218 #[test]
1219 fn lists_merge_the_layers_sorted_with_each_path_once() {
1220 let bundle = merged();
1221 assert_eq!(bundle.files_in("data"), ["data/a.ron", "data/b.ron"]);
1222 assert_eq!(bundle.files_in(""), ["top.txt"]);
1223 let listed = bundle.files_in("deep");
1225 assert!(listed.is_empty(), "{listed:?}");
1226 assert_eq!(bundle.files_under("deep"), ["deep/x/y.txt", "deep/x/z.txt"]);
1227 assert_eq!(
1228 bundle.files_under(""),
1229 [
1230 "data/a.ron",
1231 "data/b.ron",
1232 "deep/x/y.txt",
1233 "deep/x/z.txt",
1234 "portraits/new.portrait",
1235 "top.txt",
1236 ]
1237 );
1238 let listed = bundle.files_under("no_such_dir");
1239 assert!(listed.is_empty(), "{listed:?}");
1240 }
1241
1242 static FOLDER: &[(&str, &[u8])] = &[
1244 ("n/a.num", b"1"),
1245 ("n/b.num", b"2"),
1246 ("n/bad.num", b"x"),
1247 ("n/bin.num", &[0xff]),
1248 ("n/other.txt", b"9"),
1249 ];
1250
1251 fn parse_num(file: &LoadFile<'_>) -> Result<u32, Vec<ContentError>> {
1252 file.text
1253 .parse()
1254 .map_err(|_| vec![ContentError::new(file.path, "not a number")])
1255 }
1256
1257 #[test]
1258 fn load_each_gathers_a_map_by_name_or_every_error() {
1259 let bundle = Bundle::new(vec![Layer::table("base", FOLDER)]);
1260 let errors = bundle.load_each("n", "num", parse_num).unwrap_err();
1261 let files: Vec<_> = errors.iter().map(ToString::to_string).collect();
1262 assert_eq!(files.len(), 2);
1263 assert!(files[0].contains("base/n/bad.num") && files[0].contains("not a number"));
1264 assert!(files[1].contains("base/n/bin.num") && files[1].contains("not valid UTF-8"));
1265 let good = Bundle::new(vec![Layer::table("base", &FOLDER[..2])]);
1266 let map = good.load_each("n", "num", parse_num).unwrap();
1267 assert_eq!(
1268 map.into_iter().collect::<Vec<_>>(),
1269 [("a".to_owned(), 1), ("b".to_owned(), 2)]
1270 );
1271 let none = good.load_each("nope", "num", parse_num).unwrap();
1272 assert!(none.is_empty(), "{none:?}");
1273 }
1274
1275 static BINARY: &[(&str, &[u8])] = &[("x.bin", &[0xff, 0xfe])];
1277
1278 static OVER: &[(&str, &[u8])] = &[("data/a.ron", b"table a")];
1280
1281 #[test]
1282 fn reads_bytes_and_text() {
1283 let bundle = merged();
1284 assert_eq!(bundle.bytes("top.txt"), Some(&b"public top"[..]));
1285 assert_eq!(bundle.file("top.txt"), Some("public top"));
1286 let binary = Bundle::new(vec![Layer::table("base", BINARY)]);
1287 assert_eq!(binary.bytes("x.bin"), Some(&[0xff, 0xfe][..]));
1288 assert_eq!(binary.file("x.bin"), None);
1289 }
1290
1291 #[test]
1294 fn a_table_layer_answers_as_the_same_directory() {
1295 let dir = Bundle::new(vec![PUBLIC]);
1296 let table = Bundle::new(vec![Layer::table("base", PUBLIC_TABLE)]);
1297 let all = dir.files_under("");
1298 assert_eq!(table.files_under(""), all);
1299 for path in all.iter().copied().chain(["nope", "data/nope.ron", ""]) {
1300 assert_eq!(table.bytes(path), dir.bytes(path), "{path}");
1301 assert_eq!(table.display_path(path), dir.display_path(path));
1302 }
1303 for dir_name in ["", "data", "deep", "deep/x", "dat", "no_such_dir"] {
1304 assert_eq!(
1305 table.files_in(dir_name),
1306 dir.files_in(dir_name),
1307 "{dir_name}"
1308 );
1309 assert_eq!(
1310 table.files_under(dir_name),
1311 dir.files_under(dir_name),
1312 "{dir_name}"
1313 );
1314 }
1315 let laid = Bundle::new(vec![Layer::table("over", OVER), PUBLIC]);
1317 assert_eq!(laid.file("data/a.ron"), Some("table a"));
1318 assert_eq!(laid.display_path("data/a.ron"), "over/data/a.ron");
1319 assert_eq!(laid.files_in("data"), ["data/a.ron", "data/b.ron"]);
1320 }
1321
1322 fn owned(name: &str, files: &[(&str, &str)]) -> Layer {
1324 let files = files
1325 .iter()
1326 .map(|(path, text)| ((*path).to_owned(), text.as_bytes().to_vec()))
1327 .collect();
1328 Layer::owned(name.to_owned(), files).unwrap()
1329 }
1330
1331 #[test]
1332 fn a_run_time_file_replaces_an_embedded_one_and_a_new_one_is_added() {
1333 let layer = owned(
1334 "mods/more",
1335 &[
1336 ("data/a.ron", "mod a"),
1337 ("data/c.ron", "mod c"),
1338 ("x/y.txt", "mod y"),
1339 ],
1340 );
1341 let over_table = Bundle::new(vec![layer, Layer::table("base", PUBLIC_TABLE)]);
1342 for bundle in [&over_table, &Bundle::new(vec![layer, PUBLIC])] {
1343 assert_eq!(bundle.file("data/a.ron"), Some("mod a"));
1344 assert_eq!(bundle.file("data/c.ron"), Some("mod c"));
1345 assert_eq!(bundle.file("data/b.ron"), Some("public b"));
1346 assert_eq!(
1347 bundle.files_in("data"),
1348 ["data/a.ron", "data/b.ron", "data/c.ron"]
1349 );
1350 assert_eq!(bundle.files_in("x"), ["x/y.txt"]);
1351 assert_eq!(
1352 bundle.files_under(""),
1353 [
1354 "data/a.ron",
1355 "data/b.ron",
1356 "data/c.ron",
1357 "deep/x/z.txt",
1358 "top.txt",
1359 "x/y.txt",
1360 ]
1361 );
1362 assert!(bundle.has("x/y.txt"));
1363 }
1364 let under = Bundle::new(vec![PUBLIC, layer]);
1366 assert_eq!(under.file("data/a.ron"), Some("public a"));
1367 assert_eq!(under.file("data/c.ron"), Some("mod c"));
1368 }
1369
1370 #[test]
1371 fn of_two_run_time_layers_the_first_wins() {
1372 let first = owned("first", &[("a.txt", "first a"), ("b.txt", "first b")]);
1373 let second = owned("second", &[("a.txt", "second a"), ("c.txt", "second c")]);
1374 let bundle = Bundle::new(vec![first, second]);
1375 assert_eq!(bundle.file("a.txt"), Some("first a"));
1376 assert_eq!(bundle.file("c.txt"), Some("second c"));
1377 assert_eq!(bundle.files_in(""), ["a.txt", "b.txt", "c.txt"]);
1378 let swapped = Bundle::new(vec![second, first]);
1379 assert_eq!(swapped.file("a.txt"), Some("second a"));
1380 }
1381
1382 #[test]
1383 fn a_run_time_file_is_shown_under_its_layers_name() {
1384 let layer = owned("mods/more", &[("data/a.ron", "mod a")]);
1385 let bundle = Bundle::new(vec![layer, PUBLIC]);
1386 assert_eq!(bundle.display_path("data/a.ron"), "mods/more/data/a.ron");
1387 assert_eq!(bundle.display_path("data/b.ron"), "base/data/b.ron");
1388 assert_eq!(bundle.display_path("nope"), "mods/more/nope");
1390 assert_eq!(names(bundle.layers()), ["mods/more", "base"]);
1391 }
1392
1393 #[test]
1394 fn a_run_time_layer_takes_the_flags_of_any_layer() {
1395 let bought = owned("bought", &[("data/a.ron", "bought a")]).private();
1396 let tests = owned("tests", &[("data/t.ron", "test t"), ("data/a.ron", "t")]).test();
1397 let bundle = Bundle::new(vec![tests, bought, PUBLIC]);
1398 assert_eq!(names(bundle.layers()), ["bought", "base", "tests"]);
1399 assert_eq!(bundle.file("data/a.ron"), Some("bought a"));
1400 assert!(bundle.is_private(&bundle.display_path("data/a.ron")));
1401 assert!(!bundle.is_private(&bundle.display_path("data/b.ron")));
1402 assert_eq!(bundle.file("data/t.ron"), Some("test t"));
1403 assert_eq!(story_only(|| bundle.file("data/t.ron")), None);
1404 let trimmed = owned("base", &[("a.txt", "a")]).leaving_out(&["b.txt"]);
1405 let bundle = Bundle::new(vec![trimmed]);
1406 assert!(bundle.left_out("b.txt"));
1407 assert!(!bundle.left_out("a.txt"));
1408 }
1409
1410 #[test]
1413 fn a_run_time_layer_answers_as_the_same_table() {
1414 let table = Bundle::new(vec![Layer::table("base", PUBLIC_TABLE)]);
1415 let files: Vec<(&str, &str)> = PUBLIC_TABLE
1416 .iter()
1417 .map(|(path, bytes)| (*path, core::str::from_utf8(bytes).unwrap_or_default()))
1418 .collect();
1419 let run_time = Bundle::new(vec![owned("base", &files)]);
1420 let all = table.files_under("");
1421 assert_eq!(run_time.files_under(""), all);
1422 for path in all.iter().copied().chain(["nope", "data/nope.ron", ""]) {
1423 assert_eq!(run_time.bytes(path), table.bytes(path), "{path}");
1424 assert_eq!(run_time.display_path(path), table.display_path(path));
1425 }
1426 for dir in ["", "data", "deep", "deep/x", "dat", "no_such_dir"] {
1427 assert_eq!(run_time.files_in(dir), table.files_in(dir), "{dir}");
1428 assert_eq!(run_time.files_under(dir), table.files_under(dir), "{dir}");
1429 }
1430 let empty = Layer::owned("empty".to_owned(), BTreeMap::new()).unwrap();
1432 let listed = Bundle::new(vec![empty]).files_under("");
1433 assert!(listed.is_empty(), "{listed:?}");
1434 }
1435
1436 #[test]
1437 fn a_bad_path_is_refused_with_the_layers_name_and_the_path() {
1438 let cases = [
1439 ("", BadPath::Empty),
1440 ("/data/a.ron", BadPath::Absolute),
1441 ("C:/data/a.ron", BadPath::Absolute),
1442 ("c:", BadPath::Absolute),
1443 ("data\\a.ron", BadPath::Backslash),
1444 ("..", BadPath::Parent),
1445 ("../a.ron", BadPath::Parent),
1446 ("data/../../a.ron", BadPath::Parent),
1447 ("data//a.ron", BadPath::NotNormal),
1448 ("./a.ron", BadPath::NotNormal),
1449 ("data/./a.ron", BadPath::NotNormal),
1450 ("data/", BadPath::NotNormal),
1451 ];
1452 for (path, problem) in cases {
1453 let files = BTreeMap::from([
1454 ("good.txt".to_owned(), Vec::new()),
1455 (path.to_owned(), b"x".to_vec()),
1456 ]);
1457 let error = Layer::owned("mods/bad".to_owned(), files).unwrap_err();
1458 let expected = LayerError {
1459 layer: "mods/bad".to_owned(),
1460 path: path.to_owned(),
1461 problem,
1462 };
1463 assert_eq!(error, expected, "{path}");
1464 let message = error.to_string();
1465 assert!(
1466 message.contains("mods/bad") && message.contains(&format!("`{path}`")),
1467 "{message}"
1468 );
1469 }
1470 for path in [
1472 "a..b", "..a/b", "data/a.", "ab:/c", "data/c:", "-/x", "a b.txt",
1473 ] {
1474 let files = BTreeMap::from([(path.to_owned(), Vec::new())]);
1475 assert!(Layer::owned("ok".to_owned(), files).is_ok(), "{path}");
1476 }
1477 }
1478
1479 proptest::proptest! {
1480 #[test]
1484 fn a_listing_is_sorted_once_each_and_reads_agree_with_it(
1485 layers in proptest::collection::vec(
1486 (
1487 proptest::collection::btree_map("[ab]{1,2}(/[ab]{1,2}){0,2}", "[xyz]{0,3}", 0..6),
1488 proptest::bool::ANY,
1489 proptest::bool::ANY,
1490 ),
1491 0..5,
1492 ),
1493 dir in "|[ab]{1,2}|[ab]{1,2}/[ab]{1,2}",
1494 deep in proptest::bool::ANY,
1495 ) {
1496 let layers: Vec<Layer> = layers
1497 .into_iter()
1498 .enumerate()
1499 .map(|(at, (files, run_time, test))| {
1500 let name = format!("layer{at}");
1501 let layer = if run_time {
1502 let files = files.into_iter().map(|(p, t)| (p, t.into_bytes())).collect();
1503 Layer::owned(name, files).unwrap()
1504 } else {
1505 let table: Vec<(&'static str, &'static [u8])> = files
1506 .into_iter()
1507 .map(|(p, t)| {
1508 let p: &'static str = Box::leak(p.into_boxed_str());
1509 let t: &'static [u8] = Box::leak(t.into_bytes().into_boxed_slice());
1510 (p, t)
1511 })
1512 .collect();
1513 Layer::table(Box::leak(name.into_boxed_str()), Box::leak(table.into_boxed_slice()))
1514 };
1515 if test { layer.test() } else { layer }
1516 })
1517 .collect();
1518 let bundle = Bundle::new(layers);
1519 let listed = if deep { bundle.files_under(&dir) } else { bundle.files_in(&dir) };
1520 proptest::prop_assert!(listed.windows(2).all(|w| w[0] < w[1]), "{listed:?}");
1521 for path in &listed {
1522 let first = bundle.layers().iter().find_map(|l| Some((l.name, l.files.get(path)?)));
1523 proptest::prop_assert!(first.is_some(), "{path}");
1524 let (name, bytes) = first.unwrap_or_default();
1525 proptest::prop_assert_eq!(bundle.bytes(path), Some(bytes));
1526 proptest::prop_assert_eq!(bundle.display_path(path), format!("{name}/{path}"));
1527 }
1528 for layer in bundle.layers() {
1530 let mut own = Vec::new();
1531 layer.files.list(&dir, deep, &mut own);
1532 for path in own {
1533 proptest::prop_assert!(listed.binary_search(&path).is_ok(), "{path}");
1534 }
1535 }
1536 }
1537 }
1538
1539 #[test]
1540 #[cfg_attr(
1541 target_os = "wasi",
1542 ignore = "needs threads and unwinding, which WASI preview 1 lacks"
1543 )]
1544 fn story_only_hides_the_test_layers_from_this_thread_until_it_returns() {
1545 let bundle = with_tests();
1546 let path = "data/t.ron";
1547 let inside = story_only(|| {
1548 story_only(|| ());
1550 (
1551 bundle.file(path),
1552 bundle.bytes(path),
1553 bundle.files_in("data").contains(&path),
1554 bundle.files_under("").contains(&path),
1555 bundle.display_path(path),
1556 bundle.file("data/b.ron").is_some(),
1557 )
1558 });
1559 assert_eq!(
1560 inside,
1561 (None, None, false, false, "base/data/t.ron".to_owned(), true)
1562 );
1563 assert!(bundle.file(path).is_some());
1564 assert!(std::panic::catch_unwind(|| story_only(|| panic!("inside"))).is_err());
1566 assert!(bundle.file(path).is_some());
1567 let elsewhere = story_only(|| {
1569 std::thread::spawn(move || with_tests().file("data/t.ron").is_some()).join()
1570 });
1571 assert!(matches!(elsewhere, Ok(true)));
1572 }
1573
1574 #[test]
1575 fn a_bundle_has_the_files_a_layer_this_thread_reads_has() {
1576 let bundle = with_tests();
1577 assert!(bundle.has("top.txt"));
1578 assert!(bundle.has("portraits/new.portrait"));
1579 assert!(bundle.has("data/t.ron"));
1580 assert!(!bundle.has("data/nope.ron"));
1581 assert!(!bundle.has("data"));
1582 assert!(!story_only(|| bundle.has("data/t.ron")));
1584 assert!(story_only(|| bundle.has("data/b.ron")));
1585 }
1586
1587 static TRIMMED: &[(&str, &[u8])] = &[
1589 ("data/a.ron", b"public a"),
1590 ("deep/x/z.txt", b"public z"),
1591 ("top.txt", b"public top"),
1592 ];
1593
1594 #[test]
1595 fn a_file_a_layer_leaves_out_is_missing_and_left_out() {
1596 let trimmed = Layer::table("base", TRIMMED).leaving_out(&["data/b.ron"]);
1597 let bundle = Bundle::new(vec![trimmed]);
1598 assert_eq!(bundle.file("data/b.ron"), None);
1599 assert!(bundle.left_out("data/b.ron"));
1600 assert!(!bundle.left_out("data/nope.ron"));
1602 assert!(!bundle.left_out("data/a.ron"));
1603 assert!(!bundle.left_out("data"));
1604 assert_eq!(bundle.files_in("data"), ["data/a.ron"]);
1605 assert!(!merged().left_out("data/b.ron"));
1607 let test = Layer::table("tests", TRIMMED).leaving_out(&["x"]).test();
1609 let with_test = Bundle::new(vec![PUBLIC, test]);
1610 assert!(with_test.left_out("x"));
1611 assert!(!story_only(|| with_test.left_out("x")));
1612 }
1613
1614 std::thread_local! {
1615 static SEEN: core::cell::RefCell<Option<Vec<(String, bool)>>> =
1617 const { core::cell::RefCell::new(None) };
1618 }
1619
1620 fn note(path: &str, found: bool) {
1622 SEEN.with_borrow_mut(|seen| {
1623 if let Some(seen) = seen {
1624 seen.push((path.to_owned(), found));
1625 }
1626 });
1627 }
1628
1629 fn watched(f: impl FnOnce()) -> Vec<(String, bool)> {
1631 SEEN.set(Some(Vec::new()));
1632 f();
1633 SEEN.take().unwrap_or_default()
1634 }
1635
1636 #[derive(Debug, Default, PartialEq, Eq, serde::Deserialize)]
1638 #[serde(default, deny_unknown_fields)]
1639 struct Flip {
1640 flip: bool,
1641 }
1642
1643 impl crate::import_settings::ImportSettings for Flip {
1644 const SECTION: &'static str = "flip";
1645 }
1646
1647 static SETTINGS: &[(&str, &[u8])] = &[("images/_folder.meta", b"(flip: (flip: true))")];
1649
1650 #[test]
1652 fn the_watcher_is_told_of_every_file_read() {
1653 assert!(watch(note).is_ok());
1654 let refused: Watcher = |_, _| {};
1655 assert!(watch(refused).is_err());
1656 let bundle = merged();
1657 let seen = watched(|| {
1658 let _ = bundle.file("data/a.ron");
1659 let _ = bundle.bytes("data/nope.ron");
1660 let _ = bundle.files_under("");
1662 let _ = bundle.display_path("top.txt");
1663 let _ = bundle.left_out("top.txt");
1664 let _ = bundle.has("top.txt");
1665 let _ = bundle.fingerprint();
1667 });
1668 assert_eq!(
1669 seen,
1670 [
1671 ("data/a.ron".to_owned(), true),
1672 ("data/nope.ron".to_owned(), false)
1673 ]
1674 );
1675 let settings = Bundle::new(vec![Layer::table("base", SETTINGS)]);
1678 let hero = crate::format::AssetPath::new("images/hero.png");
1679 let mut read = None;
1680 let seen = watched(|| {
1681 read =
1682 Some(crate::format::DecodeCx::in_bundle(&settings).import_settings::<Flip>(&hero));
1683 });
1684 assert_eq!(read, Some(Ok(Flip { flip: true })));
1685 assert_eq!(
1686 seen,
1687 [
1688 ("images/hero.png.meta".to_owned(), false),
1689 ("images/_folder.meta".to_owned(), true)
1690 ]
1691 );
1692 }
1693
1694 #[test]
1695 #[cfg_attr(
1696 target_os = "wasi",
1697 ignore = "needs threads and unwinding, which WASI preview 1 lacks"
1698 )]
1699 fn reading_makes_this_thread_read_another_bundle_until_it_returns() {
1700 let other: &'static Bundle =
1701 Box::leak(Box::new(Bundle::new(vec![Layer::table("other", OVER)])));
1702 let is_other = move |bundle: &Bundle| core::ptr::eq(bundle, other);
1703 let inside = reading(other, || {
1704 reading(other, || ());
1706 (
1707 installed().is_some_and(is_other),
1708 is_other(get_or_install(merged)),
1709 )
1710 });
1711 assert_eq!(inside, (true, true));
1712 assert!(!installed().is_some_and(is_other));
1714 assert!(std::panic::catch_unwind(|| reading(other, || panic!("inside"))).is_err());
1716 assert!(!installed().is_some_and(is_other));
1717 let elsewhere = reading(other, || {
1719 std::thread::spawn(move || installed().is_some_and(is_other)).join()
1720 });
1721 assert!(matches!(elsewhere, Ok(false)));
1722 }
1723
1724 #[test]
1726 fn a_flag_starts_off_and_gives_back_its_old_value() {
1727 let flag = Flag::new();
1728 assert!(!flag.get());
1729 assert!(!flag.replace(true));
1730 assert!(flag.get());
1731 assert!(flag.replace(false));
1732 assert!(!flag.get());
1733 }
1734
1735 #[test]
1736 fn a_name_is_shown_under_a_layer_after_its_name_and_a_slash() {
1737 assert!(shown_under("base/x.png", "base"));
1738 assert!(shown_under("bought-tests/a/b", "bought-tests"));
1739 assert!(!shown_under("bases/x.png", "base"));
1740 assert!(!shown_under("base", "base"));
1741 assert!(!shown_under("x/base/x.png", "base"));
1742 }
1743
1744 #[test]
1746 fn a_bundle_is_installed_once() {
1747 assert!(installed().is_none());
1748 let first = get_or_install(merged);
1749 assert_eq!(first.file("data/a.ron"), Some("private a"));
1750 let refused = install(Bundle::new(vec![PUBLIC]));
1752 assert!(refused.is_err_and(|bundle| names(bundle.layers()) == ["base"]));
1753 let again = get_or_install(|| Bundle::new(vec![PUBLIC]));
1754 assert!(core::ptr::eq(first, again));
1755 assert!(installed().is_some_and(|bundle| core::ptr::eq(bundle, first)));
1756 }
1757}