1mod utf8_reader;
46
47use std::borrow::Cow;
48use std::collections::{BTreeMap, BTreeSet, HashMap};
49use std::error::Error;
50use std::fmt::{Display, Formatter};
51use std::fs::{File, OpenOptions};
52use std::io::{BufReader, BufWriter, ErrorKind, Read, Seek, Write};
53use std::path::Path;
54use std::sync::Arc;
55use std::{fmt, io};
56
57use parking_lot::Mutex;
58use regex::Regex;
59use serde::{Deserialize, Deserializer, Serialize};
60use thiserror::Error;
61use zip::{write::SimpleFileOptions, ZipWriter};
62
63use symbolic_common::{Arch, AsSelf, CodeId, DebugId, SourceLinkMappings};
64
65use self::utf8_reader::Utf8Reader;
66use crate::base::*;
67use crate::js::{
68 discover_debug_id, discover_sourcemap_embedded_debug_id, discover_sourcemaps_location,
69};
70
71static BUNDLE_MAGIC: [u8; 4] = *b"SYSB";
73
74static BUNDLE_VERSION: u32 = 2;
76
77static MANIFEST_PATH: &str = "manifest.json";
79
80static FILES_PATH: &str = "files";
82
83lazy_static::lazy_static! {
84 static ref SANE_PATH_RE: Regex = Regex::new(r":?[/\\]+").unwrap();
85}
86
87#[non_exhaustive]
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum SourceBundleErrorKind {
91 BadZip,
93
94 BadManifest,
96
97 BadDebugFile,
99
100 WriteFailed,
102
103 ReadFailed,
105}
106
107impl fmt::Display for SourceBundleErrorKind {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 Self::BadZip => write!(f, "malformed zip archive"),
111 Self::BadManifest => write!(f, "failed to read/write source bundle manifest"),
112 Self::BadDebugFile => write!(f, "malformed debug info file"),
113 Self::WriteFailed => write!(f, "failed to write source bundle"),
114 Self::ReadFailed => write!(f, "file could not be read as UTF-8"),
115 }
116 }
117}
118
119#[derive(Debug, Error)]
121#[error("{kind}")]
122pub struct SourceBundleError {
123 kind: SourceBundleErrorKind,
124 #[source]
125 source: Option<Box<dyn Error + Send + Sync + 'static>>,
126}
127
128impl SourceBundleError {
129 pub fn new<E>(kind: SourceBundleErrorKind, source: E) -> Self
136 where
137 E: Into<Box<dyn Error + Send + Sync>>,
138 {
139 let source = Some(source.into());
140 Self { kind, source }
141 }
142
143 pub fn kind(&self) -> SourceBundleErrorKind {
145 self.kind
146 }
147}
148
149impl From<SourceBundleErrorKind> for SourceBundleError {
150 fn from(kind: SourceBundleErrorKind) -> Self {
151 Self { kind, source: None }
152 }
153}
154
155fn trim_end_matches<F>(string: &mut String, pat: F)
157where
158 F: FnMut(char) -> bool,
159{
160 let cutoff = string.trim_end_matches(pat).len();
161 string.truncate(cutoff);
162}
163
164#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
166#[serde(rename_all = "snake_case")]
167pub enum SourceFileType {
168 Source,
170
171 MinifiedSource,
173
174 SourceMap,
176
177 IndexedRamBundle,
179}
180
181#[derive(Clone, Debug, Default, Serialize, Deserialize)]
183pub struct SourceFileInfo {
184 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
185 ty: Option<SourceFileType>,
186
187 #[serde(default, skip_serializing_if = "String::is_empty")]
188 path: String,
189
190 #[serde(default, skip_serializing_if = "String::is_empty")]
191 url: String,
192
193 #[serde(
194 default,
195 skip_serializing_if = "BTreeMap::is_empty",
196 deserialize_with = "deserialize_headers"
197 )]
198 headers: BTreeMap<String, String>,
199}
200
201fn deserialize_headers<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
203where
204 D: Deserializer<'de>,
205{
206 let rv: BTreeMap<String, String> = Deserialize::deserialize(deserializer)?;
207 if rv.is_empty()
208 || rv
209 .keys()
210 .all(|x| !x.chars().any(|c| c.is_ascii_uppercase()))
211 {
212 Ok(rv)
213 } else {
214 Ok(rv
215 .into_iter()
216 .map(|(k, v)| (k.to_ascii_lowercase(), v))
217 .collect())
218 }
219}
220
221impl SourceFileInfo {
222 pub fn new() -> Self {
224 Self::default()
225 }
226
227 pub fn ty(&self) -> Option<SourceFileType> {
229 self.ty
230 }
231
232 pub fn set_ty(&mut self, ty: SourceFileType) {
234 self.ty = Some(ty);
235 }
236
237 pub fn path(&self) -> Option<&str> {
239 match self.path.as_str() {
240 "" => None,
241 path => Some(path),
242 }
243 }
244
245 pub fn set_path(&mut self, path: String) {
247 self.path = path;
248 }
249
250 pub fn url(&self) -> Option<&str> {
252 match self.url.as_str() {
253 "" => None,
254 url => Some(url),
255 }
256 }
257
258 pub fn set_url(&mut self, url: String) {
260 self.url = url;
261 }
262
263 pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
265 self.headers.iter().map(|(k, v)| (k.as_str(), v.as_str()))
266 }
267
268 pub fn header(&self, header: &str) -> Option<&str> {
270 if !header.chars().any(|x| x.is_ascii_uppercase()) {
271 self.headers.get(header).map(String::as_str)
272 } else {
273 self.headers.iter().find_map(|(k, v)| {
274 if k.eq_ignore_ascii_case(header) {
275 Some(v.as_str())
276 } else {
277 None
278 }
279 })
280 }
281 }
282
283 pub fn add_header(&mut self, header: String, value: String) {
296 let mut header = header;
297 if header.chars().any(|x| x.is_ascii_uppercase()) {
298 header = header.to_ascii_lowercase();
299 }
300 self.headers.insert(header, value);
301 }
302
303 pub fn debug_id(&self) -> Option<DebugId> {
309 self.header("debug-id").and_then(|x| x.parse().ok())
310 }
311
312 pub fn source_mapping_url(&self) -> Option<&str> {
318 self.header("sourcemap")
319 .or_else(|| self.header("x-sourcemap"))
320 }
321
322 pub fn is_empty(&self) -> bool {
324 self.path.is_empty() && self.ty.is_none() && self.headers.is_empty()
325 }
326}
327
328pub struct SourceFileDescriptor<'a> {
343 contents: Option<Cow<'a, str>>,
344 remote_url: Option<Cow<'a, str>>,
345 file_info: Option<&'a SourceFileInfo>,
346}
347
348impl<'a> SourceFileDescriptor<'a> {
349 pub(crate) fn new_embedded(
351 content: Cow<'a, str>,
352 file_info: Option<&'a SourceFileInfo>,
353 ) -> SourceFileDescriptor<'a> {
354 SourceFileDescriptor {
355 contents: Some(content),
356 remote_url: None,
357 file_info,
358 }
359 }
360
361 pub(crate) fn new_remote(remote_url: Cow<'a, str>) -> SourceFileDescriptor<'a> {
363 SourceFileDescriptor {
364 contents: None,
365 remote_url: Some(remote_url),
366 file_info: None,
367 }
368 }
369
370 pub fn ty(&self) -> SourceFileType {
372 self.file_info
373 .and_then(|x| x.ty())
374 .unwrap_or(SourceFileType::Source)
375 }
376
377 pub fn contents(&self) -> Option<&str> {
384 self.contents.as_deref()
385 }
386
387 pub fn into_contents(self) -> Option<Cow<'a, str>> {
392 self.contents
393 }
394
395 pub fn url(&self) -> Option<&str> {
402 if let Some(ref url) = self.remote_url {
403 Some(url)
404 } else {
405 self.file_info.and_then(|x| x.url())
406 }
407 }
408
409 pub fn path(&self) -> Option<&str> {
414 self.file_info.and_then(|x| x.path())
415 }
416
417 pub fn debug_id(&self) -> Option<DebugId> {
423 self.file_info.and_then(|x| x.debug_id()).or_else(|| {
424 if matches!(
425 self.ty(),
426 SourceFileType::Source | SourceFileType::MinifiedSource
427 ) {
428 self.contents().and_then(discover_debug_id)
429 } else if matches!(self.ty(), SourceFileType::SourceMap) {
430 self.contents()
431 .and_then(discover_sourcemap_embedded_debug_id)
432 } else {
433 None
434 }
435 })
436 }
437
438 pub fn source_mapping_url(&self) -> Option<&str> {
444 self.file_info
445 .and_then(|x| x.source_mapping_url())
446 .or_else(|| {
447 if matches!(
448 self.ty(),
449 SourceFileType::Source | SourceFileType::MinifiedSource
450 ) {
451 self.contents().and_then(discover_sourcemaps_location)
452 } else {
453 None
454 }
455 })
456 }
457}
458
459#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
461pub struct SourceBundleVersion(pub u32);
462
463impl SourceBundleVersion {
464 pub fn new(version: u32) -> Self {
466 Self(version)
467 }
468
469 pub fn is_valid(self) -> bool {
474 self.0 <= BUNDLE_VERSION
475 }
476
477 pub fn is_latest(self) -> bool {
479 self.0 == BUNDLE_VERSION
480 }
481}
482
483impl Default for SourceBundleVersion {
484 fn default() -> Self {
485 Self(BUNDLE_VERSION)
486 }
487}
488
489#[repr(C, packed)]
493#[derive(Clone, Copy, Debug)]
494struct SourceBundleHeader {
495 pub magic: [u8; 4],
497
498 pub version: u32,
500}
501
502impl SourceBundleHeader {
503 fn as_bytes(&self) -> &[u8] {
504 let ptr = self as *const Self as *const u8;
505 unsafe { std::slice::from_raw_parts(ptr, std::mem::size_of::<Self>()) }
506 }
507}
508
509impl Default for SourceBundleHeader {
510 fn default() -> Self {
511 SourceBundleHeader {
512 magic: BUNDLE_MAGIC,
513 version: BUNDLE_VERSION,
514 }
515 }
516}
517
518#[derive(Clone, Debug, Default, Serialize, Deserialize)]
522struct SourceBundleManifest {
523 #[serde(default)]
525 pub files: BTreeMap<String, SourceFileInfo>,
526
527 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
528 pub source_links: BTreeMap<String, String>,
529
530 #[serde(flatten)]
532 pub attributes: BTreeMap<String, String>,
533}
534
535struct SourceBundleIndex<'data> {
536 manifest: SourceBundleManifest,
537 indexed_files: HashMap<FileKey<'data>, Arc<String>>,
538}
539
540impl<'data> SourceBundleIndex<'data> {
541 pub fn parse(
542 archive: &mut zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>,
543 ) -> Result<Self, SourceBundleError> {
544 let manifest_file = archive
545 .by_name("manifest.json")
546 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
547 let manifest: SourceBundleManifest = serde_json::from_reader(manifest_file)
548 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
549
550 let files = &manifest.files;
551 let mut indexed_files = HashMap::with_capacity(files.len());
552
553 for (zip_path, file_info) in files {
554 let zip_path = Arc::new(zip_path.clone());
555 if !file_info.path.is_empty() {
556 indexed_files.insert(
557 FileKey::Path(normalize_path(&file_info.path).into()),
558 zip_path.clone(),
559 );
560 }
561 if !file_info.url.is_empty() {
562 indexed_files.insert(FileKey::Url(file_info.url.clone().into()), zip_path.clone());
563 }
564 if let (Some(debug_id), Some(ty)) = (file_info.debug_id(), file_info.ty()) {
565 indexed_files.insert(FileKey::DebugId(debug_id, ty), zip_path.clone());
566 }
567 }
568
569 Ok(Self {
570 manifest,
571 indexed_files,
572 })
573 }
574}
575
576pub struct SourceBundle<'data> {
584 data: &'data [u8],
585 archive: zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>,
586 index: Arc<SourceBundleIndex<'data>>,
587}
588
589impl fmt::Debug for SourceBundle<'_> {
590 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591 f.debug_struct("SourceBundle")
592 .field("code_id", &self.code_id())
593 .field("debug_id", &self.debug_id())
594 .field("arch", &self.arch())
595 .field("kind", &self.kind())
596 .field("load_address", &format_args!("{:#x}", self.load_address()))
597 .field("has_symbols", &self.has_symbols())
598 .field("has_debug_info", &self.has_debug_info())
599 .field("has_unwind_info", &self.has_unwind_info())
600 .field("has_sources", &self.has_sources())
601 .field("is_malformed", &self.is_malformed())
602 .finish()
603 }
604}
605
606impl<'data> SourceBundle<'data> {
607 pub fn test(bytes: &[u8]) -> bool {
609 bytes.starts_with(&BUNDLE_MAGIC)
610 }
611
612 pub fn parse(data: &'data [u8]) -> Result<SourceBundle<'data>, SourceBundleError> {
614 let mut archive = zip::read::ZipArchive::new(std::io::Cursor::new(data))
615 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
616
617 let index = Arc::new(SourceBundleIndex::parse(&mut archive)?);
618
619 Ok(SourceBundle {
620 archive,
621 data,
622 index,
623 })
624 }
625
626 pub fn version(&self) -> SourceBundleVersion {
628 SourceBundleVersion(BUNDLE_VERSION)
629 }
630
631 pub fn file_format(&self) -> FileFormat {
633 FileFormat::SourceBundle
634 }
635
636 pub fn code_id(&self) -> Option<CodeId> {
644 self.index
645 .manifest
646 .attributes
647 .get("code_id")
648 .and_then(|x| x.parse().ok())
649 }
650
651 pub fn debug_id(&self) -> DebugId {
659 self.index
660 .manifest
661 .attributes
662 .get("debug_id")
663 .and_then(|x| x.parse().ok())
664 .unwrap_or_default()
665 }
666
667 pub fn name(&self) -> Option<&str> {
675 self.index
676 .manifest
677 .attributes
678 .get("object_name")
679 .map(|x| x.as_str())
680 }
681
682 pub fn arch(&self) -> Arch {
690 self.index
691 .manifest
692 .attributes
693 .get("arch")
694 .and_then(|s| s.parse().ok())
695 .unwrap_or_default()
696 }
697
698 fn kind(&self) -> ObjectKind {
702 ObjectKind::Sources
703 }
704
705 pub fn load_address(&self) -> u64 {
709 0
710 }
711
712 pub fn has_symbols(&self) -> bool {
716 false
717 }
718
719 pub fn symbols(&self) -> SourceBundleSymbolIterator<'data> {
721 std::iter::empty()
722 }
723
724 pub fn symbol_map(&self) -> SymbolMap<'data> {
726 self.symbols().collect()
727 }
728
729 pub fn has_debug_info(&self) -> bool {
733 false
734 }
735
736 pub fn debug_session(&self) -> Result<SourceBundleDebugSession<'data>, SourceBundleError> {
742 let archive = Mutex::new(self.archive.clone());
747 let source_links = SourceLinkMappings::new(
748 self.index
749 .manifest
750 .source_links
751 .iter()
752 .map(|(k, v)| (&k[..], &v[..])),
753 );
754 Ok(SourceBundleDebugSession {
755 index: Arc::clone(&self.index),
756 archive,
757 source_links,
758 })
759 }
760
761 pub fn has_unwind_info(&self) -> bool {
763 false
764 }
765
766 pub fn has_sources(&self) -> bool {
768 true
769 }
770
771 pub fn is_malformed(&self) -> bool {
773 false
774 }
775
776 pub fn data(&self) -> &'data [u8] {
778 self.data
779 }
780
781 pub fn is_empty(&self) -> bool {
783 self.index.manifest.files.is_empty()
784 }
785}
786
787impl<'slf, 'data: 'slf> AsSelf<'slf> for SourceBundle<'data> {
788 type Ref = SourceBundle<'slf>;
789
790 fn as_self(&'slf self) -> &'slf Self::Ref {
791 unsafe { std::mem::transmute(self) }
792 }
793}
794
795impl<'data> Parse<'data> for SourceBundle<'data> {
796 type Error = SourceBundleError;
797
798 fn parse(data: &'data [u8]) -> Result<Self, Self::Error> {
799 SourceBundle::parse(data)
800 }
801
802 fn test(data: &'data [u8]) -> bool {
803 SourceBundle::test(data)
804 }
805}
806
807impl<'data: 'object, 'object> ObjectLike<'data, 'object> for SourceBundle<'data> {
808 type Error = SourceBundleError;
809 type Session = SourceBundleDebugSession<'data>;
810 type SymbolIterator = SourceBundleSymbolIterator<'data>;
811
812 fn file_format(&self) -> FileFormat {
813 self.file_format()
814 }
815
816 fn code_id(&self) -> Option<CodeId> {
817 self.code_id()
818 }
819
820 fn debug_id(&self) -> DebugId {
821 self.debug_id()
822 }
823
824 fn arch(&self) -> Arch {
825 self.arch()
826 }
827
828 fn kind(&self) -> ObjectKind {
829 self.kind()
830 }
831
832 fn load_address(&self) -> u64 {
833 self.load_address()
834 }
835
836 fn has_symbols(&self) -> bool {
837 self.has_symbols()
838 }
839
840 fn symbol_map(&self) -> SymbolMap<'data> {
841 self.symbol_map()
842 }
843
844 fn symbols(&self) -> Self::SymbolIterator {
845 self.symbols()
846 }
847
848 fn has_debug_info(&self) -> bool {
849 self.has_debug_info()
850 }
851
852 fn debug_session(&self) -> Result<Self::Session, Self::Error> {
853 self.debug_session()
854 }
855
856 fn has_unwind_info(&self) -> bool {
857 self.has_unwind_info()
858 }
859
860 fn has_sources(&self) -> bool {
861 self.has_sources()
862 }
863
864 fn is_malformed(&self) -> bool {
865 self.is_malformed()
866 }
867}
868
869pub type SourceBundleSymbolIterator<'data> = std::iter::Empty<Symbol<'data>>;
871
872#[derive(Debug, Hash, PartialEq, Eq)]
873enum FileKey<'a> {
874 Path(Cow<'a, str>),
875 Url(Cow<'a, str>),
876 DebugId(DebugId, SourceFileType),
877}
878
879pub struct SourceBundleDebugSession<'data> {
881 archive: Mutex<zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>>,
882 index: Arc<SourceBundleIndex<'data>>,
883 source_links: SourceLinkMappings,
884}
885
886impl SourceBundleDebugSession<'_> {
887 pub fn files(&self) -> SourceBundleFileIterator<'_> {
889 SourceBundleFileIterator {
890 files: self.index.manifest.files.values(),
891 }
892 }
893
894 pub fn functions(&self) -> SourceBundleFunctionIterator<'_> {
896 std::iter::empty()
897 }
898
899 fn source_by_zip_path(&self, zip_path: &str) -> Result<String, SourceBundleError> {
901 let mut archive = self.archive.lock();
902 let mut file = archive
903 .by_name(zip_path)
904 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
905 let mut source_content = String::new();
906
907 file.read_to_string(&mut source_content)
908 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
909 Ok(source_content)
910 }
911
912 fn get_source_file_descriptor(
917 &self,
918 key: FileKey,
919 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
920 if let Some(zip_path) = self.index.indexed_files.get(&key) {
921 let zip_path = zip_path.as_str();
922 let content = Cow::Owned(self.source_by_zip_path(zip_path)?);
923 let info = self.index.manifest.files.get(zip_path);
924 let descriptor = SourceFileDescriptor::new_embedded(content, info);
925 return Ok(Some(descriptor));
926 }
927
928 let FileKey::Path(path) = key else {
929 return Ok(None);
930 };
931
932 Ok(self
933 .source_links
934 .resolve(&path)
935 .map(|s| SourceFileDescriptor::new_remote(s.into())))
936 }
937
938 pub fn source_by_path(
940 &self,
941 path: &str,
942 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
943 self.get_source_file_descriptor(FileKey::Path(normalize_path(path).into()))
944 }
945
946 pub fn source_by_url(
948 &self,
949 url: &str,
950 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
951 self.get_source_file_descriptor(FileKey::Url(url.into()))
952 }
953
954 pub fn source_by_debug_id(
968 &self,
969 debug_id: DebugId,
970 ty: SourceFileType,
971 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
972 self.get_source_file_descriptor(FileKey::DebugId(debug_id, ty))
973 }
974}
975
976impl<'session> DebugSession<'session> for SourceBundleDebugSession<'_> {
977 type Error = SourceBundleError;
978 type FunctionIterator = SourceBundleFunctionIterator<'session>;
979 type FileIterator = SourceBundleFileIterator<'session>;
980
981 fn functions(&'session self) -> Self::FunctionIterator {
982 self.functions()
983 }
984
985 fn files(&'session self) -> Self::FileIterator {
986 self.files()
987 }
988
989 fn source_by_path(&self, path: &str) -> Result<Option<SourceFileDescriptor<'_>>, Self::Error> {
990 self.source_by_path(path)
991 }
992}
993
994impl<'slf, 'data: 'slf> AsSelf<'slf> for SourceBundleDebugSession<'data> {
995 type Ref = SourceBundleDebugSession<'slf>;
996
997 fn as_self(&'slf self) -> &'slf Self::Ref {
998 unsafe { std::mem::transmute(self) }
999 }
1000}
1001
1002pub struct SourceBundleFileIterator<'s> {
1004 files: std::collections::btree_map::Values<'s, String, SourceFileInfo>,
1005}
1006
1007impl<'s> Iterator for SourceBundleFileIterator<'s> {
1008 type Item = Result<FileEntry<'s>, SourceBundleError>;
1009
1010 fn next(&mut self) -> Option<Self::Item> {
1011 let source_file = self.files.next()?;
1012 Some(Ok(FileEntry::new(
1013 Cow::default(),
1014 FileInfo::from_path(source_file.path.as_bytes()),
1015 )))
1016 }
1017}
1018
1019pub type SourceBundleFunctionIterator<'s> =
1021 std::iter::Empty<Result<Function<'s>, SourceBundleError>>;
1022
1023impl SourceBundleManifest {
1024 pub fn new() -> Self {
1026 Self::default()
1027 }
1028}
1029
1030fn sanitize_bundle_path(path: &str) -> String {
1035 let mut sanitized = SANE_PATH_RE.replace_all(path, "/").into_owned();
1036 if sanitized.starts_with('/') {
1037 sanitized.remove(0);
1038 }
1039 sanitized
1040}
1041
1042fn normalize_path(path: &str) -> String {
1044 path.replace('\\', "/")
1045}
1046
1047#[derive(Debug)]
1049pub struct SkippedFileInfo<'a> {
1050 path: &'a str,
1051 reason: &'a str,
1052}
1053
1054impl<'a> SkippedFileInfo<'a> {
1055 fn new(path: &'a str, reason: &'a str) -> Self {
1056 Self { path, reason }
1057 }
1058
1059 pub fn path(&self) -> &str {
1061 self.path
1062 }
1063
1064 pub fn reason(&self) -> &str {
1066 self.reason
1067 }
1068}
1069
1070impl Display for SkippedFileInfo<'_> {
1071 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1072 write!(f, "Skipped file {} due to: {}", self.path, self.reason)
1073 }
1074}
1075
1076pub struct SourceBundleWriter<W>
1105where
1106 W: Seek + Write,
1107{
1108 manifest: SourceBundleManifest,
1109 writer: ZipWriter<W>,
1110 collect_il2cpp: bool,
1111 skipped_file_callback: Box<dyn FnMut(SkippedFileInfo)>,
1112}
1113
1114fn default_file_options() -> SimpleFileOptions {
1115 SimpleFileOptions::default().last_modified_time(zip::DateTime::default())
1122}
1123
1124impl<W> SourceBundleWriter<W>
1125where
1126 W: Seek + Write,
1127{
1128 pub fn start(mut writer: W) -> Result<Self, SourceBundleError> {
1130 let header = SourceBundleHeader::default();
1131 writer
1132 .write_all(header.as_bytes())
1133 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1134
1135 Ok(SourceBundleWriter {
1136 manifest: SourceBundleManifest::new(),
1137 writer: ZipWriter::new(writer),
1138 collect_il2cpp: false,
1139 skipped_file_callback: Box::new(|_| ()),
1140 })
1141 }
1142
1143 pub fn is_empty(&self) -> bool {
1145 self.manifest.files.is_empty()
1146 }
1147
1148 pub fn collect_il2cpp_sources(&mut self, collect_il2cpp: bool) {
1151 self.collect_il2cpp = collect_il2cpp;
1152 }
1153
1154 pub fn set_attribute<K, V>(&mut self, key: K, value: V) -> Option<String>
1163 where
1164 K: Into<String>,
1165 V: Into<String>,
1166 {
1167 self.manifest.attributes.insert(key.into(), value.into())
1168 }
1169
1170 pub fn remove_attribute<K>(&mut self, key: K) -> Option<String>
1174 where
1175 K: AsRef<str>,
1176 {
1177 self.manifest.attributes.remove(key.as_ref())
1178 }
1179
1180 pub fn attribute<K>(&mut self, key: K) -> Option<&str>
1182 where
1183 K: AsRef<str>,
1184 {
1185 self.manifest
1186 .attributes
1187 .get(key.as_ref())
1188 .map(String::as_str)
1189 }
1190
1191 pub fn has_file<S>(&self, path: S) -> bool
1193 where
1194 S: AsRef<str>,
1195 {
1196 let full_path = &self.file_path(path.as_ref());
1197 self.manifest.files.contains_key(full_path)
1198 }
1199
1200 pub fn add_file<S, R>(
1226 &mut self,
1227 path: S,
1228 file: R,
1229 info: SourceFileInfo,
1230 ) -> Result<(), SourceBundleError>
1231 where
1232 S: AsRef<str>,
1233 R: Read,
1234 {
1235 let mut file_reader = Utf8Reader::new(file);
1236
1237 let full_path = self.file_path(path.as_ref());
1238 let unique_path = self.unique_path(full_path);
1239
1240 self.writer
1241 .start_file(unique_path.clone(), default_file_options())
1242 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1243
1244 match io::copy(&mut file_reader, &mut self.writer) {
1245 Err(e) => {
1246 self.writer
1247 .abort_file()
1248 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1249
1250 let error_kind = match e.kind() {
1252 ErrorKind::InvalidData => SourceBundleErrorKind::ReadFailed,
1253 _ => SourceBundleErrorKind::WriteFailed,
1254 };
1255
1256 Err(SourceBundleError::new(error_kind, e))
1257 }
1258 Ok(_) => {
1259 self.manifest.files.insert(unique_path, info);
1260 Ok(())
1261 }
1262 }
1263 }
1264
1265 fn add_file_skip_read_failed<S, R>(
1267 &mut self,
1268 path: S,
1269 file: R,
1270 info: SourceFileInfo,
1271 ) -> Result<(), SourceBundleError>
1272 where
1273 S: AsRef<str>,
1274 R: Read,
1275 {
1276 let result = self.add_file(&path, file, info);
1277
1278 if let Err(e) = &result {
1279 if e.kind == SourceBundleErrorKind::ReadFailed {
1280 let reason = e.to_string();
1281 let skipped_info = SkippedFileInfo::new(path.as_ref(), &reason);
1282 (self.skipped_file_callback)(skipped_info);
1283
1284 return Ok(());
1285 }
1286 }
1287
1288 result
1289 }
1290
1291 pub fn with_skipped_file_callback(
1294 mut self,
1295 callback: impl FnMut(SkippedFileInfo) + 'static,
1296 ) -> Self {
1297 self.skipped_file_callback = Box::new(callback);
1298 self
1299 }
1300
1301 pub fn write_object<'data, 'object, O, E>(
1308 self,
1309 object: &'object O,
1310 object_name: &str,
1311 ) -> Result<bool, SourceBundleError>
1312 where
1313 O: ObjectLike<'data, 'object, Error = E>,
1314 E: std::error::Error + Send + Sync + 'static,
1315 {
1316 self.write_object_with_filter(object, object_name, |_, _| true)
1317 }
1318
1319 pub fn write_object_with_filter<'data, 'object, O, E, F>(
1328 mut self,
1329 object: &'object O,
1330 object_name: &str,
1331 mut filter: F,
1332 ) -> Result<bool, SourceBundleError>
1333 where
1334 O: ObjectLike<'data, 'object, Error = E>,
1335 E: std::error::Error + Send + Sync + 'static,
1336 F: FnMut(&FileEntry, &Option<SourceFileDescriptor<'_>>) -> bool,
1337 {
1338 let mut files_handled = BTreeSet::new();
1339 let mut referenced_files = BTreeSet::new();
1340
1341 let session = object
1342 .debug_session()
1343 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1344
1345 self.set_attribute("arch", object.arch().to_string());
1346 self.set_attribute("debug_id", object.debug_id().to_string());
1347 self.set_attribute("object_name", object_name);
1348 if let Some(code_id) = object.code_id() {
1349 self.set_attribute("code_id", code_id.to_string());
1350 }
1351
1352 for file_result in session.files() {
1353 let file = file_result
1354 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1355 let filename = file.abs_path_str();
1356
1357 if files_handled.contains(&filename) {
1358 continue;
1359 }
1360
1361 let source = if filename.starts_with('<') && filename.ends_with('>') {
1362 None
1363 } else {
1364 let source_from_object = session
1365 .source_by_path(&filename)
1366 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1367 if filter(&file, &source_from_object) {
1368 std::fs::read(&filename).ok()
1371 } else {
1372 None
1373 }
1374 };
1375
1376 if let Some(source) = source {
1377 let bundle_path = sanitize_bundle_path(&filename);
1378 let mut info = SourceFileInfo::new();
1379 info.set_ty(SourceFileType::Source);
1380 info.set_path(filename.clone());
1381
1382 if self.collect_il2cpp {
1383 collect_il2cpp_sources(&source, &mut referenced_files);
1384 }
1385
1386 self.add_file_skip_read_failed(bundle_path, source.as_slice(), info)?;
1387 }
1388
1389 files_handled.insert(filename);
1390 }
1391
1392 for filename in referenced_files {
1393 if files_handled.contains(&filename) {
1394 continue;
1395 }
1396
1397 if let Some(source) = File::open(&filename).ok().map(BufReader::new) {
1398 let bundle_path = sanitize_bundle_path(&filename);
1399 let mut info = SourceFileInfo::new();
1400 info.set_ty(SourceFileType::Source);
1401 info.set_path(filename.clone());
1402
1403 self.add_file_skip_read_failed(bundle_path, source, info)?
1404 }
1405 }
1406
1407 let is_empty = self.is_empty();
1408 self.finish()?;
1409
1410 Ok(!is_empty)
1411 }
1412
1413 pub fn finish(mut self) -> Result<(), SourceBundleError> {
1415 self.write_manifest()?;
1416 self.writer
1417 .finish()
1418 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1419 Ok(())
1420 }
1421
1422 fn file_path(&self, path: &str) -> String {
1424 format!("{FILES_PATH}/{path}")
1425 }
1426
1427 fn unique_path(&self, mut path: String) -> String {
1432 let mut duplicates = 0;
1433
1434 while self.manifest.files.contains_key(&path) {
1435 duplicates += 1;
1436 match duplicates {
1437 1 => path.push_str(".1"),
1438 _ => {
1439 use std::fmt::Write;
1440 trim_end_matches(&mut path, char::is_numeric);
1441 write!(path, ".{duplicates}").unwrap();
1442 }
1443 }
1444 }
1445
1446 path
1447 }
1448
1449 fn write_manifest(&mut self) -> Result<(), SourceBundleError> {
1451 self.writer
1452 .start_file(MANIFEST_PATH, default_file_options())
1453 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1454
1455 serde_json::to_writer(&mut self.writer, &self.manifest)
1456 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
1457
1458 Ok(())
1459 }
1460}
1461
1462fn collect_il2cpp_sources(source: &[u8], referenced_files: &mut BTreeSet<String>) {
1466 if let Ok(source) = std::str::from_utf8(source) {
1467 for line in source.lines() {
1468 let line = line.trim();
1469
1470 if let Some(source_ref) = line.strip_prefix("//<source_info:") {
1471 if let Some((file, _line)) = source_ref.rsplit_once(':') {
1472 if !referenced_files.contains(file) {
1473 referenced_files.insert(file.to_string());
1474 }
1475 }
1476 }
1477 }
1478 }
1479}
1480
1481impl SourceBundleWriter<BufWriter<File>> {
1482 pub fn create<P>(path: P) -> Result<SourceBundleWriter<BufWriter<File>>, SourceBundleError>
1487 where
1488 P: AsRef<Path>,
1489 {
1490 let file = OpenOptions::new()
1491 .read(true)
1492 .write(true)
1493 .create(true)
1494 .truncate(true)
1495 .open(path)
1496 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1497
1498 Self::start(BufWriter::new(file))
1499 }
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504 use super::*;
1505
1506 use std::io::Cursor;
1507
1508 use similar_asserts::assert_eq;
1509 use tempfile::NamedTempFile;
1510
1511 #[test]
1512 fn test_has_file() -> Result<(), SourceBundleError> {
1513 let writer = Cursor::new(Vec::new());
1514 let mut bundle = SourceBundleWriter::start(writer)?;
1515
1516 bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
1517 assert!(bundle.has_file("bar.txt"));
1518
1519 bundle.finish()?;
1520 Ok(())
1521 }
1522
1523 #[test]
1524 fn test_non_utf8() -> Result<(), SourceBundleError> {
1525 let writer = Cursor::new(Vec::new());
1526 let mut bundle = SourceBundleWriter::start(writer)?;
1527
1528 assert!(bundle
1529 .add_file(
1530 "bar.txt",
1531 &[0, 159, 146, 150][..],
1532 SourceFileInfo::default()
1533 )
1534 .is_err());
1535
1536 Ok(())
1537 }
1538
1539 #[test]
1540 fn test_duplicate_files() -> Result<(), SourceBundleError> {
1541 let writer = Cursor::new(Vec::new());
1542 let mut bundle = SourceBundleWriter::start(writer)?;
1543
1544 bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
1545 bundle.add_file("bar.txt", &b"othercontents"[..], SourceFileInfo::default())?;
1546 assert!(bundle.has_file("bar.txt"));
1547 assert!(bundle.has_file("bar.txt.1"));
1548
1549 bundle.finish()?;
1550 Ok(())
1551 }
1552
1553 #[test]
1554 fn debugsession_is_sendsync() {
1555 fn is_sendsync<T: Send + Sync>() {}
1556 is_sendsync::<SourceBundleDebugSession>();
1557 }
1558
1559 #[test]
1560 fn test_normalize_paths() -> Result<(), SourceBundleError> {
1561 let mut writer = Cursor::new(Vec::new());
1562 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1563
1564 for filename in &[
1565 "C:\\users\\martin\\mydebugfile.cs",
1566 "/usr/martin/mydebugfile.h",
1567 ] {
1568 let mut info = SourceFileInfo::new();
1569 info.set_ty(SourceFileType::Source);
1570 info.set_path(filename.to_string());
1571 bundle.add_file_skip_read_failed(
1572 sanitize_bundle_path(filename),
1573 &b"somerandomdata"[..],
1574 info,
1575 )?;
1576 }
1577
1578 bundle.finish()?;
1579 let bundle_bytes = writer.into_inner();
1580 let bundle = SourceBundle::parse(&bundle_bytes)?;
1581
1582 let session = bundle.debug_session().unwrap();
1583
1584 assert!(session
1585 .source_by_path("C:\\users\\martin\\mydebugfile.cs")?
1586 .is_some());
1587 assert!(session
1588 .source_by_path("C:/users/martin/mydebugfile.cs")?
1589 .is_some());
1590 assert!(session
1591 .source_by_path("C:\\users\\martin/mydebugfile.cs")?
1592 .is_some());
1593 assert!(session
1594 .source_by_path("/usr/martin/mydebugfile.h")?
1595 .is_some());
1596 assert!(session
1597 .source_by_path("\\usr\\martin\\mydebugfile.h")?
1598 .is_some());
1599
1600 Ok(())
1601 }
1602
1603 #[test]
1604 fn test_source_descriptor() -> Result<(), SourceBundleError> {
1605 let mut writer = Cursor::new(Vec::new());
1606 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1607
1608 let mut info = SourceFileInfo::default();
1609 info.set_url("https://example.com/bar.js.min".into());
1610 info.set_path("/files/bar.js.min".into());
1611 info.set_ty(SourceFileType::MinifiedSource);
1612 info.add_header(
1613 "debug-id".into(),
1614 "5e618b9f-54a9-4389-b196-519819dd7c47".into(),
1615 );
1616 info.add_header("sourcemap".into(), "bar.js.map".into());
1617 bundle.add_file("bar.js", &b"filecontents"[..], info)?;
1618 assert!(bundle.has_file("bar.js"));
1619
1620 bundle.finish()?;
1621 let bundle_bytes = writer.into_inner();
1622 let bundle = SourceBundle::parse(&bundle_bytes)?;
1623
1624 let sess = bundle.debug_session().unwrap();
1625 let f = sess
1626 .source_by_debug_id(
1627 "5e618b9f-54a9-4389-b196-519819dd7c47".parse().unwrap(),
1628 SourceFileType::MinifiedSource,
1629 )
1630 .unwrap()
1631 .expect("should exist");
1632 assert_eq!(f.contents(), Some("filecontents"));
1633 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1634 assert_eq!(f.url(), Some("https://example.com/bar.js.min"));
1635 assert_eq!(f.path(), Some("/files/bar.js.min"));
1636 assert_eq!(f.source_mapping_url(), Some("bar.js.map"));
1637
1638 assert!(sess
1639 .source_by_debug_id(
1640 "5e618b9f-54a9-4389-b196-519819dd7c47".parse().unwrap(),
1641 SourceFileType::Source
1642 )
1643 .unwrap()
1644 .is_none());
1645
1646 Ok(())
1647 }
1648
1649 #[test]
1650 fn test_source_mapping_url() -> Result<(), SourceBundleError> {
1651 let mut writer = Cursor::new(Vec::new());
1652 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1653
1654 let mut info = SourceFileInfo::default();
1655 info.set_url("https://example.com/bar.min.js".into());
1656 info.set_ty(SourceFileType::MinifiedSource);
1657 bundle.add_file(
1658 "bar.js",
1659 &b"filecontents\n//# sourceMappingURL=bar.js.map"[..],
1660 info,
1661 )?;
1662
1663 bundle.finish()?;
1664 let bundle_bytes = writer.into_inner();
1665 let bundle = SourceBundle::parse(&bundle_bytes)?;
1666
1667 let sess = bundle.debug_session().unwrap();
1668 let f = sess
1669 .source_by_url("https://example.com/bar.min.js")
1670 .unwrap()
1671 .expect("should exist");
1672 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1673 assert_eq!(f.url(), Some("https://example.com/bar.min.js"));
1674 assert_eq!(f.source_mapping_url(), Some("bar.js.map"));
1675
1676 Ok(())
1677 }
1678
1679 #[test]
1680 fn test_source_embedded_debug_id() -> Result<(), SourceBundleError> {
1681 let mut writer = Cursor::new(Vec::new());
1682 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1683
1684 let mut info = SourceFileInfo::default();
1685 info.set_url("https://example.com/bar.min.js".into());
1686 info.set_ty(SourceFileType::MinifiedSource);
1687 bundle.add_file(
1688 "bar.js",
1689 &b"filecontents\n//# debugId=5b65abfb23384f0bb3b964c8f734d43f"[..],
1690 info,
1691 )?;
1692
1693 bundle.finish()?;
1694 let bundle_bytes = writer.into_inner();
1695 let bundle = SourceBundle::parse(&bundle_bytes)?;
1696
1697 let sess = bundle.debug_session().unwrap();
1698 let f = sess
1699 .source_by_url("https://example.com/bar.min.js")
1700 .unwrap()
1701 .expect("should exist");
1702 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1703 assert_eq!(
1704 f.debug_id(),
1705 Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
1706 );
1707
1708 Ok(())
1709 }
1710
1711 #[test]
1712 fn test_sourcemap_embedded_debug_id() -> Result<(), SourceBundleError> {
1713 let mut writer = Cursor::new(Vec::new());
1714 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1715
1716 let mut info = SourceFileInfo::default();
1717 info.set_url("https://example.com/bar.js.map".into());
1718 info.set_ty(SourceFileType::SourceMap);
1719 bundle.add_file(
1720 "bar.js.map",
1721 &br#"{"debug_id": "5b65abfb-2338-4f0b-b3b9-64c8f734d43f"}"#[..],
1722 info,
1723 )?;
1724
1725 bundle.finish()?;
1726 let bundle_bytes = writer.into_inner();
1727 let bundle = SourceBundle::parse(&bundle_bytes)?;
1728
1729 let sess = bundle.debug_session().unwrap();
1730 let f = sess
1731 .source_by_url("https://example.com/bar.js.map")
1732 .unwrap()
1733 .expect("should exist");
1734 assert_eq!(f.ty(), SourceFileType::SourceMap);
1735 assert_eq!(
1736 f.debug_id(),
1737 Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
1738 );
1739
1740 Ok(())
1741 }
1742
1743 #[test]
1744 fn test_il2cpp_reference() -> Result<(), Box<dyn std::error::Error>> {
1745 let mut cpp_file = NamedTempFile::new()?;
1746 let mut cs_file = NamedTempFile::new()?;
1747
1748 let cpp_contents = format!("foo\n//<source_info:{}:111>\nbar", cs_file.path().display());
1749
1750 let object_buf = {
1752 let mut writer = Cursor::new(Vec::new());
1753 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1754
1755 let path = cpp_file.path().to_string_lossy();
1756 let mut info = SourceFileInfo::new();
1757 info.set_ty(SourceFileType::Source);
1758 info.set_path(path.to_string());
1759 bundle.add_file(path, cpp_contents.as_bytes(), info)?;
1760
1761 bundle.finish()?;
1762 writer.into_inner()
1763 };
1764 let object = SourceBundle::parse(&object_buf)?;
1765
1766 cpp_file.write_all(cpp_contents.as_bytes())?;
1768 cs_file.write_all(b"some C# source")?;
1769
1770 let mut output_buf = Cursor::new(Vec::new());
1772 let mut writer = SourceBundleWriter::start(&mut output_buf)?;
1773 writer.collect_il2cpp_sources(true);
1774
1775 let written = writer.write_object(&object, "whatever")?;
1776 assert!(written);
1777 let output_buf = output_buf.into_inner();
1778
1779 let source_bundle = SourceBundle::parse(&output_buf)?;
1781 let session = source_bundle.debug_session()?;
1782 let actual_files: BTreeMap<_, _> = session
1783 .files()
1784 .flatten()
1785 .flat_map(|f| {
1786 let path = f.abs_path_str();
1787 session
1788 .source_by_path(&path)
1789 .ok()
1790 .flatten()
1791 .map(|source| (path, source.contents().unwrap().to_string()))
1792 })
1793 .collect();
1794
1795 let mut expected_files = BTreeMap::new();
1796 expected_files.insert(cpp_file.path().to_string_lossy().into_owned(), cpp_contents);
1797 expected_files.insert(
1798 cs_file.path().to_string_lossy().into_owned(),
1799 String::from("some C# source"),
1800 );
1801
1802 assert_eq!(actual_files, expected_files);
1803
1804 Ok(())
1805 }
1806
1807 #[test]
1808 fn test_bundle_paths() {
1809 assert_eq!(sanitize_bundle_path("foo"), "foo");
1810 assert_eq!(sanitize_bundle_path("foo/bar"), "foo/bar");
1811 assert_eq!(sanitize_bundle_path("/foo/bar"), "foo/bar");
1812 assert_eq!(sanitize_bundle_path("C:/foo/bar"), "C/foo/bar");
1813 assert_eq!(sanitize_bundle_path("\\foo\\bar"), "foo/bar");
1814 assert_eq!(sanitize_bundle_path("\\\\UNC\\foo\\bar"), "UNC/foo/bar");
1815 }
1816
1817 #[test]
1818 fn test_source_links() -> Result<(), SourceBundleError> {
1819 let mut writer = Cursor::new(Vec::new());
1820 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1821
1822 let mut info = SourceFileInfo::default();
1823 info.set_url("https://example.com/bar/index.min.js".into());
1824 info.set_path("/files/bar/index.min.js".into());
1825 info.set_ty(SourceFileType::MinifiedSource);
1826 bundle.add_file("bar/index.js", &b"filecontents"[..], info)?;
1827 assert!(bundle.has_file("bar/index.js"));
1828
1829 bundle
1830 .manifest
1831 .source_links
1832 .insert("/files/bar/*".to_string(), "https://nope.com/*".into());
1833 bundle
1834 .manifest
1835 .source_links
1836 .insert("/files/foo/*".to_string(), "https://example.com/*".into());
1837
1838 bundle.finish()?;
1839 let bundle_bytes = writer.into_inner();
1840 let bundle = SourceBundle::parse(&bundle_bytes)?;
1841
1842 let sess = bundle.debug_session().unwrap();
1843
1844 let foo = sess
1846 .source_by_path("/files/foo/index.min.js")
1847 .unwrap()
1848 .expect("should exist");
1849 assert_eq!(foo.contents(), None);
1850 assert_eq!(foo.ty(), SourceFileType::Source);
1851 assert_eq!(foo.url(), Some("https://example.com/index.min.js"));
1852 assert_eq!(foo.path(), None);
1853
1854 let bar = sess
1856 .source_by_path("/files/bar/index.min.js")
1857 .unwrap()
1858 .expect("should exist");
1859 assert_eq!(bar.contents(), Some("filecontents"));
1860 assert_eq!(bar.ty(), SourceFileType::MinifiedSource);
1861 assert_eq!(bar.url(), Some("https://example.com/bar/index.min.js"));
1862 assert_eq!(bar.path(), Some("/files/bar/index.min.js"));
1863
1864 Ok(())
1865 }
1866}