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}