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
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/// An error returned when handling [`SourceBundle`](struct.SourceBundle.html).
119#[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    /// Creates a new SourceBundle error from a known kind of error as well as an arbitrary error
129    /// payload.
130    ///
131    /// This function is used to generically create source bundle errors which do not originate from
132    /// `symbolic` itself. The `source` argument is an arbitrary payload which will be contained in
133    /// this [`SourceBundleError`].
134    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    /// Returns the corresponding [`SourceBundleErrorKind`] for this error.
143    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
154/// Trims matching suffices of a string in-place.
155fn 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/// The type of a [`SourceFileInfo`](struct.SourceFileInfo.html).
164#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
165#[serde(rename_all = "snake_case")]
166pub enum SourceFileType {
167    /// Regular source file.
168    Source,
169
170    /// Minified source code.
171    MinifiedSource,
172
173    /// JavaScript sourcemap.
174    SourceMap,
175
176    /// Indexed JavaScript RAM bundle.
177    IndexedRamBundle,
178}
179
180impl SourceFileType {
181    /// Returns the name of the source file type.
182    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/// Meta data information of a file in a [`SourceBundle`](struct.SourceBundle.html).
199#[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
218/// Helper to ensure that header keys are normalized to lowercase
219fn 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    /// Creates default file information.
240    pub fn new() -> Self {
241        Self::default()
242    }
243
244    /// Returns the type of the source file.
245    pub fn ty(&self) -> Option<SourceFileType> {
246        self.ty
247    }
248
249    /// Sets the type of the source file.
250    pub fn set_ty(&mut self, ty: SourceFileType) {
251        self.ty = Some(ty);
252    }
253
254    /// Returns the absolute file system path of this file.
255    pub fn path(&self) -> Option<&str> {
256        match self.path.as_str() {
257            "" => None,
258            path => Some(path),
259        }
260    }
261
262    /// Sets the absolute file system path of this file.
263    pub fn set_path(&mut self, path: String) {
264        self.path = path;
265    }
266
267    /// Returns the web URL that of this file.
268    pub fn url(&self) -> Option<&str> {
269        match self.url.as_str() {
270            "" => None,
271            url => Some(url),
272        }
273    }
274
275    /// Sets the web URL of this file.
276    pub fn set_url(&mut self, url: String) {
277        self.url = url;
278    }
279
280    /// Iterates over all attributes represented as headers.
281    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    /// Retrieves the specified header, if it exists.
286    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    /// Adds a custom attribute following header conventions.
301    ///
302    /// Header keys are converted to lowercase before writing as this is
303    /// the canonical format for headers. However, the file format does
304    /// support headers to be case insensitive and they will be lower cased
305    /// upon reading.
306    ///
307    /// Headers on files are primarily be used to add auxiliary information
308    /// to files.  The following headers are known and processed:
309    ///
310    /// - `debug-id`: see [`debug_id`](Self::debug_id)
311    /// - `sourcemap` (and `x-sourcemap`): see [`source_mapping_url`](Self::source_mapping_url)
312    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    /// The debug ID of this minified source or sourcemap if it has any.
321    ///
322    /// Files have a debug ID if they have a header with the key `debug-id`.
323    /// At present debug IDs in source bundles are only ever given to minified
324    /// source files.
325    pub fn debug_id(&self) -> Option<DebugId> {
326        self.header("debug-id").and_then(|x| x.parse().ok())
327    }
328
329    /// The source mapping URL of the given minified source.
330    ///
331    /// Files have a source mapping URL if they have a header with the
332    /// key `sourcemap` (or the `x-sourcemap` legacy header) as part the
333    /// source map specification.
334    pub fn source_mapping_url(&self) -> Option<&str> {
335        self.header("sourcemap")
336            .or_else(|| self.header("x-sourcemap"))
337    }
338
339    /// Returns `true` if this instance does not carry any information.
340    pub fn is_empty(&self) -> bool {
341        self.path.is_empty() && self.ty.is_none() && self.headers.is_empty()
342    }
343}
344
345/// A descriptor that provides information about a source file.
346///
347/// This descriptor is returned from [`source_by_path`](DebugSession::source_by_path)
348/// and friends.
349///
350/// This descriptor holds information that can be used to retrieve information
351/// about the source file.  A descriptor has to have at least one of the following
352/// to be valid:
353///
354/// - [`contents`](Self::contents)
355/// - [`url`](Self::url)
356/// - [`debug_id`](Self::debug_id)
357///
358/// Debug sessions are not permitted to return invalid source file descriptors.
359pub 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    /// Creates an embedded source file descriptor.
367    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    /// Creates an remote source file descriptor.
379    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    /// The type of the file the descriptor points to.
388    pub fn ty(&self) -> SourceFileType {
389        self.file_info
390            .and_then(|x| x.ty())
391            .unwrap_or(SourceFileType::Source)
392    }
393
394    /// The contents of the source file as string, if it's available.
395    ///
396    /// Portable PDBs for instance will often have source information, but rely on
397    /// remote file fetching via Sourcelink to get to the contents.  In that case
398    /// a file descriptor is created, but the contents are missing and instead the
399    /// [`url`](Self::url) can be used.
400    pub fn contents(&self) -> Option<&str> {
401        self.contents.as_deref()
402    }
403
404    /// The contents of the source file as string, if it's available.
405    ///
406    /// This unwraps the [`SourceFileDescriptor`] directly and might avoid a copy of `contents`
407    /// later on.
408    pub fn into_contents(self) -> Option<Cow<'a, str>> {
409        self.contents
410    }
411
412    /// If available returns the URL of this source.
413    ///
414    /// For certain files this is the canoncial URL of where the file is placed.  This
415    /// for instance is the case for minified JavaScript files or source maps which might
416    /// have a canonical URL.  In case of portable PDBs this is also where you would fetch
417    /// the source code from if source links are used.
418    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    /// If available returns the file path of this source.
427    ///
428    /// For source bundles that are a companion file to a debug file, this is the canonical
429    /// path of the source file.
430    pub fn path(&self) -> Option<&str> {
431        self.file_info.and_then(|x| x.path())
432    }
433
434    /// The debug ID of the file if available.
435    ///
436    /// For source maps or minified source files symbolic supports embedded debug IDs.  If they
437    /// are in use, the debug ID is returned from here.  The debug ID is discovered from the
438    /// file's `debug-id` header or the embedded `debugId` reference in the file body.
439    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    /// The source mapping URL reference of the file.
456    ///
457    /// This is used to refer to a source map from a minified file.  Only minified source files
458    /// will have a relationship to a source map.  The source mapping is discovered either from
459    /// a `sourcemap` header in the source manifest, or the `sourceMappingURL` reference in the body.
460    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/// Version number of a [`SourceBundle`](struct.SourceBundle.html).
477#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
478pub struct SourceBundleVersion(pub u32);
479
480impl SourceBundleVersion {
481    /// Creates a new source bundle version.
482    pub fn new(version: u32) -> Self {
483        Self(version)
484    }
485
486    /// Determines whether this version can be handled.
487    ///
488    /// This will return `false`, if the version is newer than what is supported by this library
489    /// version.
490    pub fn is_valid(self) -> bool {
491        self.0 <= BUNDLE_VERSION
492    }
493
494    /// Returns whether the given bundle is at the latest supported versino.
495    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/// Binary header of the source bundle archive.
507///
508/// This header precedes the ZIP archive. It is used to detect these files on the file system.
509#[repr(C, packed)]
510#[derive(Clone, Copy, Debug)]
511struct SourceBundleHeader {
512    /// Magic bytes header.
513    pub magic: [u8; 4],
514
515    /// Version of the bundle.
516    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/// Manifest of a [`SourceBundle`] containing information on its contents.
536///
537/// [`SourceBundle`]: struct.SourceBundle.html
538#[derive(Clone, Debug, Default, Serialize, Deserialize)]
539struct SourceBundleManifest {
540    /// Descriptors for all files in this bundle.
541    #[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    /// Arbitrary attributes to include in the bundle.
548    #[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
593/// A bundle of source code files.
594///
595/// To create a source bundle, see [`SourceBundleWriter`]. For more information, see the [module
596/// level documentation].
597///
598/// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
599/// [module level documentation]: index.html
600pub 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    /// Tests whether the buffer could contain a `SourceBundle`.
625    pub fn test(bytes: &[u8]) -> bool {
626        bytes.starts_with(&BUNDLE_MAGIC)
627    }
628
629    /// Tries to parse a `SourceBundle` from the given slice.
630    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    /// Returns the version of this source bundle format.
644    pub fn version(&self) -> SourceBundleVersion {
645        SourceBundleVersion(BUNDLE_VERSION)
646    }
647
648    /// The container file format, which is always `FileFormat::SourceBundle`.
649    pub fn file_format(&self) -> FileFormat {
650        FileFormat::SourceBundle
651    }
652
653    /// The code identifier of this object.
654    ///
655    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
656    /// in the [`SourceBundleWriter`] by setting the `"code_id"` attribute.
657    ///
658    /// [`ObjectLike`]: ../trait.ObjectLike.html
659    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
660    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    /// The code identifier of this object.
669    ///
670    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
671    /// in the [`SourceBundleWriter`] by setting the `"debug_id"` attribute.
672    ///
673    /// [`ObjectLike`]: ../trait.ObjectLike.html
674    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
675    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    /// The debug file name of this object.
685    ///
686    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
687    /// in the [`SourceBundleWriter`] by setting the `"object_name"` attribute.
688    ///
689    /// [`ObjectLike`]: ../trait.ObjectLike.html
690    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
691    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    /// The CPU architecture of this object.
700    ///
701    /// This is only set if the source bundle was created from an [`ObjectLike`]. It can also be set
702    /// in the [`SourceBundleWriter`] by setting the `"arch"` attribute.
703    ///
704    /// [`ObjectLike`]: ../trait.ObjectLike.html
705    /// [`SourceBundleWriter`]: struct.SourceBundleWriter.html
706    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    /// The kind of this object.
716    ///
717    /// Because source bundles do not contain real objects this is always `ObjectKind::None`.
718    fn kind(&self) -> ObjectKind {
719        ObjectKind::Sources
720    }
721
722    /// The address at which the image prefers to be loaded into memory.
723    ///
724    /// Because source bundles do not contain this information is always `0`.
725    pub fn load_address(&self) -> u64 {
726        0
727    }
728
729    /// Determines whether this object exposes a public symbol table.
730    ///
731    /// Source bundles never have symbols.
732    pub fn has_symbols(&self) -> bool {
733        false
734    }
735
736    /// Returns an iterator over symbols in the public symbol table.
737    pub fn symbols(&self) -> SourceBundleSymbolIterator<'data> {
738        std::iter::empty()
739    }
740
741    /// Returns an ordered map of symbols in the symbol table.
742    pub fn symbol_map(&self) -> SymbolMap<'data> {
743        self.symbols().collect()
744    }
745
746    /// Determines whether this object contains debug information.
747    ///
748    /// Source bundles never have debug info.
749    pub fn has_debug_info(&self) -> bool {
750        false
751    }
752
753    /// Constructs a debugging session.
754    ///
755    /// A debugging session loads certain information from the object file and creates caches for
756    /// efficient access to various records in the debug information. Since this can be quite a
757    /// costly process, try to reuse the debugging session as long as possible.
758    pub fn debug_session(&self) -> Result<SourceBundleDebugSession<'data>, SourceBundleError> {
759        // NOTE: The `SourceBundleDebugSession` still needs interior mutability, so it still needs
760        // to carry its own Mutex. However that is still preferable to sharing the Mutex of the
761        // `SourceBundle`, which might be shared by multiple threads.
762        // The only thing here that really needs to be `mut` is the `Cursor` / `Seek` position.
763        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    /// Determines whether this object contains stack unwinding information.
779    pub fn has_unwind_info(&self) -> bool {
780        false
781    }
782
783    /// Determines whether this object contains embedded source.
784    pub fn has_sources(&self) -> bool {
785        true
786    }
787
788    /// Determines whether this object is malformed and was only partially parsed
789    pub fn is_malformed(&self) -> bool {
790        false
791    }
792
793    /// Returns the raw data of the source bundle.
794    pub fn data(&self) -> &'data [u8] {
795        self.data
796    }
797
798    /// Returns true if this source bundle contains no source code.
799    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
886/// An iterator yielding symbols from a source bundle.
887pub 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
896/// Debug session for SourceBundle objects.
897pub 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    /// Returns an iterator over all source files in this debug file.
905    pub fn files(&self) -> SourceBundleFileIterator<'_> {
906        SourceBundleFileIterator {
907            files: self.index.manifest.files.values(),
908        }
909    }
910
911    /// Returns an iterator over all functions in this debug file.
912    pub fn functions(&self) -> SourceBundleFunctionIterator<'_> {
913        std::iter::empty()
914    }
915
916    /// Get source by the path of a file in the bundle.
917    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    /// Looks up a source file descriptor.
930    ///
931    /// The file is looked up in both the embedded files and
932    /// in the included source link mappings, in that order.
933    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    /// See [DebugSession::source_by_path] for more information.
956    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    /// Like [`source_by_path`](Self::source_by_path) but looks up by URL.
964    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    /// Looks up some source by debug ID and file type.
972    ///
973    /// Lookups by [`DebugId`] require knowledge of the file that is supposed to be
974    /// looked up as multiple files (one per type) can share the same debug ID.
975    /// Special care needs to be taken about [`SourceFileType::IndexedRamBundle`]
976    /// and [`SourceFileType::SourceMap`] which are different file types despite
977    /// the name of it.
978    ///
979    /// # Note on Abstractions
980    ///
981    /// This method is currently not exposed via a standardized debug session
982    /// as it's primarily used for the JavaScript processing system which uses
983    /// different abstractions.
984    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
1019/// An iterator over source files in a SourceBundle object.
1020pub 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
1036/// An iterator over functions in a SourceBundle object.
1037pub type SourceBundleFunctionIterator<'s> =
1038    std::iter::Empty<Result<Function<'s>, SourceBundleError>>;
1039
1040impl SourceBundleManifest {
1041    /// Creates a new, empty manifest.
1042    pub fn new() -> Self {
1043        Self::default()
1044    }
1045}
1046
1047/// Generates a normalized path for a file in the bundle.
1048///
1049/// This removes all special characters. The path in the bundle will mostly resemble the original
1050/// path, except for unsupported components.
1051fn 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
1059/// Normalizes all paths to follow the Linux standard of using forward slashes.
1060fn normalize_path(path: &str) -> String {
1061    path.replace('\\', "/")
1062}
1063
1064/// Contains information about a file skipped in the SourceBundleWriter
1065#[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    /// Returns the path of the skipped file.
1077    pub fn path(&self) -> &str {
1078        self.path
1079    }
1080
1081    /// Get the human-readable reason why the file was skipped
1082    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
1093/// Writer to create [`SourceBundles`].
1094///
1095/// Writers can either [create a new file] or be created from an [existing file]. Then, use
1096/// [`add_file`] to add files and finally call [`finish`] to flush the archive to
1097/// the underlying writer.
1098///
1099/// Note that dropping the writer without calling [`finish`] will result in an incomplete bundle.
1100///
1101/// ```no_run
1102/// # use std::fs::File;
1103/// # use symbolic_debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo};
1104/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1105/// let mut bundle = SourceBundleWriter::create("bundle.zip")?;
1106///
1107/// // Add file called "foo.txt"
1108/// let file = File::open("my_file.txt")?;
1109/// bundle.add_file("foo.txt", file, SourceFileInfo::default())?;
1110///
1111/// // Flush the bundle to disk
1112/// bundle.finish()?;
1113/// # Ok(()) }
1114/// ```
1115///
1116/// [`SourceBundles`]: struct.SourceBundle.html
1117/// [create a new file]: struct.SourceBundleWriter.html#method.create
1118/// [existing file]: struct.SourceBundleWriter.html#method.new
1119/// [`add_file`]: struct.SourceBundleWriter.html#method.add_file
1120/// [`finish`]: struct.SourceBundleWriter.html#method.finish
1121pub 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    // TODO: should we maybe acknowledge that its the year 2023 and switch to zstd eventually?
1133    // Though it obviously needs to be supported across the whole platform,
1134    // which does not seem to be the case for Python?
1135
1136    // Depending on `zip` crate feature flags, it might default to the current time.
1137    // Using an explicit `DateTime::default` gives us a deterministic `1980-01-01T00:00:00`.
1138    SimpleFileOptions::default().last_modified_time(zip::DateTime::default())
1139}
1140
1141impl<W> SourceBundleWriter<W>
1142where
1143    W: Seek + Write,
1144{
1145    /// Creates a bundle writer on the given file.
1146    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    /// Returns whether the bundle contains any files.
1161    pub fn is_empty(&self) -> bool {
1162        self.manifest.files.is_empty()
1163    }
1164
1165    /// This controls if source files should be scanned for Il2cpp-specific source annotations,
1166    /// and the referenced C# files should be bundled up as well.
1167    pub fn collect_il2cpp_sources(&mut self, collect_il2cpp: bool) {
1168        self.collect_il2cpp = collect_il2cpp;
1169    }
1170
1171    /// Sets a meta data attribute of the bundle.
1172    ///
1173    /// Attributes are flushed to the bundle when it is [finished]. Thus, they can be retrieved or
1174    /// changed at any time before flushing the writer.
1175    ///
1176    /// If the attribute was set before, the prior value is returned.
1177    ///
1178    /// [finished]: struct.SourceBundleWriter.html#method.remove_attribute
1179    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    /// Removes a meta data attribute of the bundle.
1188    ///
1189    /// If the attribute was set, the last value is returned.
1190    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    /// Returns the value of a meta data attribute.
1198    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    /// Determines whether a file at the given path has been added already.
1209    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    /// Adds a file and its info to the bundle.
1218    ///
1219    /// Only files containing valid UTF-8 are accepted.
1220    ///
1221    /// Multiple files can be added at the same path. For the first duplicate, a counter will be
1222    /// appended to the file name. Any subsequent duplicate increases that counter. For example:
1223    ///
1224    /// ```no_run
1225    /// # use std::fs::File;
1226    /// # use symbolic_debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo};
1227    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1228    /// let mut bundle = SourceBundleWriter::create("bundle.zip")?;
1229    ///
1230    /// // Add file at "foo.txt"
1231    /// bundle.add_file("foo.txt", File::open("my_duplicate.txt")?, SourceFileInfo::default())?;
1232    /// assert!(bundle.has_file("foo.txt"));
1233    ///
1234    /// // Add duplicate at "foo.txt.1"
1235    /// bundle.add_file("foo.txt", File::open("my_duplicate.txt")?, SourceFileInfo::default())?;
1236    /// assert!(bundle.has_file("foo.txt.1"));
1237    /// # Ok(()) }
1238    /// ```
1239    ///
1240    /// Returns `Ok(true)` if the file was successfully added, or `Ok(false)` if the file aready
1241    /// existed. Otherwise, an error is returned if writing the file fails.
1242    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                // ErrorKind::InvalidData is returned by Utf8Reader when the file is not valid UTF-8.
1268                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    /// Calls add_file, and handles any ReadFailed errors by calling the skipped_file_callback.
1283    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    /// Set a callback, which is called for every file that is skipped from being included in the
1309    /// source bundle. The callback receives information about the file being skipped.
1310    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    /// Writes a single object into the bundle.
1319    ///
1320    /// Returns `Ok(true)` if any source files were added to the bundle, or `Ok(false)` if no
1321    /// sources could be resolved. Otherwise, an error is returned if writing the bundle fails.
1322    ///
1323    /// This finishes the source bundle and flushes the underlying writer.
1324    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    /// Writes a single object into the bundle.
1337    ///
1338    /// Returns `Ok(true)` if any source files were added to the bundle, or `Ok(false)` if no
1339    /// sources could be resolved. Otherwise, an error is returned if writing the bundle fails.
1340    ///
1341    /// This finishes the source bundle and flushes the underlying writer.
1342    ///
1343    /// Before a file is written a callback is invoked which can return `false` to skip a file.
1344    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        // Read source files from the local filesystem.
1356        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    /// Writes a single object into the bundle, obtaining source file contents
1363    /// from `provider`.
1364    ///
1365    /// This is the filesystem-free counterpart of [`Self::write_object_with_filter`],
1366    /// for environments without filesystem access (e.g. WebAssembly): enumerate
1367    /// the object's referenced source paths via its debug session, read them
1368    /// however the host can, and return a reader from `provider` (returning
1369    /// `None` skips a file). Each referenced path is requested at most once.
1370    ///
1371    /// Returns the underlying writer and whether if any source files were added to the bundle.
1372    ///
1373    /// This finishes the source bundle and flushes the underlying writer.
1374    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            // Read the whole source up front so it can be scanned for il2cpp
1412            // references before being added (matching the historical behavior).
1413            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                    // Need to aggressively read the source here to store it in `referenced_files`.
1434                    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                    // No need to aggressively consume the source here, we can use the `Read` instance.
1441                    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    /// Writes the manifest to the bundle and flushes the underlying file handle.
1470    pub fn finish(self) -> Result<(), SourceBundleError> {
1471        self.do_finish().map(drop)
1472    }
1473
1474    /// Writes the manifest to the bundle and flushes the underlying file handle.
1475    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    /// Returns the full path for a file within the source bundle.
1485    fn file_path(&self, path: &str) -> String {
1486        format!("{FILES_PATH}/{path}")
1487    }
1488
1489    /// Returns a unique path for a file.
1490    ///
1491    /// Returns the path if the file does not exist already. Otherwise, a counter is appended to the
1492    /// file path (e.g. `.1`, `.2`, etc).
1493    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    /// Flushes the manifest file to the bundle.
1512    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
1524/// Processes the `source`, looking for `il2cpp` specific reference comments.
1525///
1526/// The files referenced by those comments are added to the `referenced_files` Set.
1527fn 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    /// Create a bundle writer that writes its output to the given path.
1545    ///
1546    /// If the file does not exist at the given path, it is created. If the file does exist, it is
1547    /// overwritten.
1548    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        // well, a source bundle itself is an `ObjectLike` :-)
1815        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        // write file contents to temp files
1831        cpp_file.write_all(cpp_contents.as_bytes())?;
1832        cs_file.write_all(b"some C# source")?;
1833
1834        // write the actual source bundle based on the `object`
1835        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        // and collect all the included files
1844        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        // This should be resolved by source link
1909        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        // This should be resolved by embedded file, even though the link also exists
1919        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        // All object referenced files must be in the bundle.
1970        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        // Only referenced files are allowed to be in the bundle, no extras.
1979        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}