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