From 1245f6c99c0d993acd211b8e8c0477c34c72adc8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 21 Feb 2023 15:33:19 +0100 Subject: [PATCH 1/4] feat: Add support for embedded debug IDs in minified files --- symbolic-debuginfo/src/sourcebundle.rs | 78 +++++++++++++++++++++----- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/symbolic-debuginfo/src/sourcebundle.rs b/symbolic-debuginfo/src/sourcebundle.rs index 82f4052b8..d871eda57 100644 --- a/symbolic-debuginfo/src/sourcebundle.rs +++ b/symbolic-debuginfo/src/sourcebundle.rs @@ -400,27 +400,33 @@ 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 { - 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 { + 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 + 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 + } + }) } } @@ -434,6 +440,16 @@ fn discover_sourcemaps_location(contents: &str) -> Option<&str> { None } +/// Parses a debugId comment in a file to discover a sourcemap debug ID. +fn discover_debug_id(contents: &str) -> Option { + for line in contents.lines().rev() { + if let Some(rest) = line.strip_prefix("//# debugId=") { + return rest.trim().parse().ok(); + } + } + None +} + /// Version number of a [`SourceBundle`](struct.SourceBundle.html). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub struct SourceBundleVersion(pub u32); @@ -1416,7 +1432,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, )?; @@ -1436,6 +1452,38 @@ mod tests { Ok(()) } + #[test] + fn test_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_il2cpp_reference() -> Result<(), Box> { let mut cpp_file = NamedTempFile::new()?; From 3e0391af87d30c20b330d1aa89cc57c4db5726f2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 21 Feb 2023 15:48:36 +0100 Subject: [PATCH 2/4] Expose parsing functions independently --- symbolic-debuginfo/Cargo.toml | 7 ++- symbolic-debuginfo/src/js.rs | 40 +++++++++++++++++ symbolic-debuginfo/src/lib.rs | 2 + symbolic-debuginfo/src/sourcebundle.rs | 60 +++++++++++++++++--------- 4 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 symbolic-debuginfo/src/js.rs diff --git a/symbolic-debuginfo/Cargo.toml b/symbolic-debuginfo/Cargo.toml index 88da29d34..aef4a5c68 100644 --- a/symbolic-debuginfo/Cargo.toml +++ b/symbolic-debuginfo/Cargo.toml @@ -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. @@ -70,8 +70,11 @@ sourcebundle = [ "regex", "serde_json", "zip", - "debugid/serde" + "js", + "debugid/serde", ] +# JavaScript stuff +js = [] # WASM processing wasm = ["bitvec", "dwarf", "wasmparser"] diff --git a/symbolic-debuginfo/src/js.rs b/symbolic-debuginfo/src/js.rs new file mode 100644 index 000000000..a81e6065c --- /dev/null +++ b/symbolic-debuginfo/src/js.rs @@ -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 { + #[derive(Deserialize)] + struct DebugIdInSourceMap { + debug_id: Option, + } + + 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 { + for line in contents.lines().rev() { + if let Some(rest) = line.strip_prefix("//# debugId=") { + return rest.trim().parse().ok(); + } + } + None +} diff --git a/symbolic-debuginfo/src/lib.rs b/symbolic-debuginfo/src/lib.rs index 623aad350..031538456 100644 --- a/symbolic-debuginfo/src/lib.rs +++ b/symbolic-debuginfo/src/lib.rs @@ -67,6 +67,8 @@ pub mod pe; pub mod ppdb; #[cfg(feature = "sourcebundle")] pub mod sourcebundle; +#[cfg(feature = "js")] +pub mod js; #[cfg(feature = "wasm")] pub mod wasm; diff --git a/symbolic-debuginfo/src/sourcebundle.rs b/symbolic-debuginfo/src/sourcebundle.rs index d871eda57..d79cbc895 100644 --- a/symbolic-debuginfo/src/sourcebundle.rs +++ b/symbolic-debuginfo/src/sourcebundle.rs @@ -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. @@ -406,6 +409,9 @@ impl<'a> SourceFileDescriptor<'a> { 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 } @@ -430,26 +436,6 @@ impl<'a> SourceFileDescriptor<'a> { } } -/// 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()); - } - } - None -} - -/// Parses a debugId comment in a file to discover a sourcemap debug ID. -fn discover_debug_id(contents: &str) -> Option { - for line in contents.lines().rev() { - if let Some(rest) = line.strip_prefix("//# debugId=") { - return rest.trim().parse().ok(); - } - } - None -} - /// Version number of a [`SourceBundle`](struct.SourceBundle.html). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub struct SourceBundleVersion(pub u32); @@ -1453,7 +1439,7 @@ mod tests { } #[test] - fn test_embedded_debug_id() -> Result<(), SourceBundleError> { + fn test_source_embedded_debug_id() -> Result<(), SourceBundleError> { let mut writer = Cursor::new(Vec::new()); let mut bundle = SourceBundleWriter::start(&mut writer)?; @@ -1484,6 +1470,38 @@ mod tests { 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> { let mut cpp_file = NamedTempFile::new()?; From 2c51b8e0278fbac87223ba5a8a3058454df34baf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 21 Feb 2023 20:38:14 +0100 Subject: [PATCH 3/4] rustfmt --- symbolic-debuginfo/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/symbolic-debuginfo/src/lib.rs b/symbolic-debuginfo/src/lib.rs index 031538456..bda74645f 100644 --- a/symbolic-debuginfo/src/lib.rs +++ b/symbolic-debuginfo/src/lib.rs @@ -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")] @@ -67,8 +69,6 @@ pub mod pe; pub mod ppdb; #[cfg(feature = "sourcebundle")] pub mod sourcebundle; -#[cfg(feature = "js")] -pub mod js; #[cfg(feature = "wasm")] pub mod wasm; From da65287a91d8af41ae6cfc6e0263216d0c2ccd28 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 21 Feb 2023 20:38:39 +0100 Subject: [PATCH 4/4] Added changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee2e56f6..fc71f7f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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**: