Skip to main content

symbolic_debuginfo/sourcebundle/
mod.rs

1//! Support for Source Bundles, a proprietary archive containing source code.
2//!
3//! This module defines the [`SourceBundle`] type. Since not all object file containers specify a
4//! standardized way to inline sources into debug information, this can be used to associate source
5//! contents to debug files.
6//!
7//! Source bundles are ZIP archives with a well-defined internal structure. Most importantly, they
8//! contain source files in a nested directory structure. Additionally, there is meta data
9//! associated to every source file, which allows to store additional properties, such as the
10//! original file system path, a web URL, and custom headers.
11//!
12//! The internal structure is as follows:
13//!
14//! ```txt
15//! manifest.json
16//! files/
17//!   file1.txt
18//!   subfolder/
19//!     file2.txt
20//! ```
21//!
22//! `SourceBundle` implements the [`ObjectLike`] trait. When created from another object, it carries
23//! over its meta data, such as the [`debug_id`] or [`code_id`]. However, source bundles never store
24//! symbols or debug information. To obtain sources or iterate files stored in this source bundle,
25//! use [`SourceBundle::debug_session`].
26//!
27//! Source bundles can be created manually or by converting any `ObjectLike` using
28//! [`SourceBundleWriter`].
29//!
30//! [`ObjectLike`]: ../trait.ObjectLike.html
31//! [`SourceBundle`]: struct.SourceBundle.html
32//! [`debug_id`]: struct.SourceBundle.html#method.debug_id
33//! [`code_id`]: struct.SourceBundle.html#method.code_id
34//! [`SourceBundle::debug_session`]: struct.SourceBundle.html#method.debug_session
35//! [`SourceBundleWriter`]: struct.SourceBundleWriter.html
36//!
37//! ## Artifact Bundles
38//!
39//! Source bundles share the format with a related concept, called an "artifact bundle".  Artifact
40//! bundles are essentially source bundles but they typically contain sources referred to by
41//! JavaScript source maps and source maps themselves.  For instance in an artifact
42//! bundle a file entry has a `url` and might carry `headers` or individual debug IDs
43//! per source file.
44
45mod 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
72/// Magic bytes of a source bundle. They are prepended to the ZIP file.
73static BUNDLE_MAGIC: [u8; 4] = *b"SYSB";
74
75/// Version of the bundle and manifest format.
76static BUNDLE_VERSION: u32 = 2;
77
78/// Relative path to the manifest file in the bundle file.
79static MANIFEST_PATH: &str = "manifest.json";
80
81/// Path at which files will be written into the bundle.
82static FILES_PATH: &str = "files";
83
84static SANE_PATH_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":?[/\\]+").unwrap());
85
86/// The error type for [`SourceBundleError`].
87#[non_exhaustive]
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum SourceBundleErrorKind {
90    /// The source bundle container is damaged.
91    BadZip,
92
93    /// An error when reading/writing the manifest.
94    BadManifest,
95
96    /// The `Object` contains invalid data and cannot be converted.
97    BadDebugFile,
98
99    /// Generic error when writing a source bundle, most likely IO.
100    WriteFailed,
101
102    /// The file is not valid UTF-8 or could not be read for another reason.
103    ReadFailed,
104
105    /// A source file exceeded the configured size limit.
106    ///
107    /// The size limit for a source bundle can be configured
108    /// by the [`max_decompressed_embedded_source_size`](crate::ParseObjectOptions::max_decompressed_embedded_source_size)
109    /// parameter in [`SourceBundle::parse_with_opts`].
110    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/// An error returned when handling [`SourceBundle`](struct.SourceBundle.html).
127#[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    /// Creates a new SourceBundle error from a known kind of error as well as an arbitrary error
137    /// payload.
138    ///
139    /// This function is used to generically create source bundle errors which do not originate from
140    /// `symbolic` itself. The `source` argument is an arbitrary payload which will be contained in
141    /// this [`SourceBundleError`].
142    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    /// Returns the corresponding [`SourceBundleErrorKind`] for this error.
151    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
162/// Trims matching suffices of a string in-place.
163fn 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/// The type of a [`SourceFileInfo`](struct.SourceFileInfo.html).
172#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
173#[serde(rename_all = "snake_case")]
174pub enum SourceFileType {
175    /// Regular source file.
176    Source,
177
178    /// Minified source code.
179    MinifiedSource,
180
181    /// JavaScript sourcemap.
182    SourceMap,
183
184    /// Indexed JavaScript RAM bundle.
185    IndexedRamBundle,
186}
187
188impl SourceFileType {
189    /// Returns the name of the source file type.
190    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/// Meta data information of a file in a [`SourceBundle`](struct.SourceBundle.html).
207#[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
226/// Helper to ensure that header keys are normalized to lowercase
227fn 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    /// Creates default file information.
248    pub fn new() -> Self {
249        Self::default()
250    }
251
252    /// Returns the type of the source file.
253    pub fn ty(&self) -> Option<SourceFileType> {
254        self.ty
255    }
256
257    /// Sets the type of the source file.
258    pub fn set_ty(&mut self, ty: SourceFileType) {
259        self.ty = Some(ty);
260    }
261
262    /// Returns the absolute file system path of this file.
263    pub fn path(&self) -> Option<&str> {
264        match self.path.as_str() {
265            "" => None,
266            path => Some(path),
267        }
268    }
269
270    /// Sets the absolute file system path of this file.
271    pub fn set_path(&mut self, path: String) {
272        self.path = path;
273    }
274
275    /// Returns the web URL that of this file.
276    pub fn url(&self) -> Option<&str> {
277        match self.url.as_str() {
278            "" => None,
279            url => Some(url),
280        }
281    }
282
283    /// Sets the web URL of this file.
284    pub fn set_url(&mut self, url: String) {
285        self.url = url;
286    }
287
288    /// Iterates over all attributes represented as headers.
289    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    /// Retrieves the specified header, if it exists.
294    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    /// Adds a custom attribute following header conventions.
309    ///
310    /// Header keys are converted to lowercase before writing as this is
311    /// the canonical format for headers. However, the file format does
312    /// support headers to be case insensitive and they will be lower cased
313    /// upon reading.
314    ///
315    /// Headers on files are primarily be used to add auxiliary information
316    /// to files.  The following headers are known and processed:
317    ///
318    /// - `debug-id`: see [`debug_id`](Self::debug_id)
319    /// - `sourcemap` (and `x-sourcemap`): see [`source_mapping_url`](Self::source_mapping_url)
320    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    /// The debug ID of this minified source or sourcemap if it has any.
329    ///
330    /// Files have a debug ID if they have a header with the key `debug-id`.
331    /// At present debug IDs in source bundles are only ever given to minified
332    /// source files.
333    pub fn debug_id(&self) -> Option<DebugId> {
334        self.header("debug-id").and_then(|x| x.parse().ok())
335    }
336
337    /// The source mapping URL of the given minified source.
338    ///
339    /// Files have a source mapping URL if they have a header with the
340    /// key `sourcemap` (or the `x-sourcemap` legacy header) as part the
341    /// source map specification.
342    pub fn source_mapping_url(&self) -> Option<&str> {
343        self.header("sourcemap")
344            .or_else(|| self.header("x-sourcemap"))
345    }
346
347    /// Returns `true` if this instance does not carry any information.
348    pub fn is_empty(&self) -> bool {
349        self.path.is_empty() && self.ty.is_none() && self.headers.is_empty()
350    }
351}
352
353/// A descriptor that provides information about a source file.
354///
355/// This descriptor is returned from [`source_by_path`](DebugSession::source_by_path)
356/// and friends.
357///
358/// This descriptor holds information that can be used to retrieve information
359/// about the source file.  A descriptor has to have at least one of the following
360/// to be valid:
361///
362/// - [`contents`](Self::contents)
363/// - [`url`](Self::url)
364/// - [`debug_id`](Self::debug_id)
365///
366/// Debug sessions are not permitted to return invalid source file descriptors.
367pub 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    /// Creates an embedded source file descriptor.
375    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    /// Creates an remote source file descriptor.
387    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    /// The type of the file the descriptor points to.
396    pub fn ty(&self) -> SourceFileType {
397        self.file_info
398            .and_then(|x| x.ty())
399            .unwrap_or(SourceFileType::Source)
400    }
401
402    /// The contents of the source file as string, if it's available.
403    ///
404    /// Portable PDBs for instance will often have source information, but rely on
405    /// remote file fetching via Sourcelink to get to the contents.  In that case
406    /// a file descriptor is created, but the contents are missing and instead the
407    /// [`url`](Self::url) can be used.
408    pub fn contents(&self) -> Option<&str> {
409        self.contents.as_deref()
410    }
411
412    /// The contents of the source file as string, if it's available.
413    ///
414    /// This unwraps the [`SourceFileDescriptor`] directly and might avoid a copy of `contents`
415    /// later on.
416    pub fn into_contents(self) -> Option<Cow<'a, str>> {
417        self.contents
418    }
419
420    /// If available returns the URL of this source.
421    ///
422    /// For certain files this is the canoncial URL of where the file is placed.  This
423    /// for instance is the case for minified JavaScript files or source maps which might
424    /// have a canonical URL.  In case of portable PDBs this is also where you would fetch
425    /// the source code from if source links are used.
426    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    /// If available returns the file path of this source.
435    ///
436    /// For source bundles that are a companion file to a debug file, this is the canonical
437    /// path of the source file.
438    pub fn path(&self) -> Option<&str> {
439        self.file_info.and_then(|x| x.path())
440    }
441
442    /// The debug ID of the file if available.
443    ///
444    /// For source maps or minified source files symbolic supports embedded debug IDs.  If they
445    /// are in use, the debug ID is returned from here.  The debug ID is discovered from the
446    /// file's `debug-id` header or the embedded `debugId` reference in the file body.
447    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    /// The source mapping URL reference of the file.
464    ///
465    /// This is used to refer to a source map from a minified file.  Only minified source files
466    /// will have a relationship to a source map.  The source mapping is discovered either from
467    /// a `sourcemap` header in the source manifest, or the `sourceMappingURL` reference in the body.
468    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/// Version number of a [`SourceBundle`](struct.SourceBundle.html).
496#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
497pub struct SourceBundleVersion(pub u32);
498
499impl SourceBundleVersion {
500    /// Creates a new source bundle version.
501    pub fn new(version: u32) -> Self {
502        Self(version)
503    }
504
505    /// Determines whether this version can be handled.
506    ///
507    /// This will return `false`, if the version is newer than what is supported by this library
508    /// version.
509    pub fn is_valid(self) -> bool {
510        self.0 <= BUNDLE_VERSION
511    }
512
513    /// Returns whether the given bundle is at the latest supported versino.
514    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/// Binary header of the source bundle archive.
526///
527/// This header precedes the ZIP archive. It is used to detect these files on the file system.
528#[repr(C, packed)]
529#[derive(Clone, Copy, Debug)]
530struct SourceBundleHeader {
531    /// Magic bytes header.
532    pub magic: [u8; 4],
533
534    /// Version of the bundle.
535    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/// Manifest of a [`SourceBundle`] containing information on its contents.
555///
556/// [`SourceBundle`]: struct.SourceBundle.html
557#[derive(Clone, Debug, Default, Serialize, Deserialize)]
558struct SourceBundleManifest {
559    /// Descriptors for all files in this bundle.
560    #[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    /// Arbitrary attributes to include in the bundle.
567    #[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
612/// A bundle of source code files.
613///
614/// To create a source bundle, see [`SourceBundleWriter`]. For more information, see the [module
615/// level documentation].
616///
617/// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
618/// [module level documentation]: index.html
619pub 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    /// Tests whether the buffer could contain a `SourceBundle`.
649    pub fn test(bytes: &[u8]) -> bool {
650        bytes.starts_with(&BUNDLE_MAGIC)
651    }
652
653    /// Tries to parse a `SourceBundle` from the given slice, with default options.
654    pub fn parse(data: &'data [u8]) -> Result<SourceBundle<'data>, SourceBundleError> {
655        Self::parse_with_opts(data, Default::default())
656    }
657
658    /// Tries to parse a `SourceBundle` from the given slice.
659    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    /// Returns the version of this source bundle format.
677    pub fn version(&self) -> SourceBundleVersion {
678        SourceBundleVersion(BUNDLE_VERSION)
679    }
680
681    /// The container file format, which is always `FileFormat::SourceBundle`.
682    pub fn file_format(&self) -> FileFormat {
683        FileFormat::SourceBundle
684    }
685
686    /// The code identifier of this object.
687    ///
688    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
689    /// in the [`SourceBundleWriter`] by setting the `"code_id"` attribute.
690    ///
691    /// [`ObjectLike`]: ../trait.ObjectLike.html
692    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
693    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    /// The code identifier of this object.
702    ///
703    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
704    /// in the [`SourceBundleWriter`] by setting the `"debug_id"` attribute.
705    ///
706    /// [`ObjectLike`]: ../trait.ObjectLike.html
707    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
708    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    /// The debug file name of this object.
718    ///
719    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
720    /// in the [`SourceBundleWriter`] by setting the `"object_name"` attribute.
721    ///
722    /// [`ObjectLike`]: ../trait.ObjectLike.html
723    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
724    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    /// The CPU architecture of this object.
733    ///
734    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
735    /// in the [`SourceBundleWriter`] by setting the `"arch"` attribute.
736    ///
737    /// [`ObjectLike`]: ../trait.ObjectLike.html
738    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
739    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    /// The kind of this object.
749    ///
750    /// Because source bundles do not contain real objects this is always `ObjectKind::None`.
751    fn kind(&self) -> ObjectKind {
752        ObjectKind::Sources
753    }
754
755    /// The address at which the image prefers to be loaded into memory.
756    ///
757    /// Because source bundles do not contain this information is always `0`.
758    pub fn load_address(&self) -> u64 {
759        0
760    }
761
762    /// Determines whether this object exposes a public symbol table.
763    ///
764    /// Source bundles never have symbols.
765    pub fn has_symbols(&self) -> bool {
766        false
767    }
768
769    /// Returns an iterator over symbols in the public symbol table.
770    pub fn symbols(&self) -> SourceBundleSymbolIterator<'data> {
771        std::iter::empty()
772    }
773
774    /// Returns an ordered map of symbols in the symbol table.
775    pub fn symbol_map(&self) -> SymbolMap<'data> {
776        self.symbols().collect()
777    }
778
779    /// Determines whether this object contains debug information.
780    ///
781    /// Source bundles never have debug info.
782    pub fn has_debug_info(&self) -> bool {
783        false
784    }
785
786    /// Constructs a debugging session.
787    ///
788    /// A debugging session loads certain information from the object file and creates caches for
789    /// efficient access to various records in the debug information. Since this can be quite a
790    /// costly process, try to reuse the debugging session as long as possible.
791    pub fn debug_session(&self) -> Result<SourceBundleDebugSession<'data>, SourceBundleError> {
792        // NOTE: The `SourceBundleDebugSession` still needs interior mutability, so it still needs
793        // to carry its own Mutex. However that is still preferable to sharing the Mutex of the
794        // `SourceBundle`, which might be shared by multiple threads.
795        // The only thing here that really needs to be `mut` is the `Cursor` / `Seek` position.
796        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    /// Determines whether this object contains stack unwinding information.
813    pub fn has_unwind_info(&self) -> bool {
814        false
815    }
816
817    /// Determines whether this object contains embedded source.
818    pub fn has_sources(&self) -> bool {
819        true
820    }
821
822    /// Determines whether this object is malformed and was only partially parsed
823    pub fn is_malformed(&self) -> bool {
824        false
825    }
826
827    /// Returns the raw data of the source bundle.
828    pub fn data(&self) -> &'data [u8] {
829        self.data
830    }
831
832    /// Returns true if this source bundle contains no source code.
833    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
920/// An iterator yielding symbols from a source bundle.
921pub 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
930/// Debug session for SourceBundle objects.
931pub 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    /// Returns an iterator over all source files in this debug file.
940    pub fn files(&self) -> SourceBundleFileIterator<'_> {
941        SourceBundleFileIterator {
942            files: self.index.manifest.files.values(),
943        }
944    }
945
946    /// Returns an iterator over all functions in this debug file.
947    pub fn functions(&self) -> SourceBundleFunctionIterator<'_> {
948        std::iter::empty()
949    }
950
951    /// Get source by the path of a file in the bundle.
952    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    /// Looks up a source file descriptor.
981    ///
982    /// The file is looked up in both the embedded files and
983    /// in the included source link mappings, in that order.
984    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    /// See [DebugSession::source_by_path] for more information.
1009    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    /// Like [`source_by_path`](Self::source_by_path) but looks up by URL.
1017    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    /// Looks up some source by debug ID and file type.
1025    ///
1026    /// Lookups by [`DebugId`] require knowledge of the file that is supposed to be
1027    /// looked up as multiple files (one per type) can share the same debug ID.
1028    /// Special care needs to be taken about [`SourceFileType::IndexedRamBundle`]
1029    /// and [`SourceFileType::SourceMap`] which are different file types despite
1030    /// the name of it.
1031    ///
1032    /// # Note on Abstractions
1033    ///
1034    /// This method is currently not exposed via a standardized debug session
1035    /// as it's primarily used for the JavaScript processing system which uses
1036    /// different abstractions.
1037    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
1072/// An iterator over source files in a SourceBundle object.
1073pub 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
1089/// An iterator over functions in a SourceBundle object.
1090pub type SourceBundleFunctionIterator<'s> =
1091    std::iter::Empty<Result<Function<'s>, SourceBundleError>>;
1092
1093impl SourceBundleManifest {
1094    /// Creates a new, empty manifest.
1095    pub fn new() -> Self {
1096        Self::default()
1097    }
1098}
1099
1100/// Generates a normalized path for a file in the bundle.
1101///
1102/// This removes all special characters. The path in the bundle will mostly resemble the original
1103/// path, except for unsupported components.
1104fn 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
1112/// Normalizes all paths to follow the Linux standard of using forward slashes.
1113fn normalize_path(path: &str) -> String {
1114    path.replace('\\', "/")
1115}
1116
1117/// Contains information about a file skipped in the SourceBundleWriter
1118#[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    /// Returns the path of the skipped file.
1130    pub fn path(&self) -> &str {
1131        self.path
1132    }
1133
1134    /// Get the human-readable reason why the file was skipped
1135    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
1146/// Writer to create [`SourceBundles`].
1147///
1148/// Writers can either [create a new file] or be created from an [existing file]. Then, use
1149/// [`add_file`] to add files and finally call [`finish`] to flush the archive to
1150/// the underlying writer.
1151///
1152/// Note that dropping the writer without calling [`finish`] will result in an incomplete bundle.
1153///
1154/// ```no_run
1155/// # use std::fs::File;
1156/// # use symbolic_debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo};
1157/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1158/// let mut bundle = SourceBundleWriter::create("bundle.zip")?;
1159///
1160/// // Add file called "foo.txt"
1161/// let file = File::open("my_file.txt")?;
1162/// bundle.add_file("foo.txt", file, SourceFileInfo::default())?;
1163///
1164/// // Flush the bundle to disk
1165/// bundle.finish()?;
1166/// # Ok(()) }
1167/// ```
1168///
1169/// [`SourceBundles`]: struct.SourceBundle.html
1170/// [create a new file]: struct.SourceBundleWriter.html#method.create
1171/// [existing file]: struct.SourceBundleWriter.html#method.new
1172/// [`add_file`]: struct.SourceBundleWriter.html#method.add_file
1173/// [`finish`]: struct.SourceBundleWriter.html#method.finish
1174pub 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    // TODO: should we maybe acknowledge that its the year 2023 and switch to zstd eventually?
1186    // Though it obviously needs to be supported across the whole platform,
1187    // which does not seem to be the case for Python?
1188
1189    // Depending on `zip` crate feature flags, it might default to the current time.
1190    // Using an explicit `DateTime::default` gives us a deterministic `1980-01-01T00:00:00`.
1191    SimpleFileOptions::default().last_modified_time(zip::DateTime::default())
1192}
1193
1194impl<W> SourceBundleWriter<W>
1195where
1196    W: Seek + Write,
1197{
1198    /// Creates a bundle writer on the given file.
1199    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    /// Returns whether the bundle contains any files.
1214    pub fn is_empty(&self) -> bool {
1215        self.manifest.files.is_empty()
1216    }
1217
1218    /// This controls if source files should be scanned for Il2cpp-specific source annotations,
1219    /// and the referenced C# files should be bundled up as well.
1220    pub fn collect_il2cpp_sources(&mut self, collect_il2cpp: bool) {
1221        self.collect_il2cpp = collect_il2cpp;
1222    }
1223
1224    /// Sets a meta data attribute of the bundle.
1225    ///
1226    /// Attributes are flushed to the bundle when it is [finished]. Thus, they can be retrieved or
1227    /// changed at any time before flushing the writer.
1228    ///
1229    /// If the attribute was set before, the prior value is returned.
1230    ///
1231    /// [finished]: struct.SourceBundleWriter.html#method.remove_attribute
1232    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    /// Removes a meta data attribute of the bundle.
1241    ///
1242    /// If the attribute was set, the last value is returned.
1243    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    /// Returns the value of a meta data attribute.
1251    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    /// Determines whether a file at the given path has been added already.
1262    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    /// Adds a file and its info to the bundle.
1271    ///
1272    /// Only files containing valid UTF-8 are accepted.
1273    ///
1274    /// Multiple files can be added at the same path. For the first duplicate, a counter will be
1275    /// appended to the file name. Any subsequent duplicate increases that counter. For example:
1276    ///
1277    /// ```no_run
1278    /// # use std::fs::File;
1279    /// # use symbolic_debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo};
1280    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1281    /// let mut bundle = SourceBundleWriter::create("bundle.zip")?;
1282    ///
1283    /// // Add file at "foo.txt"
1284    /// bundle.add_file("foo.txt", File::open("my_duplicate.txt")?, SourceFileInfo::default())?;
1285    /// assert!(bundle.has_file("foo.txt"));
1286    ///
1287    /// // Add duplicate at "foo.txt.1"
1288    /// bundle.add_file("foo.txt", File::open("my_duplicate.txt")?, SourceFileInfo::default())?;
1289    /// assert!(bundle.has_file("foo.txt.1"));
1290    /// # Ok(()) }
1291    /// ```
1292    ///
1293    /// Returns `Ok(true)` if the file was successfully added, or `Ok(false)` if the file aready
1294    /// existed. Otherwise, an error is returned if writing the file fails.
1295    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                // ErrorKind::InvalidData is returned by Utf8Reader when the file is not valid UTF-8.
1321                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    /// Calls add_file, and handles any ReadFailed errors by calling the skipped_file_callback.
1336    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    /// Set a callback, which is called for every file that is skipped from being included in the
1362    /// source bundle. The callback receives information about the file being skipped.
1363    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    /// Writes a single object into the bundle.
1372    ///
1373    /// Returns `Ok(true)` if any source files were added to the bundle, or `Ok(false)` if no
1374    /// sources could be resolved. Otherwise, an error is returned if writing the bundle fails.
1375    ///
1376    /// This finishes the source bundle and flushes the underlying writer.
1377    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    /// Writes a single object into the bundle.
1390    ///
1391    /// Returns `Ok(true)` if any source files were added to the bundle, or `Ok(false)` if no
1392    /// sources could be resolved. Otherwise, an error is returned if writing the bundle fails.
1393    ///
1394    /// This finishes the source bundle and flushes the underlying writer.
1395    ///
1396    /// Before a file is written a callback is invoked which can return `false` to skip a file.
1397    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        // Read source files from the local filesystem.
1409        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    /// Writes a single object into the bundle, obtaining source file contents
1416    /// from `provider`.
1417    ///
1418    /// This is the filesystem-free counterpart of [`Self::write_object_with_filter`],
1419    /// for environments without filesystem access (e.g. WebAssembly): enumerate
1420    /// the object's referenced source paths via its debug session, read them
1421    /// however the host can, and return a reader from `provider` (returning
1422    /// `None` skips a file). Each referenced path is requested at most once.
1423    ///
1424    /// Returns the underlying writer and whether if any source files were added to the bundle.
1425    ///
1426    /// This finishes the source bundle and flushes the underlying writer.
1427    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            // Read the whole source up front so it can be scanned for il2cpp
1465            // references before being added (matching the historical behavior).
1466            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                    // Need to aggressively read the source here to store it in `referenced_files`.
1487                    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                    // No need to aggressively consume the source here, we can use the `Read` instance.
1494                    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    /// Writes the manifest to the bundle and flushes the underlying file handle.
1523    pub fn finish(self) -> Result<(), SourceBundleError> {
1524        self.do_finish().map(drop)
1525    }
1526
1527    /// Writes the manifest to the bundle and flushes the underlying file handle.
1528    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    /// Returns the full path for a file within the source bundle.
1538    fn file_path(&self, path: &str) -> String {
1539        format!("{FILES_PATH}/{path}")
1540    }
1541
1542    /// Returns a unique path for a file.
1543    ///
1544    /// Returns the path if the file does not exist already. Otherwise, a counter is appended to the
1545    /// file path (e.g. `.1`, `.2`, etc).
1546    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    /// Flushes the manifest file to the bundle.
1565    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
1577/// Processes the `source`, looking for `il2cpp` specific reference comments.
1578///
1579/// The files referenced by those comments are added to the `referenced_files` Set.
1580fn 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    /// Create a bundle writer that writes its output to the given path.
1598    ///
1599    /// If the file does not exist at the given path, it is created. If the file does exist, it is
1600    /// overwritten.
1601    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        // well, a source bundle itself is an `ObjectLike` :-)
1868        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        // write file contents to temp files
1884        cpp_file.write_all(cpp_contents.as_bytes())?;
1885        cs_file.write_all(b"some C# source")?;
1886
1887        // write the actual source bundle based on the `object`
1888        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        // and collect all the included files
1897        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        // This should be resolved by source link
1962        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        // This should be resolved by embedded file, even though the link also exists
1972        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        // All object referenced files must be in the bundle.
2023        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        // Only referenced files are allowed to be in the bundle, no extras.
2032        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}