1use std::error::Error;
4use std::fmt;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct SourceOffset(u32);
9
10impl SourceOffset {
11 pub fn try_from_usize(value: usize) -> Result<Self, DebugMetadataError> {
13 u32::try_from(value)
14 .map(Self)
15 .map_err(|_| DebugMetadataError::SourceTooLarge)
16 }
17
18 #[must_use]
20 pub const fn get(self) -> u32 {
21 self.0
22 }
23
24 #[must_use]
26 pub const fn as_usize(self) -> usize {
27 self.0 as usize
28 }
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
33pub struct LineColumn {
34 pub line: u32,
35 pub column: u32,
36}
37
38impl LineColumn {
39 #[must_use]
40 pub const fn new(line: u32, column: u32) -> Self {
41 Self { line, column }
42 }
43
44 #[must_use]
46 pub const fn one_based(self) -> Option<(u32, u32)> {
47 match (self.line.checked_add(1), self.column.checked_add(1)) {
48 (Some(line), Some(column)) => Some((line, column)),
49 _ => None,
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum DebugMetadataError {
57 SourceTooLarge,
58 OffsetOutOfBounds,
59 OffsetNotUtf8Boundary,
60 LineOrColumnOverflow,
61}
62
63impl fmt::Display for DebugMetadataError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(match self {
66 Self::SourceTooLarge => "source is too large for QuickJS debug metadata",
67 Self::OffsetOutOfBounds => "source offset is out of bounds",
68 Self::OffsetNotUtf8Boundary => "source offset is not a UTF-8 boundary",
69 Self::LineOrColumnOverflow => "QuickJS debug line or column overflowed",
70 })
71 }
72}
73
74impl Error for DebugMetadataError {}
75
76#[derive(Clone, Copy, Debug)]
81pub struct QuickJsSourceLocator<'source> {
82 source: &'source [u8],
83 utf8: Option<&'source str>,
84}
85
86impl<'source> QuickJsSourceLocator<'source> {
87 #[must_use]
88 pub const fn new(source: &'source str) -> Self {
89 Self {
90 source: source.as_bytes(),
91 utf8: Some(source),
92 }
93 }
94
95 #[must_use]
96 pub const fn from_bytes(source: &'source [u8]) -> Self {
97 Self { source, utf8: None }
98 }
99
100 pub fn locate(self, offset: SourceOffset) -> Result<LineColumn, DebugMetadataError> {
101 self.locate_byte_offset(offset.as_usize())
102 }
103
104 pub fn locate_byte_offset(self, byte_offset: usize) -> Result<LineColumn, DebugMetadataError> {
105 if byte_offset > self.source.len() {
106 return Err(DebugMetadataError::OffsetOutOfBounds);
107 }
108 if self
109 .utf8
110 .is_some_and(|source| !source.is_char_boundary(byte_offset))
111 {
112 return Err(DebugMetadataError::OffsetNotUtf8Boundary);
113 }
114
115 advance_position(LineColumn::default(), &self.source[..byte_offset])
116 }
117
118 pub fn index(self) -> Result<QuickJsSourceIndex<'source>, DebugMetadataError> {
121 let mut checkpoints = Vec::with_capacity(self.source.len() / SOURCE_CHECKPOINT_BYTES + 1);
122 let mut position = LineColumn::default();
123 checkpoints.push(position);
124 for chunk in self.source.as_chunks::<SOURCE_CHECKPOINT_BYTES>().0 {
125 position = advance_position(position, chunk)?;
126 checkpoints.push(position);
127 }
128 Ok(QuickJsSourceIndex {
129 locator: self,
130 checkpoints,
131 })
132 }
133}
134
135const SOURCE_CHECKPOINT_BYTES: usize = 256;
136
137#[derive(Debug)]
142pub struct QuickJsSourceIndex<'source> {
143 locator: QuickJsSourceLocator<'source>,
144 checkpoints: Vec<LineColumn>,
145}
146
147impl<'source> QuickJsSourceIndex<'source> {
148 pub fn locate(&self, offset: SourceOffset) -> Result<LineColumn, DebugMetadataError> {
149 self.locate_byte_offset(offset.as_usize())
150 }
151
152 pub fn locate_byte_offset(&self, byte_offset: usize) -> Result<LineColumn, DebugMetadataError> {
153 self.validate_byte_offset(byte_offset)?;
154 self.locate_validated_byte_offset(byte_offset)
155 }
156
157 pub(crate) fn cursor(&self) -> QuickJsSourceCursor<'_, 'source> {
159 QuickJsSourceCursor {
160 index: self,
161 previous: None,
162 }
163 }
164
165 pub(crate) fn locate_all(
168 &self,
169 mut offsets: Vec<SourceOffset>,
170 ) -> QuickJsLocatedOffsets<'_, 'source> {
171 offsets.sort_unstable();
172 offsets.dedup();
173 let mut cursor = self.cursor();
174 let mut positions = Vec::with_capacity(offsets.len());
175 let mut errors = Vec::new();
176 for (at, &offset) in offsets.iter().enumerate() {
177 positions.push(cursor.locate(offset).unwrap_or_else(|error| {
178 errors.push((at, error));
179 LineColumn::default()
180 }));
181 }
182 QuickJsLocatedOffsets {
183 index: self,
184 offsets,
185 positions,
186 errors,
187 }
188 }
189
190 fn validate_byte_offset(&self, byte_offset: usize) -> Result<(), DebugMetadataError> {
191 if byte_offset > self.locator.source.len() {
192 return Err(DebugMetadataError::OffsetOutOfBounds);
193 }
194 if self
195 .locator
196 .utf8
197 .is_some_and(|source| !source.is_char_boundary(byte_offset))
198 {
199 return Err(DebugMetadataError::OffsetNotUtf8Boundary);
200 }
201 Ok(())
202 }
203
204 fn locate_validated_byte_offset(
205 &self,
206 byte_offset: usize,
207 ) -> Result<LineColumn, DebugMetadataError> {
208 let checkpoint = byte_offset / SOURCE_CHECKPOINT_BYTES;
209 advance_position(
210 self.checkpoints[checkpoint],
211 &self.locator.source[checkpoint * SOURCE_CHECKPOINT_BYTES..byte_offset],
212 )
213 }
214}
215
216pub(crate) struct QuickJsSourceCursor<'index, 'source> {
221 index: &'index QuickJsSourceIndex<'source>,
222 previous: Option<(usize, LineColumn)>,
223}
224
225impl QuickJsSourceCursor<'_, '_> {
226 pub(crate) fn locate(
227 &mut self,
228 offset: SourceOffset,
229 ) -> Result<LineColumn, DebugMetadataError> {
230 self.locate_byte_offset(offset.as_usize())
231 }
232
233 fn locate_byte_offset(&mut self, byte_offset: usize) -> Result<LineColumn, DebugMetadataError> {
234 self.index.validate_byte_offset(byte_offset)?;
235 let position = match self.previous {
236 Some((previous_offset, position))
237 if previous_offset <= byte_offset
238 && previous_offset / SOURCE_CHECKPOINT_BYTES
239 == byte_offset / SOURCE_CHECKPOINT_BYTES =>
240 {
241 advance_position(
242 position,
243 &self.index.locator.source[previous_offset..byte_offset],
244 )?
245 }
246 _ => self.index.locate_validated_byte_offset(byte_offset)?,
247 };
248 self.previous = Some((byte_offset, position));
249 Ok(position)
250 }
251}
252
253pub(crate) struct QuickJsLocatedOffsets<'index, 'source> {
256 index: &'index QuickJsSourceIndex<'source>,
257 offsets: Vec<SourceOffset>,
259 positions: Vec<LineColumn>,
260 errors: Vec<(usize, DebugMetadataError)>,
262}
263
264impl QuickJsLocatedOffsets<'_, '_> {
265 pub(crate) fn locate(&self, offset: SourceOffset) -> Result<LineColumn, DebugMetadataError> {
266 let Ok(at) = self.offsets.binary_search(&offset) else {
267 return self.index.locate(offset);
268 };
269 match self.errors.binary_search_by_key(&at, |&(at, _)| at) {
270 Ok(error) => Err(self.errors[error].1),
271 Err(_) => Ok(self.positions[at]),
272 }
273 }
274}
275
276fn advance_position(
278 mut position: LineColumn,
279 bytes: &[u8],
280) -> Result<LineColumn, DebugMetadataError> {
281 for &byte in bytes {
282 if byte == b'\n' {
283 position.line = position
284 .line
285 .checked_add(1)
286 .ok_or(DebugMetadataError::LineOrColumnOverflow)?;
287 position.column = 0;
288 } else if byte & 0xc0 != 0x80 {
289 position.column = position
290 .column
291 .checked_add(1)
292 .ok_or(DebugMetadataError::LineOrColumnOverflow)?;
293 }
294 }
295 Ok(position)
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn indexed_coordinates_match_scans_at_every_raw_byte_and_utf8_boundary() {
304 let raw = [b'a', 0x80, 0xff, b'\r', b'\n', 0xe2, 0x80, 0xa8].repeat(100);
305 let locator = QuickJsSourceLocator::from_bytes(&raw);
306 let indexed = locator.index().unwrap();
307 for offset in (0..=raw.len() + 1).rev() {
308 assert_eq!(
309 indexed.locate_byte_offset(offset),
310 locator.locate_byte_offset(offset)
311 );
312 }
313 let text = ("é\r\n𝄞\u{2028}".to_owned() + &"x".repeat(600)).repeat(3);
314 let locator = QuickJsSourceLocator::new(&text);
315 let indexed = locator.index().unwrap();
316 for offset in 0..=text.len() + 1 {
317 assert_eq!(
318 indexed.locate_byte_offset(offset),
319 locator.locate_byte_offset(offset)
320 );
321 }
322 let empty = QuickJsSourceLocator::from_bytes(b"").index().unwrap();
323 assert_eq!(empty.locate_byte_offset(0), Ok(LineColumn::new(0, 0)));
324 }
325 #[test]
326 fn cursor_matches_index_and_scan_for_forward_backward_and_invalid_offsets() {
327 let raw = [b'a', 0x80, 0xff, b'\r', b'\n', 0xe2, 0x80, 0xa8].repeat(100);
328 let text = ("é\r\n𝄞\u{2028}".to_owned() + &"x".repeat(600)).repeat(3);
329 for locator in [
330 QuickJsSourceLocator::from_bytes(&raw),
331 QuickJsSourceLocator::new(&text),
332 QuickJsSourceLocator::from_bytes(b""),
333 ] {
334 let index = locator.index().unwrap();
335 let mut cursor = index.cursor();
336 let length = locator.source.len();
337 let offsets = (0..=length + 1)
338 .chain((0..=length + 1).rev())
339 .chain((0..=length).flat_map(|offset| {
340 [offset, usize::MAX, offset, length.saturating_sub(offset)]
341 }));
342 for offset in offsets {
343 let previous = cursor.previous;
344 let expected = index.locate_byte_offset(offset);
345 assert_eq!(expected, locator.locate_byte_offset(offset));
346 assert_eq!(
347 cursor.locate_byte_offset(offset),
348 expected,
349 "offset={offset}"
350 );
351 if expected.is_err() {
352 assert_eq!(cursor.previous, previous);
353 }
354 }
355 }
356 }
357
358 #[test]
359 fn cursor_preserves_line_and_column_overflow_and_last_success() {
360 for (bytes, position) in [
363 (&b"\nx"[..], LineColumn::new(u32::MAX, 0)),
364 (&b"x\n"[..], LineColumn::new(0, u32::MAX)),
365 ] {
366 let index = QuickJsSourceIndex {
367 locator: QuickJsSourceLocator::from_bytes(bytes),
368 checkpoints: vec![position],
369 };
370 let mut cursor = index.cursor();
371 assert_eq!(cursor.locate_byte_offset(0), Ok(position));
372 for offset in [1, 2, 0, usize::MAX, 1] {
373 let previous = cursor.previous;
374 let expected = index.locate_byte_offset(offset);
375 assert_eq!(cursor.locate_byte_offset(offset), expected);
376 if expected.is_err() {
377 assert_eq!(cursor.previous, previous);
378 }
379 }
380 }
381 }
382}