From 1785aa6a65ce7b645e4690a00141e6253c41d4f3 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Mon, 30 Jun 2025 08:57:13 +0200 Subject: [PATCH 1/5] chore: Fix 1.88.0 clippy lints (#130) --- src/errors.rs | 2 +- src/types.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index d833dc34..01342f96 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -125,7 +125,7 @@ impl fmt::Display for Error { Error::InvalidRamBundleEntry => write!(f, "invalid ram bundle module entry"), Error::NotARamBundle => write!(f, "not a ram bundle"), Error::InvalidRangeMappingIndex(err) => write!(f, "invalid range mapping index: {err}"), - Error::InvalidBase64(c) => write!(f, "invalid base64 character: {}", c), + Error::InvalidBase64(c) => write!(f, "invalid base64 character: {c}"), } } } diff --git a/src/types.rs b/src/types.rs index 5ef415bc..cc6fd7b2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -563,10 +563,7 @@ impl SourceMap { let mut buf = vec![]; encode(self, &mut buf)?; let b64 = base64_simd::STANDARD.encode_to_string(&buf); - Ok(format!( - "data:application/json;charset=utf-8;base64,{}", - b64 - )) + Ok(format!("data:application/json;charset=utf-8;base64,{b64}")) } /// Creates a sourcemap from a reader over a JSON byte slice in UTF-8 From 986b6919f87de3f4a26684db1bb5f0a396fb2588 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 8 Aug 2025 18:48:46 +0200 Subject: [PATCH 2/5] fix: Fix 1.89.0 clippy lints (#132) --- src/ram_bundle.rs | 8 ++++---- src/sourceview.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ram_bundle.rs b/src/ram_bundle.rs index d37af652..23353714 100644 --- a/src/ram_bundle.rs +++ b/src/ram_bundle.rs @@ -161,7 +161,7 @@ impl<'a> RamBundle<'a> { } /// Looks up a module by ID in the bundle - pub fn get_module(&self, id: usize) -> Result> { + pub fn get_module(&self, id: usize) -> Result>> { match self.repr { RamBundleImpl::Indexed(ref indexed) => indexed.get_module(id), RamBundleImpl::Unbundle(ref file) => file.get_module(id), @@ -184,7 +184,7 @@ impl<'a> RamBundle<'a> { } } /// Returns an iterator over all modules in the bundle - pub fn iter_modules(&self) -> RamBundleModuleIter { + pub fn iter_modules(&self) -> RamBundleModuleIter<'_> { RamBundleModuleIter { range: 0..self.module_count(), ram_bundle: self, @@ -267,7 +267,7 @@ impl UnbundleRamBundle { } /// Looks up a module by ID in the bundle - pub fn get_module(&self, id: usize) -> Result> { + pub fn get_module(&self, id: usize) -> Result>> { match self.modules.get(&id) { Some(data) => Ok(Some(RamBundleModule { id, data })), None => Ok(None), @@ -320,7 +320,7 @@ impl<'a> IndexedRamBundle<'a> { } /// Looks up a module by ID in the bundle - pub fn get_module(&self, id: usize) -> Result> { + pub fn get_module(&self, id: usize) -> Result>> { if id >= self.module_count { return Err(Error::InvalidRamBundleIndex); } diff --git a/src/sourceview.rs b/src/sourceview.rs index e0ff52ea..e556c8c8 100644 --- a/src/sourceview.rs +++ b/src/sourceview.rs @@ -258,7 +258,7 @@ impl SourceView { } /// Returns an iterator over all lines. - pub fn lines(&self) -> Lines { + pub fn lines(&self) -> Lines<'_> { Lines { sv: self, idx: 0 } } From f96e3d13909f411431153e0f7553f43066061723 Mon Sep 17 00:00:00 2001 From: Noa Date: Thu, 4 Dec 2025 02:44:16 -0600 Subject: [PATCH 3/5] ref: Store SourceView linecache as offsets rather than pointers (#133) --- src/sourceview.rs | 111 +++++++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 35 deletions(-) diff --git a/src/sourceview.rs b/src/sourceview.rs index e556c8c8..699faf52 100644 --- a/src/sourceview.rs +++ b/src/sourceview.rs @@ -1,8 +1,5 @@ use std::fmt; -use std::slice; use std::str; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex; @@ -129,16 +126,14 @@ impl<'a> Iterator for Lines<'a> { /// operations. pub struct SourceView { source: Arc, - processed_until: AtomicUsize, - lines: Mutex>, + line_end_offsets: Mutex>, } impl Clone for SourceView { fn clone(&self) -> SourceView { SourceView { source: self.source.clone(), - processed_until: AtomicUsize::new(0), - lines: Mutex::new(vec![]), + line_end_offsets: Mutex::new(vec![]), } } } @@ -162,8 +157,7 @@ impl SourceView { pub fn new(source: Arc) -> SourceView { SourceView { source, - processed_until: AtomicUsize::new(0), - lines: Mutex::new(vec![]), + line_end_offsets: Mutex::new(vec![]), } } @@ -171,50 +165,66 @@ impl SourceView { pub fn from_string(source: String) -> SourceView { SourceView { source: source.into(), - processed_until: AtomicUsize::new(0), - lines: Mutex::new(vec![]), + line_end_offsets: Mutex::new(vec![]), } } /// Returns a requested minified line. pub fn get_line(&self, idx: u32) -> Option<&str> { let idx = idx as usize; - { - let lines = self.lines.lock().unwrap(); - if idx < lines.len() { - return Some(lines[idx]); - } + + let get_from_line_ends = |line_ends: &[LineEndOffset]| { + let end = line_ends.get(idx)?.to_end_index(); + let start = if idx == 0 { + 0 + } else { + line_ends[idx - 1].to_start_index() + }; + Some(&self.source[start..end]) + }; + + let mut line_ends = self + .line_end_offsets + .lock() + .unwrap_or_else(|e| e.into_inner()); + + if let Some(line) = get_from_line_ends(&line_ends) { + return Some(line); } - // fetched everything - if self.processed_until.load(Ordering::Relaxed) > self.source.len() { + // check whether we've processed the entire string - the end of the + // last-processed line would be the same as the end of the string + if line_ends + .last() + .is_some_and(|i| i.to_end_index() == self.source.len()) + { return None; } - let mut lines = self.lines.lock().unwrap(); + let mut rest_offset = line_ends.last().map_or(0, |i| i.to_start_index()); + let mut rest = &self.source[rest_offset..]; let mut done = false; while !done { - let rest = &self.source.as_bytes()[self.processed_until.load(Ordering::Relaxed)..]; - - let rv = if let Some(mut idx) = rest.iter().position(|&x| x == b'\n' || x == b'\r') { - let rv = &rest[..idx]; - if rest[idx] == b'\r' && rest.get(idx + 1) == Some(&b'\n') { - idx += 1; + let line_term = if let Some(idx) = rest.find(['\n', '\r']) { + rest_offset += idx; + rest = &rest[idx..]; + if rest.starts_with("\r\n") { + LineTerminator::CrLf + } else { + LineTerminator::LfOrCr } - self.processed_until.fetch_add(idx + 1, Ordering::Relaxed); - rv } else { - self.processed_until - .fetch_add(rest.len() + 1, Ordering::Relaxed); + rest_offset += rest.len(); + rest = &rest[rest.len()..]; done = true; - rest + LineTerminator::Eof }; - lines.push(unsafe { - str::from_utf8_unchecked(slice::from_raw_parts(rv.as_ptr(), rv.len())) - }); - if let Some(&line) = lines.get(idx) { + line_ends.push(LineEndOffset::new(rest_offset, line_term)); + rest_offset += line_term as usize; + rest = &rest[line_term as usize..]; + if let Some(line) = get_from_line_ends(&line_ends) { return Some(line); } } @@ -311,7 +321,7 @@ impl SourceView { /// Returns the number of lines. pub fn line_count(&self) -> usize { self.get_line(!0); - self.lines.lock().unwrap().len() + self.line_end_offsets.lock().unwrap().len() } /// Returns the source map reference in the source view. @@ -320,6 +330,37 @@ impl SourceView { } } +/// A wrapper around an index that stores a [`LineTerminator`] in its 2 lowest bits. +// We use `u64` instead of `usize` in order to not lose data when bit-packing +// on 32-bit targets. +#[derive(Clone, Copy)] +struct LineEndOffset(u64); + +#[derive(Clone, Copy)] +enum LineTerminator { + Eof = 0, + LfOrCr = 1, + CrLf = 2, +} + +impl LineEndOffset { + fn new(index: usize, line_end: LineTerminator) -> Self { + let shifted = (index as u64) << 2; + + Self(shifted | line_end as u64) + } + + /// Return the index of the end of this line. + fn to_end_index(self) -> usize { + (self.0 >> 2) as usize + } + + /// Return the index of the start of the next line. + fn to_start_index(self) -> usize { + self.to_end_index() + (self.0 & 0b11) as usize + } +} + #[cfg(test)] mod tests { use super::*; From fb19fa4b449ed20dfa22f65413f37f4d92be3908 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:24:28 +0100 Subject: [PATCH 4/5] feat(debugId): Serialize source maps with `debugId`, not `debug_id` (#134) The debug ID field on all source maps is now **serialized as `debugId` rather than `debug_id`**. To maintain backwards-compatibility with source maps that contain a `debug_id` field, we can still read the `debug_id` field. If a source map contains a `debugId` field and a `debug_id` field, **the `debugId` field takes precedence, which is a change from the old behavior**. Corresponding Sentry CLI PR: https://github.com/getsentry/sentry-cli/pull/3005 Closes #96 Closes [CLI-240](https://linear.app/getsentry/issue/CLI-240/turn-debug-id-field-to-debugid) --- src/decoder.rs | 44 ++----------------- src/encoder.rs | 13 ++---- src/jsontypes.rs | 99 ++++++++++++++++++++++++++++++++++++++++--- src/types.rs | 4 +- tests/test_encoder.rs | 32 ++++++++++++++ tests/test_index.rs | 30 +++++++++++++ 6 files changed, 164 insertions(+), 58 deletions(-) diff --git a/src/decoder.rs b/src/decoder.rs index 1581d00f..132f657d 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -267,9 +267,7 @@ pub fn decode_regular(rsm: RawSourceMap) -> Result { let mut sm = SourceMap::new(file, tokens, names, sources, source_content); sm.set_source_root(rsm.source_root); - // Use _debug_id_new (from "debugId" key) only if debug_id - // from ( "debug_id" key) is unset - sm.set_debug_id(rsm.debug_id.or(rsm._debug_id_new)); + sm.set_debug_id(rsm.debug_id.into()); if let Some(ignore_list) = rsm.ignore_list { for idx in ignore_list { sm.add_to_ignore_list(idx); @@ -307,7 +305,7 @@ fn decode_index(rsm: RawSourceMap) -> Result { rsm.x_facebook_offsets, rsm.x_metro_module_paths, ) - .with_debug_id(rsm._debug_id_new.or(rsm.debug_id))) + .with_debug_id(rsm.debug_id.into())) } fn decode_common(rsm: RawSourceMap) -> Result { @@ -419,8 +417,7 @@ mod tests { x_facebook_offsets: None, x_metro_module_paths: None, x_facebook_sources: None, - debug_id: None, - _debug_id_new: None, + debug_id: None.into(), }; let decoded = decode_common(raw).expect("should decoded"); @@ -448,40 +445,7 @@ mod tests { x_facebook_offsets: None, x_metro_module_paths: None, x_facebook_sources: None, - debug_id: None, - _debug_id_new: Some(DEBUG_ID.parse().expect("valid debug id")), - }; - - let decoded = decode_common(raw).expect("should decode"); - assert_eq!( - decoded, - DecodedMap::Index( - SourceMapIndex::new(Some("test.js".into()), vec![]) - .with_debug_id(Some(DEBUG_ID.parse().expect("valid debug id"))) - ) - ); - } - - #[test] - fn test_decode_sourcemap_index_debug_id_from_legacy_key() { - const DEBUG_ID: &str = "0123456789abcdef0123456789abcdef"; - - let raw = RawSourceMap { - version: Some(3), - file: Some("test.js".into()), - sources: None, - source_root: None, - sources_content: None, - sections: Some(vec![]), - names: None, - range_mappings: None, - mappings: None, - ignore_list: None, - x_facebook_offsets: None, - x_metro_module_paths: None, - x_facebook_sources: None, - debug_id: Some(DEBUG_ID.parse().expect("valid debug id")), - _debug_id_new: None, + debug_id: Some(DEBUG_ID.parse().expect("valid debug id")).into(), }; let decoded = decode_common(raw).expect("should decode"); diff --git a/src/encoder.rs b/src/encoder.rs index abc3286f..c4e5c8cf 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -178,8 +178,7 @@ impl Encodable for SourceMap { x_facebook_offsets: None, x_metro_module_paths: None, x_facebook_sources: None, - debug_id: self.get_debug_id(), - _debug_id_new: None, + debug_id: self.get_debug_id().into(), } } } @@ -213,9 +212,7 @@ impl Encodable for SourceMapIndex { x_facebook_offsets: None, x_metro_module_paths: None, x_facebook_sources: None, - debug_id: None, - // Put the debug ID on _debug_id_new to serialize it to the debugId field. - _debug_id_new: self.debug_id(), + debug_id: self.debug_id().into(), } } } @@ -278,8 +275,7 @@ mod tests { x_facebook_offsets: None, x_metro_module_paths: None, x_facebook_sources: None, - debug_id: None, - _debug_id_new: None, + debug_id: None.into(), } ); } @@ -308,8 +304,7 @@ mod tests { x_facebook_offsets: None, x_metro_module_paths: None, x_facebook_sources: None, - debug_id: None, - _debug_id_new: Some(DEBUG_ID.parse().expect("valid debug id")), + debug_id: Some(DEBUG_ID.parse().expect("valid debug id")).into(), } ); } diff --git a/src/jsontypes.rs b/src/jsontypes.rs index 281ba6b4..9185bf5a 100644 --- a/src/jsontypes.rs +++ b/src/jsontypes.rs @@ -1,7 +1,8 @@ use debugid::DebugId; use serde::de::IgnoredAny; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; +use std::fmt::Debug; #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct RawSectionOffset { @@ -54,12 +55,8 @@ pub struct RawSourceMap { pub x_metro_module_paths: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub x_facebook_sources: FacebookSources, - #[serde(skip_serializing_if = "Option::is_none")] - pub debug_id: Option, - // This field only exists to be able to deserialize from "debugId" keys - // if "debug_id" is unset. - #[serde(skip_serializing_if = "Option::is_none", rename = "debugId")] - pub(crate) _debug_id_new: Option, + #[serde(flatten)] + pub debug_id: DebugIdField, } #[derive(Deserialize)] @@ -75,3 +72,91 @@ pub struct MinimalRawSourceMap { pub names: Option, pub mappings: Option, } + +/// This struct represents a `RawSourceMap`'s debug ID fields. +/// +/// The reason this exists as a seperate struct is so that we can have custom deserialization +/// logic, which can read both the legacy snake_case debug_id and the new camelCase debugId +/// fields. In case both are provided, the camelCase field takes precedence. +/// +/// The field is always serialized as `debugId`. +#[derive(Serialize, Clone, PartialEq, Debug, Default)] +pub(crate) struct DebugIdField { + #[serde(rename = "debugId", skip_serializing_if = "Option::is_none")] + value: Option, +} + +impl<'de> Deserialize<'de> for DebugIdField { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // We cannot use serde(alias), as that would cause an error when both fields are present. + + #[derive(Deserialize)] + struct Helper { + #[serde(rename = "debugId")] + camel: Option, + #[serde(rename = "debug_id")] + legacy: Option, + } + + let Helper { camel, legacy } = Helper::deserialize(deserializer)?; + Ok(camel.or(legacy).into()) + } +} + +impl From> for DebugIdField { + fn from(value: Option) -> Self { + Self { value } + } +} + +impl From for Option { + fn from(value: DebugIdField) -> Self { + value.value + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn parse_debug_id(input: &str) -> DebugId { + input.parse().expect("valid debug id") + } + + fn empty_sourcemap() -> RawSourceMap { + serde_json::from_value::(serde_json::json!({})) + .expect("can deserialize empty JSON to RawSourceMap") + } + + #[test] + fn raw_sourcemap_serializes_camel_case_debug_id() { + let camel = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + let raw = RawSourceMap { + debug_id: Some(parse_debug_id(camel)).into(), + ..empty_sourcemap() + }; + + let value = serde_json::to_value(raw).expect("should serialize without error"); + let obj = value.as_object().expect("should be an object"); + assert!(obj.get("debug_id").is_none()); + assert_eq!(obj.get("debugId"), Some(&json!(parse_debug_id(camel)))); + } + + #[test] + fn raw_sourcemap_prefers_camel_case_on_deserialize() { + let legacy = "ffffffffffffffffffffffffffffffff"; + let camel = "00000000000000000000000000000000"; + let json = serde_json::json!({ + "debug_id": legacy, + "debugId": camel + }); + let raw: RawSourceMap = + serde_json::from_value(json).expect("can deserialize as RawSourceMap"); + let value: Option = raw.debug_id.into(); + assert_eq!(value, Some(parse_debug_id(camel))); + } +} diff --git a/src/types.rs b/src/types.rs index cc6fd7b2..908f9f75 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1441,8 +1441,8 @@ mod tests { "sources":["coolstuff.js"], "names":["x","alert"], "mappings":"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM", - "debug_id":"00000000-0000-0000-0000-000000000000", - "debugId": "11111111-1111-1111-1111-111111111111" + "debug_id": "11111111-1111-1111-1111-111111111111", + "debugId":"00000000-0000-0000-0000-000000000000" }"#; let sm = SourceMap::from_slice(input).unwrap(); diff --git a/tests/test_encoder.rs b/tests/test_encoder.rs index b67f087d..840ddb87 100644 --- a/tests/test_encoder.rs +++ b/tests/test_encoder.rs @@ -62,3 +62,35 @@ fn test_empty_range() { let out = String::from_utf8(out).unwrap(); assert!(!out.contains("rangeMappings")); } + +#[test] +fn test_sourcemap_serializes_camel_case_debug_id() { + const DEBUG_ID: &str = "0123456789abcdef0123456789abcdef"; + let input = format!( + r#"{{ + "version": 3, + "sources": [], + "names": [], + "mappings": "", + "debug_id": "{}" + }}"#, + DEBUG_ID + ); + + let sm = SourceMap::from_reader(input.as_bytes()).unwrap(); + let expected = sm.get_debug_id().expect("debug id parsed").to_string(); + let mut out: Vec = vec![]; + sm.to_writer(&mut out).unwrap(); + let serialized = String::from_utf8(out).unwrap(); + + assert!( + serialized.contains(&format!(r#""debugId":"{}""#, expected)), + "expected camelCase debugId in {}", + serialized + ); + assert!( + !serialized.contains("debug_id"), + "unexpected snake_case key in {}", + serialized + ); +} diff --git a/tests/test_index.rs b/tests/test_index.rs index b3983576..0540bb0e 100644 --- a/tests/test_index.rs +++ b/tests/test_index.rs @@ -205,3 +205,33 @@ fn test_flatten_indexed_sourcemap_with_ignore_list() { vec![1] ); } + +#[test] +fn test_sourcemap_index_serializes_camel_case_debug_id() { + const DEBUG_ID: &str = "fedcba9876543210fedcba9876543210"; + let input = format!( + r#"{{ + "version": 3, + "file": "bundle.js", + "sections": [], + "debugId": "{}" + }}"#, + DEBUG_ID + ); + + let smi = SourceMapIndex::from_reader(input.as_bytes()).unwrap(); + let mut out = Vec::new(); + smi.to_writer(&mut out).unwrap(); + let serialized = String::from_utf8(out).unwrap(); + + assert!( + serialized.contains(r#""debugId":"#), + "expected camelCase debugId in {}", + serialized + ); + assert!( + !serialized.contains("debug_id"), + "unexpected snake_case key in {}", + serialized + ); +} From 210972ff0400f785385d2e3f38c7e558a0755864 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 4 Dec 2025 10:25:24 +0000 Subject: [PATCH 5/5] release: 9.3.0 --- CHANGELOG.md | 14 ++++++++++++++ Cargo.toml | 2 +- cli/Cargo.toml | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 396ed04a..0ed3c47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 9.3.0 + +### New Features ✨ + +- feat(debugId): Serialize source maps with `debugId`, not `debug_id` by @szokeasaurusrex in [#134](https://github.com/getsentry/rust-sourcemap/pull/134) + +### Build / dependencies / internal 🔧 + +- chore: Fix 1.88.0 clippy lints by @loewenheim in [#130](https://github.com/getsentry/rust-sourcemap/pull/130) + +### Other + +- Store SourceView linecache as offsets rather than pointers by @coolreader18 in [#133](https://github.com/getsentry/rust-sourcemap/pull/133) + ## 9.2.2 ### Various fixes & improvements diff --git a/Cargo.toml b/Cargo.toml index 25936d6c..ad5fdac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sourcemap" -version = "9.2.2" +version = "9.3.0" authors = ["Sentry "] keywords = ["javascript", "sourcemap", "sourcemaps"] description = "Basic sourcemap handling for Rust" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5878c26e..ad276e77 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cli" -version = "9.2.2" +version = "9.3.0" authors = ["Armin Ronacher "] edition = "2018"