Skip to main content

mib_rs/source/
document.rs

1//! Compilation-local source document storage.
2
3use std::fmt;
4use std::num::NonZeroU32;
5use std::ops::Range;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9/// Identifies a source within one compilation.
10///
11/// IDs can only be allocated by [`SourceSet`]. They have no default or
12/// distinguished sentinel value.
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14#[repr(transparent)]
15pub struct SourceId(NonZeroU32);
16
17impl SourceId {
18    /// Return the compilation-local numeric identifier.
19    pub const fn get(self) -> u32 {
20        self.0.get()
21    }
22
23    fn for_index(index: usize) -> Result<Self, SourceRangeError> {
24        let value = index
25            .checked_add(1)
26            .and_then(|value| u32::try_from(value).ok())
27            .and_then(NonZeroU32::new)
28            .ok_or(SourceRangeError::TooManySources)?;
29        Ok(Self(value))
30    }
31
32    fn index(self) -> usize {
33        usize::try_from(self.0.get() - 1).expect("u32 source ID fits in usize")
34    }
35}
36
37/// A byte position within a source document.
38///
39/// The `u32` representation keeps every offset representable in the compiler
40/// coordinate space. [`SourceDocument::offset`] additionally checks that an
41/// offset lies within a particular document, including its exclusive end.
42#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
43#[repr(transparent)]
44pub struct ByteOffset(u32);
45
46impl ByteOffset {
47    /// Create a byte offset from its `u32` representation.
48    pub const fn new(value: u32) -> Self {
49        Self(value)
50    }
51
52    /// Return this offset as a `u32` byte index.
53    pub const fn get(self) -> u32 {
54        self.0
55    }
56
57    /// Return this offset as a host byte index.
58    pub const fn as_usize(self) -> usize {
59        self.0 as usize
60    }
61}
62
63impl TryFrom<usize> for ByteOffset {
64    type Error = SourceRangeError;
65
66    fn try_from(offset: usize) -> Result<Self, Self::Error> {
67        u32::try_from(offset)
68            .map(Self)
69            .map_err(|_| SourceRangeError::UnrepresentableOffset {
70                offset,
71                max: u32::MAX as usize,
72            })
73    }
74}
75
76impl fmt::Display for ByteOffset {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        self.0.fmt(formatter)
79    }
80}
81
82/// A zero-based byte position within a source document.
83///
84/// Lines are separated by LF, CRLF, or lone CR. Unlike editor positions, byte
85/// positions can identify every source byte, including both bytes of CRLF and
86/// bytes in invalid UTF-8.
87#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
88pub struct BytePosition {
89    line: u32,
90    column: u32,
91}
92
93impl BytePosition {
94    /// Create a zero-based line and byte-column position.
95    pub const fn new(line: u32, column: u32) -> Self {
96        Self { line, column }
97    }
98
99    /// Return the zero-based line.
100    pub const fn line(self) -> u32 {
101        self.line
102    }
103
104    /// Return the zero-based byte column.
105    pub const fn column(self) -> u32 {
106        self.column
107    }
108}
109
110/// A zero-based editor position.
111///
112/// The character field is measured in the explicitly selected
113/// position encoding. Line terminators are excluded, matching LSP position
114/// semantics.
115#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
116pub struct Position {
117    line: u32,
118    character: u32,
119}
120
121impl Position {
122    /// Create a zero-based line and encoded-character position.
123    pub const fn new(line: u32, character: u32) -> Self {
124        Self { line, character }
125    }
126
127    /// Return the zero-based line.
128    pub const fn line(self) -> u32 {
129        self.line
130    }
131
132    /// Return the zero-based encoded-character offset.
133    pub const fn character(self) -> u32 {
134        self.character
135    }
136}
137
138/// Encoding used for the character field of an editor position.
139#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
140pub enum PositionEncoding {
141    /// UTF-8 code units (bytes), restricted to Unicode scalar boundaries.
142    Utf8,
143    /// UTF-16 code units, as used by the original LSP position model.
144    Utf16,
145    /// UTF-32 code units (Unicode scalar values).
146    Utf32,
147}
148
149impl fmt::Display for PositionEncoding {
150    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151        formatter.write_str(match self {
152            Self::Utf8 => "UTF-8",
153            Self::Utf16 => "UTF-16",
154            Self::Utf32 => "UTF-32",
155        })
156    }
157}
158
159/// A half-open byte range within one source document.
160///
161/// Ranges are created by [`SourceDocument::range`] and
162/// [`SourceDocument::empty_range`], which ensure that their endpoints are
163/// ordered and within the source bytes.
164#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
165pub struct SourceRange {
166    source: SourceId,
167    start: ByteOffset,
168    end: ByteOffset,
169}
170
171impl SourceRange {
172    /// Return the document containing this range.
173    pub const fn source(self) -> SourceId {
174        self.source
175    }
176
177    /// Return the inclusive start offset.
178    pub const fn start(self) -> ByteOffset {
179        self.start
180    }
181
182    /// Return the exclusive end offset.
183    pub const fn end(self) -> ByteOffset {
184        self.end
185    }
186
187    /// Return this range as byte indices suitable for slicing.
188    pub fn byte_range(self) -> Range<usize> {
189        self.start.as_usize()..self.end.as_usize()
190    }
191
192    /// Return the smallest range covering both ranges.
193    ///
194    /// Both ranges must identify the same source document.
195    pub fn cover(first: Self, last: Self) -> Result<Self, SourceRangeError> {
196        if first.source != last.source {
197            return Err(SourceRangeError::SourceMismatch {
198                expected: first.source,
199                actual: last.source,
200            });
201        }
202        if first.start > first.end {
203            return Err(SourceRangeError::ReversedRange {
204                start: first.start,
205                end: first.end,
206            });
207        }
208        if last.start > last.end {
209            return Err(SourceRangeError::ReversedRange {
210                start: last.start,
211                end: last.end,
212            });
213        }
214
215        Ok(Self {
216            source: first.source,
217            start: first.start.min(last.start),
218            end: first.end.max(last.end),
219        })
220    }
221}
222
223impl fmt::Display for SourceId {
224    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
225        self.0.fmt(formatter)
226    }
227}
228
229/// The stable identity of source content, independent of its display label.
230#[derive(Clone, Debug, Eq, Hash, PartialEq)]
231pub enum SourceOrigin {
232    /// A file identified by its path.
233    File { path: PathBuf },
234    /// A source bundled into the library or another component.
235    Embedded { identity: Arc<str> },
236    /// An in-memory or editor buffer.
237    Memory { identity: Arc<str> },
238    /// A source supplied by another kind of provider.
239    Custom {
240        provider: Arc<str>,
241        identity: Arc<str>,
242    },
243}
244
245impl SourceOrigin {
246    /// Identify a source by its filesystem path.
247    pub fn file(path: impl Into<PathBuf>) -> Self {
248        Self::File { path: path.into() }
249    }
250
251    /// Identify content bundled into a library or application.
252    pub fn embedded(identity: impl Into<Arc<str>>) -> Self {
253        Self::Embedded {
254            identity: identity.into(),
255        }
256    }
257
258    /// Identify an in-memory or editor document.
259    pub fn memory(identity: impl Into<Arc<str>>) -> Self {
260        Self::Memory {
261            identity: identity.into(),
262        }
263    }
264
265    /// Identify content supplied by a custom kind of provider.
266    pub fn custom(provider: impl Into<Arc<str>>, identity: impl Into<Arc<str>>) -> Self {
267        Self::Custom {
268            provider: provider.into(),
269            identity: identity.into(),
270        }
271    }
272}
273
274/// Byte offsets at which each line in a source begins.
275///
276/// The first entry is always zero, including for an empty source. The basic
277/// byte-based line conversion is retained here; editor encoding conversion is
278/// handled separately.
279#[derive(Debug)]
280pub(crate) struct LineIndex {
281    starts: Box<[usize]>,
282}
283
284impl LineIndex {
285    fn new(bytes: &[u8]) -> Self {
286        let mut starts = Vec::new();
287        starts.push(0);
288        let mut index = 0;
289        while index < bytes.len() {
290            match bytes[index] {
291                b'\r' if bytes.get(index + 1) == Some(&b'\n') => {
292                    index += 2;
293                    starts.push(index);
294                }
295                b'\r' | b'\n' => {
296                    index += 1;
297                    starts.push(index);
298                }
299                _ => index += 1,
300            }
301        }
302        Self {
303            starts: starts.into_boxed_slice(),
304        }
305    }
306
307    pub(crate) fn line_count(&self) -> usize {
308        self.starts.len()
309    }
310
311    pub(crate) fn line_start(&self, line_index: usize) -> Option<usize> {
312        self.starts.get(line_index).copied()
313    }
314
315    pub(crate) fn line_starts(&self) -> &[usize] {
316        &self.starts
317    }
318}
319
320/// Immutable source content retained by a compilation.
321#[derive(Debug)]
322pub struct SourceDocument {
323    id: SourceId,
324    origin: SourceOrigin,
325    label: Arc<str>,
326    bytes: Arc<[u8]>,
327    line_index: LineIndex,
328}
329
330impl SourceDocument {
331    /// Return this document's compilation-local identity.
332    pub fn id(&self) -> SourceId {
333        self.id
334    }
335
336    /// Return this document's stable physical or logical origin.
337    pub fn origin(&self) -> &SourceOrigin {
338        &self.origin
339    }
340
341    /// Return the display label used for this document.
342    pub fn label(&self) -> &str {
343        &self.label
344    }
345
346    /// Return the immutable source bytes.
347    pub fn bytes(&self) -> &[u8] {
348        &self.bytes
349    }
350
351    /// Return the source length as a validated compiler byte offset.
352    pub fn len(&self) -> ByteOffset {
353        ByteOffset(u32::try_from(self.bytes.len()).expect("source length was validated"))
354    }
355
356    /// Return whether this document contains no bytes.
357    pub fn is_empty(&self) -> bool {
358        self.bytes.is_empty()
359    }
360
361    /// Return the number of logical lines.
362    ///
363    /// Every document has at least one line. A trailing LF, CRLF, or lone CR
364    /// creates a final empty line.
365    pub fn line_count(&self) -> usize {
366        self.line_index.starts.len()
367    }
368
369    /// Validate and convert a byte index into a compiler byte offset.
370    ///
371    /// The exclusive end-of-document offset is valid.
372    pub fn offset(&self, offset: usize) -> Result<ByteOffset, SourceRangeError> {
373        let offset = ByteOffset::try_from(offset)?;
374        if offset > self.len() {
375            return Err(SourceRangeError::OffsetOutOfBounds {
376                offset,
377                len: self.len(),
378            });
379        }
380        Ok(offset)
381    }
382
383    /// Create a checked half-open byte range in this document.
384    pub fn range(&self, range: Range<usize>) -> Result<SourceRange, SourceRangeError> {
385        let start = ByteOffset::try_from(range.start)?;
386        let end = ByteOffset::try_from(range.end)?;
387        if start > end {
388            return Err(SourceRangeError::ReversedRange { start, end });
389        }
390        if end > self.len() {
391            return Err(SourceRangeError::OffsetOutOfBounds {
392                offset: end,
393                len: self.len(),
394            });
395        }
396        Ok(SourceRange {
397            source: self.id,
398            start,
399            end,
400        })
401    }
402
403    /// Create a checked empty range at a byte offset in this document.
404    pub fn empty_range(&self, offset: usize) -> Result<SourceRange, SourceRangeError> {
405        self.range(offset..offset)
406    }
407
408    /// Return the bytes covered by a checked source range.
409    pub fn slice(&self, range: SourceRange) -> Result<&[u8], SourceRangeError> {
410        if range.source != self.id {
411            return Err(SourceRangeError::SourceMismatch {
412                expected: self.id,
413                actual: range.source,
414            });
415        }
416        if range.start > range.end {
417            return Err(SourceRangeError::ReversedRange {
418                start: range.start,
419                end: range.end,
420            });
421        }
422        if range.end > self.len() {
423            return Err(SourceRangeError::OffsetOutOfBounds {
424                offset: range.end,
425                len: self.len(),
426            });
427        }
428        Ok(&self.bytes[range.byte_range()])
429    }
430
431    pub(crate) fn line_index(&self) -> &LineIndex {
432        &self.line_index
433    }
434
435    /// Convert a checked byte offset to a one-based line and byte column.
436    ///
437    /// The exclusive end-of-document offset is valid. Columns count bytes;
438    /// callers needing editor character encodings must convert explicitly.
439    pub fn line_column(&self, offset: ByteOffset) -> Result<(usize, usize), SourceRangeError> {
440        if offset > self.len() {
441            return Err(SourceRangeError::OffsetOutOfBounds {
442                offset,
443                len: self.len(),
444            });
445        }
446        let offset = offset.as_usize();
447        let line_index = self
448            .line_index
449            .line_starts()
450            .partition_point(|&start| start <= offset)
451            .saturating_sub(1);
452        Ok((
453            line_index + 1,
454            offset - self.line_index.line_starts()[line_index] + 1,
455        ))
456    }
457
458    /// Convert a byte offset to a zero-based byte position.
459    ///
460    /// Every offset from zero through EOF is representable, including offsets
461    /// on either byte of CRLF and offsets inside invalid UTF-8.
462    pub fn byte_position(&self, offset: ByteOffset) -> Result<BytePosition, PositionError> {
463        if offset > self.len() {
464            return Err(PositionError::OffsetOutOfBounds {
465                offset,
466                len: self.len(),
467            });
468        }
469        let offset = offset.as_usize();
470        let line = self
471            .line_index
472            .starts
473            .partition_point(|&start| start <= offset)
474            .saturating_sub(1);
475        let column = offset - self.line_index.starts[line];
476        Ok(BytePosition::new(
477            u32::try_from(line).expect("source line index fits in u32"),
478            u32::try_from(column).expect("source byte column fits in u32"),
479        ))
480    }
481
482    /// Convert a zero-based byte position to its byte offset.
483    ///
484    /// On non-final lines, positions identify every terminator byte. The offset
485    /// immediately after an LF, CRLF, or lone CR is column zero of the next
486    /// line. On the final line, its end position identifies EOF.
487    pub fn byte_offset(&self, position: BytePosition) -> Result<ByteOffset, PositionError> {
488        let line = position.line as usize;
489        let Some(&start) = self.line_index.starts.get(line) else {
490            return Err(PositionError::LineOutOfBounds {
491                line: position.line,
492                line_count: self.line_count(),
493            });
494        };
495        let is_final = line + 1 == self.line_count();
496        let full_end = self
497            .line_index
498            .starts
499            .get(line + 1)
500            .copied()
501            .unwrap_or_else(|| self.bytes.len());
502        let max_column = full_end - start - usize::from(!is_final);
503        if position.column as usize > max_column {
504            return Err(PositionError::ByteColumnOutOfBounds {
505                line: position.line,
506                column: position.column,
507                max_column: u32::try_from(max_column).expect("source byte column fits in u32"),
508            });
509        }
510        Ok(ByteOffset::new(
511            u32::try_from(start + position.column as usize)
512                .expect("validated source offset fits in u32"),
513        ))
514    }
515
516    /// Convert a byte offset to an editor position in an explicit encoding.
517    ///
518    /// The source must be valid UTF-8, and the offset must lie on a Unicode
519    /// scalar boundary. Line terminators are excluded from editor columns. The
520    /// start of LF, CRLF, or lone CR maps to the line's end position; the
521    /// offset between CR and LF has no editor-position representation.
522    pub fn position(
523        &self,
524        offset: ByteOffset,
525        encoding: PositionEncoding,
526    ) -> Result<Position, PositionError> {
527        let byte_position = self.byte_position(offset)?;
528        let text = self.valid_utf8()?;
529        let extent = self
530            .line_extent(byte_position.line)
531            .expect("validated byte position identifies a line");
532        let offset = offset.as_usize();
533        if offset > extent.content_end {
534            return Err(PositionError::OffsetInsideLineTerminator {
535                offset: ByteOffset::new(
536                    u32::try_from(offset).expect("validated source offset fits in u32"),
537                ),
538                line: byte_position.line,
539            });
540        }
541        if !text.is_char_boundary(offset) {
542            return Err(PositionError::MidCodePoint {
543                offset: ByteOffset::new(
544                    u32::try_from(offset).expect("validated source offset fits in u32"),
545                ),
546            });
547        }
548        let prefix = &text[extent.start..offset];
549        let character = match encoding {
550            PositionEncoding::Utf8 => prefix.len(),
551            PositionEncoding::Utf16 => prefix.encode_utf16().count(),
552            PositionEncoding::Utf32 => prefix.chars().count(),
553        };
554        Ok(Position::new(
555            byte_position.line,
556            u32::try_from(character).expect("encoded source column fits in u32"),
557        ))
558    }
559
560    /// Convert an editor position in an explicit encoding to a byte offset.
561    ///
562    /// Lines and characters are zero-based. End-of-line positions map to the
563    /// first byte of LF, CRLF, or lone CR; a trailing terminator creates a final
564    /// empty line whose zero position maps to EOF.
565    pub fn position_offset(
566        &self,
567        position: Position,
568        encoding: PositionEncoding,
569    ) -> Result<ByteOffset, PositionError> {
570        let Some(extent) = self.line_extent(position.line) else {
571            return Err(PositionError::LineOutOfBounds {
572                line: position.line,
573                line_count: self.line_count(),
574            });
575        };
576        let text = self.valid_utf8()?;
577        let line_text = &text[extent.start..extent.content_end];
578        let character = position.character as usize;
579        let relative_offset = match encoding {
580            PositionEncoding::Utf8 => {
581                if character > line_text.len() {
582                    return Err(PositionError::CharacterOutOfBounds {
583                        line: position.line,
584                        character: position.character,
585                        max_character: u32::try_from(line_text.len())
586                            .expect("encoded source column fits in u32"),
587                        encoding,
588                    });
589                }
590                if !line_text.is_char_boundary(character) {
591                    return Err(PositionError::MidCodePoint {
592                        offset: ByteOffset::new(
593                            u32::try_from(extent.start + character)
594                                .expect("validated source offset fits in u32"),
595                        ),
596                    });
597                }
598                character
599            }
600            PositionEncoding::Utf16 => {
601                let mut units = 0usize;
602                let mut result = None;
603                for (byte_index, value) in line_text.char_indices() {
604                    if units == character {
605                        result = Some(byte_index);
606                        break;
607                    }
608                    let next = units + value.len_utf16();
609                    if character < next {
610                        return Err(PositionError::MidUtf16Surrogate {
611                            line: position.line,
612                            character: position.character,
613                        });
614                    }
615                    units = next;
616                }
617                if result.is_none() && units == character {
618                    result = Some(line_text.len());
619                }
620                match result {
621                    Some(offset) => offset,
622                    None => {
623                        return Err(PositionError::CharacterOutOfBounds {
624                            line: position.line,
625                            character: position.character,
626                            max_character: u32::try_from(units)
627                                .expect("encoded source column fits in u32"),
628                            encoding,
629                        });
630                    }
631                }
632            }
633            PositionEncoding::Utf32 => {
634                let count = line_text.chars().count();
635                if character > count {
636                    return Err(PositionError::CharacterOutOfBounds {
637                        line: position.line,
638                        character: position.character,
639                        max_character: u32::try_from(count)
640                            .expect("encoded source column fits in u32"),
641                        encoding,
642                    });
643                }
644                line_text
645                    .char_indices()
646                    .map(|(byte_index, _)| byte_index)
647                    .chain(std::iter::once(line_text.len()))
648                    .nth(character)
649                    .expect("validated UTF-32 column has a byte boundary")
650            }
651        };
652        Ok(ByteOffset::new(
653            u32::try_from(extent.start + relative_offset)
654                .expect("validated source offset fits in u32"),
655        ))
656    }
657
658    fn valid_utf8(&self) -> Result<&str, PositionError> {
659        std::str::from_utf8(&self.bytes).map_err(|error| PositionError::InvalidUtf8 {
660            valid_up_to: ByteOffset::new(
661                u32::try_from(error.valid_up_to()).expect("source offset fits in u32"),
662            ),
663            error_len: error.error_len(),
664        })
665    }
666
667    fn line_extent(&self, line: u32) -> Option<LineExtent> {
668        let line = line as usize;
669        let &start = self.line_index.starts.get(line)?;
670        let full_end = self
671            .line_index
672            .starts
673            .get(line + 1)
674            .copied()
675            .unwrap_or_else(|| self.bytes.len());
676        let mut content_end = full_end;
677        if line + 1 < self.line_count() {
678            match self.bytes[full_end - 1] {
679                b'\n' => {
680                    content_end -= 1;
681                    if content_end > start && self.bytes[content_end - 1] == b'\r' {
682                        content_end -= 1;
683                    }
684                }
685                b'\r' => content_end -= 1,
686                _ => unreachable!("line index ends non-final lines after terminators"),
687            }
688        }
689        Some(LineExtent { start, content_end })
690    }
691}
692
693#[derive(Clone, Copy, Debug)]
694struct LineExtent {
695    start: usize,
696    content_end: usize,
697}
698
699/// Failure to create or use a source coordinate or retain another source.
700#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
701pub enum SourceRangeError {
702    /// A host byte index cannot be represented by the compiler coordinate type.
703    #[error("byte offset {offset} cannot be represented (maximum is {max})")]
704    UnrepresentableOffset { offset: usize, max: usize },
705    /// A byte offset is beyond the exclusive end of a source document.
706    #[error("byte offset {offset} is outside a source of length {len}")]
707    OffsetOutOfBounds { offset: ByteOffset, len: ByteOffset },
708    /// The start of a range follows its end.
709    #[error("source range start {start} follows end {end}")]
710    ReversedRange { start: ByteOffset, end: ByteOffset },
711    /// A range identifies a different source document.
712    #[error("source range belongs to source {actual}, not source {expected}")]
713    SourceMismatch {
714        expected: SourceId,
715        actual: SourceId,
716    },
717    /// Source byte offsets must fit in the compiler's `u32` coordinate space.
718    #[error("source is too large ({len} bytes; maximum is {max})")]
719    SourceTooLarge { len: usize, max: usize },
720    /// All representable compilation-local IDs have been allocated.
721    #[error("too many sources in one compilation")]
722    TooManySources,
723}
724
725/// Failure to convert between byte offsets, byte positions, and editor positions.
726#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
727pub enum PositionError {
728    /// A byte offset lies beyond EOF.
729    #[error("byte offset {offset} is outside a source of length {len}")]
730    OffsetOutOfBounds { offset: ByteOffset, len: ByteOffset },
731    /// A zero-based line does not exist.
732    #[error("line {line} is outside a source with {line_count} lines")]
733    LineOutOfBounds { line: u32, line_count: usize },
734    /// A byte column does not identify a byte or the final EOF position.
735    #[error("byte column {column} on line {line} exceeds maximum {max_column}")]
736    ByteColumnOutOfBounds {
737        line: u32,
738        column: u32,
739        max_column: u32,
740    },
741    /// An editor character lies beyond the logical end of its line.
742    #[error("{encoding} character {character} on line {line} exceeds maximum {max_character}")]
743    CharacterOutOfBounds {
744        line: u32,
745        character: u32,
746        max_character: u32,
747        encoding: PositionEncoding,
748    },
749    /// A byte offset falls between the CR and LF bytes of a CRLF terminator.
750    #[error("byte offset {offset} falls inside the line {line} terminator")]
751    OffsetInsideLineTerminator { offset: ByteOffset, line: u32 },
752    /// A UTF-8 position falls within a multi-byte code point.
753    #[error("byte offset {offset} falls inside a UTF-8 code point")]
754    MidCodePoint { offset: ByteOffset },
755    /// A UTF-16 position falls between the surrogate code units of an astral character.
756    #[error("UTF-16 character {character} on line {line} falls inside a surrogate pair")]
757    MidUtf16Surrogate { line: u32, character: u32 },
758    /// Editor conversion requires a valid UTF-8 source document.
759    #[error("source is not valid UTF-8 after byte {valid_up_to} (invalid length {error_len:?})")]
760    InvalidUtf8 {
761        valid_up_to: ByteOffset,
762        error_len: Option<usize>,
763    },
764}
765
766/// Owns the source documents retained for one compilation.
767///
768/// A source set is mutable while callers build a parse-only compilation, but
769/// it cannot be cloned into independently mutable arenas whose future IDs
770/// would alias. Resolved MIBs and diagnostic reports share one internal arena.
771///
772/// ```compile_fail
773/// use mib_rs::SourceSet;
774///
775/// let sources = SourceSet::new();
776/// let fork = sources.clone();
777/// ```
778#[derive(Debug, Default)]
779pub struct SourceSet {
780    documents: Vec<Arc<SourceDocument>>,
781}
782
783impl SourceSet {
784    /// Create an empty compilation-local source collection.
785    pub fn new() -> Self {
786        Self::default()
787    }
788
789    /// Return the number of retained documents.
790    pub fn len(&self) -> usize {
791        self.documents.len()
792    }
793
794    /// Return whether no documents have been retained.
795    pub fn is_empty(&self) -> bool {
796        self.documents.is_empty()
797    }
798
799    /// Retain a source document and return its compilation-local identity.
800    pub fn insert(
801        &mut self,
802        origin: SourceOrigin,
803        label: impl Into<Arc<str>>,
804        bytes: Arc<[u8]>,
805    ) -> Result<SourceId, SourceRangeError> {
806        self.insert_shared(origin, label, bytes)
807            .map(|document| document.id())
808    }
809
810    pub(crate) fn insert_shared(
811        &mut self,
812        origin: SourceOrigin,
813        label: impl Into<Arc<str>>,
814        bytes: Arc<[u8]>,
815    ) -> Result<Arc<SourceDocument>, SourceRangeError> {
816        validate_source_len(bytes.len())?;
817        let id = SourceId::for_index(self.documents.len())?;
818        let document = Arc::new(SourceDocument {
819            id,
820            origin,
821            label: label.into(),
822            line_index: LineIndex::new(&bytes),
823            bytes,
824        });
825        self.documents.push(Arc::clone(&document));
826        Ok(document)
827    }
828
829    /// Return a retained document by its compilation-local identity.
830    pub fn get(&self, id: SourceId) -> Option<&SourceDocument> {
831        self.documents.get(id.index()).map(Arc::as_ref)
832    }
833
834    /// Iterate over retained documents in identity allocation order.
835    pub fn iter(&self) -> impl ExactSizeIterator<Item = &SourceDocument> {
836        self.documents.iter().map(Arc::as_ref)
837    }
838}
839
840fn validate_source_len(len: usize) -> Result<(), SourceRangeError> {
841    let max = u32::MAX as usize;
842    if len > max {
843        return Err(SourceRangeError::SourceTooLarge { len, max });
844    }
845    Ok(())
846}
847
848#[cfg(test)]
849mod tests {
850    use super::*;
851
852    fn memory_origin(identity: &str) -> SourceOrigin {
853        SourceOrigin::Memory {
854            identity: Arc::from(identity),
855        }
856    }
857
858    fn test_document(bytes: &[u8]) -> Arc<SourceDocument> {
859        let mut sources = SourceSet::new();
860        sources
861            .insert_shared(
862                memory_origin("position-test"),
863                "position-test",
864                Arc::from(bytes),
865            )
866            .unwrap()
867    }
868
869    #[test]
870    fn source_set_allocates_nonzero_unique_ids() {
871        let mut sources = SourceSet::new();
872        let first = sources
873            .insert(memory_origin("buffer:one"), "one", Arc::from(&b"one"[..]))
874            .unwrap();
875        let second = sources
876            .insert(memory_origin("buffer:two"), "two", Arc::from(&b"two"[..]))
877            .unwrap();
878
879        assert_ne!(first, second);
880        assert_eq!(first.get(), 1);
881        assert_eq!(second.get(), 2);
882        assert_eq!(first.to_string(), "1");
883        assert_eq!(second.to_string(), "2");
884        assert_eq!(sources.len(), 2);
885        assert!(!sources.is_empty());
886        assert_eq!(
887            sources.iter().map(SourceDocument::id).collect::<Vec<_>>(),
888            [first, second]
889        );
890    }
891
892    #[test]
893    fn origin_identity_is_distinct_from_display_label() {
894        let mut sources = SourceSet::new();
895        let origin = SourceOrigin::Custom {
896            provider: Arc::from("workspace"),
897            identity: Arc::from("document/42"),
898        };
899        let id = sources
900            .insert(origin.clone(), "ACME-MIB", Arc::from(&b"contents"[..]))
901            .unwrap();
902        let document = sources.get(id).unwrap();
903
904        assert_eq!(document.origin(), &origin);
905        assert_eq!(document.label(), "ACME-MIB");
906        assert_ne!(document.label(), "document/42");
907    }
908
909    #[test]
910    fn document_retains_shared_bytes_without_copying() {
911        let bytes: Arc<[u8]> = Arc::from(&b"first\nsecond"[..]);
912        let mut sources = SourceSet::new();
913        let id = sources
914            .insert(memory_origin("buffer"), "buffer", Arc::clone(&bytes))
915            .unwrap();
916        let document = sources.get(id).unwrap();
917
918        assert_eq!(document.bytes(), bytes.as_ref());
919        assert_eq!(document.bytes().as_ptr(), bytes.as_ptr());
920        assert_eq!(Arc::strong_count(&bytes), 2);
921    }
922
923    #[test]
924    fn source_lookup_checks_id_bounds() {
925        let mut sources = SourceSet::new();
926        let id = sources
927            .insert(memory_origin("buffer"), "buffer", Arc::from(&b""[..]))
928            .unwrap();
929
930        assert_eq!(sources.get(id).unwrap().id(), id);
931        assert!(sources.get(SourceId::for_index(1).unwrap()).is_none());
932    }
933
934    #[test]
935    fn line_index_owns_all_line_starts_and_checks_bounds() {
936        let mut sources = SourceSet::new();
937        let id = sources
938            .insert(
939                memory_origin("buffer"),
940                "buffer",
941                Arc::from(&b"first\n\nthird\n"[..]),
942            )
943            .unwrap();
944        let document = sources.get(id).unwrap();
945        let index = document.line_index();
946
947        assert_eq!(index.line_starts(), &[0, 6, 7, 13]);
948        assert_eq!(index.line_count(), 4);
949        assert_eq!(index.line_start(0), Some(0));
950        assert_eq!(index.line_start(3), Some(13));
951        assert_eq!(index.line_start(4), None);
952    }
953
954    #[test]
955    fn document_converts_checked_byte_offsets_to_one_based_positions() {
956        let mut sources = SourceSet::new();
957        let id = sources
958            .insert(
959                memory_origin("buffer"),
960                "buffer",
961                Arc::from(&b"first\nsecond"[..]),
962            )
963            .unwrap();
964        let document = sources.get(id).unwrap();
965
966        assert_eq!(document.line_column(ByteOffset::new(0)).unwrap(), (1, 1));
967        assert_eq!(document.line_column(ByteOffset::new(6)).unwrap(), (2, 1));
968        assert_eq!(document.line_column(document.len()).unwrap(), (2, 7));
969        assert_eq!(
970            document.line_column(ByteOffset::new(13)),
971            Err(SourceRangeError::OffsetOutOfBounds {
972                offset: ByteOffset::new(13),
973                len: document.len(),
974            })
975        );
976    }
977
978    #[test]
979    fn all_origin_kinds_retain_typed_identity() {
980        let origins = [
981            SourceOrigin::File {
982                path: PathBuf::from("/mibs/IF-MIB"),
983            },
984            SourceOrigin::Embedded {
985                identity: Arc::from("SNMPv2-SMI"),
986            },
987            memory_origin("untitled:1"),
988            SourceOrigin::Custom {
989                provider: Arc::from("database"),
990                identity: Arc::from("mib/7"),
991            },
992        ];
993
994        assert_eq!(origins.len(), 4);
995        assert!(origins.iter().all(|origin| origins.contains(origin)));
996    }
997
998    #[test]
999    fn rejects_unrepresentable_source_lengths_and_id_overflow() {
1000        let too_large = (u32::MAX as usize).checked_add(1).unwrap();
1001        assert_eq!(
1002            validate_source_len(too_large),
1003            Err(SourceRangeError::SourceTooLarge {
1004                len: too_large,
1005                max: u32::MAX as usize,
1006            })
1007        );
1008        assert_eq!(
1009            SourceId::for_index(u32::MAX as usize),
1010            Err(SourceRangeError::TooManySources)
1011        );
1012        let maximum = ByteOffset::try_from(u32::MAX as usize).unwrap();
1013        assert_eq!(maximum, ByteOffset::new(u32::MAX));
1014        assert_eq!(maximum.get(), u32::MAX);
1015        assert_eq!(maximum.as_usize(), u32::MAX as usize);
1016    }
1017
1018    #[test]
1019    fn empty_document_accepts_only_its_eof_offset_and_range() {
1020        let mut sources = SourceSet::new();
1021        let id = sources
1022            .insert(memory_origin("empty"), "empty", Arc::from(&b""[..]))
1023            .unwrap();
1024        let document = sources.get(id).unwrap();
1025
1026        assert!(document.is_empty());
1027        assert_eq!(document.len().get(), 0);
1028        assert_eq!(document.offset(0).unwrap().get(), 0);
1029        let range = document.empty_range(0).unwrap();
1030        assert_eq!(range.source(), id);
1031        assert_eq!(range.start().get(), 0);
1032        assert_eq!(range.end().get(), 0);
1033        assert_eq!(range.byte_range(), 0..0);
1034        assert_eq!(document.slice(range).unwrap(), b"");
1035        assert_eq!(
1036            document.offset(1),
1037            Err(SourceRangeError::OffsetOutOfBounds {
1038                offset: ByteOffset(1),
1039                len: ByteOffset(0),
1040            })
1041        );
1042    }
1043
1044    #[test]
1045    fn document_ranges_include_eof_and_slice_bytes() {
1046        let mut sources = SourceSet::new();
1047        let id = sources
1048            .insert(memory_origin("buffer"), "buffer", Arc::from(&b"abcdef"[..]))
1049            .unwrap();
1050        let document = sources.get(id).unwrap();
1051
1052        assert_eq!(document.len().get(), 6);
1053        assert_eq!(document.offset(6).unwrap().get(), 6);
1054        assert_eq!(
1055            document.slice(document.range(1..4).unwrap()).unwrap(),
1056            b"bcd"
1057        );
1058        assert_eq!(
1059            document.slice(document.empty_range(6).unwrap()).unwrap(),
1060            b""
1061        );
1062    }
1063
1064    #[test]
1065    fn document_rejects_reversed_and_out_of_bounds_ranges() {
1066        let mut sources = SourceSet::new();
1067        let id = sources
1068            .insert(memory_origin("buffer"), "buffer", Arc::from(&b"abcd"[..]))
1069            .unwrap();
1070        let document = sources.get(id).unwrap();
1071        let reversed_start = 3;
1072        let reversed_end = 2;
1073        let out_of_bounds_start = document.bytes().len() + 1;
1074
1075        assert_eq!(
1076            document.range(reversed_start..reversed_end),
1077            Err(SourceRangeError::ReversedRange {
1078                start: ByteOffset(3),
1079                end: ByteOffset(2),
1080            })
1081        );
1082        assert_eq!(
1083            document.range(0..5),
1084            Err(SourceRangeError::OffsetOutOfBounds {
1085                offset: ByteOffset(5),
1086                len: ByteOffset(4),
1087            })
1088        );
1089        assert_eq!(
1090            document.range(out_of_bounds_start..0),
1091            Err(SourceRangeError::ReversedRange {
1092                start: ByteOffset(5),
1093                end: ByteOffset(0),
1094            })
1095        );
1096    }
1097
1098    #[test]
1099    fn ranges_reject_cross_source_cover_and_slice() {
1100        let mut sources = SourceSet::new();
1101        let first_id = sources
1102            .insert(memory_origin("first"), "first", Arc::from(&b"first"[..]))
1103            .unwrap();
1104        let second_id = sources
1105            .insert(memory_origin("second"), "second", Arc::from(&b"second"[..]))
1106            .unwrap();
1107        let first = sources.get(first_id).unwrap().range(1..3).unwrap();
1108        let second = sources.get(second_id).unwrap().range(2..4).unwrap();
1109
1110        assert_eq!(
1111            SourceRange::cover(first, second),
1112            Err(SourceRangeError::SourceMismatch {
1113                expected: first_id,
1114                actual: second_id,
1115            })
1116        );
1117        assert_eq!(
1118            sources.get(first_id).unwrap().slice(second),
1119            Err(SourceRangeError::SourceMismatch {
1120                expected: first_id,
1121                actual: second_id,
1122            })
1123        );
1124    }
1125
1126    #[test]
1127    fn cover_spans_ordered_disjoint_and_nested_ranges() {
1128        let mut sources = SourceSet::new();
1129        let id = sources
1130            .insert(
1131                memory_origin("buffer"),
1132                "buffer",
1133                Arc::from(&b"0123456789"[..]),
1134            )
1135            .unwrap();
1136        let document = sources.get(id).unwrap();
1137        let left = document.range(1..3).unwrap();
1138        let right = document.range(7..9).unwrap();
1139        let nested = document.range(2..8).unwrap();
1140
1141        assert_eq!(
1142            SourceRange::cover(left, right).unwrap(),
1143            document.range(1..9).unwrap()
1144        );
1145        assert_eq!(
1146            SourceRange::cover(right, left).unwrap(),
1147            document.range(1..9).unwrap()
1148        );
1149        assert_eq!(
1150            SourceRange::cover(nested, left).unwrap(),
1151            document.range(1..8).unwrap()
1152        );
1153    }
1154
1155    #[test]
1156    fn byte_positions_round_trip_every_offset_for_arbitrary_bytes() {
1157        let cases: &[&[u8]] = &[
1158            b"",
1159            b"a",
1160            b"a\n",
1161            b"\n",
1162            b"a\r\nb",
1163            b"a\rb\n",
1164            &[0x00, 0xff, b'\r', b'\n', 0x80],
1165        ];
1166
1167        for &bytes in cases {
1168            let document = test_document(bytes);
1169            for raw_offset in 0..=bytes.len() {
1170                let offset = document.offset(raw_offset).unwrap();
1171                let position = document.byte_position(offset).unwrap();
1172                assert_eq!(
1173                    document.byte_offset(position).unwrap(),
1174                    offset,
1175                    "bytes={bytes:?}, offset={raw_offset}, position={position:?}"
1176                );
1177                assert_eq!(
1178                    document
1179                        .byte_position(document.byte_offset(position).unwrap())
1180                        .unwrap(),
1181                    position
1182                );
1183            }
1184        }
1185    }
1186
1187    #[test]
1188    fn byte_positions_cover_empty_eof_trailing_newline_and_crlf_bytes() {
1189        let empty = test_document(b"");
1190        assert_eq!(empty.line_count(), 1);
1191        assert_eq!(
1192            empty.byte_position(ByteOffset::new(0)).unwrap(),
1193            BytePosition::new(0, 0)
1194        );
1195        assert_eq!(
1196            empty.byte_offset(BytePosition::new(0, 0)).unwrap(),
1197            ByteOffset::new(0)
1198        );
1199
1200        let document = test_document(b"a\r\nb\rc\n");
1201        assert_eq!(document.line_count(), 4);
1202        let expected = [
1203            BytePosition::new(0, 0),
1204            BytePosition::new(0, 1),
1205            BytePosition::new(0, 2),
1206            BytePosition::new(1, 0),
1207            BytePosition::new(1, 1),
1208            BytePosition::new(2, 0),
1209            BytePosition::new(2, 1),
1210            BytePosition::new(3, 0),
1211        ];
1212        for (offset, expected) in expected.into_iter().enumerate() {
1213            assert_eq!(
1214                document.byte_position(ByteOffset::new(offset as u32)),
1215                Ok(expected)
1216            );
1217            assert_eq!(
1218                document.byte_offset(expected),
1219                Ok(ByteOffset::new(offset as u32))
1220            );
1221        }
1222        assert_eq!(expected[2].line(), 0);
1223        assert_eq!(expected[2].column(), 2);
1224    }
1225
1226    #[test]
1227    fn byte_position_rejects_invalid_offset_line_and_column() {
1228        let document = test_document(b"a\nb");
1229
1230        assert_eq!(
1231            document.byte_position(ByteOffset::new(4)),
1232            Err(PositionError::OffsetOutOfBounds {
1233                offset: ByteOffset::new(4),
1234                len: ByteOffset::new(3),
1235            })
1236        );
1237        assert_eq!(
1238            document.byte_offset(BytePosition::new(2, 0)),
1239            Err(PositionError::LineOutOfBounds {
1240                line: 2,
1241                line_count: 2,
1242            })
1243        );
1244        assert_eq!(
1245            document.byte_offset(BytePosition::new(0, 2)),
1246            Err(PositionError::ByteColumnOutOfBounds {
1247                line: 0,
1248                column: 2,
1249                max_column: 1,
1250            })
1251        );
1252        assert_eq!(
1253            document.byte_offset(BytePosition::new(1, 2)),
1254            Err(PositionError::ByteColumnOutOfBounds {
1255                line: 1,
1256                column: 2,
1257                max_column: 1,
1258            })
1259        );
1260    }
1261
1262    #[test]
1263    fn editor_positions_use_explicit_utf_code_units() {
1264        let document = test_document("Aé𝄞".as_bytes());
1265        let cases = [
1266            (
1267                0,
1268                Position::new(0, 0),
1269                Position::new(0, 0),
1270                Position::new(0, 0),
1271            ),
1272            (
1273                1,
1274                Position::new(0, 1),
1275                Position::new(0, 1),
1276                Position::new(0, 1),
1277            ),
1278            (
1279                3,
1280                Position::new(0, 3),
1281                Position::new(0, 2),
1282                Position::new(0, 2),
1283            ),
1284            (
1285                7,
1286                Position::new(0, 7),
1287                Position::new(0, 4),
1288                Position::new(0, 3),
1289            ),
1290        ];
1291
1292        for (offset, utf8, utf16, utf32) in cases {
1293            let offset = ByteOffset::new(offset);
1294            assert_eq!(document.position(offset, PositionEncoding::Utf8), Ok(utf8));
1295            assert_eq!(
1296                document.position(offset, PositionEncoding::Utf16),
1297                Ok(utf16)
1298            );
1299            assert_eq!(
1300                document.position(offset, PositionEncoding::Utf32),
1301                Ok(utf32)
1302            );
1303            assert_eq!(
1304                document.position_offset(utf8, PositionEncoding::Utf8),
1305                Ok(offset)
1306            );
1307            assert_eq!(
1308                document.position_offset(utf16, PositionEncoding::Utf16),
1309                Ok(offset)
1310            );
1311            assert_eq!(
1312                document.position_offset(utf32, PositionEncoding::Utf32),
1313                Ok(offset)
1314            );
1315        }
1316
1317        assert_eq!(Position::new(2, 3).line(), 2);
1318        assert_eq!(Position::new(2, 3).character(), 3);
1319        assert_eq!(PositionEncoding::Utf8.to_string(), "UTF-8");
1320        assert_eq!(PositionEncoding::Utf16.to_string(), "UTF-16");
1321        assert_eq!(PositionEncoding::Utf32.to_string(), "UTF-32");
1322    }
1323
1324    #[test]
1325    fn editor_positions_follow_lsp_line_terminator_and_eof_semantics() {
1326        let document = test_document(b"a\r\nb\rc\n");
1327
1328        for encoding in [
1329            PositionEncoding::Utf8,
1330            PositionEncoding::Utf16,
1331            PositionEncoding::Utf32,
1332        ] {
1333            assert_eq!(
1334                document.position(ByteOffset::new(1), encoding),
1335                Ok(Position::new(0, 1))
1336            );
1337            assert_eq!(
1338                document.position(ByteOffset::new(2), encoding),
1339                Err(PositionError::OffsetInsideLineTerminator {
1340                    offset: ByteOffset::new(2),
1341                    line: 0,
1342                })
1343            );
1344            assert_eq!(
1345                document.position_offset(Position::new(0, 1), encoding),
1346                Ok(ByteOffset::new(1))
1347            );
1348            assert_eq!(
1349                document.position(ByteOffset::new(3), encoding),
1350                Ok(Position::new(1, 0))
1351            );
1352            assert_eq!(
1353                document.position(ByteOffset::new(4), encoding),
1354                Ok(Position::new(1, 1))
1355            );
1356            assert_eq!(
1357                document.position(ByteOffset::new(6), encoding),
1358                Ok(Position::new(2, 1))
1359            );
1360            assert_eq!(
1361                document.position(ByteOffset::new(7), encoding),
1362                Ok(Position::new(3, 0))
1363            );
1364            assert_eq!(
1365                document.position_offset(Position::new(3, 0), encoding),
1366                Ok(ByteOffset::new(7))
1367            );
1368        }
1369    }
1370
1371    #[test]
1372    fn editor_positions_round_trip_all_representable_offsets_and_positions() {
1373        let cases = ["", "plain", "trailing\n", "\r\n", "é\r\n𝄞\n", "a\rb"];
1374        for text in cases {
1375            let document = test_document(text.as_bytes());
1376            for encoding in [
1377                PositionEncoding::Utf8,
1378                PositionEncoding::Utf16,
1379                PositionEncoding::Utf32,
1380            ] {
1381                for raw_offset in 0..=text.len() {
1382                    let offset = ByteOffset::new(raw_offset as u32);
1383                    match document.position(offset, encoding) {
1384                        Ok(position) => {
1385                            assert_eq!(
1386                                document.position_offset(position, encoding),
1387                                Ok(offset),
1388                                "text={text:?}, encoding={encoding}, offset={raw_offset}"
1389                            );
1390                        }
1391                        Err(
1392                            PositionError::MidCodePoint { .. }
1393                            | PositionError::OffsetInsideLineTerminator { .. },
1394                        ) => {}
1395                        Err(error) => panic!(
1396                            "unexpected conversion error for text={text:?}, encoding={encoding}, offset={raw_offset}: {error}"
1397                        ),
1398                    }
1399                }
1400
1401                for line in 0..document.line_count() {
1402                    let extent = document.line_extent(line as u32).unwrap();
1403                    let end = document
1404                        .position(ByteOffset::new(extent.content_end as u32), encoding)
1405                        .unwrap();
1406                    for character in 0..=end.character() {
1407                        let position = Position::new(line as u32, character);
1408                        match document.position_offset(position, encoding) {
1409                            Ok(offset) => assert_eq!(
1410                                document.position(offset, encoding),
1411                                Ok(position),
1412                                "text={text:?}, encoding={encoding}, position={position:?}"
1413                            ),
1414                            Err(
1415                                PositionError::MidCodePoint { .. }
1416                                | PositionError::MidUtf16Surrogate { .. },
1417                            ) => {}
1418                            Err(error) => panic!(
1419                                "unexpected inverse error for text={text:?}, encoding={encoding}, position={position:?}: {error}"
1420                            ),
1421                        }
1422                    }
1423                }
1424            }
1425        }
1426    }
1427
1428    #[test]
1429    fn editor_positions_reject_mid_codepoint_mid_surrogate_and_bad_coordinates() {
1430        let document = test_document("Aé𝄞".as_bytes());
1431
1432        for encoding in [
1433            PositionEncoding::Utf8,
1434            PositionEncoding::Utf16,
1435            PositionEncoding::Utf32,
1436        ] {
1437            assert_eq!(
1438                document.position(ByteOffset::new(2), encoding),
1439                Err(PositionError::MidCodePoint {
1440                    offset: ByteOffset::new(2),
1441                })
1442            );
1443        }
1444        assert_eq!(
1445            document.position_offset(Position::new(0, 2), PositionEncoding::Utf8),
1446            Err(PositionError::MidCodePoint {
1447                offset: ByteOffset::new(2),
1448            })
1449        );
1450        assert_eq!(
1451            document.position_offset(Position::new(0, 3), PositionEncoding::Utf16),
1452            Err(PositionError::MidUtf16Surrogate {
1453                line: 0,
1454                character: 3,
1455            })
1456        );
1457        assert_eq!(
1458            document.position_offset(Position::new(1, 0), PositionEncoding::Utf32),
1459            Err(PositionError::LineOutOfBounds {
1460                line: 1,
1461                line_count: 1,
1462            })
1463        );
1464        assert_eq!(
1465            document.position_offset(Position::new(0, 8), PositionEncoding::Utf8),
1466            Err(PositionError::CharacterOutOfBounds {
1467                line: 0,
1468                character: 8,
1469                max_character: 7,
1470                encoding: PositionEncoding::Utf8,
1471            })
1472        );
1473        assert_eq!(
1474            document.position_offset(Position::new(0, 5), PositionEncoding::Utf16),
1475            Err(PositionError::CharacterOutOfBounds {
1476                line: 0,
1477                character: 5,
1478                max_character: 4,
1479                encoding: PositionEncoding::Utf16,
1480            })
1481        );
1482        assert_eq!(
1483            document.position_offset(Position::new(0, 4), PositionEncoding::Utf32),
1484            Err(PositionError::CharacterOutOfBounds {
1485                line: 0,
1486                character: 4,
1487                max_character: 3,
1488                encoding: PositionEncoding::Utf32,
1489            })
1490        );
1491        assert_eq!(
1492            document.position(ByteOffset::new(8), PositionEncoding::Utf16),
1493            Err(PositionError::OffsetOutOfBounds {
1494                offset: ByteOffset::new(8),
1495                len: ByteOffset::new(7),
1496            })
1497        );
1498    }
1499
1500    #[test]
1501    fn invalid_utf8_retains_byte_positions_but_rejects_editor_positions() {
1502        let bytes = [b'a', 0xff, b'\n', 0x80];
1503        let document = test_document(&bytes);
1504
1505        for raw_offset in 0..=bytes.len() {
1506            let offset = ByteOffset::new(raw_offset as u32);
1507            let position = document.byte_position(offset).unwrap();
1508            assert_eq!(document.byte_offset(position), Ok(offset));
1509        }
1510        for encoding in [
1511            PositionEncoding::Utf8,
1512            PositionEncoding::Utf16,
1513            PositionEncoding::Utf32,
1514        ] {
1515            assert_eq!(
1516                document.position(ByteOffset::new(0), encoding),
1517                Err(PositionError::InvalidUtf8 {
1518                    valid_up_to: ByteOffset::new(1),
1519                    error_len: Some(1),
1520                })
1521            );
1522            assert_eq!(
1523                document.position_offset(Position::new(0, 0), encoding),
1524                Err(PositionError::InvalidUtf8 {
1525                    valid_up_to: ByteOffset::new(1),
1526                    error_len: Some(1),
1527                })
1528            );
1529        }
1530        assert_eq!(
1531            document.position(ByteOffset::new(5), PositionEncoding::Utf8),
1532            Err(PositionError::OffsetOutOfBounds {
1533                offset: ByteOffset::new(5),
1534                len: ByteOffset::new(4),
1535            })
1536        );
1537        assert_eq!(
1538            document.position_offset(Position::new(3, 0), PositionEncoding::Utf8),
1539            Err(PositionError::LineOutOfBounds {
1540                line: 3,
1541                line_count: 2,
1542            })
1543        );
1544    }
1545
1546    #[cfg(target_pointer_width = "64")]
1547    #[test]
1548    fn offsets_beyond_u32_are_reported_as_unrepresentable() {
1549        let mut sources = SourceSet::new();
1550        let id = sources
1551            .insert(memory_origin("buffer"), "buffer", Arc::from(&b"bytes"[..]))
1552            .unwrap();
1553        let document = sources.get(id).unwrap();
1554        let offset = u32::MAX as usize + 1;
1555
1556        assert_eq!(
1557            document.offset(offset),
1558            Err(SourceRangeError::UnrepresentableOffset {
1559                offset,
1560                max: u32::MAX as usize,
1561            })
1562        );
1563    }
1564}