1use crate::prelude::*;
44
45use alloc::collections::BTreeMap;
46use core::any::Any;
47use core::fmt;
48
49use once_cell::race::OnceBox;
50
51use crate::bundle::Bundle;
52use crate::error::{ContentError, codes};
53use crate::import_settings::{
54 self, CheckSection, ImportSettings, SETTINGS_EXTENSION, is_settings_file, settings_chain,
55};
56
57pub trait AssetKind: Sized + 'static {
61 const NAME: &'static str;
64}
65
66pub const DEFAULT_MAX_BYTES: usize = 16 << 20;
69
70pub const TEXT_MAX_BYTES: usize = 4 << 20;
73
74pub const NOT_FOUND: &str = "file not found in asset bundle";
76
77pub trait Format: Send + Sync + 'static {
79 type Asset: AssetKind;
81
82 fn id(&self) -> &'static str;
85
86 fn extensions(&self) -> &'static [&'static str];
90
91 fn sniff(&self, bytes: &[u8]) -> bool {
94 let _ = bytes;
95 false
96 }
97
98 fn max_bytes(&self) -> usize {
102 DEFAULT_MAX_BYTES
103 }
104
105 fn decode(
107 &self,
108 file: &AssetPath,
109 bytes: &[u8],
110 cx: &DecodeCx<'_>,
111 ) -> Result<Self::Asset, Vec<ContentError>>;
112}
113
114pub trait Encode: Format {
117 fn encode(&self, asset: &Self::Asset) -> Vec<u8>;
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125pub struct AssetPath {
126 path: String,
127 shown: String,
128}
129
130impl AssetPath {
131 pub fn new(path: impl Into<String>) -> Self {
133 let path = path.into();
134 Self {
135 shown: path.clone(),
136 path,
137 }
138 }
139
140 pub fn in_bundle(bundle: &Bundle, path: &str) -> Self {
143 Self {
144 path: path.to_owned(),
145 shown: bundle.display_path(path),
146 }
147 }
148
149 #[must_use]
151 pub fn shown_as(self, shown: impl Into<String>) -> Self {
152 Self {
153 shown: shown.into(),
154 ..self
155 }
156 }
157
158 pub fn path(&self) -> &str {
160 &self.path
161 }
162
163 pub fn shown(&self) -> &str {
165 &self.shown
166 }
167
168 pub fn name(&self) -> &str {
170 self.path
171 .rsplit_once('/')
172 .map_or(&self.path, |(_, name)| name)
173 }
174
175 pub fn dir(&self) -> &str {
178 self.path.rsplit_once('/').map_or("", |(dir, _)| dir)
179 }
180
181 pub fn extension(&self) -> Option<&str> {
184 let (stem, extension) = self.name().rsplit_once('.')?;
185 (!stem.is_empty()).then_some(extension)
186 }
187
188 pub fn stem(&self) -> &str {
190 let name = self.name();
191 match self.extension() {
192 Some(extension) => &name[..name.len() - extension.len() - 1],
193 None => name,
194 }
195 }
196
197 pub fn sibling(&self, name: &str) -> String {
199 join(self.dir(), name)
200 }
201
202 pub fn with_extension(&self, extension: &str) -> String {
205 self.sibling(&format!("{}.{extension}", self.stem()))
206 }
207}
208
209pub fn join(dir: &str, name: &str) -> String {
211 if dir.is_empty() {
212 name.to_owned()
213 } else {
214 format!("{dir}/{name}")
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct Limits {
222 pub max_bytes: usize,
226}
227
228impl Limits {
229 pub const NONE: Self = Self {
231 max_bytes: usize::MAX,
232 };
233}
234
235impl Default for Limits {
236 fn default() -> Self {
237 Self::NONE
238 }
239}
240
241#[derive(Debug, Clone, Copy, Default)]
245pub struct DecodeCx<'a> {
246 pub limits: Limits,
248 pub bundle: Option<&'a Bundle>,
250}
251
252impl<'a> DecodeCx<'a> {
253 pub const fn new() -> Self {
255 Self {
256 limits: Limits::NONE,
257 bundle: None,
258 }
259 }
260
261 pub const fn in_bundle(bundle: &'a Bundle) -> Self {
263 Self {
264 limits: Limits::NONE,
265 bundle: Some(bundle),
266 }
267 }
268
269 #[must_use]
271 pub const fn with_limits(self, limits: Limits) -> Self {
272 Self { limits, ..self }
273 }
274
275 pub fn has(&self, path: &str) -> bool {
277 self.bundle.is_some_and(|bundle| bundle.has(path))
278 }
279
280 pub fn bytes(&self, path: &str) -> Option<&'static [u8]> {
283 self.bundle?.bytes(path)
284 }
285
286 pub fn text(&self, path: &str) -> Option<&'static str> {
289 self.bundle?.file(path)
290 }
291
292 pub fn shown(&self, path: &str) -> String {
294 self.bundle
295 .map_or_else(|| path.to_owned(), |bundle| bundle.display_path(path))
296 }
297
298 pub fn left_out(&self, path: &str) -> bool {
301 self.bundle.is_some_and(|bundle| bundle.left_out(path))
302 }
303
304 pub fn files_under(&self, dir: &str) -> Vec<&'static str> {
307 self.bundle
308 .map(|bundle| bundle.files_under(dir))
309 .unwrap_or_default()
310 }
311
312 pub fn import_settings<S: ImportSettings>(
321 &self,
322 file: &AssetPath,
323 ) -> Result<S, Vec<ContentError>> {
324 let max = TEXT_MAX_BYTES.min(self.limits.max_bytes);
325 for path in settings_chain(file.path()) {
326 let Some(bytes) = self.bytes(&path) else {
327 continue;
328 };
329 let shown = self.shown(&path);
330 let sections = import_settings::sections(&shown, bytes, max)?;
331 if let Some(section) = sections.iter().find(|s| s.name == S::SECTION) {
332 return import_settings::read_section(&shown, section).map_err(|e| vec![e]);
333 }
334 }
335 Ok(S::default())
336 }
337}
338
339pub fn text<'b>(file: &AssetPath, bytes: &'b [u8]) -> Result<&'b str, Vec<ContentError>> {
342 core::str::from_utf8(bytes).map_err(|_| {
343 vec![ContentError::new(file.shown(), "file is not valid UTF-8").with_code(codes::NOT_TEXT)]
344 })
345}
346
347pub fn decode_with<F: Format + ?Sized>(
350 format: &F,
351 file: &AssetPath,
352 bytes: &[u8],
353 cx: &DecodeCx<'_>,
354) -> Result<F::Asset, Vec<ContentError>> {
355 let max = format.max_bytes().min(cx.limits.max_bytes);
356 if bytes.len() > max {
357 let message = format!(
358 "the file is {} bytes; the \"{}\" {} format reads at most {max}",
359 bytes.len(),
360 format.id(),
361 <F::Asset as AssetKind>::NAME
362 );
363 return Err(vec![
364 ContentError::new(file.shown(), message).with_code(codes::TOO_BIG),
365 ]);
366 }
367 format.decode(file, bytes, cx)
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373#[non_exhaustive]
374pub struct Decoded<'a> {
375 pub kind: &'static str,
377 pub path: &'a str,
379 pub bytes: usize,
381 pub ok: bool,
383}
384
385impl<'a> Decoded<'a> {
386 pub const fn new(kind: &'static str, path: &'a str, bytes: usize, ok: bool) -> Self {
389 Self {
390 kind,
391 path,
392 bytes,
393 ok,
394 }
395 }
396}
397
398#[derive(Debug, Clone, Copy)]
402#[non_exhaustive]
403pub struct DecodeWatch {
404 pub before: fn(kind: &'static str) -> u32,
407 pub after: fn(token: u32, decoded: &Decoded<'_>),
409}
410
411impl DecodeWatch {
412 pub const fn new(
415 before: fn(kind: &'static str) -> u32,
416 after: fn(token: u32, decoded: &Decoded<'_>),
417 ) -> Self {
418 Self { before, after }
419 }
420}
421
422static DECODE_WATCH: OnceBox<DecodeWatch> = OnceBox::new();
424
425pub fn watch_decodes(watch: DecodeWatch) -> Result<(), DecodeWatch> {
432 DECODE_WATCH.set(Box::new(watch)).map_err(|watch| *watch)
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
438#[non_exhaustive]
439pub enum RegisterError {
440 #[error(
442 "{kind} format \"{format}\" claims the extension \"{extension}\", which {kind} format \
443 \"{earlier}\" claims already; replace \"{earlier}\" to read .{extension} files with \
444 \"{format}\""
445 )]
446 Extension {
447 kind: &'static str,
449 format: &'static str,
451 earlier: &'static str,
453 extension: &'static str,
455 },
456 #[error("{kind} format \"{format}\" is registered already; replace it to use the new one")]
458 Id {
459 kind: &'static str,
461 format: &'static str,
463 },
464 #[error(
466 "{kind} format \"{format}\" claims the extension \"{extension}\"; an extension is \
467 lowercase, without the dot, and not empty"
468 )]
469 BadExtension {
470 kind: &'static str,
472 format: &'static str,
474 extension: &'static str,
476 },
477 #[error(
479 "{kind} format \"{format}\" claims the extension \"meta\", which is kept for import \
480 settings files (<file>.meta and _folder.meta)"
481 )]
482 Reserved {
483 kind: &'static str,
485 format: &'static str,
487 },
488 #[error("the import settings section \"{section}\" is registered already")]
490 SectionTwice {
491 section: &'static str,
493 },
494 #[error(
496 "the import settings section \"{section}\" can't read (): a field has no default; add \
497 #[serde(default)] to its struct"
498 )]
499 SectionWithoutDefault {
500 section: &'static str,
502 },
503 #[error(
506 "the import settings section \"{section}\" reads fields it doesn't have, so a typo \
507 would go unseen; add #[serde(deny_unknown_fields)] to its struct"
508 )]
509 SectionAllowsUnknownFields {
510 section: &'static str,
512 },
513}
514
515struct KindList<K> {
517 formats: Vec<Box<dyn Format<Asset = K>>>,
518}
519
520trait Kind: Any + Send + Sync {
522 fn name(&self) -> &'static str;
524
525 fn ids(&self) -> Vec<&'static str>;
527}
528
529impl<K: AssetKind> Kind for KindList<K> {
530 fn name(&self) -> &'static str {
531 K::NAME
532 }
533
534 fn ids(&self) -> Vec<&'static str> {
535 self.formats.iter().map(|format| format.id()).collect()
536 }
537}
538
539fn claims<K: AssetKind>(format: &dyn Format<Asset = K>, extension: &str) -> bool {
541 format
542 .extensions()
543 .iter()
544 .any(|claimed| claimed.eq_ignore_ascii_case(extension))
545}
546
547fn conflict<F: Format>(
550 earlier: &[Box<dyn Format<Asset = F::Asset>>],
551 format: &F,
552) -> Option<RegisterError> {
553 let kind = <F::Asset as AssetKind>::NAME;
554 earlier.iter().find_map(|old| {
555 if old.id() == format.id() {
556 return Some(RegisterError::Id {
557 kind,
558 format: format.id(),
559 });
560 }
561 let extension = format
562 .extensions()
563 .iter()
564 .find(|&&extension| claims(&**old, extension))?;
565 Some(RegisterError::Extension {
566 kind,
567 format: format.id(),
568 earlier: old.id(),
569 extension,
570 })
571 })
572}
573
574fn bad_extension<F: Format>(format: &F) -> Option<RegisterError> {
577 let extension = format.extensions().iter().find(|extension| {
578 extension.is_empty() || extension.contains('.') || extension.chars().any(char::is_uppercase)
579 })?;
580 Some(RegisterError::BadExtension {
581 kind: <F::Asset as AssetKind>::NAME,
582 format: format.id(),
583 extension,
584 })
585}
586
587fn reserved<F: Format>(format: &F) -> Option<RegisterError> {
589 format
590 .extensions()
591 .iter()
592 .any(|extension| extension.eq_ignore_ascii_case(SETTINGS_EXTENSION))
593 .then(|| RegisterError::Reserved {
594 kind: <F::Asset as AssetKind>::NAME,
595 format: format.id(),
596 })
597}
598
599#[derive(Default)]
601pub struct Formats {
602 kinds: Vec<Box<dyn Kind>>,
604 sections: BTreeMap<&'static str, CheckSection>,
607 pub limits: Limits,
609}
610
611impl fmt::Debug for Formats {
612 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613 let kinds: Vec<(&str, Vec<&str>)> = self
614 .kinds
615 .iter()
616 .map(|kind| (kind.name(), kind.ids()))
617 .collect();
618 f.debug_struct("Formats")
619 .field("kinds", &kinds)
620 .field("sections", &self.import_settings_sections())
621 .field("limits", &self.limits)
622 .finish()
623 }
624}
625
626impl Formats {
627 pub fn new() -> Self {
629 Self::default()
630 }
631
632 pub fn standard() -> Self {
635 let mut formats = Self::new();
636 let _ = formats.register_standard();
639 formats
640 }
641
642 pub fn register_standard(&mut self) -> Result<(), RegisterError> {
648 use crate::{
649 audio, bitmap, credits, dialogue, font, image, lighting, names, onebit, palette, target,
650 };
651 self.register(image::PngInfo)?;
652 self.register(bitmap::OneBitInfo)?;
653 self.register(bitmap::OneBitFile)?;
654 #[cfg(feature = "png")]
655 self.register(image::Png)?;
656 #[cfg(feature = "png")]
657 self.register(bitmap::dither::Dithered)?;
658 self.register(audio::Wav)?;
659 self.register(audio::OggVorbis)?;
660 self.register(audio::OggVorbisTrack)?;
661 self.register(audio::AudioManifestRon)?;
662 self.register(credits::CreditsRon::default())?;
663 self.register(font::FontAtlasRon::default())?;
664 self.register(palette::PaletteRon::default())?;
665 self.register(onebit::PatternMapRon)?;
666 self.register(lighting::LightingRon::default())?;
667 self.register(names::NamesRon)?;
668 self.register(dialogue::Dlg::<dialogue::NoDirective>::new())?;
669 self.register(target::TargetRon)?;
670 self.register_import_settings::<bitmap::dither::OneBitSettings>()?;
671 Ok(())
672 }
673
674 fn list<K: AssetKind>(&self) -> &[Box<dyn Format<Asset = K>>] {
676 self.kinds
677 .iter()
678 .find_map(|kind| (&**kind as &dyn Any).downcast_ref::<KindList<K>>())
679 .map_or(&[], |list| &list.formats)
680 }
681
682 fn list_mut<K: AssetKind>(&mut self) -> Option<&mut KindList<K>> {
684 self.kinds
685 .iter_mut()
686 .find_map(|kind| (&mut **kind as &mut dyn Any).downcast_mut::<KindList<K>>())
687 }
688
689 pub fn register<F: Format>(&mut self, format: F) -> Result<(), RegisterError> {
692 if let Some(error) = bad_extension(&format)
693 .or_else(|| reserved(&format))
694 .or_else(|| conflict(self.list(), &format))
695 {
696 return Err(error);
697 }
698 self.insert(None, Box::new(format));
699 Ok(())
700 }
701
702 pub fn replace<F: Format>(&mut self, format: F) -> Result<Vec<&'static str>, RegisterError> {
706 if let Some(error) = bad_extension(&format).or_else(|| reserved(&format)) {
707 return Err(error);
708 }
709 let mut replaced = Vec::new();
710 let mut at = None;
711 if let Some(list) = self.list_mut::<F::Asset>() {
712 let clash = |old: &dyn Format<Asset = F::Asset>| {
713 old.id() == format.id()
714 || format
715 .extensions()
716 .iter()
717 .any(|&extension| claims(old, extension))
718 };
719 for old in core::mem::take(&mut list.formats) {
720 if clash(&*old) {
721 at.get_or_insert(list.formats.len());
724 replaced.push(old.id());
725 } else {
726 list.formats.push(old);
727 }
728 }
729 }
730 self.insert(at, Box::new(format));
731 Ok(replaced)
732 }
733
734 fn insert<K: AssetKind>(&mut self, at: Option<usize>, format: Box<dyn Format<Asset = K>>) {
736 match self.list_mut::<K>() {
737 Some(list) => list
738 .formats
739 .insert(at.unwrap_or(list.formats.len()), format),
740 None => self.kinds.push(Box::new(KindList {
741 formats: vec![format],
742 })),
743 }
744 }
745
746 pub fn register_import_settings<S: ImportSettings>(&mut self) -> Result<(), RegisterError> {
752 let section = S::SECTION;
753 if self.sections.contains_key(section) {
754 return Err(RegisterError::SectionTwice { section });
755 }
756 if ::ron::from_str::<S>("()").is_err() {
757 return Err(RegisterError::SectionWithoutDefault { section });
758 }
759 if ::ron::from_str::<S>("(shuyi_probe: ())").is_ok() {
760 return Err(RegisterError::SectionAllowsUnknownFields { section });
761 }
762 self.sections
763 .insert(section, import_settings::check_section::<S>);
764 Ok(())
765 }
766
767 pub fn import_settings_sections(&self) -> Vec<&'static str> {
769 self.sections.keys().copied().collect()
770 }
771
772 pub fn check_import_settings(&self, bundle: &Bundle) -> Vec<ContentError> {
777 let max = TEXT_MAX_BYTES.min(self.limits.max_bytes);
778 let mut errors = Vec::new();
779 for path in bundle.files_under("") {
780 if !is_settings_file(path) {
781 continue;
782 }
783 let shown = bundle.display_path(path);
784 errors.extend(import_settings::orphan(bundle, path, &shown));
785 let bytes = bundle.bytes(path).unwrap_or_default();
786 match import_settings::sections(&shown, bytes, max) {
787 Ok(sections) => {
788 for section in §ions {
789 match self.sections.get(section.name.as_str()) {
790 Some(check) => errors.extend(check(&shown, section)),
791 None => errors.push(self.unknown_section(&shown, section)),
792 }
793 }
794 }
795 Err(e) => errors.extend(e),
796 }
797 }
798 errors
799 }
800
801 fn unknown_section(&self, shown: &str, section: &import_settings::Section<'_>) -> ContentError {
804 let known = if self.sections.is_empty() {
805 "no section is registered".to_owned()
806 } else {
807 format!(
808 "the sections registered are {}",
809 self.import_settings_sections().join(", ")
810 )
811 };
812 let message = format!("unknown section \"{}\"; {known}", section.name);
813 section.error(shown, message).with_code(codes::UNKNOWN_NAME)
814 }
815
816 pub fn ids<K: AssetKind>(&self) -> Vec<&'static str> {
818 self.list::<K>().iter().map(|format| format.id()).collect()
819 }
820
821 pub fn extensions<K: AssetKind>(&self) -> Vec<&'static str> {
823 self.list::<K>()
824 .iter()
825 .flat_map(|format| format.extensions().iter().copied())
826 .collect()
827 }
828
829 pub fn claims<K: AssetKind>(&self, path: &str) -> bool {
832 let file = AssetPath::new(path);
833 let Some(extension) = file.extension() else {
834 return false;
835 };
836 self.list::<K>()
837 .iter()
838 .any(|format| claims(&**format, extension))
839 }
840
841 pub fn decode<K: AssetKind>(
847 &self,
848 file: &AssetPath,
849 bytes: &[u8],
850 cx: &DecodeCx<'_>,
851 ) -> Result<K, Vec<ContentError>> {
852 let Some(watch) = DECODE_WATCH.get() else {
853 return self.decode_unwatched(file, bytes, cx);
854 };
855 let token = (watch.before)(K::NAME);
856 let decoded = self.decode_unwatched(file, bytes, cx);
857 let told = Decoded::new(K::NAME, file.path(), bytes.len(), decoded.is_ok());
858 (watch.after)(token, &told);
859 decoded
860 }
861
862 fn decode_unwatched<K: AssetKind>(
864 &self,
865 file: &AssetPath,
866 bytes: &[u8],
867 cx: &DecodeCx<'_>,
868 ) -> Result<K, Vec<ContentError>> {
869 let formats = self.list::<K>();
870 let sniffed: Vec<bool> = formats.iter().map(|format| format.sniff(bytes)).collect();
871 let extension = file.extension();
872 let by_sniff = formats.iter().zip(&sniffed).filter(|&(_, &yes)| yes);
873 let by_extension = formats.iter().zip(&sniffed).filter(|&(format, &yes)| {
874 !yes && extension.is_some_and(|extension| claims(&**format, extension))
875 });
876 let mut first = None;
877 for (format, _) in by_sniff.chain(by_extension) {
878 match decode_with(&**format, file, bytes, cx) {
879 Ok(asset) => return Ok(asset),
880 Err(errors) => {
881 first.get_or_insert(errors);
882 }
883 }
884 }
885 Err(first.unwrap_or_else(|| vec![self.unread::<K>(file)]))
886 }
887
888 fn unread<K: AssetKind>(&self, file: &AssetPath) -> ContentError {
890 let kind = K::NAME;
891 let why = match file.extension() {
892 Some(extension) => {
893 format!("no {kind} format knows its bytes or claims its extension \"{extension}\"")
894 }
895 None => format!("no {kind} format knows its bytes, and it has no extension"),
896 };
897 let ids = self.ids::<K>();
898 let known = if ids.is_empty() {
899 format!("no {kind} format is registered")
900 } else {
901 format!("the {kind} formats are {}", ids.join(", "))
902 };
903 ContentError::new(file.shown(), format!("{why}; {known}")).with_code(codes::NO_FORMAT)
904 }
905
906 pub fn find<K: AssetKind>(&self, bundle: &Bundle, id: &str) -> Result<String, ContentError> {
912 if bundle.has(id) {
913 return Ok(id.to_owned());
914 }
915 let tried: Vec<String> = self
916 .extensions::<K>()
917 .iter()
918 .map(|extension| format!("{id}.{extension}"))
919 .collect();
920 let found: Vec<&String> = tried.iter().filter(|path| bundle.has(path)).collect();
921 let (code, message) = match found.as_slice() {
922 [path] => return Ok((*path).clone()),
923 [] if tried.is_empty() => (codes::MISSING_FILE, NOT_FOUND.to_owned()),
924 [] => (
925 codes::MISSING_FILE,
926 format!("{NOT_FOUND}, nor {}", tried.join(" nor ")),
927 ),
928 many => {
929 let shown: Vec<String> =
930 many.iter().map(|path| bundle.display_path(path)).collect();
931 let message = format!(
932 "{} files could be this {}: {}; name the one meant with its extension",
933 shown.len(),
934 K::NAME,
935 shown.join(", ")
936 );
937 (codes::AMBIGUOUS_FILE, message)
938 }
939 };
940 Err(ContentError::new(bundle.display_path(id), message).with_code(code))
941 }
942
943 pub fn load<K: AssetKind>(&self, bundle: &Bundle, id: &str) -> Result<K, Vec<ContentError>> {
946 let path = self.find::<K>(bundle, id).map_err(|e| vec![e])?;
947 self.read(bundle, &path)
948 }
949
950 fn read<K: AssetKind>(&self, bundle: &Bundle, path: &str) -> Result<K, Vec<ContentError>> {
952 let bytes = bundle.bytes(path).unwrap_or_default();
953 let cx = DecodeCx::in_bundle(bundle).with_limits(self.limits);
954 self.decode(&AssetPath::in_bundle(bundle, path), bytes, &cx)
955 }
956
957 pub fn load_under<K: AssetKind>(
965 &self,
966 bundle: &Bundle,
967 dir: &str,
968 ) -> Result<BTreeMap<&'static str, K>, Vec<ContentError>> {
969 let mut assets = BTreeMap::new();
970 let mut errors = Vec::new();
971 for path in bundle.files_under(dir) {
972 if !self.claims::<K>(path) {
973 continue;
974 }
975 match self.read(bundle, path) {
976 Ok(asset) => {
977 assets.insert(path, asset);
978 }
979 Err(e) => errors.extend(e),
980 }
981 }
982 if errors.is_empty() {
983 Ok(assets)
984 } else {
985 Err(errors)
986 }
987 }
988
989 pub fn load_each<K: AssetKind>(
998 &self,
999 bundle: &Bundle,
1000 dir: &str,
1001 ) -> BTreeMap<&'static str, Result<K, Vec<ContentError>>> {
1002 bundle
1003 .files_in(dir)
1004 .into_iter()
1005 .filter(|path| self.claims::<K>(path))
1006 .map(|path| (path, self.read(bundle, path)))
1007 .collect()
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests;
1013
1014#[cfg(test)]
1015pub(crate) mod builtin_tests;
1016
1017#[cfg(test)]
1018mod golden_tests;