Skip to main content

source_lang/
map.rs

1//! The source map: many sources laid out across one global position space.
2
3use alloc::boxed::Box;
4use alloc::vec::Vec;
5
6use span_lang::{BytePos, LineCol, Span};
7
8use crate::{SourceFile, SourceId, SourceMapError};
9
10/// A collection of sources laid out end to end in a single global position space.
11///
12/// A `SourceMap` is the multi-file coordinate layer of a front-end. Each source
13/// added to it gets a stable [`SourceId`] and a non-overlapping range in one
14/// shared position space, so a single global [`BytePos`] names a point across the
15/// whole project. [`locate`](SourceMap::locate) maps such a position back to its
16/// `(SourceId, local offset)` — the inverse of the layout — which is how a
17/// diagnostic rendered against a global span knows *which file* to point at.
18///
19/// # Layout
20///
21/// Sources are placed in the order they are added: the first occupies
22/// `0..len₀`, the next `len₀..len₀ + len₁`, and so on. Because the bases only
23/// increase, the internal list is always sorted by start offset, so a lookup is a
24/// binary search over it — `O(log files)` — with no separate index to maintain.
25/// The whole space is 32 bits wide, the same envelope a single
26/// [`BytePos`] addresses, so the combined length of every source is capped at
27/// `u32::MAX`; overrunning it is the [`SpaceExhausted`] error, never a silent
28/// wrap into a neighbour's range.
29///
30/// [`SpaceExhausted`]: SourceMapError::SpaceExhausted
31///
32/// # Examples
33///
34/// ```
35/// use source_lang::{BytePos, SourceMap};
36///
37/// let mut map = SourceMap::new();
38/// let main = map.add("main.rs", "fn main() {}").expect("fits"); // global 0..12
39/// let util = map.add("util.rs", "fn helper() {}").expect("fits"); // global 12..26
40///
41/// // A global position resolves to the file it lands in and the local offset.
42/// let (id, local) = map.locate(BytePos::new(13)).expect("inside util.rs");
43/// assert_eq!(id, util);
44/// assert_eq!(local, BytePos::new(1)); // 13 - 12
45/// assert_eq!(map.source(id).unwrap().name(), "util.rs");
46///
47/// // Position 0 is the very start of the first file.
48/// assert_eq!(map.locate(BytePos::new(0)).unwrap().0, main);
49///
50/// // The end of the last file is still in it (its end-of-input position)...
51/// assert_eq!(map.locate(BytePos::new(26)), Some((util, BytePos::new(14))));
52/// // ...but anything past it belongs to no file.
53/// assert_eq!(map.locate(BytePos::new(27)), None);
54/// ```
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct SourceMap {
57    /// Sources in insertion order; always sorted by `span().start()` because
58    /// each new base is the previous high-water mark.
59    files: Vec<SourceFile>,
60    /// Each source's global start offset, parallel to `files`. `locate` binary
61    /// searches this dense `u32` array rather than the larger `SourceFile`
62    /// records, so a lookup touches four bytes per probe.
63    starts: Vec<u32>,
64    /// Each source's line-start table, parallel to `files`, built once in
65    /// `push` (see the `lines` module). Pure functions of the texts, so the
66    /// derived equality is unaffected.
67    lines: Vec<Box<[u32]>>,
68    /// The next free global offset — the exclusive end of the last source's
69    /// range, and the base the next source will be placed at.
70    next_base: u32,
71    /// The largest a single source may be, in bytes. Defaults to `u32::MAX`; a
72    /// smaller value bounds how much one untrusted input can load.
73    max_source_len: u32,
74}
75
76impl Default for SourceMap {
77    #[inline]
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl SourceMap {
84    /// Creates an empty map whose global position space starts at `0`.
85    ///
86    /// The per-source size ceiling starts at `u32::MAX`; lower it with
87    /// [`set_max_source_len`](SourceMap::set_max_source_len) to bound untrusted
88    /// input.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use source_lang::SourceMap;
94    ///
95    /// let map = SourceMap::new();
96    /// assert!(map.is_empty());
97    /// ```
98    #[inline]
99    #[must_use]
100    pub const fn new() -> Self {
101        Self {
102            files: Vec::new(),
103            starts: Vec::new(),
104            lines: Vec::new(),
105            next_base: 0,
106            max_source_len: u32::MAX,
107        }
108    }
109
110    /// Creates an empty map with room for `capacity` sources preallocated.
111    ///
112    /// A hint only: it sizes the internal list so that adding up to `capacity`
113    /// sources does not reallocate, which matters when the source count is known
114    /// up front. The global position space still starts empty.
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use source_lang::SourceMap;
120    ///
121    /// let mut map = SourceMap::with_capacity(2);
122    /// map.add("a", "x").expect("fits");
123    /// map.add("b", "y").expect("fits");
124    /// assert_eq!(map.len(), 2);
125    /// ```
126    #[inline]
127    #[must_use]
128    pub fn with_capacity(capacity: usize) -> Self {
129        Self {
130            files: Vec::with_capacity(capacity),
131            starts: Vec::with_capacity(capacity),
132            lines: Vec::with_capacity(capacity),
133            next_base: 0,
134            max_source_len: u32::MAX,
135        }
136    }
137
138    /// Returns the current per-source size ceiling, in bytes.
139    ///
140    /// A source longer than this is rejected with
141    /// [`SourceMapError::Oversize`] before it consumes any global space. The
142    /// default is `u32::MAX`.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use source_lang::SourceMap;
148    ///
149    /// let map = SourceMap::new();
150    /// assert_eq!(map.max_source_len(), u32::MAX);
151    /// ```
152    #[inline]
153    #[must_use]
154    pub const fn max_source_len(&self) -> u32 {
155        self.max_source_len
156    }
157
158    /// Sets the largest a single source may be, in bytes.
159    ///
160    /// Use it to bound how much one untrusted input — a file named on a command
161    /// line, a buffer from the network — can pull into memory. The limit applies
162    /// to every later [`add`](SourceMap::add), [`add_bytes`](SourceMap::add_bytes),
163    /// and [`add_file`](SourceMap::add_file); for a file it is checked against the
164    /// path's metadata before any bytes are read. Sources already in the map are
165    /// unaffected.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use source_lang::{SourceMap, SourceMapError};
171    ///
172    /// let mut map = SourceMap::new();
173    /// map.set_max_source_len(8);
174    ///
175    /// assert!(map.add("ok", "12345678").is_ok()); // exactly 8 bytes
176    /// let err = map.add("big", "123456789").unwrap_err(); // 9 bytes
177    /// assert!(matches!(err, SourceMapError::Oversize { len: 9, .. }));
178    /// ```
179    #[inline]
180    pub fn set_max_source_len(&mut self, max: u32) {
181        self.max_source_len = max;
182    }
183
184    /// Adds a source under `name` with the given `text`, returning its
185    /// [`SourceId`].
186    ///
187    /// The source is appended after every existing one: it takes the range
188    /// `next..next + text.len()` where `next` is the current end of the global
189    /// space. Both `name` and `text` are taken by value (anything that converts
190    /// into a `Box<str>` — a `String` or a `&str`), so the map owns the text and
191    /// callers can borrow it back for the life of the map.
192    ///
193    /// Adding an empty `text` is allowed: it yields a valid id whose source has a
194    /// zero-width span and does not advance the global space. It is located only
195    /// while it is the last source (its position is then the end of the space,
196    /// see [`locate`](SourceMap::locate)); otherwise its offset is the next
197    /// source's first byte.
198    ///
199    /// This is also where the source's line table is built, in one pass, so that
200    /// [`line_col`](SourceMap::line_col) never rescans the text afterwards.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`SourceMapError::SpaceExhausted`] if `text` does not fit in the
205    /// bytes left in the 32-bit global space, or if the map already holds the
206    /// maximum number of sources. The map is left unchanged, so the failure is
207    /// recoverable.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use source_lang::SourceMap;
213    ///
214    /// let mut map = SourceMap::new();
215    /// let id = map.add("config.toml", "name = \"demo\"").expect("fits");
216    /// assert_eq!(map.source(id).unwrap().text(), "name = \"demo\"");
217    ///
218    /// // A String works just as well as a &str.
219    /// let owned = String::from("generated");
220    /// let _ = map.add("out.txt", owned).expect("fits");
221    /// ```
222    pub fn add(
223        &mut self,
224        name: impl Into<Box<str>>,
225        text: impl Into<Box<str>>,
226    ) -> Result<SourceId, SourceMapError> {
227        self.push(name.into(), text.into())
228    }
229
230    /// Validates raw bytes as UTF-8 and adds them as a source under `name`.
231    ///
232    /// This is the in-memory counterpart to [`add_file`](SourceMap::add_file):
233    /// both turn untrusted bytes — from a buffer here, from disk there — into a
234    /// stored source through the same checks, so a network buffer and a file on
235    /// disk fail and succeed the same way.
236    ///
237    /// # Errors
238    ///
239    /// - [`SourceMapError::NotUtf8`] if `bytes` are not valid UTF-8.
240    /// - [`SourceMapError::Oversize`] if they exceed
241    ///   [`max_source_len`](SourceMap::max_source_len).
242    /// - [`SourceMapError::SpaceExhausted`] if they do not fit in the remaining
243    ///   global space.
244    ///
245    /// On any error the map is left unchanged.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use source_lang::{SourceMap, SourceMapError};
251    ///
252    /// let mut map = SourceMap::new();
253    /// let id = map.add_bytes("greeting.txt", b"hello").expect("valid UTF-8");
254    /// assert_eq!(map.source(id).unwrap().text(), "hello");
255    ///
256    /// // A stray binary byte is rejected, not stored as corrupt text.
257    /// let err = map.add_bytes("blob", &[0xff]).unwrap_err();
258    /// assert!(matches!(err, SourceMapError::NotUtf8 { .. }));
259    /// ```
260    pub fn add_bytes(
261        &mut self,
262        name: impl Into<Box<str>>,
263        bytes: &[u8],
264    ) -> Result<SourceId, SourceMapError> {
265        let name = name.into();
266        match core::str::from_utf8(bytes) {
267            Ok(text) => self.push(name, Box::from(text)),
268            Err(_) => Err(SourceMapError::NotUtf8 { name }),
269        }
270    }
271
272    /// Reads a file from disk and adds its contents as a source named by `path`.
273    ///
274    /// The file's size is checked against [`max_source_len`](SourceMap::max_source_len)
275    /// from its metadata *before* a single byte is read, so an oversize file is
276    /// rejected without being loaded into memory. The read itself is bounded too:
277    /// it stops one byte past the ceiling, so a source whose metadata reports no
278    /// length (a pipe, a `/proc` file) or a file that grows while it is read can
279    /// never pull more than `max_source_len + 1` bytes into memory. The bytes are
280    /// then validated as UTF-8 and stored. The source's name is the path as given.
281    ///
282    /// # Errors
283    ///
284    /// - [`SourceMapError::Oversize`] if the file's metadata length exceeds
285    ///   [`max_source_len`](SourceMap::max_source_len), or if the read reaches
286    ///   past it (then `len` is the number of bytes read before stopping,
287    ///   `max_source_len + 1`, a lower bound on the true size).
288    /// - [`SourceMapError::Io`] if the path cannot be opened or read (missing
289    ///   file, a directory, permission denied).
290    /// - [`SourceMapError::NotUtf8`] if the contents are not valid UTF-8.
291    /// - [`SourceMapError::SpaceExhausted`] if they do not fit in the remaining
292    ///   global space.
293    ///
294    /// On any error the map is left unchanged.
295    ///
296    /// # Examples
297    ///
298    /// ```no_run
299    /// use source_lang::SourceMap;
300    ///
301    /// let mut map = SourceMap::new();
302    /// let id = map.add_file("src/main.rs")?;
303    /// assert_eq!(map.source(id).unwrap().name(), "src/main.rs");
304    /// # Ok::<(), source_lang::SourceMapError>(())
305    /// ```
306    #[cfg(feature = "std")]
307    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
308    pub fn add_file(
309        &mut self,
310        path: impl AsRef<std::path::Path>,
311    ) -> Result<SourceId, SourceMapError> {
312        let path = path.as_ref();
313        let name: Box<str> = Box::from(path.to_string_lossy().as_ref());
314
315        // Reject from metadata before reading, so an oversize file never lands in
316        // memory. A file without a queryable length (a pipe, some virtual files)
317        // falls through to the bounded read below.
318        let file = match std::fs::File::open(path) {
319            Ok(file) => file,
320            Err(e) => {
321                return Err(SourceMapError::Io {
322                    name,
323                    kind: e.kind(),
324                });
325            }
326        };
327        let hint = match file.metadata() {
328            Ok(meta) if meta.len() > u64::from(self.max_source_len) => {
329                return Err(SourceMapError::Oversize {
330                    name,
331                    len: meta.len(),
332                });
333            }
334            Ok(meta) => meta.len(),
335            Err(_) => 0,
336        };
337
338        let bytes = match read_bounded(file, self.max_source_len, hint) {
339            Ok(Bounded::Within(bytes)) => bytes,
340            Ok(Bounded::Over(len)) => return Err(SourceMapError::Oversize { name, len }),
341            Err(e) => {
342                return Err(SourceMapError::Io {
343                    name,
344                    kind: e.kind(),
345                });
346            }
347        };
348        match core::str::from_utf8(&bytes) {
349            Ok(text) => self.push(name, Box::from(text)),
350            Err(_) => Err(SourceMapError::NotUtf8 { name }),
351        }
352    }
353
354    /// The single insertion seam every loader funnels through: enforce the size
355    /// limits, assign a non-overlapping range, and append. Nothing is mutated
356    /// until both limits pass, so a rejected add leaves the map untouched.
357    fn push(&mut self, name: Box<str>, text: Box<str>) -> Result<SourceId, SourceMapError> {
358        let needed = text.len() as u64;
359        if needed > u64::from(self.max_source_len) {
360            return Err(SourceMapError::Oversize { name, len: needed });
361        }
362
363        let base = self.next_base;
364        // `base` never exceeds `u32::MAX`, so this is the bytes still free.
365        let available = u64::from(u32::MAX - base);
366
367        // The source must fit in the remaining bytes, and the map must have an
368        // unused id left to mint. Both are checked before anything is mutated.
369        let index = u32::try_from(self.files.len());
370        let (len, index) = match (needed <= available, index) {
371            // `needed <= available <= u32::MAX`, so the narrowing is lossless.
372            (true, Ok(index)) => (needed as u32, index),
373            _ => return Err(SourceMapError::SpaceExhausted { needed, available }),
374        };
375
376        // `base + len <= base + available == u32::MAX`, so this cannot overflow.
377        let end = base + len;
378        let span = Span::new(base, end);
379        let id = SourceId::from_index(index);
380        self.lines.push(crate::lines::line_starts(&text));
381        self.starts.push(base);
382        self.files.push(SourceFile::new(name, text, span));
383        self.next_base = end;
384        Ok(id)
385    }
386
387    /// Resolves a global position to the source it falls in and the local offset
388    /// within that source.
389    ///
390    /// The returned [`BytePos`] is `pos` minus the source's base, i.e. the offset
391    /// into [`SourceFile::text`]. Resolution is a binary search over the sources'
392    /// start offsets, so it is `O(log files)` and borrows the located source
393    /// rather than copying it.
394    ///
395    /// Membership is half-open: a source covering `start..end` contains `start`
396    /// but not `end`, so the boundary between two adjacent sources resolves to the
397    /// second, never to both. One extension (since 1.0.1): the end of the **last**
398    /// source, the current end of the global space, resolves to that source with a
399    /// local offset equal to its length. That is where an end-of-input diagnostic
400    /// (a zero-width span at the end of the text) points, so end-of-input errors in
401    /// the most recently added source (in particular in a single-source map)
402    /// resolve to it, and an empty last source is locatable. Up to 1.0.0 that
403    /// position returned `None`.
404    ///
405    /// Returns `None` past the end of the space, on an empty map, and at the
406    /// offset of an empty source that is not the last one (that offset is the next
407    /// source's first byte).
408    ///
409    /// # Known limitation
410    ///
411    /// Sources are laid out contiguously, so the end of a source that is **not**
412    /// the last one is the same position as the next source's first byte. That
413    /// position resolves to the next source, at local offset 0: an end-of-input
414    /// diagnostic for an earlier source is attributed to the following source's
415    /// `1:1`. Telling the two apart needs a layout change (one reserved end
416    /// position per source, as rustc does), which moves every later source's base
417    /// and so waits for 2.0.
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// use source_lang::{BytePos, SourceMap};
423    ///
424    /// let mut map = SourceMap::new();
425    /// let a = map.add("a", "abc").expect("fits");  // 0..3
426    /// let b = map.add("b", "de").expect("fits");   // 3..5
427    ///
428    /// assert_eq!(map.locate(BytePos::new(2)), Some((a, BytePos::new(2))));
429    /// // The shared boundary at 3 is the start of `b`, not the end of `a`.
430    /// assert_eq!(map.locate(BytePos::new(3)), Some((b, BytePos::new(0))));
431    /// // The end of the last source is its end-of-input position (since 1.0.1).
432    /// assert_eq!(map.locate(BytePos::new(5)), Some((b, BytePos::new(2))));
433    /// assert_eq!(map.locate(BytePos::new(6)), None);
434    /// ```
435    #[must_use]
436    pub fn locate(&self, pos: BytePos) -> Option<(SourceId, BytePos)> {
437        let at = pos.to_u32();
438
439        // The list is sorted by start offset, so the last source whose range
440        // begins at or before `at` is the only one that can contain it.
441        let after = self.starts.partition_point(|&start| start <= at);
442        let index = after.checked_sub(1)?;
443        // Contiguous layout: a source ends where the next begins, and the last
444        // one at the high-water mark. Half-open membership, plus the last
445        // source's end position.
446        let is_last = after == self.starts.len();
447        let end = if is_last {
448            self.next_base
449        } else {
450            self.starts[after]
451        };
452        if at < end || (is_last && at == end) {
453            let local = at - self.starts[index];
454            // `index < files.len() <= u32::MAX`, so the cast is lossless.
455            Some((SourceId::from_index(index as u32), BytePos::new(local)))
456        } else {
457            None
458        }
459    }
460
461    /// Resolves a global position to its source and 1-based line/column.
462    ///
463    /// This is [`locate`](SourceMap::locate) composed with `span-lang`'s line
464    /// index: the position is mapped to its source and local offset, then that
465    /// offset is turned into a [`LineCol`] within the source's own text. The
466    /// column counts Unicode scalar values, so a multi-byte character advances
467    /// the column by one, not by its byte width.
468    ///
469    /// The source is chosen exactly as [`locate`](SourceMap::locate) chooses it,
470    /// so the end of the last source resolves to it, one column past its last
471    /// character. Returns `None` exactly when `locate` does.
472    ///
473    /// Each call is `O(log files + log lines)` plus a character count within the
474    /// one located line: since 1.0.1 the source's line table is built once, when
475    /// the source is added, never per call.
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use source_lang::{BytePos, LineCol, SourceMap};
481    ///
482    /// let mut map = SourceMap::new();
483    /// map.add("a.rs", "fn a() {}").expect("fits"); // 0..9
484    /// let b = map.add("b.rs", "let x = 1;\nlet y = 2;").expect("fits"); // 9..30
485    ///
486    /// // Global 20 is the second line of b.rs ("let y = 2;").
487    /// let (id, lc) = map.line_col(BytePos::new(20)).expect("in range");
488    /// assert_eq!(id, b);
489    /// assert_eq!(lc, LineCol::new(2, 1));
490    /// ```
491    #[must_use]
492    pub fn line_col(&self, pos: BytePos) -> Option<(SourceId, LineCol)> {
493        let (id, local) = self.locate(pos)?;
494        // `locate` returned this id, so the source is present.
495        let index = id.to_u32() as usize;
496        let line_col = crate::lines::line_col(self.files[index].text(), &self.lines[index], local);
497        Some((id, line_col))
498    }
499
500    /// Borrows the source named by `id`, or `None` if the id is not from this map.
501    ///
502    /// # Examples
503    ///
504    /// ```
505    /// use source_lang::SourceMap;
506    ///
507    /// let mut map = SourceMap::new();
508    /// let id = map.add("readme.md", "# title").expect("fits");
509    /// assert_eq!(map.source(id).unwrap().name(), "readme.md");
510    /// ```
511    #[inline]
512    #[must_use]
513    pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
514        self.files.get(id.to_u32() as usize)
515    }
516
517    /// Returns the number of sources in the map.
518    ///
519    /// # Examples
520    ///
521    /// ```
522    /// use source_lang::SourceMap;
523    ///
524    /// let mut map = SourceMap::new();
525    /// assert_eq!(map.len(), 0);
526    /// map.add("a", "x").expect("fits");
527    /// assert_eq!(map.len(), 1);
528    /// ```
529    #[inline]
530    #[must_use]
531    pub fn len(&self) -> usize {
532        self.files.len()
533    }
534
535    /// Returns `true` if the map holds no sources.
536    ///
537    /// # Examples
538    ///
539    /// ```
540    /// use source_lang::SourceMap;
541    ///
542    /// let mut map = SourceMap::new();
543    /// assert!(map.is_empty());
544    /// map.add("a", "x").expect("fits");
545    /// assert!(!map.is_empty());
546    /// ```
547    #[inline]
548    #[must_use]
549    pub fn is_empty(&self) -> bool {
550        self.files.is_empty()
551    }
552
553    /// Iterates over the sources in insertion order, pairing each with its id.
554    ///
555    /// The order is also id order (`0`, `1`, …) and global-offset order, so the
556    /// iterator walks the global position space from start to end. Useful for
557    /// listing the loaded files or building a side table keyed by `SourceId`.
558    ///
559    /// # Examples
560    ///
561    /// ```
562    /// use source_lang::SourceMap;
563    ///
564    /// let mut map = SourceMap::new();
565    /// map.add("a.txt", "one").expect("fits");
566    /// map.add("b.txt", "two").expect("fits");
567    ///
568    /// let names: Vec<_> = map.iter().map(|(_, f)| f.name()).collect();
569    /// assert_eq!(names, ["a.txt", "b.txt"]);
570    /// ```
571    pub fn iter(&self) -> impl ExactSizeIterator<Item = (SourceId, &SourceFile)> + '_ {
572        self.files
573            .iter()
574            .enumerate()
575            // `i < files.len() <= u32::MAX`, so the cast is lossless.
576            .map(|(i, file)| (SourceId::from_index(i as u32), file))
577    }
578}
579
580/// The outcome of a [`read_bounded`]: the bytes, or proof that the input runs
581/// past the ceiling.
582#[cfg(feature = "std")]
583#[derive(Debug)]
584enum Bounded {
585    /// The whole input, no longer than the ceiling.
586    Within(Vec<u8>),
587    /// The input exceeded the ceiling; the number of bytes read before stopping.
588    Over(u64),
589}
590
591/// Reads `reader` to its end, but never more than `max + 1` bytes: the extra
592/// byte is how an over-long input is told apart from one exactly at the
593/// ceiling without reading the rest of it. This is what keeps a pipe, a
594/// zero-length-reporting virtual file, or a file growing under the reader from
595/// loading without bound when its metadata could not vouch for its size.
596///
597/// `hint` (the metadata length, or `0`) only pre-sizes the buffer, capped at the
598/// read limit so a wrong hint can never reserve more than the limit allows.
599#[cfg(feature = "std")]
600fn read_bounded(reader: impl std::io::Read, max: u32, hint: u64) -> std::io::Result<Bounded> {
601    use std::io::Read;
602
603    let limit = u64::from(max) + 1;
604    let capacity = usize::try_from(hint.min(limit)).unwrap_or(0);
605    let mut bytes = Vec::with_capacity(capacity);
606    let _ = reader.take(limit).read_to_end(&mut bytes)?;
607    let read = bytes.len() as u64;
608    if read > u64::from(max) {
609        Ok(Bounded::Over(read))
610    } else {
611        Ok(Bounded::Within(bytes))
612    }
613}
614
615#[cfg(feature = "serde")]
616mod serde_support {
617    //! `serde` for [`SourceMap`]. The wire form is the list of sources — name and
618    //! text — plus the size ceiling; everything else (spans, ids, the high-water
619    //! mark) is derived, so it is regenerated on load rather than trusted from the
620    //! bytes. Deserialisation replays the sources through the same insertion path
621    //! as [`SourceMap::add`], which keeps the non-overlap and unique-id invariants
622    //! intact even if the input was hand-edited or corrupted.
623
624    use alloc::string::String;
625    use alloc::vec::Vec;
626
627    use serde::de::Error as _;
628    use serde::ser::SerializeStruct;
629    use serde::{Deserialize, Deserializer, Serialize, Serializer};
630
631    use super::SourceMap;
632
633    /// Borrowed view of one source, so serialising copies no text.
634    #[derive(Serialize)]
635    struct SourceRef<'a> {
636        name: &'a str,
637        text: &'a str,
638    }
639
640    /// Owned form held only while deserialising, before the map is rebuilt.
641    #[derive(Deserialize)]
642    struct SourceOwned {
643        name: String,
644        text: String,
645    }
646
647    /// Default ceiling for input that predates the field, matching [`SourceMap::new`].
648    fn unbounded() -> u32 {
649        u32::MAX
650    }
651
652    #[derive(Deserialize)]
653    struct MapData {
654        sources: Vec<SourceOwned>,
655        #[serde(default = "unbounded")]
656        max_source_len: u32,
657    }
658
659    impl Serialize for SourceMap {
660        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
661            let sources: Vec<SourceRef<'_>> = self
662                .files
663                .iter()
664                .map(|f| SourceRef {
665                    name: f.name(),
666                    text: f.text(),
667                })
668                .collect();
669            let mut state = serializer.serialize_struct("SourceMap", 2)?;
670            state.serialize_field("sources", &sources)?;
671            state.serialize_field("max_source_len", &self.max_source_len)?;
672            state.end()
673        }
674    }
675
676    impl<'de> Deserialize<'de> for SourceMap {
677        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
678            let data = MapData::deserialize(deserializer)?;
679            // Rebuild through `push` so spans, ids, and the high-water mark are
680            // regenerated and validated. The ceiling is applied only afterwards, so
681            // sources accepted under a looser limit are not rejected on reload.
682            let mut map = SourceMap::with_capacity(data.sources.len());
683            for source in data.sources {
684                let _ = map
685                    .push(source.name.into(), source.text.into())
686                    .map_err(D::Error::custom)?;
687            }
688            map.max_source_len = data.max_source_len;
689            Ok(map)
690        }
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    extern crate alloc;
697    use alloc::format;
698    use alloc::vec::Vec;
699
700    use super::*;
701
702    #[test]
703    fn test_add_assigns_sequential_stable_ids() {
704        let mut map = SourceMap::new();
705        let a = map.add("a", "x").expect("fits");
706        let b = map.add("b", "yy").expect("fits");
707        let c = map.add("c", "zzz").expect("fits");
708        assert_eq!((a.to_u32(), b.to_u32(), c.to_u32()), (0, 1, 2));
709        // Earlier ids still resolve to their original source after later adds.
710        assert_eq!(map.source(a).unwrap().name(), "a");
711        assert_eq!(map.source(b).unwrap().name(), "b");
712    }
713
714    #[test]
715    fn test_layout_is_contiguous_and_non_overlapping() {
716        let mut map = SourceMap::new();
717        map.add("a", "abc").expect("fits"); // 0..3
718        map.add("b", "de").expect("fits"); // 3..5
719        map.add("c", "fghi").expect("fits"); // 5..9
720        let spans: Vec<_> = map.iter().map(|(_, f)| f.span()).collect();
721        assert_eq!(spans[0], Span::new(0, 3));
722        assert_eq!(spans[1], Span::new(3, 5));
723        assert_eq!(spans[2], Span::new(5, 9));
724    }
725
726    #[test]
727    fn test_locate_at_boundaries_zero_one_and_past_end() {
728        let mut map = SourceMap::new();
729        let a = map.add("a", "abc").expect("fits"); // 0..3
730        let b = map.add("b", "de").expect("fits"); // 3..5
731        assert_eq!(map.locate(BytePos::new(0)), Some((a, BytePos::new(0))));
732        assert_eq!(map.locate(BytePos::new(2)), Some((a, BytePos::new(2))));
733        // The boundary belongs to the second file (unchanged in 1.0.1; see the
734        // known limitation on `locate`).
735        assert_eq!(map.locate(BytePos::new(3)), Some((b, BytePos::new(0))));
736        assert_eq!(map.locate(BytePos::new(4)), Some((b, BytePos::new(1))));
737        // Behaviour change in 1.0.1 (ISSUES H04): the end of the space used to be
738        // unmapped. It is the last file's end-of-input position, where a "found
739        // end of input" diagnostic points, so it resolves to that file at its
740        // length. Beyond it is still unmapped.
741        assert_eq!(map.locate(BytePos::new(5)), Some((b, BytePos::new(2))));
742        assert_eq!(map.locate(BytePos::new(6)), None);
743    }
744
745    #[test]
746    fn test_h04_end_of_last_file_is_locatable() {
747        // The lang-forge case: one source, an error at `Span::empty(len)`.
748        let src = "rule = 'a' |";
749        let mut map = SourceMap::new();
750        let id = map.add("g.sketch", src).expect("fits");
751        let eof = Span::empty(src.len() as u32);
752        assert_eq!(
753            map.locate(eof.start()),
754            Some((id, BytePos::new(src.len() as u32)))
755        );
756        // One column past the last character, on the last line.
757        assert_eq!(map.line_col(eof.start()), Some((id, LineCol::new(1, 13))));
758        assert_eq!(map.locate(BytePos::new(src.len() as u32 + 1)), None);
759    }
760
761    #[test]
762    fn test_h04_end_of_the_most_recent_file_is_locatable() {
763        let mut map = SourceMap::new();
764        let _a = map.add("a", "x = 1\n").expect("fits"); // 0..6
765        let b = map.add("b", "y = 2\n").expect("fits"); // 6..12
766        // EOF of the last file, after its trailing newline: `b` 2:1.
767        assert_eq!(map.locate(BytePos::new(12)), Some((b, BytePos::new(6))));
768        assert_eq!(
769            map.line_col(BytePos::new(12)),
770            Some((b, LineCol::new(2, 1)))
771        );
772        // Adding a third file moves the end of the space; 12 is now its start.
773        let c = map.add("c", "z").expect("fits"); // 12..13
774        assert_eq!(map.locate(BytePos::new(12)), Some((c, BytePos::new(0))));
775        assert_eq!(map.locate(BytePos::new(13)), Some((c, BytePos::new(1))));
776    }
777
778    #[test]
779    fn test_h04_known_limitation_inner_end_is_the_next_start() {
780        // Documented limitation of the contiguous layout (fixed in 2.0): the end
781        // of a file that is not the last one is the next file's first byte, and
782        // resolves there. This test pins 1.0.0's answer so 1.0.1 cannot drift.
783        let mut map = SourceMap::new();
784        let _a = map.add("a", "x = 1\n").expect("fits"); // 0..6
785        let b = map.add("b", "y = 2").expect("fits"); // 6..11
786        assert_eq!(map.locate(BytePos::new(6)), Some((b, BytePos::new(0))));
787        assert_eq!(map.line_col(BytePos::new(6)), Some((b, LineCol::new(1, 1))));
788    }
789
790    #[test]
791    fn test_h04_empty_last_file_is_locatable() {
792        // A lone empty source owns the end of the (empty) space.
793        let mut map = SourceMap::new();
794        let only = map.add("empty", "").expect("fits");
795        assert_eq!(map.locate(BytePos::new(0)), Some((only, BytePos::new(0))));
796        assert_eq!(
797            map.line_col(BytePos::new(0)),
798            Some((only, LineCol::new(1, 1)))
799        );
800        assert_eq!(map.locate(BytePos::new(1)), None);
801
802        // An empty source after a real one: the end of the space is now the empty
803        // source's position, so it wins over the previous source's end.
804        let mut map = SourceMap::new();
805        let _a = map.add("a", "ab").expect("fits"); // 0..2
806        let empty = map.add("empty", "").expect("fits"); // 2..2
807        assert_eq!(map.locate(BytePos::new(2)), Some((empty, BytePos::new(0))));
808    }
809
810    #[test]
811    fn test_locate_on_empty_map_is_none() {
812        let map = SourceMap::new();
813        assert_eq!(map.locate(BytePos::new(0)), None);
814    }
815
816    #[test]
817    fn test_empty_source_does_not_advance_space_and_is_unlocatable() {
818        let mut map = SourceMap::new();
819        let a = map.add("a", "ab").expect("fits"); // 0..2
820        let empty = map.add("empty", "").expect("fits"); // 2..2
821        let b = map.add("b", "cd").expect("fits"); // 2..4
822
823        assert!(map.source(empty).unwrap().span().is_empty());
824        // Position 2 is the start of `b`, never the zero-width `empty`.
825        assert_eq!(map.locate(BytePos::new(2)), Some((b, BytePos::new(0))));
826        assert_eq!(map.locate(BytePos::new(1)), Some((a, BytePos::new(1))));
827    }
828
829    #[test]
830    #[cfg(feature = "std")]
831    fn test_read_bounded_stops_an_endless_stream_one_byte_past_the_ceiling() {
832        // `repeat` never ends: an unbounded `read_to_end` would never return.
833        let got = read_bounded(std::io::repeat(b'a'), 16, 0).expect("in-memory read");
834        assert!(matches!(got, Bounded::Over(17)), "got {got:?}");
835    }
836
837    #[test]
838    #[cfg(feature = "std")]
839    fn test_read_bounded_accepts_exactly_the_ceiling_whatever_the_hint() {
840        // A hint that under- or over-states the size changes nothing but capacity.
841        for hint in [0, 3, 4, 1_000_000] {
842            match read_bounded(&b"abcd"[..], 4, hint).expect("in-memory read") {
843                Bounded::Within(bytes) => assert_eq!(bytes, b"abcd"),
844                other => panic!("hint {hint}: expected Within, got {other:?}"),
845            }
846        }
847        // One byte more is over, reported as the bytes read (max + 1).
848        let got = read_bounded(&b"abcde"[..], 4, 0).expect("in-memory read");
849        assert!(matches!(got, Bounded::Over(5)), "got {got:?}");
850    }
851
852    #[test]
853    #[cfg(feature = "std")]
854    fn test_read_bounded_with_a_zero_ceiling() {
855        assert!(matches!(
856            read_bounded(&b""[..], 0, 0).expect("read"),
857            Bounded::Within(ref b) if b.is_empty()
858        ));
859        assert!(matches!(
860            read_bounded(&b"x"[..], 0, 0).expect("read"),
861            Bounded::Over(1)
862        ));
863    }
864
865    #[test]
866    fn test_source_rejects_foreign_id() {
867        let mut map = SourceMap::new();
868        let _ = map.add("a", "x").expect("fits");
869        let mut other = SourceMap::new();
870        let foreign = other.add("b", "y").expect("fits");
871        // `foreign` has index 0, which exists here too, so cross-map ids are not
872        // distinguishable by value — but an out-of-range index is rejected.
873        let beyond = other.add("c", "z").expect("fits");
874        assert!(map.source(beyond).is_none());
875        assert!(map.source(foreign).is_some());
876    }
877
878    #[test]
879    fn test_add_at_space_boundary_accepts_exact_fit() {
880        let mut map = SourceMap::new();
881        // Drive the high-water mark to four bytes below the ceiling without
882        // allocating gigabytes; the field is private to this module's tests.
883        map.next_base = u32::MAX - 4;
884        let id = map.add("edge", "abcd").expect("exactly fills the space");
885        assert_eq!(
886            map.source(id).unwrap().span(),
887            Span::new(u32::MAX - 4, u32::MAX)
888        );
889        assert_eq!(map.next_base, u32::MAX);
890        // Its end, the last position of the space, is its end-of-input position.
891        assert_eq!(
892            map.locate(BytePos::new(u32::MAX)),
893            Some((id, BytePos::new(4)))
894        );
895    }
896
897    #[test]
898    fn test_add_past_space_boundary_is_rejected() {
899        let mut map = SourceMap::new();
900        map.next_base = u32::MAX - 4;
901        let err = map.add("edge", "abcde").expect_err("one byte too many");
902        assert_eq!(
903            err,
904            SourceMapError::SpaceExhausted {
905                needed: 5,
906                available: 4,
907            },
908        );
909        // The map is unchanged after a rejected add.
910        assert!(map.is_empty());
911        assert_eq!(map.next_base, u32::MAX - 4);
912    }
913
914    #[test]
915    fn test_add_empty_source_at_full_space_still_succeeds() {
916        let mut map = SourceMap::new();
917        map.next_base = u32::MAX;
918        // Zero bytes fit even when no space remains; one byte does not.
919        let empty = map.add("nothing", "").expect("zero bytes always fit");
920        assert!(map.source(empty).unwrap().span().is_empty());
921        assert!(map.add("one", "x").is_err());
922    }
923
924    #[test]
925    fn test_iter_reports_exact_len_and_pairs_ids_in_order() {
926        let mut map = SourceMap::new();
927        for i in 0..5 {
928            map.add(format!("f{i}"), "..").expect("fits");
929        }
930        let mut iter = map.iter();
931        assert_eq!(iter.len(), 5);
932        let collected: Vec<_> = iter.by_ref().map(|(id, _)| id.to_u32()).collect();
933        assert_eq!(collected, [0, 1, 2, 3, 4]);
934    }
935
936    #[test]
937    fn test_add_bytes_stores_valid_utf8() {
938        let mut map = SourceMap::new();
939        let id = map
940            .add_bytes("greeting", "héllo".as_bytes())
941            .expect("valid");
942        assert_eq!(map.source(id).unwrap().text(), "héllo");
943    }
944
945    #[test]
946    fn test_add_bytes_rejects_invalid_utf8_and_leaves_map_unchanged() {
947        let mut map = SourceMap::new();
948        // A lone continuation byte is not valid UTF-8.
949        let err = map.add_bytes("blob", &[0x68, 0xff, 0x69]).unwrap_err();
950        match err {
951            SourceMapError::NotUtf8 { name } => assert_eq!(&*name, "blob"),
952            other => panic!("expected NotUtf8, got {other:?}"),
953        }
954        assert!(map.is_empty());
955        assert_eq!(map.next_base, 0);
956    }
957
958    #[test]
959    fn test_add_bytes_empty_is_a_zero_width_source() {
960        let mut map = SourceMap::new();
961        let id = map
962            .add_bytes("empty", b"")
963            .expect("zero bytes are valid utf8");
964        assert!(map.source(id).unwrap().span().is_empty());
965    }
966
967    #[test]
968    fn test_max_source_len_rejects_at_the_byte_boundary() {
969        let mut map = SourceMap::new();
970        map.set_max_source_len(4);
971        assert_eq!(map.max_source_len(), 4);
972
973        // Exactly the limit fits; one byte over does not.
974        let ok = map.add("ok", "abcd").expect("exactly the limit");
975        assert_eq!(map.source(ok).unwrap().span().len(), 4);
976
977        let err = map.add("big", "abcde").unwrap_err();
978        match err {
979            SourceMapError::Oversize { name, len } => {
980                assert_eq!(&*name, "big");
981                assert_eq!(len, 5);
982            }
983            other => panic!("expected Oversize, got {other:?}"),
984        }
985        // The rejected add did not advance the space.
986        assert_eq!(map.next_base, 4);
987    }
988
989    #[test]
990    fn test_oversize_is_checked_before_space_exhaustion() {
991        let mut map = SourceMap::new();
992        map.set_max_source_len(2);
993        map.next_base = u32::MAX; // no global space left at all
994        // The per-source ceiling is reported, not space exhaustion.
995        let err = map.add("x", "abc").unwrap_err();
996        assert!(matches!(err, SourceMapError::Oversize { len: 3, .. }));
997    }
998
999    #[test]
1000    fn test_line_col_resolves_across_files_and_lines() {
1001        let mut map = SourceMap::new();
1002        let a = map.add("a", "ab\ncd").expect("fits"); // 0..5, lines 1-2
1003        let b = map.add("b", "wx\nyz").expect("fits"); // 5..10, lines 1-2
1004
1005        // First file, first line.
1006        assert_eq!(map.line_col(BytePos::new(0)), Some((a, LineCol::new(1, 1))));
1007        // First file, second line, second column ('d').
1008        assert_eq!(map.line_col(BytePos::new(4)), Some((a, LineCol::new(2, 2))));
1009        // Second file resets to its own line 1, column 1.
1010        assert_eq!(map.line_col(BytePos::new(5)), Some((b, LineCol::new(1, 1))));
1011        // Second file, second line ('y').
1012        assert_eq!(map.line_col(BytePos::new(8)), Some((b, LineCol::new(2, 1))));
1013    }
1014
1015    #[test]
1016    fn test_line_col_counts_characters_not_bytes() {
1017        let mut map = SourceMap::new();
1018        // "αβ" is two characters but four bytes; the second char starts at byte 2.
1019        let id = map.add("greek", "αβ").expect("fits");
1020        assert_eq!(
1021            map.line_col(BytePos::new(2)),
1022            Some((id, LineCol::new(1, 2)))
1023        );
1024    }
1025
1026    #[test]
1027    fn test_line_col_out_of_range_is_none() {
1028        let mut map = SourceMap::new();
1029        let a = map.add("a", "abc").expect("fits"); // 0..3
1030        // Behaviour change in 1.0.1 (H04): the end of the last source is its
1031        // end-of-input position (one column past `c`), no longer `None`.
1032        assert_eq!(map.line_col(BytePos::new(3)), Some((a, LineCol::new(1, 4))));
1033        assert_eq!(map.line_col(BytePos::new(4)), None);
1034        assert_eq!(map.line_col(BytePos::new(99)), None);
1035    }
1036}