sourcemap/
types.rs

1use std::borrow::Cow;
2use std::cmp::Ordering;
3use std::collections::BTreeSet;
4use std::fmt;
5use std::io::{Read, Write};
6use std::path::Path;
7use std::sync::Arc;
8
9use crate::builder::SourceMapBuilder;
10use crate::decoder::{decode, decode_slice};
11use crate::encoder::encode;
12use crate::errors::{Error, Result};
13use crate::hermes::SourceMapHermes;
14use crate::sourceview::SourceView;
15use crate::utils::{find_common_prefix, greatest_lower_bound};
16
17use debugid::DebugId;
18
19/// Controls the `SourceMap::rewrite` behavior
20///
21/// Default configuration:
22///
23/// * `with_names`: true
24/// * `with_source_contents`: true
25/// * `load_local_source_contents`: false
26#[derive(Debug, Clone)]
27pub struct RewriteOptions<'a> {
28    /// If enabled, names are kept in the rewritten sourcemap.
29    pub with_names: bool,
30    /// If enabled source contents are kept in the sourcemap.
31    pub with_source_contents: bool,
32    /// If enabled local source contents that are not in the
33    /// file are automatically inlined.
34    #[cfg(any(unix, windows, target_os = "redox"))]
35    pub load_local_source_contents: bool,
36    /// The base path to the used for source reference resolving
37    /// when loading local source contents is used.
38    pub base_path: Option<&'a Path>,
39    /// Optionally strips common prefixes from the sources.  If
40    /// an item in the list is set to `~` then the common prefix
41    /// of all sources is stripped.
42    pub strip_prefixes: &'a [&'a str],
43}
44
45impl<'a> Default for RewriteOptions<'a> {
46    fn default() -> RewriteOptions<'a> {
47        RewriteOptions {
48            with_names: true,
49            with_source_contents: true,
50            #[cfg(any(unix, windows, target_os = "redox"))]
51            load_local_source_contents: false,
52            base_path: None,
53            strip_prefixes: &[][..],
54        }
55    }
56}
57
58/// Represents the result of a decode operation
59///
60/// This represents either an actual sourcemap or a source map index.
61/// Usually the two things are too distinct to provide a common
62/// interface however for token lookup and writing back into a writer
63/// general methods are provided.
64#[derive(Debug, Clone)]
65pub enum DecodedMap {
66    /// Indicates a regular sourcemap
67    Regular(SourceMap),
68    /// Indicates a sourcemap index
69    Index(SourceMapIndex),
70    /// Indicates a sourcemap as generated by Metro+Hermes, as used by react-native
71    Hermes(SourceMapHermes),
72}
73
74impl DecodedMap {
75    /// Alias for `decode`.
76    pub fn from_reader<R: Read>(rdr: R) -> Result<DecodedMap> {
77        decode(rdr)
78    }
79
80    /// Writes a decoded sourcemap to a writer.
81    pub fn to_writer<W: Write>(&self, w: W) -> Result<()> {
82        match *self {
83            DecodedMap::Regular(ref sm) => encode(sm, w),
84            DecodedMap::Index(ref smi) => encode(smi, w),
85            DecodedMap::Hermes(ref smh) => encode(smh, w),
86        }
87    }
88
89    /// Shortcut to look up a token on either an index or a
90    /// regular sourcemap.  This method can only be used if
91    /// the contained index actually contains embedded maps
92    /// or it will not be able to look up anything.
93    pub fn lookup_token(&self, line: u32, col: u32) -> Option<Token<'_>> {
94        match *self {
95            DecodedMap::Regular(ref sm) => sm.lookup_token(line, col),
96            DecodedMap::Index(ref smi) => smi.lookup_token(line, col),
97            DecodedMap::Hermes(ref smh) => smh.lookup_token(line, col),
98        }
99    }
100
101    /// Returns the original function name.
102    ///
103    /// `minified_name` and `source_view` are not always necessary.  For
104    /// instance hermes source maps can provide this information without
105    /// access to the original sources.
106    pub fn get_original_function_name(
107        &self,
108        line: u32,
109        col: u32,
110        minified_name: Option<&str>,
111        source_view: Option<&SourceView>,
112    ) -> Option<&str> {
113        match *self {
114            DecodedMap::Regular(ref sm) => {
115                sm.get_original_function_name(line, col, minified_name?, source_view?)
116            }
117            DecodedMap::Index(ref smi) => {
118                smi.get_original_function_name(line, col, minified_name?, source_view?)
119            }
120            DecodedMap::Hermes(ref smh) => {
121                if line != 0 {
122                    return None;
123                }
124                smh.get_original_function_name(col)
125            }
126        }
127    }
128}
129
130/// Represents a raw token
131///
132/// Raw tokens are used internally to represent the sourcemap
133/// in a memory efficient way.  If you construct sourcemaps yourself
134/// then you need to create these objects, otherwise they are invisible
135/// to you as a user.
136#[derive(PartialEq, Eq, Copy, Clone, Debug)]
137pub struct RawToken {
138    /// the destination (minified) line number (0-indexed)
139    pub dst_line: u32,
140    /// the destination (minified) column number (0-indexed)
141    pub dst_col: u32,
142    /// the source line number (0-indexed)
143    pub src_line: u32,
144    /// the source line column (0-indexed)
145    pub src_col: u32,
146    /// source identifier
147    pub src_id: u32,
148    /// name identifier (`!0` in case there is no associated name)
149    pub name_id: u32,
150
151    /// If true, this token is a range token.
152    ///
153    /// See <https://github.com/tc39/source-map-rfc/blob/main/proposals/range-mappings.md>
154    pub is_range: bool,
155}
156
157/// Represents a token from a sourcemap
158#[derive(Copy, Clone)]
159pub struct Token<'a> {
160    raw: &'a RawToken,
161    pub(crate) sm: &'a SourceMap,
162    pub(crate) idx: usize,
163    offset: u32,
164}
165
166impl<'a> Token<'a> {
167    /// The sourcemap this token is linked to.
168    pub fn sourcemap(&self) -> &'a SourceMap {
169        self.sm
170    }
171}
172
173impl PartialEq for Token<'_> {
174    fn eq(&self, other: &Token<'_>) -> bool {
175        self.raw == other.raw
176    }
177}
178
179impl Eq for Token<'_> {}
180
181impl PartialOrd for Token<'_> {
182    fn partial_cmp(&self, other: &Token<'_>) -> Option<Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187impl Ord for Token<'_> {
188    fn cmp(&self, other: &Token<'_>) -> Ordering {
189        macro_rules! try_cmp {
190            ($a:expr, $b:expr) => {
191                match $a.cmp(&$b) {
192                    Ordering::Equal => {}
193                    x => {
194                        return x;
195                    }
196                }
197            };
198        }
199        try_cmp!(self.get_dst_line(), other.get_dst_line());
200        try_cmp!(self.get_dst_col(), other.get_dst_col());
201        try_cmp!(self.get_source(), other.get_source());
202        try_cmp!(self.get_src_line(), other.get_src_line());
203        try_cmp!(self.get_src_col(), other.get_src_col());
204        try_cmp!(self.get_name(), other.get_name());
205        try_cmp!(self.is_range(), other.is_range());
206
207        Ordering::Equal
208    }
209}
210
211impl<'a> Token<'a> {
212    /// get the destination (minified) line number
213    pub fn get_dst_line(&self) -> u32 {
214        self.raw.dst_line
215    }
216
217    /// get the destination (minified) column number
218    pub fn get_dst_col(&self) -> u32 {
219        self.raw.dst_col
220    }
221
222    /// get the destination line and column
223    pub fn get_dst(&self) -> (u32, u32) {
224        (self.get_dst_line(), self.get_dst_col())
225    }
226
227    /// get the source line number
228    pub fn get_src_line(&self) -> u32 {
229        self.raw.src_line
230    }
231
232    /// get the source column number
233    pub fn get_src_col(&self) -> u32 {
234        self.raw.src_col.saturating_add(self.offset)
235    }
236
237    /// get the source line and column
238    pub fn get_src(&self) -> (u32, u32) {
239        (self.get_src_line(), self.get_src_col())
240    }
241
242    /// Return the source ID of the token
243    pub fn get_src_id(&self) -> u32 {
244        self.raw.src_id
245    }
246
247    /// get the source if it exists as string
248    pub fn get_source(&self) -> Option<&'a str> {
249        if self.raw.src_id == !0 {
250            None
251        } else {
252            self.sm.get_source(self.raw.src_id)
253        }
254    }
255
256    /// Is there a source for this token?
257    pub fn has_source(&self) -> bool {
258        self.raw.src_id != !0
259    }
260
261    /// get the name if it exists as string
262    pub fn get_name(&self) -> Option<&'a str> {
263        if self.raw.name_id == !0 {
264            None
265        } else {
266            self.sm.get_name(self.raw.name_id)
267        }
268    }
269
270    /// returns `true` if a name exists, `false` otherwise
271    pub fn has_name(&self) -> bool {
272        self.get_name().is_some()
273    }
274
275    /// Return the name ID of the token
276    pub fn get_name_id(&self) -> u32 {
277        self.raw.name_id
278    }
279
280    /// Converts the token into a debug tuple in the form
281    /// `(source, src_line, src_col, name)`
282    pub fn to_tuple(&self) -> (&'a str, u32, u32, Option<&'a str>) {
283        (
284            self.get_source().unwrap_or(""),
285            self.get_src_line(),
286            self.get_src_col(),
287            self.get_name(),
288        )
289    }
290
291    /// Get the underlying raw token
292    pub fn get_raw_token(&self) -> RawToken {
293        *self.raw
294    }
295
296    /// Returns the referenced source view.
297    pub fn get_source_view(&self) -> Option<&SourceView> {
298        self.sm.get_source_view(self.get_src_id())
299    }
300
301    /// If true, this token is a range token.
302    ///
303    /// See <https://github.com/tc39/source-map-rfc/blob/main/proposals/range-mappings.md>
304    pub fn is_range(&self) -> bool {
305        self.raw.is_range
306    }
307}
308
309/// Iterates over all tokens in a sourcemap
310pub struct TokenIter<'a> {
311    i: &'a SourceMap,
312    next_idx: usize,
313}
314
315impl TokenIter<'_> {
316    pub fn seek(&mut self, line: u32, col: u32) -> bool {
317        let token = self.i.lookup_token(line, col);
318        match token {
319            Some(token) => {
320                self.next_idx = token.idx + 1;
321                true
322            }
323            None => false,
324        }
325    }
326}
327
328impl<'a> Iterator for TokenIter<'a> {
329    type Item = Token<'a>;
330
331    fn next(&mut self) -> Option<Token<'a>> {
332        self.i.get_token(self.next_idx).inspect(|_| {
333            self.next_idx += 1;
334        })
335    }
336}
337
338/// Iterates over all sources in a sourcemap
339pub struct SourceIter<'a> {
340    i: &'a SourceMap,
341    next_idx: u32,
342}
343
344impl<'a> Iterator for SourceIter<'a> {
345    type Item = &'a str;
346
347    fn next(&mut self) -> Option<&'a str> {
348        self.i.get_source(self.next_idx).inspect(|_| {
349            self.next_idx += 1;
350        })
351    }
352}
353
354/// Iterates over all source contents in a sourcemap
355pub struct SourceContentsIter<'a> {
356    i: &'a SourceMap,
357    next_idx: u32,
358}
359
360impl<'a> Iterator for SourceContentsIter<'a> {
361    type Item = Option<&'a str>;
362
363    fn next(&mut self) -> Option<Option<&'a str>> {
364        if self.next_idx >= self.i.get_source_count() {
365            None
366        } else {
367            let rv = Some(self.i.get_source_contents(self.next_idx));
368            self.next_idx += 1;
369            rv
370        }
371    }
372}
373
374/// Iterates over all tokens in a sourcemap
375pub struct NameIter<'a> {
376    i: &'a SourceMap,
377    next_idx: u32,
378}
379
380impl<'a> Iterator for NameIter<'a> {
381    type Item = &'a str;
382
383    fn next(&mut self) -> Option<&'a str> {
384        self.i.get_name(self.next_idx).inspect(|_| {
385            self.next_idx += 1;
386        })
387    }
388}
389
390impl fmt::Debug for Token<'_> {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        write!(f, "<Token {self:#}>")
393    }
394}
395
396impl fmt::Display for Token<'_> {
397    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398        write!(
399            f,
400            "{}:{}:{}{}",
401            self.get_source().unwrap_or("<unknown>"),
402            self.get_src_line(),
403            self.get_src_col(),
404            self.get_name()
405                .map(|x| format!(" name={x}"))
406                .unwrap_or_default()
407        )?;
408        if f.alternate() {
409            write!(
410                f,
411                " ({}:{}){}",
412                self.get_dst_line(),
413                self.get_dst_col(),
414                if self.is_range() { " (range)" } else { "" }
415            )?;
416        }
417        Ok(())
418    }
419}
420
421/// Represents a section in a sourcemap index
422#[derive(Debug, Clone)]
423pub struct SourceMapSection {
424    offset: (u32, u32),
425    url: Option<String>,
426    map: Option<Box<DecodedMap>>,
427}
428
429/// Iterates over all sections in a sourcemap index
430pub struct SourceMapSectionIter<'a> {
431    i: &'a SourceMapIndex,
432    next_idx: u32,
433}
434
435impl<'a> Iterator for SourceMapSectionIter<'a> {
436    type Item = &'a SourceMapSection;
437
438    fn next(&mut self) -> Option<&'a SourceMapSection> {
439        self.i.get_section(self.next_idx).inspect(|_| {
440            self.next_idx += 1;
441        })
442    }
443}
444
445/// Represents a sourcemap index in memory
446#[derive(Debug, Clone)]
447pub struct SourceMapIndex {
448    file: Option<String>,
449    sections: Vec<SourceMapSection>,
450    x_facebook_offsets: Option<Vec<Option<u32>>>,
451    x_metro_module_paths: Option<Vec<String>>,
452}
453
454/// Represents a sourcemap in memory
455///
456/// This is always represents a regular "non-indexed" sourcemap.  Particularly
457/// in case the `from_reader` method is used an index sourcemap will be
458/// rejected with an error on reading.
459#[derive(Clone, Debug)]
460pub struct SourceMap {
461    pub(crate) file: Option<Arc<str>>,
462    pub(crate) tokens: Vec<RawToken>,
463    pub(crate) names: Vec<Arc<str>>,
464    pub(crate) source_root: Option<Arc<str>>,
465    pub(crate) sources: Vec<Arc<str>>,
466    pub(crate) sources_prefixed: Option<Vec<Arc<str>>>,
467    pub(crate) sources_content: Vec<Option<SourceView>>,
468    pub(crate) ignore_list: BTreeSet<u32>,
469    pub(crate) debug_id: Option<DebugId>,
470}
471
472impl SourceMap {
473    /// Creates a sourcemap from a reader over a JSON stream in UTF-8
474    /// format.  Optionally a "garbage header" as defined by the
475    /// sourcemap draft specification is supported.  In case an indexed
476    /// sourcemap is encountered an error is returned.
477    ///
478    /// ```rust
479    /// use sourcemap::SourceMap;
480    /// let input: &[_] = b"{
481    ///     \"version\":3,
482    ///     \"sources\":[\"coolstuff.js\"],
483    ///     \"names\":[\"x\",\"alert\"],
484    ///     \"mappings\":\"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM\"
485    /// }";
486    /// let sm = SourceMap::from_reader(input).unwrap();
487    /// ```
488    ///
489    /// While sourcemaps objects permit some modifications, it's generally
490    /// not possible to modify tokens after they have been added.  For
491    /// creating sourcemaps from scratch or for general operations for
492    /// modifying a sourcemap have a look at the `SourceMapBuilder`.
493    pub fn from_reader<R: Read>(rdr: R) -> Result<SourceMap> {
494        match decode(rdr)? {
495            DecodedMap::Regular(sm) => Ok(sm),
496            _ => Err(Error::IncompatibleSourceMap),
497        }
498    }
499
500    /// Writes a sourcemap into a writer.
501    ///
502    /// Note that this operation will generate an equivalent sourcemap to the
503    /// one that was generated on load however there might be small differences
504    /// in the generated JSON and layout. For instance `sourceRoot` will not
505    /// be set as upon parsing of the sourcemap the sources will already be
506    /// expanded.
507    ///
508    /// ```rust
509    /// # use sourcemap::SourceMap;
510    /// # let input: &[_] = b"{
511    /// #     \"version\":3,
512    /// #     \"sources\":[\"coolstuff.js\"],
513    /// #     \"names\":[\"x\",\"alert\"],
514    /// #     \"mappings\":\"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM\"
515    /// # }";
516    /// let sm = SourceMap::from_reader(input).unwrap();
517    /// let mut output : Vec<u8> = vec![];
518    /// sm.to_writer(&mut output).unwrap();
519    /// ```
520    pub fn to_writer<W: Write>(&self, w: W) -> Result<()> {
521        encode(self, w)
522    }
523
524    /// Encode a sourcemap into a data url.
525    ///
526    /// ```rust
527    /// # use sourcemap::SourceMap;
528    /// # let input: &[_] = b"{
529    /// #     \"version\":3,
530    /// #     \"sources\":[\"coolstuff.js\"],
531    /// #     \"names\":[\"x\",\"alert\"],
532    /// #     \"mappings\":\"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM\"
533    /// # }";
534    /// let sm = SourceMap::from_reader(input).unwrap();
535    /// sm.to_data_url().unwrap();
536    /// ```
537    pub fn to_data_url(&self) -> Result<String> {
538        let mut buf = vec![];
539        encode(self, &mut buf)?;
540        let b64 = base64_simd::Base64::STANDARD.encode_to_boxed_str(&buf);
541        Ok(format!(
542            "data:application/json;charset=utf-8;base64,{}",
543            b64
544        ))
545    }
546
547    /// Creates a sourcemap from a reader over a JSON byte slice in UTF-8
548    /// format.  Optionally a "garbage header" as defined by the
549    /// sourcemap draft specification is supported.  In case an indexed
550    /// sourcemap is encountered an error is returned.
551    ///
552    /// ```rust
553    /// use sourcemap::SourceMap;
554    /// let input: &[_] = b"{
555    ///     \"version\":3,
556    ///     \"sources\":[\"coolstuff.js\"],
557    ///     \"names\":[\"x\",\"alert\"],
558    ///     \"mappings\":\"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM\"
559    /// }";
560    /// let sm = SourceMap::from_slice(input).unwrap();
561    /// ```
562    pub fn from_slice(slice: &[u8]) -> Result<SourceMap> {
563        match decode_slice(slice)? {
564            DecodedMap::Regular(sm) => Ok(sm),
565            _ => Err(Error::IncompatibleSourceMap),
566        }
567    }
568
569    /// Constructs a new sourcemap from raw components.
570    ///
571    /// - `file`: an optional filename of the sourcemap
572    /// - `tokens`: a list of raw tokens
573    /// - `names`: a vector of names
574    /// - `sources` a vector of source filenames
575    /// - `sources_content` optional source contents
576    /// - `ignore_list` optional list of source indexes for devtools to ignore
577    pub fn new(
578        file: Option<Arc<str>>,
579        mut tokens: Vec<RawToken>,
580        names: Vec<Arc<str>>,
581        sources: Vec<Arc<str>>,
582        sources_content: Option<Vec<Option<Arc<str>>>>,
583    ) -> SourceMap {
584        tokens.sort_unstable_by_key(|t| (t.dst_line, t.dst_col));
585        SourceMap {
586            file,
587            tokens,
588            names,
589            source_root: None,
590            sources,
591            sources_prefixed: None,
592            sources_content: sources_content
593                .unwrap_or_default()
594                .into_iter()
595                .map(|opt| opt.map(SourceView::new))
596                .collect(),
597            ignore_list: BTreeSet::default(),
598            debug_id: None,
599        }
600    }
601
602    /// Returns the embedded debug id.
603    pub fn get_debug_id(&self) -> Option<DebugId> {
604        self.debug_id
605    }
606
607    /// Sets a new value for the debug id.
608    pub fn set_debug_id(&mut self, debug_id: Option<DebugId>) {
609        self.debug_id = debug_id
610    }
611
612    /// Returns the embedded filename in case there is one.
613    pub fn get_file(&self) -> Option<&str> {
614        self.file.as_deref()
615    }
616
617    /// Sets a new value for the file.
618    pub fn set_file<T: Into<Arc<str>>>(&mut self, value: Option<T>) {
619        self.file = value.map(Into::into);
620    }
621
622    /// Returns the embedded source_root in case there is one.
623    pub fn get_source_root(&self) -> Option<&str> {
624        self.source_root.as_deref()
625    }
626
627    fn prefix_source(source_root: &str, source: &str) -> Arc<str> {
628        let source_root = source_root.strip_suffix('/').unwrap_or(source_root);
629        let is_valid = !source.is_empty()
630            && (source.starts_with('/')
631                || source.starts_with("http:")
632                || source.starts_with("https:"));
633
634        if is_valid {
635            source.into()
636        } else {
637            format!("{source_root}/{source}").into()
638        }
639    }
640
641    /// Sets a new value for the source_root.
642    pub fn set_source_root<T: Into<Arc<str>>>(&mut self, value: Option<T>) {
643        self.source_root = value.map(Into::into);
644
645        match self.source_root.as_deref().filter(|rs| !rs.is_empty()) {
646            Some(source_root) => {
647                let sources_prefixed = self
648                    .sources
649                    .iter()
650                    .map(|source| Self::prefix_source(source_root, source))
651                    .collect();
652                self.sources_prefixed = Some(sources_prefixed)
653            }
654            None => self.sources_prefixed = None,
655        }
656    }
657
658    pub fn add_to_ignore_list(&mut self, src_id: u32) {
659        self.ignore_list.insert(src_id);
660    }
661
662    pub fn ignore_list(&self) -> impl Iterator<Item = &u32> {
663        self.ignore_list.iter()
664    }
665
666    /// Looks up a token by its index.
667    pub fn get_token(&self, idx: usize) -> Option<Token<'_>> {
668        self.tokens.get(idx).map(|raw| Token {
669            raw,
670            sm: self,
671            idx,
672            offset: 0,
673        })
674    }
675
676    /// Returns the number of tokens in the sourcemap.
677    pub fn get_token_count(&self) -> u32 {
678        self.tokens.len() as u32
679    }
680
681    /// Returns an iterator over the tokens.
682    pub fn tokens(&self) -> TokenIter<'_> {
683        TokenIter {
684            i: self,
685            next_idx: 0,
686        }
687    }
688
689    /// Looks up the closest token to a given 0-indexed line and column.
690    pub fn lookup_token(&self, line: u32, col: u32) -> Option<Token<'_>> {
691        let (idx, raw) =
692            greatest_lower_bound(&self.tokens, &(line, col), |t| (t.dst_line, t.dst_col))?;
693
694        let mut token = Token {
695            raw,
696            sm: self,
697            idx,
698            offset: 0,
699        };
700
701        if token.is_range() {
702            token.offset = col - token.get_dst_col();
703        }
704
705        Some(token)
706    }
707
708    /// Given a location, name and minified source file resolve a minified
709    /// name to an original function name.
710    ///
711    /// This invokes some guesswork and requires access to the original minified
712    /// source.  This will not yield proper results for anonymous functions or
713    /// functions that do not have clear function names.  (For instance it's
714    /// recommended that dotted function names are not passed to this
715    /// function).
716    pub fn get_original_function_name(
717        &self,
718        line: u32,
719        col: u32,
720        minified_name: &str,
721        sv: &SourceView,
722    ) -> Option<&str> {
723        self.lookup_token(line, col)
724            .and_then(|token| sv.get_original_function_name(token, minified_name))
725    }
726
727    /// Returns the number of sources in the sourcemap.
728    pub fn get_source_count(&self) -> u32 {
729        self.sources.len() as u32
730    }
731
732    /// Looks up a source for a specific index.
733    pub fn get_source(&self, idx: u32) -> Option<&str> {
734        let sources = self.sources_prefixed.as_deref().unwrap_or(&self.sources);
735        sources.get(idx as usize).map(|x| &x[..])
736    }
737
738    /// Sets a new source value for an index.  This cannot add new
739    /// sources.
740    ///
741    /// This panics if a source is set that does not exist.
742    pub fn set_source(&mut self, idx: u32, value: &str) {
743        self.sources[idx as usize] = value.into();
744
745        if let Some(sources_prefixed) = self.sources_prefixed.as_mut() {
746            // If sources_prefixed is `Some`, we must have a nonempty `source_root`.
747            sources_prefixed[idx as usize] =
748                Self::prefix_source(self.source_root.as_deref().unwrap(), value);
749        }
750    }
751
752    /// Iterates over all sources
753    pub fn sources(&self) -> SourceIter<'_> {
754        SourceIter {
755            i: self,
756            next_idx: 0,
757        }
758    }
759
760    /// Returns the sources content as source view.
761    pub fn get_source_view(&self, idx: u32) -> Option<&SourceView> {
762        self.sources_content
763            .get(idx as usize)
764            .and_then(Option::as_ref)
765    }
766
767    /// Looks up the content for a source.
768    pub fn get_source_contents(&self, idx: u32) -> Option<&str> {
769        self.sources_content
770            .get(idx as usize)
771            .and_then(Option::as_ref)
772            .map(SourceView::source)
773    }
774
775    /// Sets source contents for a source.
776    pub fn set_source_contents(&mut self, idx: u32, value: Option<&str>) {
777        if self.sources_content.len() != self.sources.len() {
778            self.sources_content.resize(self.sources.len(), None);
779        }
780        self.sources_content[idx as usize] = value.map(|x| SourceView::from_string(x.to_string()));
781    }
782
783    /// Iterates over all source contents
784    pub fn source_contents(&self) -> SourceContentsIter<'_> {
785        SourceContentsIter {
786            i: self,
787            next_idx: 0,
788        }
789    }
790
791    /// Returns an iterator over the names.
792    pub fn names(&self) -> NameIter<'_> {
793        NameIter {
794            i: self,
795            next_idx: 0,
796        }
797    }
798
799    /// Returns the number of names in the sourcemap.
800    pub fn get_name_count(&self) -> u32 {
801        self.names.len() as u32
802    }
803
804    /// Returns true if there are any names in the map.
805    pub fn has_names(&self) -> bool {
806        !self.names.is_empty()
807    }
808
809    /// Looks up a name for a specific index.
810    pub fn get_name(&self, idx: u32) -> Option<&str> {
811        self.names.get(idx as usize).map(|x| &x[..])
812    }
813
814    /// Removes all names from the sourcemap.
815    pub fn remove_names(&mut self) {
816        self.names.clear();
817    }
818
819    /// This rewrites the sourcemap according to the provided rewrite
820    /// options.
821    ///
822    /// The default behavior is to just deduplicate the sourcemap, something
823    /// that automatically takes place.  This for instance can be used to
824    /// slightly compress sourcemaps if certain data is not wanted.
825    ///
826    /// ```rust
827    /// use sourcemap::{SourceMap, RewriteOptions};
828    /// # let input: &[_] = b"{
829    /// #     \"version\":3,
830    /// #     \"sources\":[\"coolstuff.js\"],
831    /// #     \"names\":[\"x\",\"alert\"],
832    /// #     \"mappings\":\"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM\"
833    /// # }";
834    /// let sm = SourceMap::from_slice(input).unwrap();
835    /// let new_sm = sm.rewrite(&RewriteOptions {
836    ///     with_names: false,
837    ///     ..Default::default()
838    /// });
839    /// ```
840    pub fn rewrite(self, options: &RewriteOptions<'_>) -> Result<SourceMap> {
841        Ok(self.rewrite_with_mapping(options)?.0)
842    }
843
844    /// Same as `rewrite`, except also returns a remapping index for deduplicated `sources`.
845    pub(crate) fn rewrite_with_mapping(
846        self,
847        options: &RewriteOptions<'_>,
848    ) -> Result<(SourceMap, Vec<u32>)> {
849        let mut builder = SourceMapBuilder::new(self.get_file());
850        builder.set_debug_id(self.debug_id);
851
852        for token in self.tokens() {
853            let raw = builder.add_token(&token, options.with_names);
854            if raw.src_id != !0
855                && options.with_source_contents
856                && !builder.has_source_contents(raw.src_id)
857            {
858                builder
859                    .set_source_contents(raw.src_id, self.get_source_contents(token.get_src_id()));
860            }
861        }
862
863        #[cfg(any(unix, windows, target_os = "redox"))]
864        {
865            if options.load_local_source_contents {
866                builder.load_local_source_contents(options.base_path)?;
867            }
868        }
869
870        let mut prefixes = vec![];
871        let mut need_common_prefix = false;
872        for &prefix in options.strip_prefixes.iter() {
873            if prefix == "~" {
874                need_common_prefix = true;
875            } else {
876                prefixes.push(prefix.to_string());
877            }
878        }
879        if need_common_prefix {
880            if let Some(prefix) = find_common_prefix(self.sources.iter().map(AsRef::as_ref)) {
881                prefixes.push(prefix);
882            }
883        }
884        if !prefixes.is_empty() {
885            builder.strip_prefixes(&prefixes);
886        }
887
888        let mapping = builder.take_mapping();
889
890        let sm = builder.into_sourcemap();
891
892        Ok((sm, mapping))
893    }
894
895    /// Adjusts the mappings in `self` using the mappings in `adjustment`.
896    ///
897    /// Here is the intended use case for this function:
898    /// * You have a source file (for example, minified JS) `foo.js` and a
899    ///   corresponding sourcemap `foo.js.map`.
900    /// * You modify `foo.js` in some way and generate a sourcemap `transform.js.map`
901    ///   representing this modification. This can be done using `magic-string`, for example.
902    /// * You want a sourcemap that is "like" `foo.js.map`, but takes the changes you made to `foo.js` into account.
903    ///
904    /// Then `foo.js.map.adjust_mappings(transform.js.map)` is the desired sourcemap.
905    ///
906    /// This function assumes that `adjustment` contains no relevant information except for mappings.
907    ///  All information about sources and names is copied from `self`.
908    ///
909    /// Note that the resulting sourcemap will be at most as fine-grained as `self.`.
910    pub fn adjust_mappings(&mut self, adjustment: &Self) {
911        // The algorithm works by going through the tokens in `self` in order and adjusting
912        // them depending on the token in `adjustment` they're "covered" by.
913        // For example:
914        // Let `l` be a token in `adjustment` mapping `(17, 23)` to `(8, 30)` and let
915        // `r₁ : (8, 28) -> (102, 35)`, `r₂ : (8, 40) -> (102, 50)`, and
916        // `r₃ : (9, 10) -> (103, 12)` be the tokens in `self` that fall in the range of `l`.
917        // `l` offsets these tokens by `(+9, -7)`, so `r₁, … , r₃` must be offset by the same
918        // amount. Thus, the adjusted sourcemap will contain the tokens
919        // `c₁ : (17, 23) -> (102, 35)`, `c₂ : (17, 33) -> (102, 50)`, and
920        // `c3 : (18, 3) -> (103, 12)`.
921        //
922        // Or, in diagram form:
923        //
924        //    (17, 23)                                    (position in the edited source file)
925        //    ↓ l
926        //    (8, 30)
927        // (8, 28)        (8, 40)        (9, 10)          (positions in the original source file)
928        // ↓ r₁           ↓ r₂           ↓ r₃
929        // (102, 35)      (102, 50)      (103, 12)        (positions in the target file)
930        //
931        // becomes
932        //
933        //    (17, 23)       (17, 33)       (18, 3)       (positions in the edited source file)
934        //    ↓ c₁           ↓ c₂           ↓ c₃
935        //    (102, 35)      (102, 50)      (103, 12)     (positions in the target file)
936
937        // Helper struct that makes it easier to compare tokens by the start and end
938        // of the range they cover.
939        #[derive(Debug, Clone, Copy)]
940        struct Range {
941            start: (u32, u32),
942            end: (u32, u32),
943            value: RawToken,
944        }
945
946        /// Turns a list of tokens into a list of ranges, using the provided `key` function to determine the order of the tokens.
947        fn create_ranges(
948            mut tokens: Vec<RawToken>,
949            key: fn(&RawToken) -> (u32, u32),
950        ) -> Vec<Range> {
951            tokens.sort_unstable_by_key(key);
952
953            let mut token_iter = tokens.into_iter().peekable();
954            let mut ranges = Vec::new();
955
956            while let Some(t) = token_iter.next() {
957                let start = key(&t);
958                let next_start = token_iter.peek().map_or((u32::MAX, u32::MAX), key);
959                // A token extends either to the start of the next token or the end of the line, whichever comes sooner
960                let end = std::cmp::min(next_start, (start.0, u32::MAX));
961                ranges.push(Range {
962                    start,
963                    end,
964                    value: t,
965                });
966            }
967
968            ranges
969        }
970
971        // Turn `self.tokens` and `adjustment.tokens` into vectors of ranges so we have easy access to
972        // both start and end.
973        // We want to compare `self` and `adjustment` tokens by line/column numbers in the "original source" file.
974        // These line/column numbers are the `dst_line/col` for
975        // the `self` tokens and `src_line/col` for the `adjustment` tokens.
976        let self_tokens = std::mem::take(&mut self.tokens);
977        let original_ranges = create_ranges(self_tokens, |t| (t.dst_line, t.dst_col));
978        let adjustment_ranges =
979            create_ranges(adjustment.tokens.clone(), |t| (t.src_line, t.src_col));
980
981        let mut original_ranges_iter = original_ranges.iter();
982
983        let mut original_range = match original_ranges_iter.next() {
984            Some(r) => r,
985            None => return,
986        };
987
988        // Iterate over `adjustment_ranges` (sorted by `src_line/col`). For each such range, consider
989        // all `original_ranges` which overlap with it.
990        'outer: for &adjustment_range in &adjustment_ranges {
991            // The `adjustment_range` offsets lines and columns by a certain amount. All `original_ranges`
992            // it covers will get the same offset.
993            let (line_diff, col_diff) = (
994                adjustment_range.value.dst_line as i32 - adjustment_range.value.src_line as i32,
995                adjustment_range.value.dst_col as i32 - adjustment_range.value.src_col as i32,
996            );
997
998            // Skip `original_ranges` that are entirely before the `adjustment_range`.
999            while original_range.end <= adjustment_range.start {
1000                match original_ranges_iter.next() {
1001                    Some(r) => original_range = r,
1002                    None => break 'outer,
1003                }
1004            }
1005
1006            // At this point `original_range.end` > `adjustment_range.start`
1007
1008            // Iterate over `original_ranges` that fall at least partially within the `adjustment_range`.
1009            while original_range.start < adjustment_range.end {
1010                // If `original_range` started before `adjustment_range`, cut off the token's start.
1011                let (dst_line, dst_col) =
1012                    std::cmp::max(original_range.start, adjustment_range.start);
1013                let mut token = RawToken {
1014                    dst_line,
1015                    dst_col,
1016                    ..original_range.value
1017                };
1018
1019                token.dst_line = (token.dst_line as i32 + line_diff) as u32;
1020                token.dst_col = (token.dst_col as i32 + col_diff) as u32;
1021
1022                self.tokens.push(token);
1023
1024                if original_range.end >= adjustment_range.end {
1025                    // There are surely no more `original_ranges` for this `adjustment_range`.
1026                    // Break the loop without advancing the `original_range`.
1027                    break;
1028                } else {
1029                    //  Advance the `original_range`.
1030                    match original_ranges_iter.next() {
1031                        Some(r) => original_range = r,
1032                        None => break 'outer,
1033                    }
1034                }
1035            }
1036        }
1037
1038        self.tokens
1039            .sort_unstable_by_key(|t| (t.dst_line, t.dst_col));
1040    }
1041}
1042
1043impl SourceMapIndex {
1044    /// Creates a sourcemap index from a reader over a JSON stream in UTF-8
1045    /// format.  Optionally a "garbage header" as defined by the
1046    /// sourcemap draft specification is supported.  In case a regular
1047    /// sourcemap is encountered an error is returned.
1048    pub fn from_reader<R: Read>(rdr: R) -> Result<SourceMapIndex> {
1049        match decode(rdr)? {
1050            DecodedMap::Index(smi) => Ok(smi),
1051            _ => Err(Error::IncompatibleSourceMap),
1052        }
1053    }
1054
1055    /// Writes a sourcemap index into a writer.
1056    pub fn to_writer<W: Write>(&self, w: W) -> Result<()> {
1057        encode(self, w)
1058    }
1059
1060    /// Creates a sourcemap index from a reader over a JSON byte slice in UTF-8
1061    /// format.  Optionally a "garbage header" as defined by the
1062    /// sourcemap draft specification is supported.  In case a regular
1063    /// sourcemap is encountered an error is returned.
1064    pub fn from_slice(slice: &[u8]) -> Result<SourceMapIndex> {
1065        match decode_slice(slice)? {
1066            DecodedMap::Index(smi) => Ok(smi),
1067            _ => Err(Error::IncompatibleSourceMap),
1068        }
1069    }
1070
1071    /// Constructs a new sourcemap index from raw components.
1072    ///
1073    /// - `file`: an optional filename of the index
1074    /// - `sections`: a vector of source map index sections
1075    pub fn new(file: Option<String>, sections: Vec<SourceMapSection>) -> SourceMapIndex {
1076        SourceMapIndex {
1077            file,
1078            sections,
1079            x_facebook_offsets: None,
1080            x_metro_module_paths: None,
1081        }
1082    }
1083
1084    /// Constructs a new sourcemap index from raw components including the
1085    /// facebook RAM bundle extensions.
1086    ///
1087    /// - `file`: an optional filename of the index
1088    /// - `sections`: a vector of source map index sections
1089    /// - `x_facebook_offsets`: a vector of facebook offsets
1090    /// - `x_metro_module_paths`: a vector of metro module paths
1091    pub fn new_ram_bundle_compatible(
1092        file: Option<String>,
1093        sections: Vec<SourceMapSection>,
1094        x_facebook_offsets: Option<Vec<Option<u32>>>,
1095        x_metro_module_paths: Option<Vec<String>>,
1096    ) -> SourceMapIndex {
1097        SourceMapIndex {
1098            file,
1099            sections,
1100            x_facebook_offsets,
1101            x_metro_module_paths,
1102        }
1103    }
1104
1105    /// Returns the embedded filename in case there is one.
1106    pub fn get_file(&self) -> Option<&str> {
1107        self.file.as_ref().map(|x| &x[..])
1108    }
1109
1110    /// Sets a new value for the file.
1111    pub fn set_file(&mut self, value: Option<&str>) {
1112        self.file = value.map(str::to_owned);
1113    }
1114
1115    /// Returns the number of sections in this index
1116    pub fn get_section_count(&self) -> u32 {
1117        self.sections.len() as u32
1118    }
1119
1120    /// Looks up a single section and returns it
1121    pub fn get_section(&self, idx: u32) -> Option<&SourceMapSection> {
1122        self.sections.get(idx as usize)
1123    }
1124
1125    /// Looks up a single section and returns it as a mutable ref
1126    pub fn get_section_mut(&mut self, idx: u32) -> Option<&mut SourceMapSection> {
1127        self.sections.get_mut(idx as usize)
1128    }
1129
1130    /// Iterates over all sections
1131    pub fn sections(&self) -> SourceMapSectionIter<'_> {
1132        SourceMapSectionIter {
1133            i: self,
1134            next_idx: 0,
1135        }
1136    }
1137
1138    /// Given a location, name and minified source file resolve a minified
1139    /// name to an original function name.
1140    ///
1141    /// This invokes some guesswork and requires access to the original minified
1142    /// source.  This will not yield proper results for anonymous functions or
1143    /// functions that do not have clear function names.  (For instance it's
1144    /// recommended that dotted function names are not passed to this
1145    /// function).
1146    pub fn get_original_function_name(
1147        &self,
1148        line: u32,
1149        col: u32,
1150        minified_name: &str,
1151        sv: &SourceView,
1152    ) -> Option<&str> {
1153        self.lookup_token(line, col)
1154            .and_then(|token| sv.get_original_function_name(token, minified_name))
1155    }
1156
1157    /// Looks up the closest token to a given line and column.
1158    ///
1159    /// This requires that the referenced sourcemaps are actually loaded.
1160    /// If a sourcemap is encountered that is not embedded but just
1161    /// externally referenced it is silently skipped.
1162    pub fn lookup_token(&self, line: u32, col: u32) -> Option<Token<'_>> {
1163        let (_section_idx, section) =
1164            greatest_lower_bound(&self.sections, &(line, col), SourceMapSection::get_offset)?;
1165        let map = section.get_sourcemap()?;
1166        let (off_line, off_col) = section.get_offset();
1167        map.lookup_token(
1168            line - off_line,
1169            if line == off_line { col - off_col } else { col },
1170        )
1171    }
1172
1173    /// Flattens an indexed sourcemap into a regular one.  This requires
1174    /// that all referenced sourcemaps are attached.
1175    pub fn flatten(&self) -> Result<SourceMap> {
1176        let mut builder = SourceMapBuilder::new(self.get_file());
1177
1178        for section in self.sections() {
1179            let (off_line, off_col) = section.get_offset();
1180            let map = match section.get_sourcemap() {
1181                Some(map) => match map {
1182                    DecodedMap::Regular(sm) => Cow::Borrowed(sm),
1183                    DecodedMap::Index(idx) => Cow::Owned(idx.flatten()?),
1184                    DecodedMap::Hermes(smh) => Cow::Borrowed(&smh.sm),
1185                },
1186                None => {
1187                    return Err(Error::CannotFlatten(format!(
1188                        "Section has an unresolved \
1189                         sourcemap: {}",
1190                        section.get_url().unwrap_or("<unknown url>")
1191                    )));
1192                }
1193            };
1194
1195            for token in map.tokens() {
1196                let dst_col = if token.get_dst_line() == 0 {
1197                    token.get_dst_col() + off_col
1198                } else {
1199                    token.get_dst_col()
1200                };
1201                let raw = builder.add(
1202                    token.get_dst_line() + off_line,
1203                    dst_col,
1204                    token.get_src_line(),
1205                    token.get_src_col(),
1206                    token.get_source(),
1207                    token.get_name(),
1208                    token.is_range(),
1209                );
1210                if token.get_source().is_some() && !builder.has_source_contents(raw.src_id) {
1211                    builder.set_source_contents(
1212                        raw.src_id,
1213                        map.get_source_contents(token.get_src_id()),
1214                    );
1215                }
1216                if map.ignore_list.contains(&token.get_src_id()) {
1217                    builder.add_to_ignore_list(raw.src_id);
1218                }
1219            }
1220        }
1221
1222        Ok(builder.into_sourcemap())
1223    }
1224
1225    /// Flattens an indexed sourcemap into a regular one and automatically
1226    /// rewrites it.  This is more useful than plain flattening as this will
1227    /// cause the sourcemap to be properly deduplicated.
1228    pub fn flatten_and_rewrite(self, options: &RewriteOptions<'_>) -> Result<SourceMap> {
1229        self.flatten()?.rewrite(options)
1230    }
1231
1232    /// Returns `true` if this sourcemap is for a RAM bundle.
1233    pub fn is_for_ram_bundle(&self) -> bool {
1234        self.x_facebook_offsets.is_some() && self.x_metro_module_paths.is_some()
1235    }
1236
1237    /// Returns embeded x-facebook-offset values.
1238    pub fn x_facebook_offsets(&self) -> Option<&[Option<u32>]> {
1239        self.x_facebook_offsets.as_ref().map(|x| &x[..])
1240    }
1241
1242    /// Returns embedded metro module paths.
1243    pub fn x_metro_module_paths(&self) -> Option<&[String]> {
1244        self.x_metro_module_paths.as_ref().map(|x| &x[..])
1245    }
1246}
1247
1248impl SourceMapSection {
1249    /// Create a new sourcemap index section
1250    ///
1251    /// - `offset`: offset as line and column
1252    /// - `url`: optional URL of where the sourcemap is located
1253    /// - `map`: an optional already resolved internal sourcemap
1254    pub fn new(
1255        offset: (u32, u32),
1256        url: Option<String>,
1257        map: Option<DecodedMap>,
1258    ) -> SourceMapSection {
1259        SourceMapSection {
1260            offset,
1261            url,
1262            map: map.map(Box::new),
1263        }
1264    }
1265
1266    /// Returns the offset line
1267    pub fn get_offset_line(&self) -> u32 {
1268        self.offset.0
1269    }
1270
1271    /// Returns the offset column
1272    pub fn get_offset_col(&self) -> u32 {
1273        self.offset.1
1274    }
1275
1276    /// Returns the offset as tuple
1277    pub fn get_offset(&self) -> (u32, u32) {
1278        self.offset
1279    }
1280
1281    /// Returns the URL of the referenced map if available
1282    pub fn get_url(&self) -> Option<&str> {
1283        self.url.as_deref()
1284    }
1285
1286    /// Updates the URL for this section.
1287    pub fn set_url(&mut self, value: Option<&str>) {
1288        self.url = value.map(str::to_owned);
1289    }
1290
1291    /// Returns a reference to the embedded sourcemap if available
1292    pub fn get_sourcemap(&self) -> Option<&DecodedMap> {
1293        self.map.as_ref().map(Box::as_ref)
1294    }
1295
1296    /// Returns a reference to the embedded sourcemap if available
1297    pub fn get_sourcemap_mut(&mut self) -> Option<&mut DecodedMap> {
1298        self.map.as_mut().map(Box::as_mut)
1299    }
1300
1301    /// Replaces the embedded sourcemap
1302    pub fn set_sourcemap(&mut self, sm: Option<DecodedMap>) {
1303        self.map = sm.map(Box::new);
1304    }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    use super::{RewriteOptions, SourceMap};
1310    use debugid::DebugId;
1311
1312    #[test]
1313    fn test_rewrite_debugid() {
1314        let input: &[_] = br#"{
1315         "version":3,
1316         "sources":["coolstuff.js"],
1317         "names":["x","alert"],
1318         "mappings":"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM",
1319         "debug_id":"00000000-0000-0000-0000-000000000000"
1320     }"#;
1321
1322        let sm = SourceMap::from_slice(input).unwrap();
1323
1324        assert_eq!(sm.debug_id, Some(DebugId::default()));
1325
1326        let new_sm = sm
1327            .rewrite(&RewriteOptions {
1328                with_names: false,
1329                ..Default::default()
1330            })
1331            .unwrap();
1332
1333        assert_eq!(new_sm.debug_id, Some(DebugId::default()));
1334    }
1335
1336    #[test]
1337    fn test_debugid_alias() {
1338        let input: &[_] = br#"{
1339         "version":3,
1340         "sources":["coolstuff.js"],
1341         "names":["x","alert"],
1342         "mappings":"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM",
1343         "debug_id":"00000000-0000-0000-0000-000000000000",
1344         "debugId": "11111111-1111-1111-1111-111111111111"
1345     }"#;
1346
1347        let sm = SourceMap::from_slice(input).unwrap();
1348
1349        assert_eq!(sm.debug_id, Some(DebugId::default()));
1350    }
1351
1352    #[test]
1353    fn test_adjust_mappings_injection() {
1354        // A test that `adjust_mappings` does what it's supposed to for debug id injection.
1355        //
1356        // For each bundler:
1357        // * `bundle.js` and `bundle.js.map` are taken from https://github.com/kamilogorek/sourcemaps-playground/.
1358        // * `injected.bundle.js` and `injected.bundle.js.map` were created using the function`fixup_js_file` in `sentry-cli`.
1359        //   `injected.bundle.js.map` maps from `injected.bundle.js` to `bundle.js`.
1360        // * `composed.bundle.js.map` is the result of calling `adjust_mappings` on `bundle.js.map` and `injected.bundle.js.map`.
1361        //
1362        // If everything is working as intended, `composed.bundle.js.map` is a (good) sourcemap from `injected.bundle.js` to
1363        // the original sources. To verify that this is indeed the case, you can compare `bundle.js` / `bundle.js.map` with
1364        // `injected.bundle.js` / `composed.bundle.js.map` using https://sokra.github.io/source-map-visualization/#custom.
1365        //
1366        // NB: In the case of `rspack`, the sourcemap generated by the bundler is *horrible*. It's probably not useful, but
1367        // `adjust_mappings` preserves it as far as it goes.
1368        for bundler in ["esbuild", "rollup", "vite", "webpack", "rspack"] {
1369            let original_map_file = std::fs::File::open(format!(
1370                "tests/fixtures/adjust_mappings/{bundler}.bundle.js.map"
1371            ))
1372            .unwrap();
1373
1374            let injected_map_file = std::fs::File::open(format!(
1375                "tests/fixtures/adjust_mappings/{bundler}-injected.bundle.js.map"
1376            ))
1377            .unwrap();
1378
1379            let composed_map_file = std::fs::File::open(format!(
1380                "tests/fixtures/adjust_mappings/{bundler}-composed.bundle.js.map"
1381            ))
1382            .unwrap();
1383
1384            let mut original_map = SourceMap::from_reader(original_map_file).unwrap();
1385            let injected_map = SourceMap::from_reader(injected_map_file).unwrap();
1386            let composed_map = SourceMap::from_reader(composed_map_file).unwrap();
1387            original_map.adjust_mappings(&injected_map);
1388
1389            assert_eq!(
1390                original_map.tokens, composed_map.tokens,
1391                "bundler = {bundler}"
1392            );
1393        }
1394    }
1395
1396    #[test]
1397    fn test_roundtrip() {
1398        let sm = br#"{
1399            "version": 3,
1400            "file": "foo.js",
1401            "sources": [
1402                "./bar.js",
1403                "./baz.js"
1404            ],
1405            "sourceRoot": "webpack:///",
1406            "sourcesContent": [null, null],
1407            "names": [],
1408            "mappings": ""
1409        }"#;
1410
1411        let sm = SourceMap::from_slice(sm).unwrap();
1412        let mut out = Vec::new();
1413        sm.to_writer(&mut out).unwrap();
1414
1415        let sm_new = SourceMap::from_slice(&out).unwrap();
1416        assert_eq!(sm_new.sources, sm.sources);
1417    }
1418
1419    mod prop {
1420        //! This module exists to test the following property:
1421        //!
1422        //! Let `s` be a string.
1423        //! 1. Edit `s` with `magic-string` in such a way that edits (insertions, deletions) only happen *within* lines.
1424        //!    Call the resulting string `t` and the sourcemap relating the two `m₁`.
1425        //! 2. Further edit `t` with `magic-string` so that only *whole* lines are edited (inserted, deleted, prepended, appended).
1426        //!    Call the resulting string `u` and the sourcemap relating `u` to `t` `m₂`.
1427        //! 3. Do (1) and (2) in one go. The resulting string should still be `u`. Call the sourcemap
1428        //!    relating `u` and `s` `m₃`.
1429        //!
1430        //! Then `SourceMap::adjust_mappings(m₁, m₂) = m₃`.
1431        //!
1432        //! Or, in diagram form:
1433        //!
1434        //! u  -----m₂--------> t  -----m₁--------> s
1435        //! | -----------------m₃-----------------> |
1436        //!
1437        //! For the sake of simplicty, all input strings are 10 lines by 10 columns of the characters a-z.
1438        use magic_string::MagicString;
1439        use proptest::prelude::*;
1440
1441        use crate::SourceMap;
1442
1443        /// An edit in the first batch (only within a line).
1444        #[derive(Debug, Clone)]
1445        enum FirstEdit {
1446            /// Insert a string at a column.
1447            Insert(u32, String),
1448            /// Delete from one column to the other.
1449            Delete(i64, i64),
1450        }
1451
1452        impl FirstEdit {
1453            /// Applies an edit to the given line in the given `MagicString`.
1454            fn apply(&self, line: usize, ms: &mut MagicString) {
1455                // Every line is 11 bytes long, counting the newline.
1456                let line_offset = line * 11;
1457                match self {
1458                    FirstEdit::Insert(col, s) => {
1459                        ms.append_left(line_offset as u32 + *col, s).unwrap();
1460                    }
1461                    FirstEdit::Delete(start, end) => {
1462                        ms.remove(line_offset as i64 + *start, line_offset as i64 + *end)
1463                            .unwrap();
1464                    }
1465                }
1466            }
1467        }
1468
1469        /// Find the start and end index of the n'th line in the given string
1470        /// (including the terminating newline, if there is one).
1471        fn nth_line_start_end(n: usize, s: &str) -> (usize, usize) {
1472            let line = s.lines().nth(n).unwrap();
1473            let start = line.as_ptr() as usize - s.as_ptr() as usize;
1474            // All lines except line 9 have a final newline.
1475            let end = if n == 9 {
1476                start + line.len()
1477            } else {
1478                start + line.len() + 1
1479            };
1480            (start, end)
1481        }
1482
1483        /// An edit in the second batch (only whole lines).
1484        #[derive(Debug, Clone)]
1485        enum SecondEdit {
1486            /// Prepends a string.
1487            Prepend(String),
1488            /// Appends a string.
1489            Append(String),
1490            /// Inserts a string at a given line.
1491            Insert(usize, String),
1492            /// Deletes a a line.
1493            Delete(usize),
1494        }
1495
1496        impl SecondEdit {
1497            /// Applies an edit to a `MagicString`.
1498            ///
1499            /// This must know the original string (which unfortunately can't be extracted from a `MagicString`)
1500            /// to find line boundaries.
1501            fn apply(&self, orig: &str, ms: &mut MagicString) {
1502                match self {
1503                    SecondEdit::Prepend(s) => {
1504                        ms.prepend(s).unwrap();
1505                    }
1506                    SecondEdit::Append(s) => {
1507                        ms.append(s).unwrap();
1508                    }
1509                    SecondEdit::Insert(line, s) => {
1510                        let (start, _) = nth_line_start_end(*line, orig);
1511                        ms.prepend_left(start as u32, s).unwrap();
1512                    }
1513                    SecondEdit::Delete(line) => {
1514                        let (start, end) = nth_line_start_end(*line, orig);
1515                        ms.remove(start as i64, end as i64).unwrap();
1516                    }
1517                }
1518            }
1519        }
1520
1521        /// Produces a random 10x10 grid of the characters a-z.
1522        fn starting_string() -> impl Strategy<Value = String> {
1523            (vec!["[a-z]{10}"; 10]).prop_map(|v| v.join("\n"))
1524        }
1525
1526        /// Produces a random first-batch edit.
1527        fn first_edit() -> impl Strategy<Value = FirstEdit> {
1528            prop_oneof![
1529                (1u32..9, "[a-z]{5}").prop_map(|(c, s)| FirstEdit::Insert(c, s)),
1530                (1i64..10)
1531                    .prop_flat_map(|end| (0..end, Just(end)))
1532                    .prop_map(|(a, b)| FirstEdit::Delete(a, b))
1533            ]
1534        }
1535
1536        /// Produces a random sequence of first-batch edits, one per line.
1537        ///
1538        /// Thus, each line will either have an insertion or a deletion.
1539        fn first_edit_sequence() -> impl Strategy<Value = Vec<FirstEdit>> {
1540            let mut vec = Vec::with_capacity(10);
1541
1542            for _ in 0..10 {
1543                vec.push(first_edit())
1544            }
1545
1546            vec
1547        }
1548
1549        /// Produces a random sequence of second-batch edits, one per line.
1550        ///
1551        /// Each edit may delete a line, insert a line, or prepend or append something
1552        /// to the whole string. No two edits operate on the same line. The order of the edits is random.
1553        fn second_edit_sequence() -> impl Strategy<Value = Vec<SecondEdit>> {
1554            let edits = (0..10)
1555                .map(|i| {
1556                    prop_oneof![
1557                        "[a-z\n]{12}".prop_map(SecondEdit::Prepend),
1558                        "[a-z\n]{12}".prop_map(SecondEdit::Append),
1559                        "[a-z\n]{11}\n".prop_map(move |s| SecondEdit::Insert(i, s)),
1560                        Just(SecondEdit::Delete(i)),
1561                    ]
1562                })
1563                .collect::<Vec<_>>();
1564
1565            edits.prop_shuffle()
1566        }
1567
1568        proptest! {
1569            #[test]
1570            fn test_composition_identity(
1571                input in starting_string(),
1572                first_edits in first_edit_sequence(),
1573                second_edits in second_edit_sequence(),
1574            ) {
1575
1576                // Do edits in two batches and generate two sourcemaps
1577
1578                let mut ms1 = MagicString::new(&input);
1579
1580                for (line, first_edit) in first_edits.iter().enumerate() {
1581                    first_edit.apply(line, &mut ms1);
1582                }
1583
1584                let first_map = ms1.generate_map(Default::default()).unwrap().to_string().unwrap();
1585                let mut first_map = SourceMap::from_slice(first_map.as_bytes()).unwrap();
1586
1587                let transformed_input = ms1.to_string();
1588
1589                let mut ms2 = MagicString::new(&transformed_input);
1590
1591                for second_edit in second_edits.iter() {
1592                    second_edit.apply(&transformed_input, &mut ms2);
1593                }
1594
1595                let output_1 = ms2.to_string();
1596
1597                let second_map = ms2.generate_map(Default::default()).unwrap().to_string().unwrap();
1598                let second_map = SourceMap::from_slice(second_map.as_bytes()).unwrap();
1599
1600                // Do edits again in one batch and generate one big sourcemap
1601
1602                let mut ms3 = MagicString::new(&input);
1603
1604                for (line, first_edit) in first_edits.iter().enumerate() {
1605                    first_edit.apply(line, &mut ms3);
1606                }
1607
1608                for second_edit in second_edits.iter() {
1609                    second_edit.apply(&input, &mut ms3);
1610                }
1611
1612                let output_2 = ms3.to_string();
1613
1614                let third_map = ms3.generate_map(Default::default()).unwrap().to_string().unwrap();
1615                let third_map = SourceMap::from_slice(third_map.as_bytes()).unwrap();
1616
1617                // Both methods must produce the same output
1618                assert_eq!(output_1, output_2);
1619
1620                first_map.adjust_mappings(&second_map);
1621
1622                assert_eq!(first_map.tokens, third_map.tokens);
1623            }
1624        }
1625    }
1626}