oxedyne/fe2o3/fe2o3_jdat/src/string/json.rs
13.9 KiB, 1 run
created by r1870400018:61146, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Why a second reader: the text decoder with `DecoderConfig::json` still reads JDAT's own forms, |
| 2 | //! so a kind annotation `(u64|5)`, a hex integer, an unquoted word, a single-quoted string, a |
| 3 | //! trailing comma, digits run together across a space and anything after the first value all |
| 4 | //! decode there. Found by the presentation audit of 2026-09-23. Input that must mean exactly one |
| 5 | //! thing comes through `Dat::decode_json_strict` instead. |
| 6 | |
| 7 | use crate::{ |
| 8 | prelude::*, |
| 9 | bdat::limits::DecodeLimits, |
| 10 | }; |
| 11 | |
| 12 | use oxedyne_fe2o3_core::prelude::*; |
| 13 | use oxedyne_fe2o3_num::string::NumberString; |
| 14 | |
| 15 | use std::str; |
| 16 | |
| 17 | |
| 18 | impl Dat { |
| 19 | |
| 20 | /// Decodes JSON text as RFC 8259 defines it and refuses everything else. |
| 21 | /// |
| 22 | /// An object becomes a `Dat::Map` with string keys, an array a `Dat::List`, a string a |
| 23 | /// `Dat::Str`, `true` and `false` a `Dat::Bool` and `null` a `Dat::Opt` holding nothing. A |
| 24 | /// number becomes the daticle the JDAT text decoder makes of the same digits. The input may |
| 25 | /// hold one value, with only JSON's four whitespace characters around it. |
| 26 | /// |
| 27 | /// Refused as well, where RFC 8259 leaves the reader a choice: an object naming a member twice, |
| 28 | /// and a `\u` escape that leaves half a surrogate pair, since neither has one meaning. |
| 29 | /// `limits` bounds the length of the text and the depth of nesting, the root at depth 1. |
| 30 | pub fn decode_json_strict(s: &str, limits: &DecodeLimits) -> Outcome<Self> { |
| 31 | res!(limits.check_len(s.len())); |
| 32 | let mut r = Reader { |
| 33 | src: s.as_bytes(), |
| 34 | pos: 0, |
| 35 | limits: *limits, |
| 36 | }; |
| 37 | r.skip_ws(); |
| 38 | let value = res!(r.value(1)); |
| 39 | r.skip_ws(); |
| 40 | if r.pos < r.src.len() { |
| 41 | return Err(err!( |
| 42 | "JSON text holds one value, and more follows it at byte {} of {}.", |
| 43 | r.pos, r.src.len(); |
| 44 | Invalid, Input, Decode)); |
| 45 | } |
| 46 | Ok(value) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | struct Reader<'a> { |
| 51 | src: &'a [u8], |
| 52 | pos: usize, |
| 53 | limits: DecodeLimits, |
| 54 | } |
| 55 | |
| 56 | impl<'a> Reader<'a> { |
| 57 | |
| 58 | fn peek(&self) -> Option<u8> { |
| 59 | self.src.get(self.pos).copied() |
| 60 | } |
| 61 | |
| 62 | fn skip_ws(&mut self) { |
| 63 | while let Some(b' ' | b'\t' | b'\n' | b'\r') = self.peek() { |
| 64 | self.pos += 1; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | fn digit_run(&mut self) { |
| 69 | while let Some(b'0'..=b'9') = self.peek() { |
| 70 | self.pos += 1; |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | /// What the byte at the cursor is, for an error. |
| 75 | fn found(&self) -> String { |
| 76 | match self.peek() { |
| 77 | Some(b) if b.is_ascii_graphic() => fmt!("'{}'", b as char), |
| 78 | Some(b) => fmt!("byte 0x{:02x}", b), |
| 79 | None => fmt!("the end of the text"), |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | fn value(&mut self, depth: usize) -> Outcome<Dat> { |
| 84 | res!(self.limits.check_depth(depth, self.pos)); |
| 85 | match self.peek() { |
| 86 | Some(b'{') => self.object(depth), |
| 87 | Some(b'[') => self.array(depth), |
| 88 | Some(b'"') => Ok(Dat::Str(res!(self.string()))), |
| 89 | Some(b't') => self.literal("true", Dat::Bool(true)), |
| 90 | Some(b'f') => self.literal("false", Dat::Bool(false)), |
| 91 | Some(b'n') => self.literal("null", Dat::Opt(Box::new(None))), |
| 92 | Some(b'-' | b'0'..=b'9') => self.number(), |
| 93 | _ => Err(err!( |
| 94 | "A JSON value begins with '{{', '[', '\"', a digit, '-', true, false or null, \ |
| 95 | and byte {} holds {}.", self.pos, self.found(); |
| 96 | Invalid, Input, Decode)), |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | fn object(&mut self, depth: usize) -> Outcome<Dat> { |
| 101 | let open = self.pos; |
| 102 | self.pos += 1; |
| 103 | let mut map = DaticleMap::new(); |
| 104 | self.skip_ws(); |
| 105 | if self.peek() == Some(b'}') { |
| 106 | self.pos += 1; |
| 107 | return Ok(Dat::Map(map)); |
| 108 | } |
| 109 | loop { |
| 110 | self.skip_ws(); |
| 111 | if self.peek() != Some(b'"') { |
| 112 | return Err(err!( |
| 113 | "A member of the JSON object opened at byte {} is named by a string, and \ |
| 114 | byte {} holds {}.", open, self.pos, self.found(); |
| 115 | Invalid, Input, Decode)); |
| 116 | } |
| 117 | let at = self.pos; |
| 118 | let key = Dat::Str(res!(self.string())); |
| 119 | self.skip_ws(); |
| 120 | if self.peek() != Some(b':') { |
| 121 | return Err(err!( |
| 122 | "A member name in the JSON object opened at byte {} is followed by ':', and \ |
| 123 | byte {} holds {}.", open, self.pos, self.found(); |
| 124 | Invalid, Input, Decode)); |
| 125 | } |
| 126 | self.pos += 1; |
| 127 | self.skip_ws(); |
| 128 | let value = res!(self.value(depth + 1)); |
| 129 | if map.contains_key(&key) { |
| 130 | return Err(err!( |
| 131 | "The JSON object opened at byte {} names the member {:?} a second time, at \ |
| 132 | byte {}.", open, key, at; |
| 133 | Invalid, Input, Duplicate)); |
| 134 | } |
| 135 | map.insert(key, value); |
| 136 | self.skip_ws(); |
| 137 | match self.peek() { |
| 138 | Some(b',') => self.pos += 1, |
| 139 | Some(b'}') => { |
| 140 | self.pos += 1; |
| 141 | return Ok(Dat::Map(map)); |
| 142 | }, |
| 143 | _ => return Err(err!( |
| 144 | "A member of the JSON object opened at byte {} is followed by ',' or '}}', \ |
| 145 | and byte {} holds {}.", open, self.pos, self.found(); |
| 146 | Invalid, Input, Decode)), |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | fn array(&mut self, depth: usize) -> Outcome<Dat> { |
| 152 | let open = self.pos; |
| 153 | self.pos += 1; |
| 154 | let mut list = Vec::new(); |
| 155 | self.skip_ws(); |
| 156 | if self.peek() == Some(b']') { |
| 157 | self.pos += 1; |
| 158 | return Ok(Dat::List(list)); |
| 159 | } |
| 160 | loop { |
| 161 | self.skip_ws(); |
| 162 | list.push(res!(self.value(depth + 1))); |
| 163 | self.skip_ws(); |
| 164 | match self.peek() { |
| 165 | Some(b',') => self.pos += 1, |
| 166 | Some(b']') => { |
| 167 | self.pos += 1; |
| 168 | return Ok(Dat::List(list)); |
| 169 | }, |
| 170 | _ => return Err(err!( |
| 171 | "An element of the JSON array opened at byte {} is followed by ',' or ']', \ |
| 172 | and byte {} holds {}.", open, self.pos, self.found(); |
| 173 | Invalid, Input, Decode)), |
| 174 | } |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | fn literal(&mut self, word: &str, dat: Dat) -> Outcome<Dat> { |
| 179 | if !self.src[self.pos..].starts_with(word.as_bytes()) { |
| 180 | return Err(err!( |
| 181 | "The JSON value at byte {} begins like {} and is not it.", self.pos, word; |
| 182 | Invalid, Input, Decode)); |
| 183 | } |
| 184 | self.pos += word.len(); |
| 185 | Ok(dat) |
| 186 | } |
| 187 | |
| 188 | /// A number: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?` and nothing else, so no |
| 189 | /// plus sign, leading zero, bare point, radix prefix or digit separator. |
| 190 | fn number(&mut self) -> Outcome<Dat> { |
| 191 | let start = self.pos; |
| 192 | if self.peek() == Some(b'-') { |
| 193 | self.pos += 1; |
| 194 | } |
| 195 | match self.peek() { |
| 196 | Some(b'0') => { |
| 197 | self.pos += 1; |
| 198 | if let Some(b'0'..=b'9') = self.peek() { |
| 199 | return Err(err!( |
| 200 | "The JSON number at byte {} has a leading zero.", start; |
| 201 | Invalid, Input, Decode)); |
| 202 | } |
| 203 | }, |
| 204 | Some(b'1'..=b'9') => self.digit_run(), |
| 205 | _ => return Err(err!( |
| 206 | "The JSON number at byte {} has no digit after its sign; byte {} holds {}.", |
| 207 | start, self.pos, self.found(); |
| 208 | Invalid, Input, Decode)), |
| 209 | } |
| 210 | if self.peek() == Some(b'.') { |
| 211 | self.pos += 1; |
| 212 | if !matches!(self.peek(), Some(b'0'..=b'9')) { |
| 213 | return Err(err!( |
| 214 | "The JSON number at byte {} has no digit after its point.", start; |
| 215 | Invalid, Input, Decode)); |
| 216 | } |
| 217 | self.digit_run(); |
| 218 | } |
| 219 | if let Some(b'e' | b'E') = self.peek() { |
| 220 | self.pos += 1; |
| 221 | if let Some(b'+' | b'-') = self.peek() { |
| 222 | self.pos += 1; |
| 223 | } |
| 224 | if !matches!(self.peek(), Some(b'0'..=b'9')) { |
| 225 | return Err(err!( |
| 226 | "The JSON number at byte {} has no digit in its exponent.", start; |
| 227 | Invalid, Input, Decode)); |
| 228 | } |
| 229 | self.digit_run(); |
| 230 | } |
| 231 | let text = res!(str::from_utf8(&self.src[start..self.pos]), Decode, UTF8); |
| 232 | Dat::from_untyped_number(&res!(NumberString::new(text))) |
| 233 | } |
| 234 | |
| 235 | /// A string, cursor on its opening quote. Characters below U+0020 must be escaped, and the |
| 236 | /// only escapes are RFC 8259's eight and `\u` with four hex digits. |
| 237 | fn string(&mut self) -> Outcome<String> { |
| 238 | let open = self.pos; |
| 239 | self.pos += 1; |
| 240 | let mut out = String::new(); |
| 241 | let mut run = self.pos; // start of the unescaped run being read |
| 242 | loop { |
| 243 | match self.peek() { |
| 244 | Some(b'"') => { |
| 245 | out.push_str(res!(str::from_utf8(&self.src[run..self.pos]), Decode, UTF8)); |
| 246 | self.pos += 1; |
| 247 | return Ok(out); |
| 248 | }, |
| 249 | Some(b'\\') => { |
| 250 | out.push_str(res!(str::from_utf8(&self.src[run..self.pos]), Decode, UTF8)); |
| 251 | self.pos += 1; |
| 252 | out.push(res!(self.escape())); |
| 253 | run = self.pos; |
| 254 | }, |
| 255 | Some(b) if b < 0x20 => return Err(err!( |
| 256 | "The JSON string opened at byte {} holds the control byte 0x{:02x} at byte \ |
| 257 | {}, which must be escaped.", open, b, self.pos; |
| 258 | Invalid, Input, Decode)), |
| 259 | Some(_) => self.pos += 1, |
| 260 | None => return Err(err!( |
| 261 | "The JSON string opened at byte {} is never closed.", open; |
| 262 | Invalid, Input, Decode)), |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | /// The character an escape stands for, cursor just past its backslash. |
| 268 | fn escape(&mut self) -> Outcome<char> { |
| 269 | let at = self.pos - 1; |
| 270 | let c = match self.peek() { |
| 271 | Some(c) => c, |
| 272 | None => return Err(err!( |
| 273 | "The JSON text ends inside the escape at byte {}.", at; |
| 274 | Invalid, Input, Decode)), |
| 275 | }; |
| 276 | self.pos += 1; |
| 277 | let ch = match c { |
| 278 | b'"' => '"', |
| 279 | b'\\' => '\\', |
| 280 | b'/' => '/', |
| 281 | b'b' => '\u{0008}', |
| 282 | b'f' => '\u{000C}', |
| 283 | b'n' => '\n', |
| 284 | b'r' => '\r', |
| 285 | b't' => '\t', |
| 286 | b'u' => { |
| 287 | let unit = res!(self.hex4()); |
| 288 | let cp = match unit { |
| 289 | 0xD800..=0xDBFF => { |
| 290 | if !self.src[self.pos..].starts_with(b"\\u") { |
| 291 | return Err(err!( |
| 292 | "The escape at byte {} is a high surrogate with no low \ |
| 293 | surrogate escape after it.", at; |
| 294 | Invalid, Input, Decode)); |
| 295 | } |
| 296 | self.pos += 2; |
| 297 | let low = res!(self.hex4()); |
| 298 | if !(0xDC00..=0xDFFF).contains(&low) { |
| 299 | return Err(err!( |
| 300 | "The escape at byte {} is a high surrogate followed by \ |
| 301 | \\u{:04X}, which is not a low surrogate.", at, low; |
| 302 | Invalid, Input, Decode)); |
| 303 | } |
| 304 | 0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00) |
| 305 | }, |
| 306 | 0xDC00..=0xDFFF => return Err(err!( |
| 307 | "The escape at byte {} is a low surrogate with no high surrogate \ |
| 308 | before it.", at; |
| 309 | Invalid, Input, Decode)), |
| 310 | _ => unit, |
| 311 | }; |
| 312 | match char::from_u32(cp) { |
| 313 | Some(ch) => ch, |
| 314 | None => return Err(err!( |
| 315 | "The escape at byte {} names U+{:X}, which is not a character.", at, cp; |
| 316 | Invalid, Input, Decode)), |
| 317 | } |
| 318 | }, |
| 319 | _ => return Err(err!( |
| 320 | "The escape at byte {} is '\\{}', which JSON does not have.", at, |
| 321 | (c as char).escape_default(); |
| 322 | Invalid, Input, Decode)), |
| 323 | }; |
| 324 | Ok(ch) |
| 325 | } |
| 326 | |
| 327 | fn hex4(&mut self) -> Outcome<u32> { |
| 328 | let mut v = 0u32; |
| 329 | for _ in 0..4 { |
| 330 | let d = match self.peek() { |
| 331 | Some(b @ b'0'..=b'9') => b - b'0', |
| 332 | Some(b @ b'a'..=b'f') => b - b'a' + 10, |
| 333 | Some(b @ b'A'..=b'F') => b - b'A' + 10, |
| 334 | _ => return Err(err!( |
| 335 | "A \\u escape takes four hex digits, and byte {} holds {}.", |
| 336 | self.pos, self.found(); |
| 337 | Invalid, Input, Decode)), |
| 338 | }; |
| 339 | v = (v << 4) | d as u32; |
| 340 | self.pos += 1; |
| 341 | } |
| 342 | Ok(v) |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | |
| 347 | #[cfg(test)] |
| 348 | mod tests { |
| 349 | use super::*; |
| 350 | |
| 351 | fn strict(s: &str) -> Outcome<Dat> { |
| 352 | Dat::decode_json_strict(s, &DecodeLimits::text()) |
| 353 | } |
| 354 | |
| 355 | /// Nesting past the limit is refused before it is descended into. |
| 356 | #[test] |
| 357 | fn test_depth_limit_00() -> Outcome<()> { |
| 358 | let limits = DecodeLimits::new(3, 1024); |
| 359 | res!(Dat::decode_json_strict("[[1]]", &limits)); |
| 360 | assert!(Dat::decode_json_strict("[[[1]]]", &limits).is_err(), "depth 4 is past 3"); |
| 361 | let deep = fmt!("{}{}", "[".repeat(100_000), "]".repeat(100_000)); |
| 362 | assert!(strict(&deep).is_err(), "a hundred thousand levels must be refused, not recursed"); |
| 363 | Ok(()) |
| 364 | } |
| 365 | |
| 366 | /// The length limit is checked before any byte is read. |
| 367 | #[test] |
| 368 | fn test_length_limit_00() { |
| 369 | let limits = DecodeLimits::new(8, 4); |
| 370 | assert!(Dat::decode_json_strict("12345", &limits).is_err(), "five bytes is past four"); |
| 371 | } |
| 372 | } |