yyaml/
yaml.rs

1use crate::linked_hash_map::LinkedHashMap;
2use std::hash::{Hash, Hasher};
3
4/// The YAML node representation, mirroring the original design:
5/// - `Real` is an f64 stored as string (lazy parse).
6/// - `Integer` is i64.
7/// - `String` is an owned string.
8/// - `Boolean` is bool.
9/// - `Array` is a vector of `Yaml`.
10/// - `Hash` is an insertion-order map (using `linked_hash_map` logic).
11/// - `Alias` for referencing an anchor.
12/// - `Null` represents explicit YAML null.
13/// - `BadValue` is returned for invalid indexing or out-of-range lookups.
14#[derive(Clone, PartialEq, PartialOrd, Debug, Eq, Ord)]
15pub enum Yaml {
16    Real(String),
17    Integer(i64),
18    String(String),
19    Boolean(bool),
20    Array(Vec<Yaml>),
21    Hash(LinkedHashMap<Yaml, Yaml>),
22    Alias(usize),
23    Tagged(String, Box<Yaml>),
24    Null,
25    BadValue,
26}
27
28impl Hash for Yaml {
29    #[inline]
30    fn hash<H: Hasher>(&self, state: &mut H) {
31        match self {
32            Self::Real(s) => {
33                0.hash(state);
34                s.hash(state);
35            }
36            Self::Integer(i) => {
37                1.hash(state);
38                i.hash(state);
39            }
40            Self::String(s) => {
41                2.hash(state);
42                s.hash(state);
43            }
44            Self::Boolean(b) => {
45                3.hash(state);
46                b.hash(state);
47            }
48            Self::Array(a) => {
49                4.hash(state);
50                a.hash(state);
51            }
52            Self::Hash(h) => {
53                5.hash(state);
54                h.hash(state);
55            }
56            Self::Alias(i) => {
57                6.hash(state);
58                i.hash(state);
59            }
60            Self::Tagged(tag, value) => {
61                7.hash(state);
62                tag.hash(state);
63                value.hash(state);
64            }
65            Self::Null => {
66                8.hash(state);
67            }
68            Self::BadValue => {
69                9.hash(state);
70            }
71        }
72    }
73}
74
75/// This `BadValue` is used if we do `doc["unknown"]`, so indexing is graceful.
76static BAD_VALUE: Yaml = Yaml::BadValue;
77
78/// Accessors for Yaml
79impl Yaml {
80    #[inline(always)]
81    #[must_use]
82    pub const fn as_bool(&self) -> Option<bool> {
83        match *self {
84            Self::Boolean(b) => Some(b),
85            _ => None,
86        }
87    }
88
89    #[inline(always)]
90    #[must_use]
91    pub const fn as_i64(&self) -> Option<i64> {
92        match *self {
93            Self::Integer(i) => Some(i),
94            _ => None,
95        }
96    }
97
98    #[inline]
99    #[must_use]
100    pub fn as_f64(&self) -> Option<f64> {
101        match *self {
102            Self::Real(ref s) => parse_f64(s),
103            _ => None,
104        }
105    }
106
107    #[inline(always)]
108    #[must_use]
109    pub fn as_str(&self) -> Option<&str> {
110        match *self {
111            Self::String(ref s) => Some(s),
112            _ => None,
113        }
114    }
115
116    #[inline(always)]
117    #[must_use]
118    pub fn as_vec(&self) -> Option<&[Self]> {
119        match *self {
120            Self::Array(ref v) => Some(v),
121            _ => None,
122        }
123    }
124
125    #[inline(always)]
126    #[must_use]
127    pub const fn as_hash(&self) -> Option<&LinkedHashMap<Self, Self>> {
128        match *self {
129            Self::Hash(ref h) => Some(h),
130            _ => None,
131        }
132    }
133
134    #[inline(always)]
135    #[must_use]
136    pub const fn is_null(&self) -> bool {
137        matches!(*self, Self::Null)
138    }
139
140    #[inline(always)]
141    #[must_use]
142    pub const fn is_badvalue(&self) -> bool {
143        matches!(*self, Self::BadValue)
144    }
145
146    /// Parse a string into a Yaml value with automatic type detection
147    #[inline]
148    #[must_use]
149    pub fn parse_str(v: &str) -> Self {
150        // Handle hexadecimal numbers (0x, +0x, -0x)
151        if let Some(stripped) = v.strip_prefix("0x")
152            && !stripped.is_empty()
153            && stripped.chars().all(|c| c.is_ascii_hexdigit())
154            && let Ok(i) = i64::from_str_radix(stripped, 16)
155        {
156            return Self::Integer(i);
157        }
158        if let Some(stripped) = v.strip_prefix("+0x")
159            && !stripped.is_empty()
160            && stripped.chars().all(|c| c.is_ascii_hexdigit())
161            && let Ok(i) = i64::from_str_radix(stripped, 16)
162        {
163            return Self::Integer(i);
164        }
165        if let Some(stripped) = v.strip_prefix("-0x")
166            && !stripped.is_empty()
167            && stripped.chars().all(|c| c.is_ascii_hexdigit())
168            && let Ok(i) = i64::from_str_radix(stripped, 16)
169        {
170            return Self::Integer(-i);
171        }
172        // Handle octal numbers (0o, +0o, -0o)
173        if let Some(stripped) = v.strip_prefix("0o")
174            && !stripped.is_empty()
175            && stripped.chars().all(|c| c.is_ascii_digit() && c < '8')
176            && let Ok(i) = i64::from_str_radix(stripped, 8)
177        {
178            return Self::Integer(i);
179        }
180        if let Some(stripped) = v.strip_prefix("+0o")
181            && !stripped.is_empty()
182            && stripped.chars().all(|c| c.is_ascii_digit() && c < '8')
183            && let Ok(i) = i64::from_str_radix(stripped, 8)
184        {
185            return Self::Integer(i);
186        }
187        if let Some(stripped) = v.strip_prefix("-0o")
188            && !stripped.is_empty()
189            && stripped.chars().all(|c| c.is_ascii_digit() && c < '8')
190            && let Ok(i) = i64::from_str_radix(stripped, 8)
191        {
192            return Self::Integer(-i);
193        }
194        // Handle binary numbers (0b, +0b, -0b)
195        if let Some(stripped) = v.strip_prefix("0b")
196            && !stripped.is_empty()
197            && stripped.chars().all(|c| c == '0' || c == '1')
198            && let Ok(i) = i64::from_str_radix(stripped, 2)
199        {
200            return Self::Integer(i);
201        }
202        if let Some(stripped) = v.strip_prefix("+0b")
203            && !stripped.is_empty()
204            && stripped.chars().all(|c| c == '0' || c == '1')
205            && let Ok(i) = i64::from_str_radix(stripped, 2)
206        {
207            return Self::Integer(i);
208        }
209        if let Some(stripped) = v.strip_prefix("-0b")
210            && !stripped.is_empty()
211            && stripped.chars().all(|c| c == '0' || c == '1')
212            && let Ok(i) = i64::from_str_radix(stripped, 2)
213        {
214            return Self::Integer(-i);
215        }
216        if let Some(stripped) = v.strip_prefix('+')
217            && let Ok(i) = stripped.parse::<i64>()
218            && !has_invalid_leading_zeros(v)
219            && !has_invalid_sign_prefix(v)
220        {
221            return Self::Integer(i);
222        }
223        match v {
224            "~" | "null" => Self::Null,
225            "true" => Self::Boolean(true),
226            "false" => Self::Boolean(false),
227            _ if v.parse::<i64>().is_ok()
228                && !has_invalid_leading_zeros(v)
229                && !has_invalid_sign_prefix(v) =>
230            {
231                if let Ok(i) = v.parse::<i64>() {
232                    Self::Integer(i)
233                } else {
234                    Self::String(v.into())
235                }
236            }
237            _ if parse_f64(v).is_some() => Self::Real(v.into()),
238            _ => Self::String(v.into()),
239        }
240    }
241}
242
243/// Check if a string has invalid sign prefixes (++, +-, -+, --)
244fn has_invalid_sign_prefix(v: &str) -> bool {
245    v.starts_with("++") || v.starts_with("+-") || v.starts_with("-+") || v.starts_with("--")
246}
247
248/// Check if a string contains invalid leading zeros for YAML 1.2 integer parsing
249/// In YAML 1.2, integers with leading zeros should be treated as strings
250/// unless they use explicit base prefixes (0x, 0o, 0b)
251fn has_invalid_leading_zeros(v: &str) -> bool {
252    // Allow single zero
253    if v == "0" || v == "+0" || v == "-0" {
254        return false;
255    }
256
257    // Check for leading zeros in positive numbers
258    if v.starts_with('0') && v.len() > 1 && v.chars().nth(1)
259        .expect("nth(1) cannot be None when v.len() > 1").is_ascii_digit() {
260        return true;
261    }
262
263    // Check for leading zeros in signed numbers
264    if (v.starts_with("+0") || v.starts_with("-0"))
265        && v.len() > 2
266        && v.chars().nth(2)
267            .expect("nth(2) cannot be None when v.len() > 2").is_ascii_digit()
268    {
269        return true;
270    }
271
272    false
273}
274
275/// Convert string to float (including .inf, .nan).
276pub fn parse_f64(v: &str) -> Option<f64> {
277    match v {
278        ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY),
279        "-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY),
280        ".nan" | ".NaN" | ".NAN" => Some(f64::NAN),
281        _ => {
282            // Reject strings with invalid leading zeros or sign prefixes for YAML 1.2 compliance
283            if has_invalid_leading_zeros(v) || has_invalid_sign_prefix(v) {
284                None
285            } else {
286                v.parse::<f64>().ok()
287            }
288        }
289    }
290}
291
292/// Indexing by &str
293impl std::ops::Index<&str> for Yaml {
294    type Output = Self;
295    #[inline]
296    fn index(&self, idx: &str) -> &Self {
297        let key = Self::String(idx.to_owned());
298        match self.as_hash() {
299            Some(h) => h.get(&key).unwrap_or(&BAD_VALUE),
300            None => &BAD_VALUE,
301        }
302    }
303}
304
305/// Indexing by usize
306impl std::ops::Index<usize> for Yaml {
307    type Output = Self;
308    #[inline]
309    fn index(&self, idx: usize) -> &Self {
310        if let Some(v) = self.as_vec() {
311            v.get(idx).unwrap_or(&BAD_VALUE)
312        } else if let Some(h) = self.as_hash() {
313            let key = Self::Integer(idx as i64);
314            h.get(&key).unwrap_or(&BAD_VALUE)
315        } else {
316            &BAD_VALUE
317        }
318    }
319}