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
106impl fmt::Display for SourceBundleErrorKind {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Self::BadZip => write!(f, "malformed zip archive"),
110 Self::BadManifest => write!(f, "failed to read/write source bundle manifest"),
111 Self::BadDebugFile => write!(f, "malformed debug info file"),
112 Self::WriteFailed => write!(f, "failed to write source bundle"),
113 Self::ReadFailed => write!(f, "file could not be read as UTF-8"),
114 }
115 }
116}
117
118#[derive(Debug, Error)]
120#[error("{kind}")]
121pub struct SourceBundleError {
122 kind: SourceBundleErrorKind,
123 #[source]
124 source: Option<Box<dyn Error + Send + Sync + 'static>>,
125}
126
127impl SourceBundleError {
128 pub fn new<E>(kind: SourceBundleErrorKind, source: E) -> Self
135 where
136 E: Into<Box<dyn Error + Send + Sync>>,
137 {
138 let source = Some(source.into());
139 Self { kind, source }
140 }
141
142 pub fn kind(&self) -> SourceBundleErrorKind {
144 self.kind
145 }
146}
147
148impl From<SourceBundleErrorKind> for SourceBundleError {
149 fn from(kind: SourceBundleErrorKind) -> Self {
150 Self { kind, source: None }
151 }
152}
153
154fn trim_end_matches<F>(string: &mut String, pat: F)
156where
157 F: FnMut(char) -> bool,
158{
159 let cutoff = string.trim_end_matches(pat).len();
160 string.truncate(cutoff);
161}
162
163#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
165#[serde(rename_all = "snake_case")]
166pub enum SourceFileType {
167 Source,
169
170 MinifiedSource,
172
173 SourceMap,
175
176 IndexedRamBundle,
178}
179
180impl SourceFileType {
181 pub fn name(self) -> &'static str {
183 match self {
184 SourceFileType::Source => "source",
185 SourceFileType::MinifiedSource => "minified_source",
186 SourceFileType::SourceMap => "source_map",
187 SourceFileType::IndexedRamBundle => "indexed_ram_bundle",
188 }
189 }
190}
191
192impl fmt::Display for SourceFileType {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.write_str(self.name())
195 }
196}
197
198#[derive(Clone, Debug, Default, Serialize, Deserialize)]
200pub struct SourceFileInfo {
201 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
202 ty: Option<SourceFileType>,
203
204 #[serde(default, skip_serializing_if = "String::is_empty")]
205 path: String,
206
207 #[serde(default, skip_serializing_if = "String::is_empty")]
208 url: String,
209
210 #[serde(
211 default,
212 skip_serializing_if = "BTreeMap::is_empty",
213 deserialize_with = "deserialize_headers"
214 )]
215 headers: BTreeMap<String, String>,
216}
217
218fn deserialize_headers<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
220where
221 D: Deserializer<'de>,
222{
223 let rv: BTreeMap<String, String> = Deserialize::deserialize(deserializer)?;
224 if rv.is_empty()
225 || rv
226 .keys()
227 .all(|x| !x.chars().any(|c| c.is_ascii_uppercase()))
228 {
229 Ok(rv)
230 } else {
231 Ok(rv
232 .into_iter()
233 .map(|(k, v)| (k.to_ascii_lowercase(), v))
234 .collect())
235 }
236}
237
238impl SourceFileInfo {
239 pub fn new() -> Self {
241 Self::default()
242 }
243
244 pub fn ty(&self) -> Option<SourceFileType> {
246 self.ty
247 }
248
249 pub fn set_ty(&mut self, ty: SourceFileType) {
251 self.ty = Some(ty);
252 }
253
254 pub fn path(&self) -> Option<&str> {
256 match self.path.as_str() {
257 "" => None,
258 path => Some(path),
259 }
260 }
261
262 pub fn set_path(&mut self, path: String) {
264 self.path = path;
265 }
266
267 pub fn url(&self) -> Option<&str> {
269 match self.url.as_str() {
270 "" => None,
271 url => Some(url),
272 }
273 }
274
275 pub fn set_url(&mut self, url: String) {
277 self.url = url;
278 }
279
280 pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
282 self.headers.iter().map(|(k, v)| (k.as_str(), v.as_str()))
283 }
284
285 pub fn header(&self, header: &str) -> Option<&str> {
287 if !header.chars().any(|x| x.is_ascii_uppercase()) {
288 self.headers.get(header).map(String::as_str)
289 } else {
290 self.headers.iter().find_map(|(k, v)| {
291 if k.eq_ignore_ascii_case(header) {
292 Some(v.as_str())
293 } else {
294 None
295 }
296 })
297 }
298 }
299
300 pub fn add_header(&mut self, header: String, value: String) {
313 let mut header = header;
314 if header.chars().any(|x| x.is_ascii_uppercase()) {
315 header = header.to_ascii_lowercase();
316 }
317 self.headers.insert(header, value);
318 }
319
320 pub fn debug_id(&self) -> Option<DebugId> {
326 self.header("debug-id").and_then(|x| x.parse().ok())
327 }
328
329 pub fn source_mapping_url(&self) -> Option<&str> {
335 self.header("sourcemap")
336 .or_else(|| self.header("x-sourcemap"))
337 }
338
339 pub fn is_empty(&self) -> bool {
341 self.path.is_empty() && self.ty.is_none() && self.headers.is_empty()
342 }
343}
344
345pub struct SourceFileDescriptor<'a> {
360 contents: Option<Cow<'a, str>>,
361 remote_url: Option<Cow<'a, str>>,
362 file_info: Option<&'a SourceFileInfo>,
363}
364
365impl<'a> SourceFileDescriptor<'a> {
366 pub(crate) fn new_embedded(
368 content: Cow<'a, str>,
369 file_info: Option<&'a SourceFileInfo>,
370 ) -> SourceFileDescriptor<'a> {
371 SourceFileDescriptor {
372 contents: Some(content),
373 remote_url: None,
374 file_info,
375 }
376 }
377
378 pub(crate) fn new_remote(remote_url: Cow<'a, str>) -> SourceFileDescriptor<'a> {
380 SourceFileDescriptor {
381 contents: None,
382 remote_url: Some(remote_url),
383 file_info: None,
384 }
385 }
386
387 pub fn ty(&self) -> SourceFileType {
389 self.file_info
390 .and_then(|x| x.ty())
391 .unwrap_or(SourceFileType::Source)
392 }
393
394 pub fn contents(&self) -> Option<&str> {
401 self.contents.as_deref()
402 }
403
404 pub fn into_contents(self) -> Option<Cow<'a, str>> {
409 self.contents
410 }
411
412 pub fn url(&self) -> Option<&str> {
419 if let Some(ref url) = self.remote_url {
420 Some(url)
421 } else {
422 self.file_info.and_then(|x| x.url())
423 }
424 }
425
426 pub fn path(&self) -> Option<&str> {
431 self.file_info.and_then(|x| x.path())
432 }
433
434 pub fn debug_id(&self) -> Option<DebugId> {
440 self.file_info.and_then(|x| x.debug_id()).or_else(|| {
441 if matches!(
442 self.ty(),
443 SourceFileType::Source | SourceFileType::MinifiedSource
444 ) {
445 self.contents().and_then(discover_debug_id)
446 } else if matches!(self.ty(), SourceFileType::SourceMap) {
447 self.contents()
448 .and_then(discover_sourcemap_embedded_debug_id)
449 } else {
450 None
451 }
452 })
453 }
454
455 pub fn source_mapping_url(&self) -> Option<&str> {
461 self.file_info
462 .and_then(|x| x.source_mapping_url())
463 .or_else(|| {
464 if matches!(
465 self.ty(),
466 SourceFileType::Source | SourceFileType::MinifiedSource
467 ) {
468 self.contents().and_then(discover_sourcemaps_location)
469 } else {
470 None
471 }
472 })
473 }
474}
475
476#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
478pub struct SourceBundleVersion(pub u32);
479
480impl SourceBundleVersion {
481 pub fn new(version: u32) -> Self {
483 Self(version)
484 }
485
486 pub fn is_valid(self) -> bool {
491 self.0 <= BUNDLE_VERSION
492 }
493
494 pub fn is_latest(self) -> bool {
496 self.0 == BUNDLE_VERSION
497 }
498}
499
500impl Default for SourceBundleVersion {
501 fn default() -> Self {
502 Self(BUNDLE_VERSION)
503 }
504}
505
506#[repr(C, packed)]
510#[derive(Clone, Copy, Debug)]
511struct SourceBundleHeader {
512 pub magic: [u8; 4],
514
515 pub version: u32,
517}
518
519impl SourceBundleHeader {
520 fn as_bytes(&self) -> &[u8] {
521 let ptr = self as *const Self as *const u8;
522 unsafe { std::slice::from_raw_parts(ptr, std::mem::size_of::<Self>()) }
523 }
524}
525
526impl Default for SourceBundleHeader {
527 fn default() -> Self {
528 SourceBundleHeader {
529 magic: BUNDLE_MAGIC,
530 version: BUNDLE_VERSION,
531 }
532 }
533}
534
535#[derive(Clone, Debug, Default, Serialize, Deserialize)]
539struct SourceBundleManifest {
540 #[serde(default)]
542 pub files: BTreeMap<String, SourceFileInfo>,
543
544 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
545 pub source_links: BTreeMap<String, String>,
546
547 #[serde(flatten)]
549 pub attributes: BTreeMap<String, String>,
550}
551
552struct SourceBundleIndex<'data> {
553 manifest: SourceBundleManifest,
554 indexed_files: HashMap<FileKey<'data>, Arc<String>>,
555}
556
557impl<'data> SourceBundleIndex<'data> {
558 pub fn parse(
559 archive: &mut zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>,
560 ) -> Result<Self, SourceBundleError> {
561 let manifest_file = archive
562 .by_name("manifest.json")
563 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
564 let manifest: SourceBundleManifest = serde_json::from_reader(manifest_file)
565 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
566
567 let files = &manifest.files;
568 let mut indexed_files = HashMap::with_capacity(files.len());
569
570 for (zip_path, file_info) in files {
571 let zip_path = Arc::new(zip_path.clone());
572 if !file_info.path.is_empty() {
573 indexed_files.insert(
574 FileKey::Path(normalize_path(&file_info.path).into()),
575 zip_path.clone(),
576 );
577 }
578 if !file_info.url.is_empty() {
579 indexed_files.insert(FileKey::Url(file_info.url.clone().into()), zip_path.clone());
580 }
581 if let (Some(debug_id), Some(ty)) = (file_info.debug_id(), file_info.ty()) {
582 indexed_files.insert(FileKey::DebugId(debug_id, ty), zip_path.clone());
583 }
584 }
585
586 Ok(Self {
587 manifest,
588 indexed_files,
589 })
590 }
591}
592
593pub struct SourceBundle<'data> {
601 data: &'data [u8],
602 archive: zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>,
603 index: Arc<SourceBundleIndex<'data>>,
604}
605
606impl fmt::Debug for SourceBundle<'_> {
607 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608 f.debug_struct("SourceBundle")
609 .field("code_id", &self.code_id())
610 .field("debug_id", &self.debug_id())
611 .field("arch", &self.arch())
612 .field("kind", &self.kind())
613 .field("load_address", &format_args!("{:#x}", self.load_address()))
614 .field("has_symbols", &self.has_symbols())
615 .field("has_debug_info", &self.has_debug_info())
616 .field("has_unwind_info", &self.has_unwind_info())
617 .field("has_sources", &self.has_sources())
618 .field("is_malformed", &self.is_malformed())
619 .finish()
620 }
621}
622
623impl<'data> SourceBundle<'data> {
624 pub fn test(bytes: &[u8]) -> bool {
626 bytes.starts_with(&BUNDLE_MAGIC)
627 }
628
629 pub fn parse(data: &'data [u8]) -> Result<SourceBundle<'data>, SourceBundleError> {
631 let mut archive = zip::read::ZipArchive::new(std::io::Cursor::new(data))
632 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
633
634 let index = Arc::new(SourceBundleIndex::parse(&mut archive)?);
635
636 Ok(SourceBundle {
637 archive,
638 data,
639 index,
640 })
641 }
642
643 pub fn version(&self) -> SourceBundleVersion {
645 SourceBundleVersion(BUNDLE_VERSION)
646 }
647
648 pub fn file_format(&self) -> FileFormat {
650 FileFormat::SourceBundle
651 }
652
653 pub fn code_id(&self) -> Option<CodeId> {
661 self.index
662 .manifest
663 .attributes
664 .get("code_id")
665 .and_then(|x| x.parse().ok())
666 }
667
668 pub fn debug_id(&self) -> DebugId {
676 self.index
677 .manifest
678 .attributes
679 .get("debug_id")
680 .and_then(|x| x.parse().ok())
681 .unwrap_or_default()
682 }
683
684 pub fn name(&self) -> Option<&str> {
692 self.index
693 .manifest
694 .attributes
695 .get("object_name")
696 .map(|x| x.as_str())
697 }
698
699 pub fn arch(&self) -> Arch {
707 self.index
708 .manifest
709 .attributes
710 .get("arch")
711 .and_then(|s| s.parse().ok())
712 .unwrap_or_default()
713 }
714
715 fn kind(&self) -> ObjectKind {
719 ObjectKind::Sources
720 }
721
722 pub fn load_address(&self) -> u64 {
726 0
727 }
728
729 pub fn has_symbols(&self) -> bool {
733 false
734 }
735
736 pub fn symbols(&self) -> SourceBundleSymbolIterator<'data> {
738 std::iter::empty()
739 }
740
741 pub fn symbol_map(&self) -> SymbolMap<'data> {
743 self.symbols().collect()
744 }
745
746 pub fn has_debug_info(&self) -> bool {
750 false
751 }
752
753 pub fn debug_session(&self) -> Result<SourceBundleDebugSession<'data>, SourceBundleError> {
759 let archive = Mutex::new(self.archive.clone());
764 let source_links = SourceLinkMappings::new(
765 self.index
766 .manifest
767 .source_links
768 .iter()
769 .map(|(k, v)| (&k[..], &v[..])),
770 );
771 Ok(SourceBundleDebugSession {
772 index: Arc::clone(&self.index),
773 archive,
774 source_links,
775 })
776 }
777
778 pub fn has_unwind_info(&self) -> bool {
780 false
781 }
782
783 pub fn has_sources(&self) -> bool {
785 true
786 }
787
788 pub fn is_malformed(&self) -> bool {
790 false
791 }
792
793 pub fn data(&self) -> &'data [u8] {
795 self.data
796 }
797
798 pub fn is_empty(&self) -> bool {
800 self.index.manifest.files.is_empty()
801 }
802}
803
804impl<'slf, 'data: 'slf> AsSelf<'slf> for SourceBundle<'data> {
805 type Ref = SourceBundle<'slf>;
806
807 fn as_self(&'slf self) -> &'slf Self::Ref {
808 unsafe { std::mem::transmute(self) }
809 }
810}
811
812impl<'data> Parse<'data> for SourceBundle<'data> {
813 type Error = SourceBundleError;
814
815 fn parse_with_opts(data: &'data [u8], _opts: ParseObjectOptions) -> Result<Self, Self::Error> {
816 Self::parse(data)
817 }
818
819 fn test(data: &'data [u8]) -> bool {
820 SourceBundle::test(data)
821 }
822}
823
824impl<'data: 'object, 'object> ObjectLike<'data, 'object> for SourceBundle<'data> {
825 type Error = SourceBundleError;
826 type Session = SourceBundleDebugSession<'data>;
827 type SymbolIterator = SourceBundleSymbolIterator<'data>;
828
829 fn file_format(&self) -> FileFormat {
830 self.file_format()
831 }
832
833 fn code_id(&self) -> Option<CodeId> {
834 self.code_id()
835 }
836
837 fn debug_id(&self) -> DebugId {
838 self.debug_id()
839 }
840
841 fn arch(&self) -> Arch {
842 self.arch()
843 }
844
845 fn kind(&self) -> ObjectKind {
846 self.kind()
847 }
848
849 fn load_address(&self) -> u64 {
850 self.load_address()
851 }
852
853 fn has_symbols(&self) -> bool {
854 self.has_symbols()
855 }
856
857 fn symbol_map(&self) -> SymbolMap<'data> {
858 self.symbol_map()
859 }
860
861 fn symbols(&self) -> Self::SymbolIterator {
862 self.symbols()
863 }
864
865 fn has_debug_info(&self) -> bool {
866 self.has_debug_info()
867 }
868
869 fn debug_session(&self) -> Result<Self::Session, Self::Error> {
870 self.debug_session()
871 }
872
873 fn has_unwind_info(&self) -> bool {
874 self.has_unwind_info()
875 }
876
877 fn has_sources(&self) -> bool {
878 self.has_sources()
879 }
880
881 fn is_malformed(&self) -> bool {
882 self.is_malformed()
883 }
884}
885
886pub type SourceBundleSymbolIterator<'data> = std::iter::Empty<Symbol<'data>>;
888
889#[derive(Debug, Hash, PartialEq, Eq)]
890enum FileKey<'a> {
891 Path(Cow<'a, str>),
892 Url(Cow<'a, str>),
893 DebugId(DebugId, SourceFileType),
894}
895
896pub struct SourceBundleDebugSession<'data> {
898 archive: Mutex<zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>>,
899 index: Arc<SourceBundleIndex<'data>>,
900 source_links: SourceLinkMappings,
901}
902
903impl SourceBundleDebugSession<'_> {
904 pub fn files(&self) -> SourceBundleFileIterator<'_> {
906 SourceBundleFileIterator {
907 files: self.index.manifest.files.values(),
908 }
909 }
910
911 pub fn functions(&self) -> SourceBundleFunctionIterator<'_> {
913 std::iter::empty()
914 }
915
916 fn source_by_zip_path(&self, zip_path: &str) -> Result<String, SourceBundleError> {
918 let mut archive = self.archive.lock();
919 let mut file = archive
920 .by_name(zip_path)
921 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
922 let mut source_content = String::new();
923
924 file.read_to_string(&mut source_content)
925 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
926 Ok(source_content)
927 }
928
929 fn get_source_file_descriptor(
934 &self,
935 key: FileKey,
936 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
937 if let Some(zip_path) = self.index.indexed_files.get(&key) {
938 let zip_path = zip_path.as_str();
939 let content = Cow::Owned(self.source_by_zip_path(zip_path)?);
940 let info = self.index.manifest.files.get(zip_path);
941 let descriptor = SourceFileDescriptor::new_embedded(content, info);
942 return Ok(Some(descriptor));
943 }
944
945 let FileKey::Path(path) = key else {
946 return Ok(None);
947 };
948
949 Ok(self
950 .source_links
951 .resolve(&path)
952 .map(|s| SourceFileDescriptor::new_remote(s.into())))
953 }
954
955 pub fn source_by_path(
957 &self,
958 path: &str,
959 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
960 self.get_source_file_descriptor(FileKey::Path(normalize_path(path).into()))
961 }
962
963 pub fn source_by_url(
965 &self,
966 url: &str,
967 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
968 self.get_source_file_descriptor(FileKey::Url(url.into()))
969 }
970
971 pub fn source_by_debug_id(
985 &self,
986 debug_id: DebugId,
987 ty: SourceFileType,
988 ) -> Result<Option<SourceFileDescriptor<'_>>, SourceBundleError> {
989 self.get_source_file_descriptor(FileKey::DebugId(debug_id, ty))
990 }
991}
992
993impl<'session> DebugSession<'session> for SourceBundleDebugSession<'_> {
994 type Error = SourceBundleError;
995 type FunctionIterator = SourceBundleFunctionIterator<'session>;
996 type FileIterator = SourceBundleFileIterator<'session>;
997
998 fn functions(&'session self) -> Self::FunctionIterator {
999 self.functions()
1000 }
1001
1002 fn files(&'session self) -> Self::FileIterator {
1003 self.files()
1004 }
1005
1006 fn source_by_path(&self, path: &str) -> Result<Option<SourceFileDescriptor<'_>>, Self::Error> {
1007 self.source_by_path(path)
1008 }
1009}
1010
1011impl<'slf, 'data: 'slf> AsSelf<'slf> for SourceBundleDebugSession<'data> {
1012 type Ref = SourceBundleDebugSession<'slf>;
1013
1014 fn as_self(&'slf self) -> &'slf Self::Ref {
1015 unsafe { std::mem::transmute(self) }
1016 }
1017}
1018
1019pub struct SourceBundleFileIterator<'s> {
1021 files: std::collections::btree_map::Values<'s, String, SourceFileInfo>,
1022}
1023
1024impl<'s> Iterator for SourceBundleFileIterator<'s> {
1025 type Item = Result<FileEntry<'s>, SourceBundleError>;
1026
1027 fn next(&mut self) -> Option<Self::Item> {
1028 let source_file = self.files.next()?;
1029 Some(Ok(FileEntry::new(
1030 Cow::default(),
1031 FileInfo::from_path(source_file.path.as_bytes()),
1032 )))
1033 }
1034}
1035
1036pub type SourceBundleFunctionIterator<'s> =
1038 std::iter::Empty<Result<Function<'s>, SourceBundleError>>;
1039
1040impl SourceBundleManifest {
1041 pub fn new() -> Self {
1043 Self::default()
1044 }
1045}
1046
1047fn sanitize_bundle_path(path: &str) -> String {
1052 let mut sanitized = SANE_PATH_RE.replace_all(path, "/").into_owned();
1053 if sanitized.starts_with('/') {
1054 sanitized.remove(0);
1055 }
1056 sanitized
1057}
1058
1059fn normalize_path(path: &str) -> String {
1061 path.replace('\\', "/")
1062}
1063
1064#[derive(Debug)]
1066pub struct SkippedFileInfo<'a> {
1067 path: &'a str,
1068 reason: &'a str,
1069}
1070
1071impl<'a> SkippedFileInfo<'a> {
1072 fn new(path: &'a str, reason: &'a str) -> Self {
1073 Self { path, reason }
1074 }
1075
1076 pub fn path(&self) -> &str {
1078 self.path
1079 }
1080
1081 pub fn reason(&self) -> &str {
1083 self.reason
1084 }
1085}
1086
1087impl Display for SkippedFileInfo<'_> {
1088 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1089 write!(f, "Skipped file {} due to: {}", self.path, self.reason)
1090 }
1091}
1092
1093pub struct SourceBundleWriter<W>
1122where
1123 W: Seek + Write,
1124{
1125 manifest: SourceBundleManifest,
1126 writer: ZipWriter<W>,
1127 collect_il2cpp: bool,
1128 skipped_file_callback: Box<dyn FnMut(SkippedFileInfo)>,
1129}
1130
1131fn default_file_options() -> SimpleFileOptions {
1132 SimpleFileOptions::default().last_modified_time(zip::DateTime::default())
1139}
1140
1141impl<W> SourceBundleWriter<W>
1142where
1143 W: Seek + Write,
1144{
1145 pub fn start(mut writer: W) -> Result<Self, SourceBundleError> {
1147 let header = SourceBundleHeader::default();
1148 writer
1149 .write_all(header.as_bytes())
1150 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1151
1152 Ok(SourceBundleWriter {
1153 manifest: SourceBundleManifest::new(),
1154 writer: ZipWriter::new(writer),
1155 collect_il2cpp: false,
1156 skipped_file_callback: Box::new(|_| ()),
1157 })
1158 }
1159
1160 pub fn is_empty(&self) -> bool {
1162 self.manifest.files.is_empty()
1163 }
1164
1165 pub fn collect_il2cpp_sources(&mut self, collect_il2cpp: bool) {
1168 self.collect_il2cpp = collect_il2cpp;
1169 }
1170
1171 pub fn set_attribute<K, V>(&mut self, key: K, value: V) -> Option<String>
1180 where
1181 K: Into<String>,
1182 V: Into<String>,
1183 {
1184 self.manifest.attributes.insert(key.into(), value.into())
1185 }
1186
1187 pub fn remove_attribute<K>(&mut self, key: K) -> Option<String>
1191 where
1192 K: AsRef<str>,
1193 {
1194 self.manifest.attributes.remove(key.as_ref())
1195 }
1196
1197 pub fn attribute<K>(&mut self, key: K) -> Option<&str>
1199 where
1200 K: AsRef<str>,
1201 {
1202 self.manifest
1203 .attributes
1204 .get(key.as_ref())
1205 .map(String::as_str)
1206 }
1207
1208 pub fn has_file<S>(&self, path: S) -> bool
1210 where
1211 S: AsRef<str>,
1212 {
1213 let full_path = &self.file_path(path.as_ref());
1214 self.manifest.files.contains_key(full_path)
1215 }
1216
1217 pub fn add_file<S, R>(
1243 &mut self,
1244 path: S,
1245 file: R,
1246 info: SourceFileInfo,
1247 ) -> Result<(), SourceBundleError>
1248 where
1249 S: AsRef<str>,
1250 R: Read,
1251 {
1252 let mut file_reader = Utf8Reader::new(file);
1253
1254 let full_path = self.file_path(path.as_ref());
1255 let unique_path = self.unique_path(full_path);
1256
1257 self.writer
1258 .start_file(unique_path.clone(), default_file_options())
1259 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1260
1261 match io::copy(&mut file_reader, &mut self.writer) {
1262 Err(e) => {
1263 self.writer
1264 .abort_file()
1265 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1266
1267 let error_kind = match e.kind() {
1269 ErrorKind::InvalidData => SourceBundleErrorKind::ReadFailed,
1270 _ => SourceBundleErrorKind::WriteFailed,
1271 };
1272
1273 Err(SourceBundleError::new(error_kind, e))
1274 }
1275 Ok(_) => {
1276 self.manifest.files.insert(unique_path, info);
1277 Ok(())
1278 }
1279 }
1280 }
1281
1282 fn add_file_skip_read_failed<S, R>(
1284 &mut self,
1285 path: S,
1286 file: R,
1287 info: SourceFileInfo,
1288 ) -> Result<(), SourceBundleError>
1289 where
1290 S: AsRef<str>,
1291 R: Read,
1292 {
1293 let result = self.add_file(&path, file, info);
1294
1295 if let Err(e) = &result {
1296 if e.kind == SourceBundleErrorKind::ReadFailed {
1297 let reason = e.to_string();
1298 let skipped_info = SkippedFileInfo::new(path.as_ref(), &reason);
1299 (self.skipped_file_callback)(skipped_info);
1300
1301 return Ok(());
1302 }
1303 }
1304
1305 result
1306 }
1307
1308 pub fn with_skipped_file_callback(
1311 mut self,
1312 callback: impl FnMut(SkippedFileInfo) + 'static,
1313 ) -> Self {
1314 self.skipped_file_callback = Box::new(callback);
1315 self
1316 }
1317
1318 pub fn write_object<'data, 'object, O, E>(
1325 self,
1326 object: &'object O,
1327 object_name: &str,
1328 ) -> Result<bool, SourceBundleError>
1329 where
1330 O: ObjectLike<'data, 'object, Error = E>,
1331 E: std::error::Error + Send + Sync + 'static,
1332 {
1333 self.write_object_with_filter(object, object_name, |_, _| true)
1334 }
1335
1336 pub fn write_object_with_filter<'data, 'object, O, E, F>(
1345 self,
1346 object: &'object O,
1347 object_name: &str,
1348 filter: F,
1349 ) -> Result<bool, SourceBundleError>
1350 where
1351 O: ObjectLike<'data, 'object, Error = E>,
1352 E: std::error::Error + Send + Sync + 'static,
1353 F: FnMut(&FileEntry, &Option<SourceFileDescriptor<'_>>) -> bool,
1354 {
1355 self.write_object_with_filter_and_provider(object, object_name, filter, |path| {
1357 File::open(path).map(BufReader::new).ok()
1358 })
1359 .map(|w| w.0)
1360 }
1361
1362 pub fn write_object_with_filter_and_provider<'data, 'object, O, E, F, R, P>(
1375 mut self,
1376 object: &'object O,
1377 object_name: &str,
1378 mut filter: F,
1379 mut provider: P,
1380 ) -> Result<(bool, W), SourceBundleError>
1381 where
1382 O: ObjectLike<'data, 'object, Error = E>,
1383 E: std::error::Error + Send + Sync + 'static,
1384 F: FnMut(&FileEntry, &Option<SourceFileDescriptor<'_>>) -> bool,
1385 R: Read,
1386 P: FnMut(&str) -> Option<R>,
1387 {
1388 let mut files_handled = BTreeSet::new();
1389 let mut referenced_files = BTreeSet::new();
1390
1391 let session = object
1392 .debug_session()
1393 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1394
1395 self.set_attribute("arch", object.arch().to_string());
1396 self.set_attribute("debug_id", object.debug_id().to_string());
1397 self.set_attribute("object_name", object_name);
1398 if let Some(code_id) = object.code_id() {
1399 self.set_attribute("code_id", code_id.to_string());
1400 }
1401
1402 for file_result in session.files() {
1403 let file = file_result
1404 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1405 let filename = file.abs_path_str();
1406
1407 if files_handled.contains(&filename) {
1408 continue;
1409 }
1410
1411 let source = if filename.starts_with('<') && filename.ends_with('>') {
1414 None
1415 } else {
1416 let source_from_object = session
1417 .source_by_path(&filename)
1418 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
1419 if filter(&file, &source_from_object) {
1420 provider(&filename)
1421 } else {
1422 None
1423 }
1424 };
1425
1426 if let Some(mut source) = source {
1427 let bundle_path = sanitize_bundle_path(&filename);
1428 let mut info = SourceFileInfo::new();
1429 info.set_ty(SourceFileType::Source);
1430 info.set_path(filename.clone());
1431
1432 if self.collect_il2cpp {
1433 let mut buf = Vec::new();
1435 if source.read_to_end(&mut buf).is_ok() {
1436 collect_il2cpp_sources(&buf, &mut referenced_files);
1437 self.add_file_skip_read_failed(bundle_path, buf.as_slice(), info)?;
1438 }
1439 } else {
1440 self.add_file_skip_read_failed(bundle_path, source, info)?;
1442 }
1443 }
1444
1445 files_handled.insert(filename);
1446 }
1447
1448 for filename in referenced_files {
1449 if files_handled.contains(&filename) {
1450 continue;
1451 }
1452
1453 if let Some(source) = provider(&filename) {
1454 let bundle_path = sanitize_bundle_path(&filename);
1455 let mut info = SourceFileInfo::new();
1456 info.set_ty(SourceFileType::Source);
1457 info.set_path(filename.clone());
1458
1459 self.add_file_skip_read_failed(bundle_path, source, info)?;
1460 }
1461 }
1462
1463 let is_empty = self.is_empty();
1464 let writer = self.do_finish()?;
1465
1466 Ok((!is_empty, writer))
1467 }
1468
1469 pub fn finish(self) -> Result<(), SourceBundleError> {
1471 self.do_finish().map(drop)
1472 }
1473
1474 fn do_finish(mut self) -> Result<W, SourceBundleError> {
1476 self.write_manifest()?;
1477 let writer = self
1478 .writer
1479 .finish()
1480 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1481 Ok(writer)
1482 }
1483
1484 fn file_path(&self, path: &str) -> String {
1486 format!("{FILES_PATH}/{path}")
1487 }
1488
1489 fn unique_path(&self, mut path: String) -> String {
1494 let mut duplicates = 0;
1495
1496 while self.manifest.files.contains_key(&path) {
1497 duplicates += 1;
1498 match duplicates {
1499 1 => path.push_str(".1"),
1500 _ => {
1501 use std::fmt::Write;
1502 trim_end_matches(&mut path, char::is_numeric);
1503 write!(path, ".{duplicates}").unwrap();
1504 }
1505 }
1506 }
1507
1508 path
1509 }
1510
1511 fn write_manifest(&mut self) -> Result<(), SourceBundleError> {
1513 self.writer
1514 .start_file(MANIFEST_PATH, default_file_options())
1515 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1516
1517 serde_json::to_writer(&mut self.writer, &self.manifest)
1518 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
1519
1520 Ok(())
1521 }
1522}
1523
1524fn collect_il2cpp_sources(source: &[u8], referenced_files: &mut BTreeSet<String>) {
1528 if let Ok(source) = std::str::from_utf8(source) {
1529 for line in source.lines() {
1530 let line = line.trim();
1531
1532 if let Some(source_ref) = line.strip_prefix("//<source_info:") {
1533 if let Some((file, _line)) = source_ref.rsplit_once(':') {
1534 if !referenced_files.contains(file) {
1535 referenced_files.insert(file.to_string());
1536 }
1537 }
1538 }
1539 }
1540 }
1541}
1542
1543impl SourceBundleWriter<BufWriter<File>> {
1544 pub fn create<P>(path: P) -> Result<SourceBundleWriter<BufWriter<File>>, SourceBundleError>
1549 where
1550 P: AsRef<Path>,
1551 {
1552 let file = OpenOptions::new()
1553 .read(true)
1554 .write(true)
1555 .create(true)
1556 .truncate(true)
1557 .open(path)
1558 .map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
1559
1560 Self::start(BufWriter::new(file))
1561 }
1562}
1563
1564#[cfg(test)]
1565mod tests {
1566 use crate::Object;
1567
1568 use super::*;
1569
1570 use std::{collections::HashSet, io::Cursor};
1571
1572 use similar_asserts::assert_eq;
1573 use tempfile::NamedTempFile;
1574
1575 #[test]
1576 fn test_has_file() -> Result<(), SourceBundleError> {
1577 let writer = Cursor::new(Vec::new());
1578 let mut bundle = SourceBundleWriter::start(writer)?;
1579
1580 bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
1581 assert!(bundle.has_file("bar.txt"));
1582
1583 bundle.finish()?;
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn test_non_utf8() -> Result<(), SourceBundleError> {
1589 let writer = Cursor::new(Vec::new());
1590 let mut bundle = SourceBundleWriter::start(writer)?;
1591
1592 assert!(bundle
1593 .add_file(
1594 "bar.txt",
1595 &[0, 159, 146, 150][..],
1596 SourceFileInfo::default()
1597 )
1598 .is_err());
1599
1600 Ok(())
1601 }
1602
1603 #[test]
1604 fn test_duplicate_files() -> Result<(), SourceBundleError> {
1605 let writer = Cursor::new(Vec::new());
1606 let mut bundle = SourceBundleWriter::start(writer)?;
1607
1608 bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
1609 bundle.add_file("bar.txt", &b"othercontents"[..], SourceFileInfo::default())?;
1610 assert!(bundle.has_file("bar.txt"));
1611 assert!(bundle.has_file("bar.txt.1"));
1612
1613 bundle.finish()?;
1614 Ok(())
1615 }
1616
1617 #[test]
1618 fn debugsession_is_sendsync() {
1619 fn is_sendsync<T: Send + Sync>() {}
1620 is_sendsync::<SourceBundleDebugSession>();
1621 }
1622
1623 #[test]
1624 fn test_normalize_paths() -> Result<(), SourceBundleError> {
1625 let mut writer = Cursor::new(Vec::new());
1626 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1627
1628 for filename in &[
1629 "C:\\users\\martin\\mydebugfile.cs",
1630 "/usr/martin/mydebugfile.h",
1631 ] {
1632 let mut info = SourceFileInfo::new();
1633 info.set_ty(SourceFileType::Source);
1634 info.set_path(filename.to_string());
1635 bundle.add_file_skip_read_failed(
1636 sanitize_bundle_path(filename),
1637 &b"somerandomdata"[..],
1638 info,
1639 )?;
1640 }
1641
1642 bundle.finish()?;
1643 let bundle_bytes = writer.into_inner();
1644 let bundle = SourceBundle::parse(&bundle_bytes)?;
1645
1646 let session = bundle.debug_session().unwrap();
1647
1648 assert!(session
1649 .source_by_path("C:\\users\\martin\\mydebugfile.cs")?
1650 .is_some());
1651 assert!(session
1652 .source_by_path("C:/users/martin/mydebugfile.cs")?
1653 .is_some());
1654 assert!(session
1655 .source_by_path("C:\\users\\martin/mydebugfile.cs")?
1656 .is_some());
1657 assert!(session
1658 .source_by_path("/usr/martin/mydebugfile.h")?
1659 .is_some());
1660 assert!(session
1661 .source_by_path("\\usr\\martin\\mydebugfile.h")?
1662 .is_some());
1663
1664 Ok(())
1665 }
1666
1667 #[test]
1668 fn test_source_descriptor() -> Result<(), SourceBundleError> {
1669 let mut writer = Cursor::new(Vec::new());
1670 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1671
1672 let mut info = SourceFileInfo::default();
1673 info.set_url("https://example.com/bar.js.min".into());
1674 info.set_path("/files/bar.js.min".into());
1675 info.set_ty(SourceFileType::MinifiedSource);
1676 info.add_header(
1677 "debug-id".into(),
1678 "5e618b9f-54a9-4389-b196-519819dd7c47".into(),
1679 );
1680 info.add_header("sourcemap".into(), "bar.js.map".into());
1681 bundle.add_file("bar.js", &b"filecontents"[..], info)?;
1682 assert!(bundle.has_file("bar.js"));
1683
1684 bundle.finish()?;
1685 let bundle_bytes = writer.into_inner();
1686 let bundle = SourceBundle::parse(&bundle_bytes)?;
1687
1688 let sess = bundle.debug_session().unwrap();
1689 let f = sess
1690 .source_by_debug_id(
1691 "5e618b9f-54a9-4389-b196-519819dd7c47".parse().unwrap(),
1692 SourceFileType::MinifiedSource,
1693 )
1694 .unwrap()
1695 .expect("should exist");
1696 assert_eq!(f.contents(), Some("filecontents"));
1697 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1698 assert_eq!(f.url(), Some("https://example.com/bar.js.min"));
1699 assert_eq!(f.path(), Some("/files/bar.js.min"));
1700 assert_eq!(f.source_mapping_url(), Some("bar.js.map"));
1701
1702 assert!(sess
1703 .source_by_debug_id(
1704 "5e618b9f-54a9-4389-b196-519819dd7c47".parse().unwrap(),
1705 SourceFileType::Source
1706 )
1707 .unwrap()
1708 .is_none());
1709
1710 Ok(())
1711 }
1712
1713 #[test]
1714 fn test_source_mapping_url() -> Result<(), SourceBundleError> {
1715 let mut writer = Cursor::new(Vec::new());
1716 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1717
1718 let mut info = SourceFileInfo::default();
1719 info.set_url("https://example.com/bar.min.js".into());
1720 info.set_ty(SourceFileType::MinifiedSource);
1721 bundle.add_file(
1722 "bar.js",
1723 &b"filecontents\n//# sourceMappingURL=bar.js.map"[..],
1724 info,
1725 )?;
1726
1727 bundle.finish()?;
1728 let bundle_bytes = writer.into_inner();
1729 let bundle = SourceBundle::parse(&bundle_bytes)?;
1730
1731 let sess = bundle.debug_session().unwrap();
1732 let f = sess
1733 .source_by_url("https://example.com/bar.min.js")
1734 .unwrap()
1735 .expect("should exist");
1736 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1737 assert_eq!(f.url(), Some("https://example.com/bar.min.js"));
1738 assert_eq!(f.source_mapping_url(), Some("bar.js.map"));
1739
1740 Ok(())
1741 }
1742
1743 #[test]
1744 fn test_source_embedded_debug_id() -> Result<(), SourceBundleError> {
1745 let mut writer = Cursor::new(Vec::new());
1746 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1747
1748 let mut info = SourceFileInfo::default();
1749 info.set_url("https://example.com/bar.min.js".into());
1750 info.set_ty(SourceFileType::MinifiedSource);
1751 bundle.add_file(
1752 "bar.js",
1753 &b"filecontents\n//# debugId=5b65abfb23384f0bb3b964c8f734d43f"[..],
1754 info,
1755 )?;
1756
1757 bundle.finish()?;
1758 let bundle_bytes = writer.into_inner();
1759 let bundle = SourceBundle::parse(&bundle_bytes)?;
1760
1761 let sess = bundle.debug_session().unwrap();
1762 let f = sess
1763 .source_by_url("https://example.com/bar.min.js")
1764 .unwrap()
1765 .expect("should exist");
1766 assert_eq!(f.ty(), SourceFileType::MinifiedSource);
1767 assert_eq!(
1768 f.debug_id(),
1769 Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
1770 );
1771
1772 Ok(())
1773 }
1774
1775 #[test]
1776 fn test_sourcemap_embedded_debug_id() -> Result<(), SourceBundleError> {
1777 let mut writer = Cursor::new(Vec::new());
1778 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1779
1780 let mut info = SourceFileInfo::default();
1781 info.set_url("https://example.com/bar.js.map".into());
1782 info.set_ty(SourceFileType::SourceMap);
1783 bundle.add_file(
1784 "bar.js.map",
1785 &br#"{"debug_id": "5b65abfb-2338-4f0b-b3b9-64c8f734d43f"}"#[..],
1786 info,
1787 )?;
1788
1789 bundle.finish()?;
1790 let bundle_bytes = writer.into_inner();
1791 let bundle = SourceBundle::parse(&bundle_bytes)?;
1792
1793 let sess = bundle.debug_session().unwrap();
1794 let f = sess
1795 .source_by_url("https://example.com/bar.js.map")
1796 .unwrap()
1797 .expect("should exist");
1798 assert_eq!(f.ty(), SourceFileType::SourceMap);
1799 assert_eq!(
1800 f.debug_id(),
1801 Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
1802 );
1803
1804 Ok(())
1805 }
1806
1807 #[test]
1808 fn test_il2cpp_reference() -> Result<(), Box<dyn std::error::Error>> {
1809 let mut cpp_file = NamedTempFile::new()?;
1810 let mut cs_file = NamedTempFile::new()?;
1811
1812 let cpp_contents = format!("foo\n//<source_info:{}:111>\nbar", cs_file.path().display());
1813
1814 let object_buf = {
1816 let mut writer = Cursor::new(Vec::new());
1817 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1818
1819 let path = cpp_file.path().to_string_lossy();
1820 let mut info = SourceFileInfo::new();
1821 info.set_ty(SourceFileType::Source);
1822 info.set_path(path.to_string());
1823 bundle.add_file(path, cpp_contents.as_bytes(), info)?;
1824
1825 bundle.finish()?;
1826 writer.into_inner()
1827 };
1828 let object = SourceBundle::parse(&object_buf)?;
1829
1830 cpp_file.write_all(cpp_contents.as_bytes())?;
1832 cs_file.write_all(b"some C# source")?;
1833
1834 let mut output_buf = Cursor::new(Vec::new());
1836 let mut writer = SourceBundleWriter::start(&mut output_buf)?;
1837 writer.collect_il2cpp_sources(true);
1838
1839 let written = writer.write_object(&object, "whatever")?;
1840 assert!(written);
1841 let output_buf = output_buf.into_inner();
1842
1843 let source_bundle = SourceBundle::parse(&output_buf)?;
1845 let session = source_bundle.debug_session()?;
1846 let actual_files: BTreeMap<_, _> = session
1847 .files()
1848 .flatten()
1849 .flat_map(|f| {
1850 let path = f.abs_path_str();
1851 session
1852 .source_by_path(&path)
1853 .ok()
1854 .flatten()
1855 .map(|source| (path, source.contents().unwrap().to_string()))
1856 })
1857 .collect();
1858
1859 let mut expected_files = BTreeMap::new();
1860 expected_files.insert(cpp_file.path().to_string_lossy().into_owned(), cpp_contents);
1861 expected_files.insert(
1862 cs_file.path().to_string_lossy().into_owned(),
1863 String::from("some C# source"),
1864 );
1865
1866 assert_eq!(actual_files, expected_files);
1867
1868 Ok(())
1869 }
1870
1871 #[test]
1872 fn test_bundle_paths() {
1873 assert_eq!(sanitize_bundle_path("foo"), "foo");
1874 assert_eq!(sanitize_bundle_path("foo/bar"), "foo/bar");
1875 assert_eq!(sanitize_bundle_path("/foo/bar"), "foo/bar");
1876 assert_eq!(sanitize_bundle_path("C:/foo/bar"), "C/foo/bar");
1877 assert_eq!(sanitize_bundle_path("\\foo\\bar"), "foo/bar");
1878 assert_eq!(sanitize_bundle_path("\\\\UNC\\foo\\bar"), "UNC/foo/bar");
1879 }
1880
1881 #[test]
1882 fn test_source_links() -> Result<(), SourceBundleError> {
1883 let mut writer = Cursor::new(Vec::new());
1884 let mut bundle = SourceBundleWriter::start(&mut writer)?;
1885
1886 let mut info = SourceFileInfo::default();
1887 info.set_url("https://example.com/bar/index.min.js".into());
1888 info.set_path("/files/bar/index.min.js".into());
1889 info.set_ty(SourceFileType::MinifiedSource);
1890 bundle.add_file("bar/index.js", &b"filecontents"[..], info)?;
1891 assert!(bundle.has_file("bar/index.js"));
1892
1893 bundle
1894 .manifest
1895 .source_links
1896 .insert("/files/bar/*".to_string(), "https://nope.com/*".into());
1897 bundle
1898 .manifest
1899 .source_links
1900 .insert("/files/foo/*".to_string(), "https://example.com/*".into());
1901
1902 bundle.finish()?;
1903 let bundle_bytes = writer.into_inner();
1904 let bundle = SourceBundle::parse(&bundle_bytes)?;
1905
1906 let sess = bundle.debug_session().unwrap();
1907
1908 let foo = sess
1910 .source_by_path("/files/foo/index.min.js")
1911 .unwrap()
1912 .expect("should exist");
1913 assert_eq!(foo.contents(), None);
1914 assert_eq!(foo.ty(), SourceFileType::Source);
1915 assert_eq!(foo.url(), Some("https://example.com/index.min.js"));
1916 assert_eq!(foo.path(), None);
1917
1918 let bar = sess
1920 .source_by_path("/files/bar/index.min.js")
1921 .unwrap()
1922 .expect("should exist");
1923 assert_eq!(bar.contents(), Some("filecontents"));
1924 assert_eq!(bar.ty(), SourceFileType::MinifiedSource);
1925 assert_eq!(bar.url(), Some("https://example.com/bar/index.min.js"));
1926 assert_eq!(bar.path(), Some("/files/bar/index.min.js"));
1927
1928 Ok(())
1929 }
1930
1931 #[test]
1932 fn test_write_object_with_source_provider() {
1933 let view = std::fs::read(symbolic_testutils::fixture("linux/crash.debug")).unwrap();
1934 let object = Object::parse(&view).unwrap();
1935
1936 let referenced = {
1937 let session = object.debug_session().unwrap();
1938 session
1939 .files()
1940 .map(|file| file.unwrap().abs_path_str())
1941 .filter(|path| !(path.starts_with('<') && path.ends_with('>')))
1942 .collect::<HashSet<_>>()
1943 };
1944
1945 let (written, writer) = SourceBundleWriter::start(Cursor::new(Vec::new()))
1946 .unwrap()
1947 .write_object_with_filter_and_provider(
1948 &object,
1949 "crash.debug",
1950 |_, _| true,
1951 |path| {
1952 assert!(referenced.contains(path));
1953 Some(Cursor::new(
1954 format!("// synthetic source for {path}\n").into_bytes(),
1955 ))
1956 },
1957 )
1958 .unwrap();
1959 assert!(written);
1960
1961 let data = writer.into_inner();
1962
1963 let bundle = Object::parse(&data).unwrap();
1964 assert_eq!(bundle.debug_id(), object.debug_id());
1965 assert!(bundle.has_sources());
1966
1967 let session = bundle.debug_session().unwrap();
1968
1969 for path in &referenced {
1971 let descriptor = session.source_by_path(path).unwrap().unwrap();
1972 assert_eq!(
1973 descriptor.contents(),
1974 Some(format!("// synthetic source for {path}\n").as_str())
1975 );
1976 }
1977
1978 for path in session.files() {
1980 let path = path.unwrap().abs_path_str();
1981 assert!(
1982 referenced.contains(&path),
1983 "expected {path} to be in object referenced files"
1984 );
1985 }
1986 }
1987
1988 #[test]
1989 fn test_write_object_with_provider_no_sources() {
1990 let view = std::fs::read(symbolic_testutils::fixture("linux/crash.debug")).unwrap();
1991 let object = Object::parse(&view).unwrap();
1992
1993 let writer = SourceBundleWriter::start(Cursor::new(Vec::new())).unwrap();
1994 let (written, _) = writer
1995 .write_object_with_filter_and_provider(
1996 &object,
1997 "crash.debug",
1998 |_, _| true,
1999 |_| None::<&[u8]>,
2000 )
2001 .unwrap();
2002
2003 assert!(!written);
2004 }
2005
2006 #[test]
2007 fn test_write_object_with_all_filtered() {
2008 let view = std::fs::read(symbolic_testutils::fixture("linux/crash.debug")).unwrap();
2009 let object = Object::parse(&view).unwrap();
2010
2011 let writer = SourceBundleWriter::start(Cursor::new(Vec::new())).unwrap();
2012 let (written, _) = writer
2013 .write_object_with_filter_and_provider(
2014 &object,
2015 "crash.debug",
2016 |_, _| false,
2017 |_| Some([0, 1, 2].as_slice()),
2018 )
2019 .unwrap();
2020
2021 assert!(!written);
2022 }
2023}