Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
**Features**:

- Added debug IDs to source bundle JavaScript files and source maps. ([#762](https://github.com/getsentry/symbolic/pull/762))
- Add support for embedded debug IDs in minified files ([#765](https://github.com/getsentry/symbolic/pull/765))

**Breaking changes**:

Expand Down
7 changes: 5 additions & 2 deletions symbolic-debuginfo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ exclude = ["tests/**/*"]
all-features = true

[features]
default = ["breakpad", "elf", "macho", "ms", "ppdb", "sourcebundle", "wasm"]
default = ["breakpad", "elf", "macho", "ms", "ppdb", "sourcebundle", "js", "wasm"]
# Breakpad text format parsing and processing
breakpad = ["nom", "nom-supreme", "regex"]
# DWARF processing.
Expand Down Expand Up @@ -70,8 +70,11 @@ sourcebundle = [
"regex",
"serde_json",
"zip",
"debugid/serde"
"js",
"debugid/serde",
]
# JavaScript stuff
js = []
# WASM processing
wasm = ["bitvec", "dwarf", "wasmparser"]

Expand Down
40 changes: 40 additions & 0 deletions symbolic-debuginfo/src/js.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Utilities specifically for working with JavaScript specific debug info.
//!
//! This for the most part only contains utility functions to parse references
//! out of minified JavaScript files and source maps. For actually working
//! with source maps this module is insufficient.

use debugid::DebugId;
use serde::Deserialize;

/// Parses a sourceMappingURL comment in a file to discover a sourcemap reference.
pub fn discover_sourcemaps_location(contents: &str) -> Option<&str> {
for line in contents.lines().rev() {
if line.starts_with("//# sourceMappingURL=") || line.starts_with("//@ sourceMappingURL=") {
return Some(line[21..].trim());
}
}
None
}

/// Quickly reads the embedded `debug_id` key from a source map.
pub fn discover_sourcemap_embedded_debug_id(contents: &str) -> Option<DebugId> {
#[derive(Deserialize)]
struct DebugIdInSourceMap {
debug_id: Option<DebugId>,
}
Comment on lines +22 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, haven't seen this trick before.


serde_json::from_str(contents)
.ok()
.and_then(|x: DebugIdInSourceMap| x.debug_id)
}

/// Parses a `debugId` comment in a file to discover a sourcemap's debug ID.
pub fn discover_debug_id(contents: &str) -> Option<DebugId> {
for line in contents.lines().rev() {
if let Some(rest) = line.strip_prefix("//# debugId=") {
return rest.trim().parse().ok();
}
}
None
}
2 changes: 2 additions & 0 deletions symbolic-debuginfo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub mod elf;
pub mod function_builder;
#[cfg(feature = "ms")]
pub(crate) mod function_stack;
#[cfg(feature = "js")]
pub mod js;
#[cfg(feature = "macho")]
pub mod macho;
#[cfg(feature = "ms")]
Expand Down
116 changes: 91 additions & 25 deletions symbolic-debuginfo/src/sourcebundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ use zip::{write::FileOptions, ZipWriter};
use symbolic_common::{Arch, AsSelf, CodeId, DebugId};

use crate::base::*;
use crate::js::{
discover_debug_id, discover_sourcemap_embedded_debug_id, discover_sourcemaps_location,
};
use crate::{DebugSession, ObjectKind, ObjectLike};

/// Magic bytes of a source bundle. They are prepended to the ZIP file.
Expand Down Expand Up @@ -400,38 +403,37 @@ impl<'a> SourceFileDescriptor<'a> {
/// The debug ID of the file if available.
///
/// For source maps or minified source files symbolic supports embedded debug IDs. If they
/// are in use, the debug ID is returned from here.
/// are in use, the debug ID is returned from here. The debug ID is discovered from the
/// file's `debug-id` header or the embedded `debugId` reference in the file body.
pub fn debug_id(&self) -> Option<DebugId> {
self.file_info.and_then(|x| x.debug_id())
self.file_info.and_then(|x| x.debug_id()).or_else(|| {
if matches!(self.ty(), SourceFileType::MinifiedSource) {
self.contents().and_then(discover_debug_id)
} else if matches!(self.ty(), SourceFileType::SourceMap) {
self.contents()
.and_then(discover_sourcemap_embedded_debug_id)
} else {
None
}
})
}

/// The source mapping URL reference of the file.
///
/// This is used to refer to a source map from a minified file. Only minified source files
/// will have a relationship to a source map.
/// will have a relationship to a source map. The source mapping is discovered either from
/// a `sourcemap` header in the source manifest, or the `sourceMappingURL` reference in the body.
pub fn source_mapping_url(&self) -> Option<&str> {
if let Some(file_info) = self.file_info {
if let Some(url) = file_info.source_mapping_url() {
return Some(url);
}
}
if let Some(ref contents) = self.contents {
if let Some(url) = discover_sourcemaps_location(contents) {
return Some(url);
}
}
None
}
}

/// Parses a sourceMappingURL comment in a file to discover a sourcemap reference.
fn discover_sourcemaps_location(contents: &str) -> Option<&str> {
for line in contents.lines().rev() {
if line.starts_with("//# sourceMappingURL=") || line.starts_with("//@ sourceMappingURL=") {
return Some(line[21..].trim());
}
self.file_info
.and_then(|x| x.source_mapping_url())
.or_else(|| {
if matches!(self.ty(), SourceFileType::MinifiedSource) {
self.contents().and_then(discover_sourcemaps_location)
} else {
None
}
})
}
None
}

/// Version number of a [`SourceBundle`](struct.SourceBundle.html).
Expand Down Expand Up @@ -1416,7 +1418,7 @@ mod tests {
info.set_ty(SourceFileType::MinifiedSource);
bundle.add_file(
"bar.js",
&b"filecontents\n//@ sourceMappingURL=bar.js.map"[..],
&b"filecontents\n//# sourceMappingURL=bar.js.map"[..],
info,
)?;

Expand All @@ -1436,6 +1438,70 @@ mod tests {
Ok(())
}

#[test]
fn test_source_embedded_debug_id() -> Result<(), SourceBundleError> {
let mut writer = Cursor::new(Vec::new());
let mut bundle = SourceBundleWriter::start(&mut writer)?;

let mut info = SourceFileInfo::default();
info.set_url("https://example.com/bar.min.js".into());
info.set_ty(SourceFileType::MinifiedSource);
bundle.add_file(
"bar.js",
&b"filecontents\n//# debugId=5b65abfb23384f0bb3b964c8f734d43f"[..],
info,
)?;

bundle.finish()?;
let bundle_bytes = writer.into_inner();
let bundle = SourceBundle::parse(&bundle_bytes)?;

let sess = bundle.debug_session().unwrap();
let f = sess
.source_by_url("https://example.com/bar.min.js")
.unwrap()
.expect("should exist");
assert_eq!(f.ty(), SourceFileType::MinifiedSource);
assert_eq!(
f.debug_id(),
Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
);

Ok(())
}

#[test]
fn test_sourcemap_embedded_debug_id() -> Result<(), SourceBundleError> {
let mut writer = Cursor::new(Vec::new());
let mut bundle = SourceBundleWriter::start(&mut writer)?;

let mut info = SourceFileInfo::default();
info.set_url("https://example.com/bar.js.map".into());
info.set_ty(SourceFileType::SourceMap);
bundle.add_file(
"bar.js.map",
&br#"{"debug_id": "5b65abfb-2338-4f0b-b3b9-64c8f734d43f"}"#[..],
info,
)?;

bundle.finish()?;
let bundle_bytes = writer.into_inner();
let bundle = SourceBundle::parse(&bundle_bytes)?;

let sess = bundle.debug_session().unwrap();
let f = sess
.source_by_url("https://example.com/bar.js.map")
.unwrap()
.expect("should exist");
assert_eq!(f.ty(), SourceFileType::SourceMap);
assert_eq!(
f.debug_id(),
Some("5b65abfb-2338-4f0b-b3b9-64c8f734d43f".parse().unwrap())
);

Ok(())
}

#[test]
fn test_il2cpp_reference() -> Result<(), Box<dyn std::error::Error>> {
let mut cpp_file = NamedTempFile::new()?;
Expand Down