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, LazyLock};
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};
70use crate::ParseObjectOptions;
71
72static BUNDLE_MAGIC: [u8; 4] = *b"SYSB";
74
75static BUNDLE_VERSION: u32 = 2;
77
78static MANIFEST_PATH: &str = "manifest.json";
80
81static FILES_PATH: &str = "files";
83
84static SANE_PATH_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":?[/\\]+").unwrap());
85
86#[non_exhaustive]
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum SourceBundleErrorKind {
90 BadZip,
92
93 BadManifest,
95
96 BadDebugFile,
98
99 WriteFailed,
101
102 ReadFailed,
104
105 SourceFileSizeExceeded,
111}
112
113impl fmt::Display for SourceBundleErrorKind {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Self::BadZip => write!(f, "malformed zip archive"),
117 Self::BadManifest => write!(f, "failed to read/write source bundle manifest"),
118 Self::BadDebugFile => write!(f, "malformed debug info file"),
119 Self::WriteFailed => write!(f, "failed to write source bundle"),
120 Self::ReadFailed => write!(f, "file could not be read as UTF-8"),
121 Self::SourceFileSizeExceeded => write!(f, "the source file exceeded the size limit"),
122 }
123 }
124}
125
126#[derive(Debug, Error)]
128#[error("{kind}")]
129pub struct SourceBundleError {
130 kind: SourceBundleErrorKind,
131 #[source]
132 source: Option<Box<dyn Error + Send + Sync + 'static>>,
133}
134
135impl SourceBundleError {
136 pub fn new<E>(kind: SourceBundleErrorKind, source: E) -> Self
143 where
144 E: Into<Box<dyn Error + Send + Sync>>,
145 {
146 let source = Some(source.into());
147 Self { kind, source }
148 }
149
150 pub fn kind(&self) -> SourceBundleErrorKind {
152 self.kind
153 }
154}
155
156impl From<SourceBundleErrorKind> for SourceBundleError {
157 fn from(kind: SourceBundleErrorKind) -> Self {
158 Self { kind, source: None }
159 }
160}
161
162fn trim_end_matches<F>(string: &mut String, pat: F)
164where
165 F: FnMut(char) -> bool,
166{
167 let cutoff = string.trim_end_matches(pat).len();
168 string.truncate(cutoff);
169}
170
171#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
173#[serde(rename_all = "snake_case")]
174pub enum SourceFileType {
175 Source,
177
178 MinifiedSource,
180
181 SourceMap,
183
184 IndexedRamBundle,
186}
187
188impl SourceFileType {
189 pub fn name(self) -> &'static str {
191 match self {
192 SourceFileType::Source => "source",
193 SourceFileType::MinifiedSource => "minified_source",
194 SourceFileType::SourceMap => "source_map",
195 SourceFileType::IndexedRamBundle => "indexed_ram_bundle",
196 }
197 }
198}
199
200impl fmt::Display for SourceFileType {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 f.write_str(self.name())
203 }
204}
205
206#[derive(Clone, Debug, Default, Serialize, Deserialize)]
208pub struct SourceFileInfo {
209 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
210 ty: Option<SourceFileType>,
211
212 #[serde(default, skip_serializing_if = "String::is_empty")]
213 path: String,
214
215 #[serde(default, skip_serializing_if = "String::is_empty")]
216 url: String,
217
218 #[serde(
219 default,
220 skip_serializing_if = "BTreeMap::is_empty",
221 deserialize_with = "deserialize_headers"
222 )]
223 headers: BTreeMap<String, String>,
224}
225
226fn deserialize_headers<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
228where
229 D: Deserializer<'de>,
230{
231 let rv: BTreeMap<String, String> = Deserialize::deserialize(deserializer)?;
232 if rv.is_empty()
233 || rv
234 .keys()
235 .all(|x| !x.chars().any(|c| c.is_ascii_uppercase()))
236 {
237 Ok(rv)
238 } else {
239 Ok(rv
240 .into_iter()
241 .map(|(k, v)| (k.to_ascii_lowercase(), v))
242 .collect())
243 }
244}
245
246impl SourceFileInfo {
247 pub fn new() -> Self {
249 Self::default()
250 }
251
252 pub fn ty(&self) -> Option<SourceFileType> {
254 self.ty
255 }
256
257 pub fn set_ty(&mut self, ty: SourceFileType) {
259 self.ty = Some(ty);
260 }
261
262 pub fn path(&self) -> Option<&str> {
264 match self.path.as_str() {
265 "" => None,
266 path => Some(path),
267 }
268 }
269
270 pub fn set_path(&mut self, path: String) {
272 self.path = path;
273 }
274
275 pub fn url(&self) -> Option<&str> {
277 match self.url.as_str() {
278 "" => None,
279 url => Some(url),
280 }
281 }
282
283 pub fn set_url(&mut self, url: String) {
285 self.url = url;
286 }
287
288 pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
290 self.headers.iter().map(|(k, v)| (k.as_str(), v.as_str()))
291 }
292
293 pub fn header(&self, header: &str) -> Option<&str> {
295 if !header.chars().any(|x| x.is_ascii_uppercase()) {
296 self.headers.get(header).map(String::as_str)
297 } else {
298 self.headers.iter().find_map(|(k, v)| {
299 if k.eq_ignore_ascii_case(header) {
300 Some(v.as_str())
301 } else {
302 None
303 }
304 })
305 }
306 }
307
308 pub fn add_header(&mut self, header: String, value: String) {
321 let mut header = header;
322 if header.chars().any(|x| x.is_ascii_uppercase()) {
323 header = header.to_ascii_lowercase();
324 }
325 self.headers.insert(header, value);
326 }
327
328 pub fn debug_id(&self) -> Option<DebugId> {
334 self.header("debug-id").and_then(|x| x.parse().ok())
335 }
336
337 pub fn source_mapping_url(&self) -> Option<&str> {
343 self.header("sourcemap")
344 .or_else(|| self.header("x-sourcemap"))
345 }
346
347 pub fn is_empty(&self) -> bool {
349 self.path.is_empty() && self.ty.is_none() && self.headers.is_empty()
350 }
351}
352
353pub struct SourceFileDescriptor<'a> {
368 contents: Option<Cow<'a, str>>,
369 remote_url: Option<Cow<'a, str>>,
370 file_info: Option<&'a SourceFileInfo>,
371}
372
373impl<'a> SourceFileDescriptor<'a> {
374 pub(crate) fn new_embedded(
376 content: Cow<'a, str>,
377 file_info: Option<&'a SourceFileInfo>,
378 ) -> SourceFileDescriptor<'a> {
379 SourceFileDescriptor {
380 contents: Some(content),
381 remote_url: None,
382 file_info,
383 }
384 }
385
386 pub(crate) fn new_remote(remote_url: Cow<'a, str>) -> SourceFileDescriptor<'a> {
388 SourceFileDescriptor {
389 contents: None,
390 remote_url: Some(remote_url),
391 file_info: None,
392 }
393 }
394
395 pub fn ty(&self) -> SourceFileType {
397 self.file_info
398 .and_then(|x| x.ty())
399 .unwrap_or(SourceFileType::Source)
400 }
401
402 pub fn contents(&self) -> Option<&str> {
409 self.contents.as_deref()
410 }
411
412 pub fn into_contents(self) -> Option<Cow<'a, str>> {
417 self.contents
418 }
419
420 pub fn url(&self) -> Option<&str> {
427 if let Some(ref url) = self.remote_url {
428 Some(url)
429 } else {
430 self.file_info.and_then(|x| x.url())
431 }
432 }
433
434 pub fn path(&self) -> Option<&str> {
439 self.file_info.and_then(|x| x.path())
440 }
441
442 pub fn debug_id(&self) -> Option<DebugId> {
448 self.file_info.and_then(|x| x.debug_id()).or_else(|| {
449 if matches!(
450 self.ty(),
451 SourceFileType::Source | SourceFileType::MinifiedSource
452 ) {
453 self.contents().and_then(discover_debug_id)
454 } else if matches!(self.ty(), SourceFileType::SourceMap) {
455 self.contents()
456 .and_then(discover_sourcemap_embedded_debug_id)
457 } else {
458 None
459 }
460 })
461 }
462
463 pub fn source_mapping_url(&self) -> Option<&str> {
469 self.file_info
470 .and_then(|x| x.source_mapping_url())
471 .or_else(|| {
472 if matches!(
473 self.ty(),
474 SourceFileType::Source | SourceFileType::MinifiedSource
475 ) {
476 self.contents().and_then(discover_sourcemaps_location)
477 } else {
478 None
479 }
480 })
481 }
482}
483
484impl fmt::Debug for SourceFileDescriptor<'_> {
485 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
486 let contents = self.contents.as_ref().map(|contents| contents.len());
487 f.debug_struct("SourceFileDescriptor")
488 .field("contents", &contents)
489 .field("remote_url", &self.remote_url)
490 .field("file_info", &self.file_info)
491 .finish()
492 }
493}
494
495#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
497pub struct SourceBundleVersion(pub u32);
498
499impl SourceBundleVersion {
500 pub fn new(version: u32) -> Self {
502 Self(version)
503 }
504
505 pub fn is_valid(self) -> bool {
510 self.0 <= BUNDLE_VERSION
511 }
512
513 pub fn is_latest(self) -> bool {
515 self.0 == BUNDLE_VERSION
516 }
517}
518
519impl Default for SourceBundleVersion {
520 fn default() -> Self {
521 Self(BUNDLE_VERSION)
522 }
523}
524
525#[repr(C, packed)]
529#[derive(Clone, Copy, Debug)]
530struct SourceBundleHeader {
531 pub magic: [u8; 4],
533
534 pub version: u32,
536}
537
538impl SourceBundleHeader {
539 fn as_bytes(&self) -> &[u8] {
540 let ptr = self as *const Self as *const u8;
541 unsafe { std::slice::from_raw_parts(ptr, std::mem::size_of::<Self>()) }
542 }
543}
544
545impl Default for SourceBundleHeader {
546 fn default() -> Self {
547 SourceBundleHeader {
548 magic: BUNDLE_MAGIC,
549 version: BUNDLE_VERSION,
550 }
551 }
552}
553
554#[derive(Clone, Debug, Default, Serialize, Deserialize)]
558struct SourceBundleManifest {
559 #[serde(default)]
561 pub files: BTreeMap<String, SourceFileInfo>,
562
563 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
564 pub source_links: BTreeMap<String, String>,
565
566 #[serde(flatten)]
568 pub attributes: BTreeMap<String, String>,
569}
570
571struct SourceBundleIndex<'data> {
572 manifest: SourceBundleManifest,
573 indexed_files: HashMap<FileKey<'data>, Arc<String>>,
574}
575
576impl<'data> SourceBundleIndex<'data> {
577 pub fn parse(
578 archive: &mut zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>,
579 ) -> Result<Self, SourceBundleError> {
580 let manifest_file = archive
581 .by_name("manifest.json")
582 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
583 let manifest: SourceBundleManifest = serde_json::from_reader(manifest_file)
584 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
585
586 let files = &manifest.files;
587 let mut indexed_files = HashMap::with_capacity(files.len());
588
589 for (zip_path, file_info) in files {
590 let zip_path = Arc::new(zip_path.clone());
591 if !file_info.path.is_empty() {
592 indexed_files.insert(
593 FileKey::Path(normalize_path(&file_info.path).into()),
594 zip_path.clone(),
595 );
596 }
597 if !file_info.url.is_empty() {
598 indexed_files.insert(FileKey::Url(file_info.url.clone().into()), zip_path.clone());
599 }
600 if let (Some(debug_id), Some(ty)) = (file_info.debug_id(), file_info.ty()) {
601 indexed_files.insert(FileKey::DebugId(debug_id, ty), zip_path.clone());
602 }
603 }
604
605 Ok(Self {
606 manifest,
607 indexed_files,
608 })
609 }
610}
611
612pub struct SourceBundle<'data> {
620 data: &'data [u8],
621 archive: zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>,
622 index: Arc<SourceBundleIndex<'data>>,
623 max_decompressed_embedded_source_size: Option<usize>,
624}
625
626impl fmt::Debug for SourceBundle<'_> {
627 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628 f.debug_struct("SourceBundle")
629 .field("code_id", &self.code_id())
630 .field("debug_id", &self.debug_id())
631 .field("arch", &self.arch())
632 .field("kind", &self.kind())
633 .field("load_address", &format_args!("{:#x}", self.load_address()))
634 .field("has_symbols", &self.has_symbols())
635 .field("has_debug_info", &self.has_debug_info())
636 .field("has_unwind_info", &self.has_unwind_info())
637 .field("has_sources", &self.has_sources())
638 .field("is_malformed", &self.is_malformed())
639 .field(
640 "max_decompressed_embedded_source_size",
641 &self.max_decompressed_embedded_source_size,
642 )
643 .finish()
644 }
645}
646
647impl<'data> SourceBundle<'data> {
648 pub fn test(bytes: &[u8]) -> bool {
650 bytes.starts_with(&BUNDLE_MAGIC)
651 }
652
653 pub fn parse(data: &'data [u8]) -> Result<SourceBundle<'data>, SourceBundleError> {
655 Self::parse_with_opts(data, Default::default())
656 }
657
658 pub fn parse_with_opts(
660 data: &'data [u8],
661 opts: ParseObjectOptions,
662 ) -> Result<SourceBundle<'data>, SourceBundleError> {
663 let mut archive = zip::read::ZipArchive::new(std::io::Cursor::new(data))
664 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
665
666 let index = Arc::new(SourceBundleIndex::parse(&mut archive)?);
667
668 Ok(SourceBundle {
669 archive,
670 data,
671 index,
672 max_decompressed_embedded_source_size: opts.max_decompressed_embedded_source_size,
673 })
674 }
675
676 pub fn version(&self) -> SourceBundleVersion {
678 SourceBundleVersion(BUNDLE_VERSION)
679 }
680
681 pub fn file_format(&self) -> FileFormat {
683 FileFormat::SourceBundle
684 }
685
686 pub fn code_id(&self) -> Option<CodeId> {
694 self.index
695 .manifest
696 .attributes
697 .get("code_id")
698 .and_then(|x| x.parse().ok())
699 }
700
701 pub fn debug_id(&self) -> DebugId {
709 self.index
710 .manifest
711 .attributes
712 .get("debug_id")
713 .and_then(|x| x.parse().ok())
714 .unwrap_or_default()
715 }
716
717 pub fn name(&self) -> Option<&str> {
725 self.index
726 .manifest
727 .attributes
728 .get("object_name")
729 .map(|x| x.as_str())
730 }
731
732 pub fn arch(&self) -> Arch {
740 self.index
741 .manifest
742 .attributes
743 .get("arch")
744 .and_then(|s| s.parse().ok())
745 .unwrap_or_default()
746 }
747
748 fn kind(&self) -> ObjectKind {
752 ObjectKind::Sources
753 }
754
755 pub fn load_address(&self) -> u64 {
759 0
760 }
761
762 pub fn has_symbols(&self) -> bool {
766 false
767 }
768
769 pub fn symbols(&self) -> SourceBundleSymbolIterator<'data> {
771 std::iter::empty()
772 }
773
774 pub fn symbol_map(&self) -> SymbolMap<'data> {
776 self.symbols().collect()
777 }
778
779 pub fn has_debug_info(&self) -> bool {
783 false
784 }
785
786 pub fn debug_session(&self) -> Result<SourceBundleDebugSession<'data>, SourceBundleError> {
792 let archive = Mutex::new(self.archive.clone());
797 let source_links = SourceLinkMappings::new(
798 self.index
799 .manifest
800 .source_links
801 .iter()
802 .map(|(k, v)| (&k[..], &v[..])),
803 );
804 Ok(SourceBundleDebugSession {
805 index: Arc::clone(&self.index),
806 archive,
807 source_links,
808 max_decompressed_embedded_source_size: self.max_decompressed_embedded_source_size,
809 })
810 }
811
812 pub fn has_unwind_info(&self) -> bool {
814 false
815 }
816
817 pub fn has_sources(&self) -> bool {
819 true
820 }
821
822 pub fn is_malformed(&self) -> bool {
824 false
825 }
826
827 pub fn data(&self) -> &'data [u8] {
829 self.data
830 }
831
832 pub fn is_empty(&self) -> bool {
834 self.index.manifest.files.is_empty()
835 }
836}
837
838impl<'slf, 'data: 'slf> AsSelf<'slf> for SourceBundle<'data> {
839 type Ref = SourceBundle<'slf>;
840
841 fn as_self(&'slf self) -> &'slf Self::Ref {
842 unsafe { std::mem::transmute(self) }
843 }
844}
845
846impl<'data> Parse<'data> for SourceBundle<'data> {
847 type Error = SourceBundleError;
848
849 fn parse_with_opts(data: &'data [u8], opts: ParseObjectOptions) -> Result<Self, Self::Error> {
850 Self::parse_with_opts(data, opts)
851 }
852
853 fn test(data: &'data [u8]) -> bool {
854 SourceBundle::test(data)
855 }
856}
857
858impl<'data: 'object, 'object> ObjectLike<'data, 'object> for SourceBundle<'data> {
859 type Error = SourceBundleError;
860 type Session = SourceBundleDebugSession<'data>;
861 type SymbolIterator = SourceBundleSymbolIterator<'data>;
862
863 fn file_format(&self) -> FileFormat {
864 self.file_format()
865 }
866
867 fn code_id(&self) -> Option<CodeId> {
868 self.code_id()
869 }
870
871 fn debug_id(&self) -> DebugId {
872 self.debug_id()
873 }
874
875 fn arch(&self) -> Arch {
876 self.arch()
877 }
878
879 fn kind(&self) -> ObjectKind {
880 self.kind()
881 }
882
883 fn load_address(&self) -> u64 {
884 self.load_address()
885 }
886
887 fn has_symbols(&self) -> bool {
888 self.has_symbols()
889 }
890
891 fn symbol_map(&self) -> SymbolMap<'data> {
892 self.symbol_map()
893 }
894
895 fn symbols(&self) -> Self::SymbolIterator {
896 self.symbols()
897 }
898
899 fn has_debug_info(&self) -> bool {
900 self.has_debug_info()
901 }
902
903 fn debug_session(&self) -> Result<Self::Session, Self::Error> {
904 self.debug_session()
905 }
906
907 fn has_unwind_info(&self) -> bool {
908 self.has_unwind_info()
909 }
910
911 fn has_sources(&self) -> bool {
912 self.has_sources()
913 }
914
915 fn is_malformed(&self) -> bool {
916 self.is_malformed()
917 }
918}
919
920pub type SourceBundleSymbolIterator<'data> = std::iter::Empty<Symbol<'data>>;
922
923#[derive(Debug, Hash, PartialEq, Eq)]
924enum FileKey<'a> {
925 Path(Cow<'a, str>),
926 Url(Cow<'a, str>),
927 DebugId(DebugId, SourceFileType),
928}
929
930pub struct SourceBundleDebugSession<'data> {
932 archive: Mutex<zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>>,
933 index: Arc<SourceBundleIndex<'data>>,
934 source_links: SourceLinkMappings,
935 max_decompressed_embedded_source_size: Option<usize>,
936}
937
938impl SourceBundleDebugSession<'_> {
939 pub fn files(&self) -> SourceBundleFileIterator<'_> {
941 SourceBundleFileIterator {
942 files: self.index.manifest.files.values(),
943 }
944 }
945
946 pub fn functions(&self) -> SourceBundleFunctionIterator<'_> {
948 std::iter::empty()
949 }
950
951 fn source_by_zip_path(
953 &self,
954 zip_path: &str,
955 max_size: Option<usize>,
956 ) -> Result<String, SourceBundleError> {
957 let mut archive = self.archive.lock();
958 let file = archive
959 .by_name(zip_path)
960 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
961
962 let size = file.size();
963 if max_size.is_some_and(|max| size as usize > max) {
964 return Err(SourceBundleErrorKind::SourceFileSizeExceeded.into());
965 }
966
967 let mut source_content = String::new();
968
969 file.take(size + 1)
970 .read_to_string(&mut source_content)
971 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
972
973 if source_content.len() != size as usize {
974 return Err(SourceBundleErrorKind::BadZip.into());
975 }
976
977 Ok(source_content)
978 }
979
980 fn get_source_file_descriptor(
985 &self,
986 key: FileKey,
987 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
988 if let Some(zip_path) = self.index.indexed_files.get(&key) {
989 let zip_path = zip_path.as_str();
990 let content = Cow::Owned(
991 self.source_by_zip_path(zip_path, self.max_decompressed_embedded_source_size)?,
992 );
993 let info = self.index.manifest.files.get(zip_path);
994 let descriptor = SourceFileDescriptor::new_embedded(content, info);
995 return Ok(Some(descriptor));
996 }
997
998 let FileKey::Path(path) = key else {
999 return Ok(None);
1000 };
1001
1002 Ok(self
1003 .source_links
1004 .resolve(&path)
1005 .map(|s| SourceFileDescriptor::new_remote(s.into())))
1006 }
1007
1008 pub fn source_by_path(
1010 &self,
1011 path: &str,
1012 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
1013 self.get_source_file_descriptor(FileKey::Path(normalize_path(path).into()))
1014 }
1015
1016 pub fn source_by_url(
1018 &self,
1019 url: &str,
1020 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
1021 self.get_source_file_descriptor(FileKey::Url(url.into()))
1022 }
1023
1024 pub fn source_by_debug_id(
1038 &self,
1039 debug_id: DebugId,
1040 ty: SourceFileType,
1041 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
1042 self.get_source_file_descriptor(FileKey::DebugId(debug_id, ty))
1043 }
1044}
1045
1046impl<'session> DebugSession<'session> for SourceBundleDebugSession<'_> {
1047 type Error = SourceBundleError;
1048 type FunctionIterator = SourceBundleFunctionIterator<'session>;
1049 type FileIterator = SourceBundleFileIterator<'session>;
1050
1051 fn functions(&'session self) -> Self::FunctionIterator {
1052 self.functions()
1053 }
1054
1055 fn files(&'session self) -> Self::FileIterator {
1056 self.files()
1057 }
1058
1059 fn source_by_path(&self, path: &str) -> Result<Option<SourceFileDescriptor<'_>>, Self::Error> {
1060 self.source_by_path(path)
1061 }
1062}
1063
1064impl<'slf, 'data: 'slf> AsSelf<'slf> for SourceBundleDebugSession<'data> {
1065 type Ref = SourceBundleDebugSession<'slf>;
1066
1067 fn as_self(&'slf self) -> &'slf Self::Ref {
1068 unsafe { std::mem::transmute(self) }
1069 }
1070}
1071
1072pub struct SourceBundleFileIterator<'s> {
1074 files: std::collections::btree_map::Values<'s, String, SourceFileInfo>,
1075}
1076
1077impl<'s> Iterator for SourceBundleFileIterator<'s> {
1078 type Item = Result<FileEntry<'s>, SourceBundleError>;
1079
1080 fn next(&mut self) -> Option<Self::Item> {
1081 let source_file = self.files.next()?;
1082 Some(Ok(FileEntry::new(
1083 Cow::default(),
1084 FileInfo::from_path(source_file.path.as_bytes()),
1085 )))
1086 }
1087}
1088
1089pub type SourceBundleFunctionIterator<'s> =
1091 std::iter::Empty<Result<Function<'s>, SourceBundleError>>;
1092
1093impl SourceBundleManifest {
1094 pub fn new() -> Self {
1096 Self::default()
1097 }
1098}
1099
1100fn sanitize_bundle_path(path: &str) -> String {
1105 let mut sanitized = SANE_PATH_RE.replace_all(path, "/").into_owned();
1106 if sanitized.starts_with('/') {
1107 sanitized.remove(0);
1108 }
1109 sanitized
1110}
1111
1112fn normalize_path(path: &str) -> String {
1114 path.replace('\\', "/")
1115}
1116
1117#[derive(Debug)]
1119pub struct SkippedFileInfo<'a> {
1120 path: &'a str,
1121 reason: &'a str,
1122}
1123
1124impl<'a> SkippedFileInfo<'a> {
1125 fn new(path: &'a str, reason: &'a str) -> Self {
1126 Self { path, reason }
1127 }
1128
1129 pub fn path(&self) -> &str {
1131 self.path
1132 }
1133
1134 pub fn reason(&self) -> &str {
1136 self.reason
1137 }
1138}
1139
1140impl Display for SkippedFileInfo<'_> {
1141 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1142 write!(f, "Skipped file {} due to: {}", self.path, self.reason)
1143 }
1144}
1145
1146pub struct SourceBundleWriter<W>
1175where
1176 W: Seek + Write,
1177{
1178 manifest: SourceBundleManifest,
1179 writer: ZipWriter<W>,
1180 collect_il2cpp: bool,
1181 skipped_file_callback: Box<dyn FnMut(SkippedFileInfo)>,
1182}
1183
1184fn default_file_options() -> SimpleFileOptions {
1185 SimpleFileOptions::default().last_modified_time(zip::DateTime::default())
1192}
1193
1194impl<W> SourceBundleWriter<W>
1195where
1196 W: Seek + Write,
1197{
1198 pub fn start(mut writer: W) -> Result<Self, SourceBundleError> {
1200 let header = SourceBundleHeader::default();
1201 writer
1202 .write_all(header.as_bytes())
1203 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1204
1205 Ok(SourceBundleWriter {
1206 manifest: SourceBundleManifest::new(),
1207 writer: ZipWriter::new(writer),
1208 collect_il2cpp: false,
1209 skipped_file_callback: Box::new(|_| ()),
1210 })
1211 }
1212
1213 pub fn is_empty(&self) -> bool {
1215 self.manifest.files.is_empty()
1216 }
1217
1218 pub fn collect_il2cpp_sources(&mut self, collect_il2cpp: bool) {
1221 self.collect_il2cpp = collect_il2cpp;
1222 }
1223
1224 pub fn set_attribute<K, V>(&mut self, key: K, value: V) -> Option<String>
1233 where
1234 K: Into<String>,
1235 V: Into<String>,
1236 {
1237 self.manifest.attributes.insert(key.into(), value.into())
1238 }
1239
1240 pub fn remove_attribute<K>(&mut self, key: K) -> Option<String>
1244 where
1245 K: AsRef<str>,
1246 {
1247 self.manifest.attributes.remove(key.as_ref())
1248 }
1249
1250 pub fn attribute<K>(&mut self, key: K) -> Option<&str>
1252 where
1253 K: AsRef<str>,
1254 {
1255 self.manifest
1256 .attributes
1257 .get(key.as_ref())
1258 .map(String::as_str)
1259 }
1260
1261 pub fn has_file<S>(&self, path: S) -> bool
1263 where
1264 S: AsRef<str>,
1265 {
1266 let full_path = &self.file_path(path.as_ref());
1267 self.manifest.files.contains_key(full_path)
1268 }
1269
1270 pub fn add_file<S, R>(
1296 &mut self,
1297 path: S,
1298 file: R,
1299 info: SourceFileInfo,
1300 ) -> Result<(), SourceBundleError>
1301 where
1302 S: AsRef<str>,
1303 R: Read,
1304 {
1305 let mut file_reader = Utf8Reader::new(file);
1306
1307 let full_path = self.file_path(path.as_ref());
1308 let unique_path = self.unique_path(full_path);
1309
1310 self.writer
1311 .start_file(unique_path.clone(), default_file_options())
1312 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1313
1314 match io::copy(&mut file_reader, &mut self.writer) {
1315 Err(e) => {
1316 self.writer
1317 .abort_file()
1318 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1319
1320 let error_kind = match e.kind() {
1322 ErrorKind::InvalidData => SourceBundleErrorKind::ReadFailed,
1323 _ => SourceBundleErrorKind::WriteFailed,
1324 };
1325
1326 Err(SourceBundleError::new(error_kind, e))
1327 }
1328 Ok(_) => {
1329 self.manifest.files.insert(unique_path, info);
1330 Ok(())
1331 }
1332 }
1333 }
1334
1335 fn add_file_skip_read_failed<S, R>(
1337 &mut self,
1338 path: S,
1339 file: R,
1340 info: SourceFileInfo,
1341 ) -> Result<(), SourceBundleError>
1342 where
1343 S: AsRef<str>,
1344 R: Read,
1345 {
1346 let result = self.add_file(&path, file, info);
1347
1348 if let Err(e) = &result {
1349 if e.kind == SourceBundleErrorKind::ReadFailed {
1350 let reason = e.to_string();
1351 let skipped_info = SkippedFileInfo::new(path.as_ref(), &reason);
1352 (self.skipped_file_callback)(skipped_info);
1353
1354 return Ok(());
1355 }
1356 }
1357
1358 result
1359 }
1360
1361 pub fn with_skipped_file_callback(
1364 mut self,
1365 callback: impl FnMut(SkippedFileInfo) + 'static,
1366 ) -> Self {
1367 self.skipped_file_callback = Box::new(callback);
1368 self
1369 }
1370
1371 pub fn write_object<'data, 'object, O, E>(
1378 self,
1379 object: &'object O,
1380 object_name: &str,
1381 ) -> Result<bool, SourceBundleError>
1382 where
1383 O: ObjectLike<'data, 'object, Error = E>,
1384 E: std::error::Error + Send + Sync + 'static,
1385 {
1386 self.write_object_with_filter(object, object_name, |_, _| true)
1387 }
1388
1389 pub fn write_object_with_filter<'data, 'object, O, E, F>(
1398 self,
1399 object: &'object O,
1400 object_name: &str,
1401 filter: F,
1402 ) -> Result<bool, SourceBundleError>
1403 where
1404 O: ObjectLike<'data, 'object, Error = E>,
1405 E: std::error::Error + Send + Sync + 'static,
1406 F: FnMut(&FileEntry, &Option<SourceFileDescriptor<'_>>) -> bool,
1407 {
1408 self.write_object_with_filter_and_provider(object, object_name, filter, |path| {
1410 File::open(path).map(BufReader::new).ok()
1411 })
1412 .map(|w| w.0)
1413 }
1414
1415 pub fn write_object_with_filter_and_provider<'data, 'object, O, E, F, R, P>(
1428 mut self,
1429 object: &'object O,
1430 object_name: &str,
1431 mut filter: F,
1432 mut provider: P,
1433 ) -> Result<(bool, W), SourceBundleError>
1434 where
1435 O: ObjectLike<'data, 'object, Error = E>,
1436 E: std::error::Error + Send + Sync + 'static,
1437 F: FnMut(&FileEntry, &Option<SourceFileDescriptor<'_>>) -> bool,
1438 R: Read,
1439 P: FnMut(&str) -> Option<R>,
1440 {
1441 let mut files_handled = BTreeSet::new();
1442 let mut referenced_files = BTreeSet::new();
1443
1444 let session = object
1445 .debug_session()
1446 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1447
1448 self.set_attribute("arch", object.arch().to_string());
1449 self.set_attribute("debug_id", object.debug_id().to_string());
1450 self.set_attribute("object_name", object_name);
1451 if let Some(code_id) = object.code_id() {
1452 self.set_attribute("code_id", code_id.to_string());
1453 }
1454
1455 for file_result in session.files() {
1456 let file = file_result
1457 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1458 let filename = file.abs_path_str();
1459
1460 if files_handled.contains(&filename) {
1461 continue;
1462 }
1463
1464 let source = if filename.starts_with('<') && filename.ends_with('>') {
1467 None
1468 } else {
1469 let source_from_object = session
1470 .source_by_path(&filename)
1471 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1472 if filter(&file, &source_from_object) {
1473 provider(&filename)
1474 } else {
1475 None
1476 }
1477 };
1478
1479 if let Some(mut source) = source {
1480 let bundle_path = sanitize_bundle_path(&filename);
1481 let mut info = SourceFileInfo::new();
1482 info.set_ty(SourceFileType::Source);
1483 info.set_path(filename.clone());
1484
1485 if self.collect_il2cpp {
1486 let mut buf = Vec::new();
1488 if source.read_to_end(&mut buf).is_ok() {
1489 collect_il2cpp_sources(&buf, &mut referenced_files);
1490 self.add_file_skip_read_failed(bundle_path, buf.as_slice(), info)?;
1491 }
1492 } else {
1493 self.add_file_skip_read_failed(bundle_path, source, info)?;
1495 }
1496 }
1497
1498 files_handled.insert(filename);
1499 }
1500
1501 for filename in referenced_files {
1502 if files_handled.contains(&filename) {
1503 continue;
1504 }
1505
1506 if let Some(source) = provider(&filename) {
1507 let bundle_path = sanitize_bundle_path(&filename);
1508 let mut info = SourceFileInfo::new();
1509 info.set_ty(SourceFileType::Source);
1510 info.set_path(filename.clone());
1511
1512 self.add_file_skip_read_failed(bundle_path, source, info)?;
1513 }
1514 }
1515
1516 let is_empty = self.is_empty();
1517 let writer = self.do_finish()?;
1518
1519 Ok((!is_empty, writer))
1520 }
1521
1522 pub fn finish(self) -> Result<(), SourceBundleError> {
1524 self.do_finish().map(drop)
1525 }
1526
1527 fn do_finish(mut self) -> Result<W, SourceBundleError> {
1529 self.write_manifest()?;
1530 let writer = self
1531 .writer
1532 .finish()
1533 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1534 Ok(writer)
1535 }
1536
1537 fn file_path(&self, path: &str) -> String {
1539 format!("{FILES_PATH}/{path}")
1540 }
1541
1542 fn unique_path(&self, mut path: String) -> String {
1547 let mut duplicates = 0;
1548
1549 while self.manifest.files.contains_key(&path) {
1550 duplicates += 1;
1551 match duplicates {
1552 1 => path.push_str(".1"),
1553 _ => {
1554 use std::fmt::Write;
1555 trim_end_matches(&mut path, char::is_numeric);
1556 write!(path, ".{duplicates}").unwrap();
1557 }
1558 }
1559 }
1560
1561 path
1562 }
1563
1564 fn write_manifest(&mut self) -> Result<(), SourceBundleError> {
1566 self.writer
1567 .start_file(MANIFEST_PATH, default_file_options())
1568 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1569
1570 serde_json::to_writer(&mut self.writer, &self.manifest)
1571 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
1572
1573 Ok(())
1574 }
1575}
1576
1577fn collect_il2cpp_sources(source: &[u8], referenced_files: &mut BTreeSet<String>) {
1581 if let Ok(source) = std::str::from_utf8(source) {
1582 for line in source.lines() {
1583 let line = line.trim();
1584
1585 if let Some(source_ref) = line.strip_prefix("//<source_info:") {
1586 if let Some((file, _line)) = source_ref.rsplit_once(':') {
1587 if !referenced_files.contains(file) {
1588 referenced_files.insert(file.to_string());
1589 }
1590 }
1591 }
1592 }
1593 }
1594}
1595
1596impl SourceBundleWriter<BufWriter<File>> {
1597 pub fn create<P>(path: P) -> Result<SourceBundleWriter<BufWriter<File>>, SourceBundleError>
1602 where
1603 P: AsRef<Path>,
1604 {
1605 let file = OpenOptions::new()
1606 .read(true)
1607 .write(true)
1608 .create(true)
1609 .truncate(true)
1610 .open(path)
1611 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1612
1613 Self::start(BufWriter::new(file))
1614 }
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use crate::Object;
1620
1621 use super::*;
1622
1623 use std::{collections::HashSet, io::Cursor};
1624
1625 use similar_asserts::assert_eq;
1626 use tempfile::NamedTempFile;
1627
1628 #[test]
1629 fn test_has_file() -> Result<(), SourceBundleError> {
1630 let writer = Cursor::new(Vec::new());
1631 let mut bundle = SourceBundleWriter::start(writer)?;
1632
1633 bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
1634 assert!(bundle.has_file("bar.txt"));
1635
1636 bundle.finish()?;
1637 Ok(())
1638 }
1639
1640 #[test]
1641 fn test_non_utf8() -> Result<(), SourceBundleError> {
1642 let writer = Cursor::new(Vec::new());
1643 let mut bundle = SourceBundleWriter::start(writer)?;
1644
1645 assert!(bundle
1646 .add_file(
1647 "bar.txt",
1648 &[0, 159, 146, 150][..],
1649 SourceFileInfo::default()
1650 )
1651 .is_err());
1652
1653 Ok(())
1654 }
1655
1656 #[test]
1657 fn test_duplicate_files() -> Result<(), SourceBundleError> {
1658 let writer = Cursor::new(Vec::new());
1659 let mut bundle = SourceBundleWriter::start(writer)?;
1660
1661 bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
1662 bundle.add_file("bar.txt", &b"othercontents"[..], SourceFileInfo::default())?;
1663 assert!(bundle.has_file("bar.txt"));
1664 assert!(bundle.has_file("bar.txt.1"));
1665
1666 bundle.finish()?;
1667 Ok(())
1668 }
1669
1670 #[test]
1671 fn debugsession_is_sendsync() {
1672 fn is_sendsync<T: Send + Sync>() {}
1673 is_sendsync::<SourceBundleDebugSession>();
1674 }
1675
1676 #[test]
1677 fn test_normalize_paths() -> Result<(), SourceBundleError> {
1678 let mut writer = Cursor::new(Vec::new());
1679 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1680
1681 for filename in &[
1682 "C:\\users\\martin\\mydebugfile.cs",
1683 "/usr/martin/mydebugfile.h",
1684 ] {
1685 let mut info = SourceFileInfo::new();
1686 info.set_ty(SourceFileType::Source);
1687 info.set_path(filename.to_string());
1688 bundle.add_file_skip_read_failed(
1689 sanitize_bundle_path(filename),
1690 &b"somerandomdata"[..],
1691 info,
1692 )?;
1693 }
1694
1695 bundle.finish()?;
1696 let bundle_bytes = writer.into_inner();
1697 let bundle = SourceBundle::parse(&bundle_bytes)?;
1698
1699 let session = bundle.debug_session().unwrap();
1700
1701 assert!(session
1702 .source_by_path("C:\\users\\martin\\mydebugfile.cs")?
1703 .is_some());
1704 assert!(session
1705 .source_by_path("C:/users/martin/mydebugfile.cs")?
1706 .is_some());
1707 assert!(session
1708 .source_by_path("C:\\users\\martin/mydebugfile.cs")?
1709 .is_some());
1710 assert!(session
1711 .source_by_path("/usr/martin/mydebugfile.h")?
1712 .is_some());
1713 assert!(session
1714 .source_by_path("\\usr\\martin\\mydebugfile.h")?
1715 .is_some());
1716
1717 Ok(())
1718 }
1719
1720 #[test]
1721 fn test_source_descriptor() -> Result<(), SourceBundleError> {
1722 let mut writer = Cursor::new(Vec::new());
1723 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1724
1725 let mut info = SourceFileInfo::default();
1726 info.set_url("https://example.com/bar.js.min".into());
1727 info.set_path("/files/bar.js.min".into());
1728 info.set_ty(SourceFileType::MinifiedSource);
1729 info.add_header(
1730 "debug-id".into(),
1731 "5e618b9f-54a9-4389-b196-519819dd7c47".into(),
1732 );
1733 info.add_header("sourcemap".into(), "bar.js.map".into());
1734 bundle.add_file("bar.js", &b"filecontents"[..], info)?;
1735 assert!(bundle.has_file("bar.js"));
1736
1737 bundle.finish()?;
1738 let bundle_bytes = writer.into_inner();
1739 let bundle = SourceBundle::parse(&bundle_bytes)?;
1740
1741 let sess = bundle.debug_session().unwrap();
1742 let f = sess
1743 .source_by_debug_id(
1744 "5e618b9f-54a9-4389-b196-519819dd7c47".parse().unwrap(),
1745 SourceFileType::MinifiedSource,
1746 )
1747 .unwrap()
1748 .expect("should exist");
1749 assert_eq!(f.contents(), Some("filecontents"));
1750 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1751 assert_eq!(f.url(), Some("https://example.com/bar.js.min"));
1752 assert_eq!(f.path(), Some("/files/bar.js.min"));
1753 assert_eq!(f.source_mapping_url(), Some("bar.js.map"));
1754
1755 assert!(sess
1756 .source_by_debug_id(
1757 "5e618b9f-54a9-4389-b196-519819dd7c47".parse().unwrap(),
1758 SourceFileType::Source
1759 )
1760 .unwrap()
1761 .is_none());
1762
1763 Ok(())
1764 }
1765
1766 #[test]
1767 fn test_source_mapping_url() -> Result<(), SourceBundleError> {
1768 let mut writer = Cursor::new(Vec::new());
1769 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1770
1771 let mut info = SourceFileInfo::default();
1772 info.set_url("https://example.com/bar.min.js".into());
1773 info.set_ty(SourceFileType::MinifiedSource);
1774 bundle.add_file(
1775 "bar.js",
1776 &b"filecontents\n//# sourceMappingURL=bar.js.map"[..],
1777 info,
1778 )?;
1779
1780 bundle.finish()?;
1781 let bundle_bytes = writer.into_inner();
1782 let bundle = SourceBundle::parse(&bundle_bytes)?;
1783
1784 let sess = bundle.debug_session().unwrap();
1785 let f = sess
1786 .source_by_url("https://example.com/bar.min.js")
1787 .unwrap()
1788 .expect("should exist");
1789 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1790 assert_eq!(f.url(), Some("https://example.com/bar.min.js"));
1791 assert_eq!(f.source_mapping_url(), Some("bar.js.map"));
1792
1793 Ok(())
1794 }
1795
1796 #[test]
1797 fn test_source_embedded_debug_id() -> Result<(), SourceBundleError> {
1798 let mut writer = Cursor::new(Vec::new());
1799 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1800
1801 let mut info = SourceFileInfo::default();
1802 info.set_url("https://example.com/bar.min.js".into());
1803 info.set_ty(SourceFileType::MinifiedSource);
1804 bundle.add_file(
1805 "bar.js",
1806 &b"filecontents\n//# debugId=5b65abfb23384f0bb3b964c8f734d43f"[..],
1807 info,
1808 )?;
1809
1810 bundle.finish()?;
1811 let bundle_bytes = writer.into_inner();
1812 let bundle = SourceBundle::parse(&bundle_bytes)?;
1813
1814 let sess = bundle.debug_session().unwrap();
1815 let f = sess
1816 .source_by_url("https://example.com/bar.min.js")
1817 .unwrap()
1818 .expect("should exist");
1819 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1820 assert_eq!(
1821 f.debug_id(),
1822 Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
1823 );
1824
1825 Ok(())
1826 }
1827
1828 #[test]
1829 fn test_sourcemap_embedded_debug_id() -> Result<(), SourceBundleError> {
1830 let mut writer = Cursor::new(Vec::new());
1831 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1832
1833 let mut info = SourceFileInfo::default();
1834 info.set_url("https://example.com/bar.js.map".into());
1835 info.set_ty(SourceFileType::SourceMap);
1836 bundle.add_file(
1837 "bar.js.map",
1838 &br#"{"debug_id": "5b65abfb-2338-4f0b-b3b9-64c8f734d43f"}"#[..],
1839 info,
1840 )?;
1841
1842 bundle.finish()?;
1843 let bundle_bytes = writer.into_inner();
1844 let bundle = SourceBundle::parse(&bundle_bytes)?;
1845
1846 let sess = bundle.debug_session().unwrap();
1847 let f = sess
1848 .source_by_url("https://example.com/bar.js.map")
1849 .unwrap()
1850 .expect("should exist");
1851 assert_eq!(f.ty(), SourceFileType::SourceMap);
1852 assert_eq!(
1853 f.debug_id(),
1854 Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
1855 );
1856
1857 Ok(())
1858 }
1859
1860 #[test]
1861 fn test_il2cpp_reference() -> Result<(), Box<dyn std::error::Error>> {
1862 let mut cpp_file = NamedTempFile::new()?;
1863 let mut cs_file = NamedTempFile::new()?;
1864
1865 let cpp_contents = format!("foo\n//<source_info:{}:111>\nbar", cs_file.path().display());
1866
1867 let object_buf = {
1869 let mut writer = Cursor::new(Vec::new());
1870 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1871
1872 let path = cpp_file.path().to_string_lossy();
1873 let mut info = SourceFileInfo::new();
1874 info.set_ty(SourceFileType::Source);
1875 info.set_path(path.to_string());
1876 bundle.add_file(path, cpp_contents.as_bytes(), info)?;
1877
1878 bundle.finish()?;
1879 writer.into_inner()
1880 };
1881 let object = SourceBundle::parse(&object_buf)?;
1882
1883 cpp_file.write_all(cpp_contents.as_bytes())?;
1885 cs_file.write_all(b"some C# source")?;
1886
1887 let mut output_buf = Cursor::new(Vec::new());
1889 let mut writer = SourceBundleWriter::start(&mut output_buf)?;
1890 writer.collect_il2cpp_sources(true);
1891
1892 let written = writer.write_object(&object, "whatever")?;
1893 assert!(written);
1894 let output_buf = output_buf.into_inner();
1895
1896 let source_bundle = SourceBundle::parse(&output_buf)?;
1898 let session = source_bundle.debug_session()?;
1899 let actual_files: BTreeMap<_, _> = session
1900 .files()
1901 .flatten()
1902 .flat_map(|f| {
1903 let path = f.abs_path_str();
1904 session
1905 .source_by_path(&path)
1906 .ok()
1907 .flatten()
1908 .map(|source| (path, source.contents().unwrap().to_string()))
1909 })
1910 .collect();
1911
1912 let mut expected_files = BTreeMap::new();
1913 expected_files.insert(cpp_file.path().to_string_lossy().into_owned(), cpp_contents);
1914 expected_files.insert(
1915 cs_file.path().to_string_lossy().into_owned(),
1916 String::from("some C# source"),
1917 );
1918
1919 assert_eq!(actual_files, expected_files);
1920
1921 Ok(())
1922 }
1923
1924 #[test]
1925 fn test_bundle_paths() {
1926 assert_eq!(sanitize_bundle_path("foo"), "foo");
1927 assert_eq!(sanitize_bundle_path("foo/bar"), "foo/bar");
1928 assert_eq!(sanitize_bundle_path("/foo/bar"), "foo/bar");
1929 assert_eq!(sanitize_bundle_path("C:/foo/bar"), "C/foo/bar");
1930 assert_eq!(sanitize_bundle_path("\\foo\\bar"), "foo/bar");
1931 assert_eq!(sanitize_bundle_path("\\\\UNC\\foo\\bar"), "UNC/foo/bar");
1932 }
1933
1934 #[test]
1935 fn test_source_links() -> Result<(), SourceBundleError> {
1936 let mut writer = Cursor::new(Vec::new());
1937 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1938
1939 let mut info = SourceFileInfo::default();
1940 info.set_url("https://example.com/bar/index.min.js".into());
1941 info.set_path("/files/bar/index.min.js".into());
1942 info.set_ty(SourceFileType::MinifiedSource);
1943 bundle.add_file("bar/index.js", &b"filecontents"[..], info)?;
1944 assert!(bundle.has_file("bar/index.js"));
1945
1946 bundle
1947 .manifest
1948 .source_links
1949 .insert("/files/bar/*".to_string(), "https://nope.com/*".into());
1950 bundle
1951 .manifest
1952 .source_links
1953 .insert("/files/foo/*".to_string(), "https://example.com/*".into());
1954
1955 bundle.finish()?;
1956 let bundle_bytes = writer.into_inner();
1957 let bundle = SourceBundle::parse(&bundle_bytes)?;
1958
1959 let sess = bundle.debug_session().unwrap();
1960
1961 let foo = sess
1963 .source_by_path("/files/foo/index.min.js")
1964 .unwrap()
1965 .expect("should exist");
1966 assert_eq!(foo.contents(), None);
1967 assert_eq!(foo.ty(), SourceFileType::Source);
1968 assert_eq!(foo.url(), Some("https://example.com/index.min.js"));
1969 assert_eq!(foo.path(), None);
1970
1971 let bar = sess
1973 .source_by_path("/files/bar/index.min.js")
1974 .unwrap()
1975 .expect("should exist");
1976 assert_eq!(bar.contents(), Some("filecontents"));
1977 assert_eq!(bar.ty(), SourceFileType::MinifiedSource);
1978 assert_eq!(bar.url(), Some("https://example.com/bar/index.min.js"));
1979 assert_eq!(bar.path(), Some("/files/bar/index.min.js"));
1980
1981 Ok(())
1982 }
1983
1984 #[test]
1985 fn test_write_object_with_source_provider() {
1986 let view = std::fs::read(symbolic_testutils::fixture("linux/crash.debug")).unwrap();
1987 let object = Object::parse(&view).unwrap();
1988
1989 let referenced = {
1990 let session = object.debug_session().unwrap();
1991 session
1992 .files()
1993 .map(|file| file.unwrap().abs_path_str())
1994 .filter(|path| !(path.starts_with('<') && path.ends_with('>')))
1995 .collect::<HashSet<_>>()
1996 };
1997
1998 let (written, writer) = SourceBundleWriter::start(Cursor::new(Vec::new()))
1999 .unwrap()
2000 .write_object_with_filter_and_provider(
2001 &object,
2002 "crash.debug",
2003 |_, _| true,
2004 |path| {
2005 assert!(referenced.contains(path));
2006 Some(Cursor::new(
2007 format!("// synthetic source for {path}\n").into_bytes(),
2008 ))
2009 },
2010 )
2011 .unwrap();
2012 assert!(written);
2013
2014 let data = writer.into_inner();
2015
2016 let bundle = Object::parse(&data).unwrap();
2017 assert_eq!(bundle.debug_id(), object.debug_id());
2018 assert!(bundle.has_sources());
2019
2020 let session = bundle.debug_session().unwrap();
2021
2022 for path in &referenced {
2024 let descriptor = session.source_by_path(path).unwrap().unwrap();
2025 assert_eq!(
2026 descriptor.contents(),
2027 Some(format!("// synthetic source for {path}\n").as_str())
2028 );
2029 }
2030
2031 for path in session.files() {
2033 let path = path.unwrap().abs_path_str();
2034 assert!(
2035 referenced.contains(&path),
2036 "expected {path} to be in object referenced files"
2037 );
2038 }
2039 }
2040
2041 #[test]
2042 fn test_write_object_with_provider_no_sources() {
2043 let view = std::fs::read(symbolic_testutils::fixture("linux/crash.debug")).unwrap();
2044 let object = Object::parse(&view).unwrap();
2045
2046 let writer = SourceBundleWriter::start(Cursor::new(Vec::new())).unwrap();
2047 let (written, _) = writer
2048 .write_object_with_filter_and_provider(
2049 &object,
2050 "crash.debug",
2051 |_, _| true,
2052 |_| None::<&[u8]>,
2053 )
2054 .unwrap();
2055
2056 assert!(!written);
2057 }
2058
2059 #[test]
2060 fn test_write_object_with_all_filtered() {
2061 let view = std::fs::read(symbolic_testutils::fixture("linux/crash.debug")).unwrap();
2062 let object = Object::parse(&view).unwrap();
2063
2064 let writer = SourceBundleWriter::start(Cursor::new(Vec::new())).unwrap();
2065 let (written, _) = writer
2066 .write_object_with_filter_and_provider(
2067 &object,
2068 "crash.debug",
2069 |_, _| false,
2070 |_| Some([0, 1, 2].as_slice()),
2071 )
2072 .unwrap();
2073
2074 assert!(!written);
2075 }
2076
2077 #[test]
2078 fn test_size_limit() {
2079 let mut buffer = Cursor::new(Vec::new());
2080 let mut bundle = SourceBundleWriter::start(&mut buffer).unwrap();
2081
2082 bundle
2083 .add_file(
2084 "bar.txt",
2085 &b"this is 26 characters long"[..],
2086 SourceFileInfo {
2087 path: "bar.txt".to_owned(),
2088 ..Default::default()
2089 },
2090 )
2091 .unwrap();
2092 bundle.finish().unwrap();
2093
2094 let buffer = buffer.into_inner();
2095
2096 let opts = ParseObjectOptions {
2097 max_decompressed_embedded_source_size: Some(20),
2098 ..Default::default()
2099 };
2100
2101 let bundle = SourceBundle::parse_with_opts(&buffer, opts).unwrap();
2102 let session = bundle.debug_session().unwrap();
2103 let err = session.source_by_path("bar.txt").unwrap_err();
2104
2105 assert!(matches!(
2106 err.kind(),
2107 SourceBundleErrorKind::SourceFileSizeExceeded
2108 ));
2109 }
2110}