use std::borrow::Cow;
use crate::error::{Error, Result};
use crate::token::INVALID_ID;
use crate::{SourceMap, Token};
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JSONSourceMap {
#[serde(deserialize_with = "deserialize_version")]
pub version: u32,
pub file: Option<String>,
pub mappings: String,
pub source_root: Option<String>,
pub sources: Vec<String>,
pub sources_content: Option<Vec<Option<String>>>,
#[serde(default)]
pub names: Vec<String>,
pub debug_id: Option<String>,
#[serde(rename = "x_google_ignoreList", alias = "ignoreList")]
pub x_google_ignore_list: Option<Vec<u32>>,
}
fn deserialize_version<'de, D>(deserializer: D) -> std::result::Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let version = u32::deserialize(deserializer)?;
if version != 3 {
return Err(serde::de::Error::custom(format!("unsupported source map version: {version}")));
}
Ok(version)
}
pub fn decode(json: JSONSourceMap) -> Result<SourceMap<'static>> {
validate_x_google_ignore_list(json.x_google_ignore_list.as_deref(), json.sources.len())?;
let tokens = decode_mapping(&json.mappings, json.names.len(), json.sources.len())?;
Ok(SourceMap {
file: json.file.map(Cow::Owned),
names: json.names.into_iter().map(Cow::Owned).collect(),
source_root: json.source_root.map(Cow::Owned),
sources: json.sources.into_iter().map(Cow::Owned).collect(),
source_contents: json
.sources_content
.map(|content| content.into_iter().map(|c| c.map(Cow::Owned)).collect())
.unwrap_or_default(),
tokens: tokens.into_boxed_slice(),
token_chunks: None,
x_google_ignore_list: json.x_google_ignore_list,
debug_id: json.debug_id.map(Cow::Owned),
})
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct BorrowedJSONSourceMap<'a> {
#[serde(deserialize_with = "deserialize_version")]
#[expect(dead_code)]
version: u32,
#[serde(borrow)]
file: Option<Cow<'a, str>>,
#[serde(borrow)]
mappings: Cow<'a, str>,
#[serde(borrow)]
source_root: Option<Cow<'a, str>>,
#[serde(borrow)]
sources: Vec<Cow<'a, str>>,
#[serde(borrow)]
sources_content: Option<Vec<Option<Cow<'a, str>>>>,
#[serde(borrow, default)]
names: Vec<Cow<'a, str>>,
#[serde(borrow)]
debug_id: Option<Cow<'a, str>>,
#[serde(rename = "x_google_ignoreList", alias = "ignoreList")]
x_google_ignore_list: Option<Vec<u32>>,
}
pub fn decode_from_string(value: &str) -> Result<SourceMap<'_>> {
let json: BorrowedJSONSourceMap<'_> = serde_json::from_str(value)?;
validate_x_google_ignore_list(json.x_google_ignore_list.as_deref(), json.sources.len())?;
let tokens = decode_mapping(&json.mappings, json.names.len(), json.sources.len())?;
Ok(SourceMap {
file: json.file,
names: json.names,
source_root: json.source_root,
sources: json.sources,
source_contents: json.sources_content.unwrap_or_default(),
tokens: tokens.into_boxed_slice(),
token_chunks: None,
x_google_ignore_list: json.x_google_ignore_list,
debug_id: json.debug_id,
})
}
fn validate_x_google_ignore_list(ignore_list: Option<&[u32]>, sources_len: usize) -> Result<()> {
if let Some(ignore_list) = ignore_list {
for &idx in ignore_list {
if idx as usize >= sources_len {
return Err(Error::BadSourceReference(idx));
}
}
}
Ok(())
}
fn decode_mapping(mapping: &str, names_len: usize, sources_len: usize) -> Result<Vec<Token>> {
let mapping = mapping.as_bytes();
let mut tokens: Vec<Token> = Vec::with_capacity(estimate_token_capacity(mapping));
let mut dst_line = 0u32;
let mut dst_col = 0u32;
let mut src_id = 0;
let mut src_line = 0;
let mut src_col = 0;
let mut name_id = 0;
let mut cursor = 0usize;
let mut nums = [0i64; 5];
while cursor < mapping.len() {
match mapping[cursor] {
b',' => {
cursor += 1;
}
b';' => {
dst_line = dst_line.wrapping_add(1);
dst_col = 0;
cursor += 1;
}
_ => {
let nums_len = parse_vlq_segment_into(mapping, &mut cursor, &mut nums)?;
let new_dst_col = i64::from(dst_col) + nums[0];
if new_dst_col < 0 {
return Err(Error::BadSegmentSize(0)); }
dst_col = new_dst_col as u32;
let mut src = INVALID_ID;
let mut name = INVALID_ID;
if nums_len > 1 {
if nums_len != 4 && nums_len != 5 {
return Err(Error::BadSegmentSize(nums_len as u32));
}
let new_src_id = i64::from(src_id) + nums[1];
if new_src_id < 0 || new_src_id >= sources_len as i64 {
return Err(Error::BadSourceReference(src_id));
}
src_id = new_src_id as u32;
src = src_id;
let new_src_line = i64::from(src_line) + nums[2];
if new_src_line < 0 {
return Err(Error::BadSegmentSize(0)); }
src_line = new_src_line as u32;
let new_src_col = i64::from(src_col) + nums[3];
if new_src_col < 0 {
return Err(Error::BadSegmentSize(0)); }
src_col = new_src_col as u32;
if nums_len > 4 {
name_id = (i64::from(name_id) + nums[4]) as u32;
if name_id >= names_len as u32 {
return Err(Error::BadNameReference(name_id));
}
name = name_id;
}
}
tokens.push(Token::new(
dst_line,
dst_col,
src_line,
src_col,
if src == INVALID_ID { None } else { Some(src) },
if name == INVALID_ID { None } else { Some(name) },
));
}
}
}
Ok(tokens)
}
#[inline(always)]
fn try_fast_4_or_5_segment(mapping: &[u8], cursor: usize, nums: &mut [i64; 5]) -> Option<usize> {
let rest = &mapping[cursor..];
if rest.len() < 4 {
return None;
}
let v = [
B64_DECODE.0[rest[0] as usize] as u8,
B64_DECODE.0[rest[1] as usize] as u8,
B64_DECODE.0[rest[2] as usize] as u8,
B64_DECODE.0[rest[3] as usize] as u8,
];
if (v[0] | v[1] | v[2] | v[3]) & 0xE0 != 0 {
return None;
}
for i in 0..4 {
nums[i] = decode_sign(i64::from(v[i]));
}
let Some(&b4) = rest.get(4) else { return Some(4) };
let v4 = B64_DECODE.0[b4 as usize] as u8;
if is_delimiter(v4) {
return Some(4);
}
if v4 & 0xE0 != 0
|| rest.get(5).is_some_and(|&b5| !is_delimiter(B64_DECODE.0[b5 as usize] as u8))
{
return None;
}
nums[4] = decode_sign(i64::from(v4));
Some(5)
}
#[inline(always)]
fn is_delimiter(entry: u8) -> bool {
entry & 0x80 != 0
}
#[repr(align(64))]
struct Aligned64([i8; 256]);
static B64_DECODE: Aligned64 = build_b64_decode();
const fn build_b64_decode() -> Aligned64 {
let mut table = [63i8; 256];
let mut i = 0;
while i < 26 {
table[(b'A' + i) as usize] = i as i8;
table[(b'a' + i) as usize] = 26 + i as i8;
i += 1;
}
let mut i = 0;
while i < 10 {
table[(b'0' + i) as usize] = 52 + i as i8;
i += 1;
}
table[b'+' as usize] = 62;
table[b'/' as usize] = 63;
table[b',' as usize] = -1;
table[b';' as usize] = -1;
Aligned64(table)
}
fn estimate_token_capacity(mapping: &[u8]) -> usize {
const EXACT_SCAN_THRESHOLD: usize = 256;
if mapping.len() < EXACT_SCAN_THRESHOLD {
let mut n = 1usize;
for &b in mapping {
if b == b',' || b == b';' {
n += 1;
}
}
n
} else {
mapping.len() / 4 + 1
}
}
fn parse_vlq_segment_into(mapping: &[u8], cursor: &mut usize, rv: &mut [i64; 5]) -> Result<usize> {
if let Some(seg_len) = try_fast_4_or_5_segment(mapping, *cursor, rv) {
*cursor += seg_len;
return Ok(seg_len);
}
let mut rv_len = 0usize;
while let Some(value) = next_vlq(mapping, cursor)? {
if rv_len < rv.len() {
rv[rv_len] = value;
}
rv_len += 1;
}
if rv_len == 0 {
return Err(Error::VlqNoValues);
}
Ok(rv_len)
}
#[inline(always)]
fn next_vlq(mapping: &[u8], cursor: &mut usize) -> Result<Option<i64>> {
let Some(&byte) = mapping.get(*cursor) else { return Ok(None) };
let first = i64::from(B64_DECODE.0[byte as usize]);
if first < 0 {
return Ok(None);
}
*cursor += 1;
if first < 32 {
return Ok(Some(decode_sign(first)));
}
let mut cur = first & 0b11111;
let mut shift = 5u32;
loop {
let Some(&byte) = mapping.get(*cursor) else {
return Err(Error::VlqLeftover);
};
let enc = i64::from(B64_DECODE.0[byte as usize]);
if enc < 0 {
return Err(Error::VlqLeftover);
}
*cursor += 1;
if shift > 62 {
return Err(Error::VlqOverflow);
}
cur |= (enc & 0b11111) << shift;
shift += 5;
if enc < 32 {
return Ok(Some(decode_sign(cur)));
}
}
}
#[inline(always)]
fn decode_sign(cur: i64) -> i64 {
let mag = cur >> 1;
let sign = cur & 1;
(mag ^ -sign) + sign
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_sourcemap() {
let input = r#"{
"version": 3,
"sources": ["coolstuff.js"],
"sourceRoot": "x",
"names": ["x","alert"],
"mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM",
"x_google_ignoreList": [0]
}"#;
let sm = SourceMap::from_json_string(input).unwrap();
assert_eq!(sm.get_source_root(), Some("x"));
assert_eq!(sm.get_x_google_ignore_list(), Some(&[0][..]));
let mut iter = sm.get_source_view_tokens().filter(|token| token.get_name_id().is_some());
assert_eq!(iter.next().unwrap().to_tuple(), (Some("coolstuff.js"), 0, 4, Some("x")));
assert_eq!(iter.next().unwrap().to_tuple(), (Some("coolstuff.js"), 1, 4, Some("x")));
assert_eq!(iter.next().unwrap().to_tuple(), (Some("coolstuff.js"), 2, 2, Some("alert")));
assert!(iter.next().is_none());
}
#[test]
fn decode_from_json_value() {
let json = SourceMap::from_json_string(
r#"{
"version": 3,
"file": "f.js",
"sourceRoot": "r",
"names": ["n"],
"sources": ["a.js"],
"sourcesContent": ["c"],
"mappings": "AAAAA",
"debugId": "d",
"x_google_ignoreList": [0]
}"#,
)
.unwrap()
.to_json();
let sm = SourceMap::from_json(json).unwrap();
assert_eq!(sm.get_file(), Some("f.js"));
assert_eq!(sm.get_source_root(), Some("r"));
assert_eq!(sm.get_debug_id(), Some("d"));
assert_eq!(sm.get_x_google_ignore_list(), Some(&[0][..]));
assert_eq!(sm.get_source_content(0), Some("c"));
assert_eq!(sm.get_name(0), Some("n"));
}
#[test]
fn decode_sourcemap_optional_field() {
let input = r#"{
"version": 3,
"names": [],
"sources": [],
"sourcesContent": [null],
"mappings": ""
}"#;
SourceMap::from_json_string(input).expect("should success");
}
#[test]
fn decode_unsupported_version() {
let input = r#"{"version": 2, "names": [], "sources": [], "mappings": ""}"#;
let err = SourceMap::from_json_string(input).unwrap_err();
assert!(matches!(err, Error::BadJson(_)));
}
#[test]
fn decode_mapping_bad_segment_size() {
let input = r#"{"version":3,"names":[],"sources":[],"sourcesContent":[],"mappings":"AA"}"#;
let err = SourceMap::from_json_string(input).unwrap_err();
assert!(matches!(err, Error::BadSegmentSize(2)));
}
#[test]
fn decode_mapping_vlq_leftover() {
let input = r#"{"version":3,"names":[],"sources":[],"sourcesContent":[],"mappings":"g"}"#;
let err = SourceMap::from_json_string(input).unwrap_err();
assert!(matches!(err, Error::VlqLeftover));
}
#[test]
fn decode_mapping_vlq_overflow() {
let mappings = "g".repeat(14);
let input =
format!(r#"{{"version":3,"names":[],"sources":["a.js"],"mappings":"{mappings}"}}"#);
let err = SourceMap::from_json_string(&input).unwrap_err();
assert!(matches!(err, Error::VlqOverflow));
}
#[test]
fn decode_mapping_bad_source_reference() {
let input = r#"{"version":3,"names":[],"sources":[],"mappings":"AAAA"}"#;
let err = SourceMap::from_json_string(input).unwrap_err();
assert!(matches!(err, Error::BadSourceReference(_)));
}
#[test]
fn decode_mapping_bad_name_reference() {
let input = r#"{"version":3,"names":[],"sources":["a.js"],"mappings":"AAAAA"}"#;
let err = SourceMap::from_json_string(input).unwrap_err();
assert!(matches!(err, Error::BadNameReference(0)));
}
#[test]
fn decode_ignore_list_bad_source_reference() {
let input =
r#"{"version":3,"names":[],"sources":[],"mappings":"","x_google_ignoreList":[3]}"#;
let err = SourceMap::from_json_string(input).unwrap_err();
assert!(matches!(err, Error::BadSourceReference(3)));
}
#[test]
fn decode_owned_propagates_errors() {
let bad_ignore_list = JSONSourceMap {
version: 3,
file: None,
mappings: String::new(),
source_root: None,
sources: vec![],
sources_content: None,
names: vec![],
debug_id: None,
x_google_ignore_list: Some(vec![3]),
};
assert!(matches!(SourceMap::from_json(bad_ignore_list), Err(Error::BadSourceReference(3))));
let bad_mapping = JSONSourceMap {
version: 3,
file: None,
mappings: "AAAA".to_string(),
source_root: None,
sources: vec![],
sources_content: None,
names: vec![],
debug_id: None,
x_google_ignore_list: None,
};
assert!(matches!(SourceMap::from_json(bad_mapping), Err(Error::BadSourceReference(_))));
}
#[test]
fn decode_invalid_char_behaves_like_slash() {
let make = |mappings: &str| {
format!(r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"#)
};
let bang_json = make("gD,!C");
let slash_json = make("gD,/C");
let bang = SourceMap::from_json_string(&bang_json).unwrap();
let slash = SourceMap::from_json_string(&slash_json).unwrap();
let tokens: Vec<Token> = bang.get_tokens().collect();
assert_eq!(tokens, slash.get_tokens().collect::<Vec<Token>>());
assert_eq!(tokens[0].get_dst_col(), 48);
assert_eq!(tokens[1].get_dst_col(), 1);
}
#[test]
fn decode_dangling_continuation_before_delimiter_is_leftover() {
let mappings = format!("{},A", "g".repeat(13));
let input = format!(r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"#);
let err = SourceMap::from_json_string(&input).unwrap_err();
assert!(matches!(err, Error::VlqLeftover));
}
#[test]
fn decode_oversized_segments_report_exact_field_count() {
for (mappings, expected) in [("AAAAAA", 6u32), ("AAAAAAA", 7u32)] {
let input =
format!(r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"#);
let err = SourceMap::from_json_string(&input).unwrap_err();
assert!(matches!(err, Error::BadSegmentSize(n) if n == expected));
}
}
#[test]
fn decode_parse_error_beats_segment_validation() {
for mappings in ["Dg", "AAAAAg"] {
let input =
format!(r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"#);
let err = SourceMap::from_json_string(&input).unwrap_err();
assert!(matches!(err, Error::VlqLeftover), "mappings {mappings:?}: {err:?}");
}
}
#[test]
fn decode_negative_column_beats_size_check() {
let input = r#"{"version":3,"names":[],"sources":[],"mappings":"DA"}"#;
let err = SourceMap::from_json_string(input).unwrap_err();
assert!(matches!(err, Error::BadSegmentSize(0)));
}
#[test]
fn parse_vlq_segment_empty_is_no_values() {
let mut cursor = 0;
let mut out = [0i64; 5];
let err = parse_vlq_segment_into(b"", &mut cursor, &mut out).unwrap_err();
assert!(matches!(err, Error::VlqNoValues));
}
}