Skip to main content

serde_saphyr/
span.rs

1#[cfg(feature = "huge_documents")]
2use serde_core::Deserializer;
3use serde_core::de::{self, IgnoredAny, MapAccess, Visitor};
4#[cfg(feature = "huge_documents")]
5use std::fmt;
6
7#[cfg(not(feature = "huge_documents"))]
8pub(crate) type SpanIndex = u32;
9
10#[cfg(not(feature = "huge_documents"))]
11const SPAN_INDEX_SENTINEL_VALUE: u64 = u32::MAX as u64;
12
13#[cfg(feature = "huge_documents")]
14const MAX_PACKED_SPAN_INDEX: u64 = (1u64 << 48) - 1;
15
16#[cfg(feature = "huge_documents")]
17const SPAN_INDEX_SENTINEL_VALUE: u64 = MAX_PACKED_SPAN_INDEX;
18
19const MAX_REPRESENTABLE_SPAN_INDEX_VALUE: u64 = SPAN_INDEX_SENTINEL_VALUE - 1;
20
21#[cfg(feature = "huge_documents")]
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub(crate) struct SpanIndex([u8; 6]);
24
25#[cfg(feature = "huge_documents")]
26impl SpanIndex {
27    const fn from_u64_saturating(value: u64) -> Self {
28        let value = if value > MAX_PACKED_SPAN_INDEX {
29            MAX_PACKED_SPAN_INDEX
30        } else {
31            value
32        };
33
34        Self([
35            value as u8,
36            (value >> 8) as u8,
37            (value >> 16) as u8,
38            (value >> 24) as u8,
39            (value >> 32) as u8,
40            (value >> 40) as u8,
41        ])
42    }
43
44    const fn to_u64(self) -> u64 {
45        (self.0[0] as u64)
46            | ((self.0[1] as u64) << 8)
47            | ((self.0[2] as u64) << 16)
48            | ((self.0[3] as u64) << 24)
49            | ((self.0[4] as u64) << 32)
50            | ((self.0[5] as u64) << 40)
51    }
52}
53
54#[cfg(feature = "huge_documents")]
55impl fmt::Debug for SpanIndex {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        self.to_u64().fmt(f)
58    }
59}
60
61#[cfg(feature = "huge_documents")]
62impl<'de> serde_core::Deserialize<'de> for SpanIndex {
63    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64    where
65        D: Deserializer<'de>,
66    {
67        Ok(Self::from_u64_saturating(u64::deserialize(deserializer)?))
68    }
69}
70
71#[cfg(not(feature = "huge_documents"))]
72const fn span_index_from_u64_saturating(value: u64) -> SpanIndex {
73    if value > MAX_REPRESENTABLE_SPAN_INDEX_VALUE {
74        MAX_REPRESENTABLE_SPAN_INDEX_VALUE as u32
75    } else {
76        value as u32
77    }
78}
79
80#[cfg(feature = "huge_documents")]
81const fn span_index_from_u64_saturating(value: u64) -> SpanIndex {
82    let value = if value > MAX_REPRESENTABLE_SPAN_INDEX_VALUE {
83        MAX_REPRESENTABLE_SPAN_INDEX_VALUE
84    } else {
85        value
86    };
87
88    SpanIndex::from_u64_saturating(value)
89}
90
91#[cfg(not(feature = "huge_documents"))]
92const SPAN_INDEX_SENTINEL: SpanIndex = u32::MAX;
93
94#[cfg(feature = "huge_documents")]
95const SPAN_INDEX_SENTINEL: SpanIndex = SpanIndex::from_u64_saturating(SPAN_INDEX_SENTINEL_VALUE);
96
97const BYTE_INFO_UNAVAILABLE: (SpanIndex, SpanIndex) = (SPAN_INDEX_SENTINEL, SPAN_INDEX_SENTINEL);
98
99#[cfg(not(feature = "huge_documents"))]
100const fn span_index_to_u64(value: SpanIndex) -> u64 {
101    value as u64
102}
103
104#[cfg(feature = "huge_documents")]
105const fn span_index_to_u64(value: SpanIndex) -> u64 {
106    value.to_u64()
107}
108
109/// A span within the source YAML document.
110///
111/// This structure provides location information in two forms:
112/// 1. **Character-based**: `offset` and `len` count Unicode scalar values. This matches
113///    `granit-parser`'s native reporting and is always present.
114/// 2. **Byte-based**: `byte_info` contains `(byte_offset, byte_len)` counting raw bytes (UTF-8 code units).
115///    These are only populated when parsing from string inputs (`&str`, `String`).
116///
117/// By default, offsets are stored internally as `u32`, which keeps the span compact and
118/// supports YAML inputs up to 4 GiB for byte-based coordinates.
119///
120/// With the `huge_documents` feature, offsets are stored in a packed 48-bit representation.
121/// Public getters still return `u64`, and values beyond 48 bits saturate instead of wrapping.
122/// This keeps [`crate::Location`] compact enough to avoid inflating [`crate::Error`] as much
123/// as a full `u64`-based layout would.
124///
125/// The maximum storage value is reserved for [`Span::UNKNOWN`] and unavailable byte info.
126#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
127pub struct Span {
128    offset: SpanIndex,
129    len: SpanIndex,
130    byte_info: (SpanIndex, SpanIndex),
131}
132
133impl Default for Span {
134    fn default() -> Self {
135        Self::UNKNOWN
136    }
137}
138
139impl<'de> serde_core::Deserialize<'de> for Span {
140    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141    where
142        D: serde_core::Deserializer<'de>,
143    {
144        enum Field {
145            Offset,
146            Len,
147            ByteInfo,
148            Ignore,
149        }
150
151        impl<'de> serde_core::Deserialize<'de> for Field {
152            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153            where
154                D: serde_core::Deserializer<'de>,
155            {
156                struct FieldVisitor;
157
158                impl Visitor<'_> for FieldVisitor {
159                    type Value = Field;
160
161                    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
162                        f.write_str("a Span field")
163                    }
164
165                    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
166                    where
167                        E: de::Error,
168                    {
169                        Ok(match value {
170                            "offset" => Field::Offset,
171                            "len" => Field::Len,
172                            "byte_info" => Field::ByteInfo,
173                            _ => Field::Ignore,
174                        })
175                    }
176                }
177
178                deserializer.deserialize_identifier(FieldVisitor)
179            }
180        }
181
182        struct SpanVisitor;
183
184        impl<'de> Visitor<'de> for SpanVisitor {
185            type Value = Span;
186
187            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
188                f.write_str("a source span")
189            }
190
191            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
192            where
193                A: MapAccess<'de>,
194            {
195                let mut offset = None;
196                let mut len = None;
197                let mut byte_info = None;
198
199                while let Some(field) = map.next_key::<Field>()? {
200                    match field {
201                        Field::Offset => {
202                            if offset.is_some() {
203                                return Err(de::Error::duplicate_field("offset"));
204                            }
205                            offset = Some(map.next_value()?);
206                        }
207                        Field::Len => {
208                            if len.is_some() {
209                                return Err(de::Error::duplicate_field("len"));
210                            }
211                            len = Some(map.next_value()?);
212                        }
213                        Field::ByteInfo => {
214                            if byte_info.is_some() {
215                                return Err(de::Error::duplicate_field("byte_info"));
216                            }
217                            byte_info = Some(map.next_value()?);
218                        }
219                        Field::Ignore => {
220                            let _ = map.next_value::<IgnoredAny>()?;
221                        }
222                    }
223                }
224
225                let offset = offset.ok_or_else(|| de::Error::missing_field("offset"))?;
226                let len = len.ok_or_else(|| de::Error::missing_field("len"))?;
227                let byte_info = byte_info.ok_or_else(|| de::Error::missing_field("byte_info"))?;
228
229                Ok(Span {
230                    offset,
231                    len,
232                    byte_info,
233                })
234            }
235        }
236
237        const FIELDS: &[&str] = &["offset", "len", "byte_info"];
238        deserializer.deserialize_struct("Span", FIELDS, SpanVisitor)
239    }
240}
241
242impl Span {
243    /// Sentinel span meaning "unknown".
244    pub const UNKNOWN: Self = Self {
245        offset: SPAN_INDEX_SENTINEL,
246        len: SPAN_INDEX_SENTINEL,
247        byte_info: BYTE_INFO_UNAVAILABLE,
248    };
249
250    /// Construct a span from character-based offset and length.
251    ///
252    /// Values that exceed the active non-sentinel storage range are saturated.
253    #[must_use]
254    pub const fn new(offset: u64, len: u64) -> Self {
255        Self {
256            offset: span_index_from_u64_saturating(offset),
257            len: span_index_from_u64_saturating(len),
258            byte_info: BYTE_INFO_UNAVAILABLE,
259        }
260    }
261
262    /// Attach byte-based offset and length information to the span.
263    ///
264    /// Values that exceed the active non-sentinel storage range are saturated.
265    #[must_use]
266    pub const fn with_byte_info(mut self, byte_offset: u64, byte_len: u64) -> Self {
267        self.byte_info = (
268            span_index_from_u64_saturating(byte_offset),
269            span_index_from_u64_saturating(byte_len),
270        );
271        self
272    }
273
274    /// Returns the character offset within the source YAML document.
275    #[inline]
276    #[must_use]
277    pub fn offset(&self) -> u64 {
278        span_index_to_u64(self.offset)
279    }
280
281    /// Returns the character length within the source YAML document.
282    #[inline]
283    #[must_use]
284    pub fn len(&self) -> u64 {
285        span_index_to_u64(self.len)
286    }
287
288    /// Returns the byte offset within the source YAML document.
289    /// Returns `None` if byte info is unavailable.
290    #[inline]
291    #[must_use]
292    pub fn byte_offset(&self) -> Option<u64> {
293        if self.byte_info == BYTE_INFO_UNAVAILABLE {
294            None
295        } else {
296            Some(span_index_to_u64(self.byte_info.0))
297        }
298    }
299
300    /// Returns the byte length within the source YAML document.
301    /// Returns `None` if byte info is unavailable.
302    #[inline]
303    #[must_use]
304    pub fn byte_len(&self) -> Option<u64> {
305        if self.byte_info == BYTE_INFO_UNAVAILABLE {
306            None
307        } else {
308            Some(span_index_to_u64(self.byte_info.1))
309        }
310    }
311
312    #[inline]
313    #[must_use]
314    pub fn is_empty(&self) -> bool {
315        self.len() == 0
316    }
317
318    #[cfg(feature = "deserialize")]
319    #[inline]
320    pub(crate) fn byte_info_or_unavailable(&self) -> (u64, u64) {
321        (
322            span_index_to_u64(self.byte_info.0),
323            span_index_to_u64(self.byte_info.1),
324        )
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    #[cfg(feature = "huge_documents")]
332    use std::mem::size_of;
333
334    #[test]
335    fn span_constructors_round_trip_small_values() {
336        let span = Span::new(10, 5).with_byte_info(20, 5);
337        assert_eq!(span.offset(), 10);
338        assert_eq!(span.len(), 5);
339        assert_eq!(span.byte_offset(), Some(20));
340        assert_eq!(span.byte_len(), Some(5));
341    }
342
343    #[test]
344    fn unknown_span_has_no_byte_info() {
345        assert_eq!(Span::UNKNOWN.byte_offset(), None);
346        assert_eq!(Span::UNKNOWN.byte_len(), None);
347        assert_eq!(Span::UNKNOWN.offset(), SPAN_INDEX_SENTINEL_VALUE);
348        assert_eq!(Span::UNKNOWN.len(), SPAN_INDEX_SENTINEL_VALUE);
349        assert_eq!(Span::default(), Span::UNKNOWN);
350    }
351
352    #[test]
353    fn zero_length_span_at_offset_zero_is_not_unknown() {
354        let span = Span::new(0, 0);
355
356        assert_ne!(span, Span::UNKNOWN);
357        assert_eq!(span.offset(), 0);
358        assert_eq!(span.len(), 0);
359        assert!(span.is_empty());
360        assert_eq!(span.byte_offset(), None);
361        assert_eq!(span.byte_len(), None);
362    }
363
364    #[test]
365    fn byte_info_at_offset_zero_is_reportable() {
366        let span = Span::new(0, 0).with_byte_info(0, 0);
367
368        assert_ne!(span, Span::UNKNOWN);
369        assert_eq!(span.byte_offset(), Some(0));
370        assert_eq!(span.byte_len(), Some(0));
371    }
372
373    #[test]
374    fn constructor_values_saturate_below_unknown_sentinel() {
375        let span = Span::new(u64::MAX, u64::MAX).with_byte_info(u64::MAX, u64::MAX);
376
377        assert_ne!(span, Span::UNKNOWN);
378        assert_eq!(span.offset(), MAX_REPRESENTABLE_SPAN_INDEX_VALUE);
379        assert_eq!(span.len(), MAX_REPRESENTABLE_SPAN_INDEX_VALUE);
380        assert_eq!(span.byte_offset(), Some(MAX_REPRESENTABLE_SPAN_INDEX_VALUE));
381        assert_eq!(span.byte_len(), Some(MAX_REPRESENTABLE_SPAN_INDEX_VALUE));
382    }
383
384    #[test]
385    fn deserialize_span_accepts_unknown_fields() {
386        let span: Span = serde_json::from_str(
387            r#"{
388                "ignored": "value",
389                "offset": 10,
390                "len": 5,
391                "byte_info": [20, 5]
392            }"#,
393        )
394        .unwrap();
395
396        assert_eq!(span.offset(), 10);
397        assert_eq!(span.len(), 5);
398        assert_eq!(span.byte_offset(), Some(20));
399        assert_eq!(span.byte_len(), Some(5));
400    }
401
402    #[test]
403    fn deserialize_span_rejects_duplicate_fields() {
404        let err = serde_json::from_str::<Span>(
405            r#"{ "offset": 1, "offset": 2, "len": 3, "byte_info": [4, 5] }"#,
406        )
407        .unwrap_err();
408
409        assert!(err.to_string().contains("duplicate field `offset`"));
410    }
411
412    #[test]
413    fn deserialize_span_rejects_missing_fields() {
414        let err = serde_json::from_str::<Span>(r#"{ "offset": 1, "len": 2 }"#).unwrap_err();
415
416        assert!(err.to_string().contains("missing field `byte_info`"));
417    }
418
419    #[cfg(feature = "huge_documents")]
420    #[test]
421    fn huge_document_indices_saturate_to_48_bits() {
422        let span = Span::new(u64::MAX, u64::MAX).with_byte_info(u64::MAX, u64::MAX);
423        assert_eq!(span.offset(), MAX_PACKED_SPAN_INDEX - 1);
424        assert_eq!(span.len(), MAX_PACKED_SPAN_INDEX - 1);
425        assert_eq!(span.byte_offset(), Some(MAX_PACKED_SPAN_INDEX - 1));
426        assert_eq!(span.byte_len(), Some(MAX_PACKED_SPAN_INDEX - 1));
427    }
428
429    #[cfg(feature = "huge_documents")]
430    #[test]
431    fn huge_document_layout_stays_compact() {
432        assert_eq!(size_of::<Span>(), 24);
433        assert!(size_of::<crate::Location>() <= 40);
434    }
435
436    #[cfg(all(feature = "huge_documents", feature = "deserialize"))]
437    #[test]
438    fn huge_document_error_layout_stays_below_clippy_threshold() {
439        assert!(size_of::<crate::Error>() < 128);
440    }
441}