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}