<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Toast in the Machine</title>
    <link>https://toast.zeroflag.net/</link>
    <description>A Blog about Programming</description>
    <pubDate>Wed, 14 Oct 2026 00:31:42 +0000</pubDate>
    <item>
      <title>Is Parsing JSON a Minefield?</title>
      <link>https://toast.zeroflag.net/is-parsing-json-a-minefield?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[There&#39;s a really great article titled Parsing JSON is a Minefield that provides some pretty comprehensive information about the ambiguities and contradictions in the JSON &#34;spec&#34;. As is so often the case with software, it turns out JSON has more complexities than you may expect (or at the very least more than Douglas Crockford would tell you about).&#xA;&#xA;The JSON grammar was famously printed on a business card in order to demonstrate how simple it was. This got me to thinking about an interesting experiment: how correct would a JSON parser implemented purely based on that business card be? Luckily the author of the &#34;Parsing JSON is a Minefield&#34; article also created a comprehensive suite of tests that cover all the edge cases discussed in the article. So of course I decided to write and test a JSON parser.&#xA;&#xA;Baseline Assumptions&#xA;&#xA;The JSON business card is expectedly pretty vague, so I had to make several assumptions about how things should work. I did my best to make decisions based on what a typical software developer would expect, but they are still assumptions based on my own biases. Just keep that in mind going forward.&#xA;&#xA;Before I could do anything I needed to define how the parser would work. Unlike serde which deserializes specific JSON into structs, I want to parse JSON into a more generic enum that can represent any JSON file.&#xA;&#xA;pub enum JSONValue {&#xA;    Null,&#xA;    Boolean(bool),&#xA;    Number(f64),&#xA;    String(String),&#xA;    Array(VecJSONValue),&#xA;    Object(HashMapString, JSONValue),&#xA;}&#xA;&#xA;So already there were a few assumptions being made. &#34;Number&#34; was always represented as a 64-bit floating point number (the same way JavaScript does it), String was a Rust string (which is UTF-8 and satisfies the &#34;any-Unicode-character&#34; requirement), and Objects are HashMaps (meaning duplicate keys can&#39;t exist).&#xA;&#xA;Next I needed to define the behavior of the parser. I decided it should take a Rust String as inpujt, which means I&#39;m assuming all JSON is encoded as UTF-8 (or can be easily translated into UTF-8). Combined with a custom JSONParseError type this resulted in the following function signature for the parser:&#xA;&#xA;pub fn parse(input: String) -  ResultJSONValue, JSONParseError {&#xA;    ...&#xA;}&#xA;&#xA;Note: For brevity I&#39;ve omitted several boilerplate trait implementations like PartialEq, Display, Debug, and From.&#xA;&#xA;Now that data types and a function signature existed, I could take the business card and translate it into a series of test that I felt covered the described behavior. I put these tests in a separate file called businesscard.rs.&#xA;&#xA;use crate::;&#xA;&#xA;[test]&#xA;fn parsenull() {&#xA;    asserteq!(parse(String::from(&#34;null&#34;)), Ok(JSONValue::Null));&#xA;}&#xA;&#xA;[test]&#xA;fn parsetrue() {&#xA;    asserteq!(parse(String::from(&#34;true&#34;)), Ok(JSONValue::Boolean(true)));&#xA;}&#xA;&#xA;[test]&#xA;fn parsefalse() {&#xA;    asserteq!(parse(String::from(&#34;false&#34;)), Ok(JSONValue::Boolean(false)));&#xA;}&#xA;&#xA;[test]&#xA;fn parsenumber() {&#xA;    asserteq!(parse(String::from(&#34;1&#34;)), Ok(JSONValue::Number(1.0)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;123&#34;)), Ok(JSONValue::Number(123.0)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;123.45&#34;)), Ok(JSONValue::Number(123.45)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;-1&#34;)), Ok(JSONValue::Number(-1.0)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;-123&#34;)), Ok(JSONValue::Number(-123.0)));&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;-123.45&#34;)),&#xA;        Ok(JSONValue::Number(-123.45))&#xA;    );&#xA;&#xA;    asserteq!(parse(String::from(&#34;123e8&#34;)), Ok(JSONValue::Number(123e8)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;123e+8&#34;)), Ok(JSONValue::Number(123e8)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;123e-8&#34;)), Ok(JSONValue::Number(123e-8)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;123E8&#34;)), Ok(JSONValue::Number(123e8)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;123E+8&#34;)), Ok(JSONValue::Number(123e8)));&#xA;&#xA;    asserteq!(parse(String::from(&#34;123E-8&#34;)), Ok(JSONValue::Number(123e-8)));&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;-123.45e-8&#34;)),&#xA;        Ok(JSONValue::Number(-123.45e-8))&#xA;    );&#xA;}&#xA;&#xA;[test]&#xA;fn parsestring() {&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::new()))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;a\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;a&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;Hello World!\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;Hello World!&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\\&#34;\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\&#34;&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\\\\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\\&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\/\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;/&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\b\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\x08&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\f\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\x0C&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\n\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\n&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\r\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\r&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\t\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\t&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\u2603\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\u{2603}&#34;)))&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;\&#34;\\uD83D\\uDE00\&#34;&#34;)),&#xA;        Ok(JSONValue::String(String::from(&#34;\u{1F600}&#34;)))&#xA;    );&#xA;}&#xA;&#xA;[test]&#xA;fn parsearray() {&#xA;    asserteq!(parse(String::from(&#34;[]&#34;)), Ok(JSONValue::Array(vec![])));&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;[1,2,3]&#34;)),&#xA;        Ok(JSONValue::Array(vec![&#xA;            JSONValue::Number(1.0),&#xA;            JSONValue::Number(2.0),&#xA;            JSONValue::Number(3.0)&#xA;        ]))&#xA;    );&#xA;}&#xA;&#xA;[test]&#xA;fn parseobject() {&#xA;    asserteq!(&#xA;        parse(String::from(&#34;{}&#34;)),&#xA;        Ok(JSONValue::Object(HashMap::new()))&#xA;    );&#xA;&#xA;    let mut o1: HashMapString, JSONValue = HashMap::new();&#xA;    o1.insert(&#xA;        String::from(&#34;name&#34;),&#xA;        JSONValue::String(String::from(&#34;John Doe&#34;)),&#xA;    );&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;{\&#34;name\&#34;:\&#34;John Doe\&#34;}&#34;)),&#xA;        Ok(JSONValue::Object(o1))&#xA;    );&#xA;&#xA;    let mut o2: HashMapString, JSONValue = HashMap::new();&#xA;    o2.insert(&#xA;        String::from(&#34;name&#34;),&#xA;        JSONValue::String(String::from(&#34;John Doe&#34;)),&#xA;    );&#xA;    o2.insert(String::from(&#34;age&#34;), JSONValue::Number(42.0));&#xA;&#xA;    asserteq!(&#xA;        parse(String::from(&#34;{\&#34;name\&#34;:\&#34;John Doe\&#34;,\&#34;age\&#34;:42}&#34;)),&#xA;        Ok(JSONValue::Object(o2))&#xA;    );&#xA;}&#xA;&#xA;Most of these tests are pretty straightforward. The biggest assumptions were what each of the escape characters meant. Among those the major assumption was that \u four-hex-digits referred to UTF-16 encoded values and thus need to account for surrogate pairs.&#xA;&#xA;Building the Parser&#xA;&#xA;With all  of that out of the way I was free to start hacking away at a hand crafted recursive descent parser. This parser has two primary goals:&#xA;&#xA;1) Be correct (this includes not crashing)&#xA;2) Be easy to understand&#xA;&#xA;Performance, extensibility, security, and real world usability are non-goals.&#xA;&#xA;Here&#39;s the initial parser I came up with that conforms to the business card behavior:&#xA;&#xA;fn expect(cursor: &amp;mut PeekableChars, c: char) -  Result(), JSONParseError {&#xA;    if cursor.next() != Some(c) {&#xA;        return Err(JSONParseError::new(&amp;format!(&#34;Expected character: {}&#34;, c)));&#xA;    }&#xA;&#xA;    Ok(())&#xA;}&#xA;&#xA;fn parsetoken(&#xA;    cursor: &amp;mut PeekableChars,&#xA;    token: Chars,&#xA;    value: JSONValue,&#xA;) -  ResultJSONValue, JSONParseError {&#xA;    for c in token {&#xA;        expect(cursor, c)?;&#xA;    }&#xA;&#xA;    Ok(value)&#xA;}&#xA;&#xA;fn parsenull(cursor: &amp;mut PeekableChars) -  ResultJSONValue, JSONParseError {&#xA;    parsetoken(cursor, &#34;null&#34;.chars(), JSONValue::Null)&#xA;}&#xA;&#xA;fn parsetrue(cursor: &amp;mut PeekableChars) -  ResultJSONValue, JSONParseError {&#xA;    parsetoken(cursor, &#34;true&#34;.chars(), JSONValue::Boolean(true))&#xA;}&#xA;&#xA;fn parsefalse(cursor: &amp;mut PeekableChars) -  ResultJSONValue, JSONParseError {&#xA;    parsetoken(cursor, &#34;false&#34;.chars(), JSONValue::Boolean(false))&#xA;}&#xA;&#xA;fn readinteger(cursor: &amp;mut PeekableChars, number: &amp;mut String) -  Result(), JSONParseError {&#xA;    let first = cursor.peek();&#xA;&#xA;    if first  Some(&amp;&#39;0&#39;) || first  Some(&amp;&#39;9&#39;) {&#xA;        return Err(JSONParseError::new(&#34;Unexpected character&#34;));&#xA;    }&#xA;&#xA;    while let Some(c) = cursor.peek() {&#xA;        if c   = &amp;&#39;0&#39; &amp;&amp; c &lt;= &amp;&#39;9&#39; {&#xA;            number.push(c);&#xA;            cursor.next();&#xA;        } else {&#xA;            break;&#xA;        }&#xA;    }&#xA;&#xA;    Ok(())&#xA;}&#xA;&#xA;fn parsenumberraw(cursor: &amp;mut PeekableChars) -  Resultf64, JSONParseError {&#xA;    let mut number = String::new();&#xA;&#xA;    if cursor.peek() == Some(&amp;&#39;-&#39;) {&#xA;        number.push(&#39;-&#39;);&#xA;        cursor.next();&#xA;    }&#xA;&#xA;    if cursor.peek() == Some(&amp;&#39;0&#39;) {&#xA;        number.push(&#39;0&#39;);&#xA;        cursor.next();&#xA;    } else {&#xA;        readinteger(cursor, &amp;mut number)?;&#xA;    }&#xA;&#xA;    if cursor.peek() == Some(&amp;&#39;.&#39;) {&#xA;        number.push(&#39;.&#39;);&#xA;        cursor.next();&#xA;        readinteger(cursor, &amp;mut number)?;&#xA;    }&#xA;&#xA;    if cursor.peek() == Some(&amp;&#39;e&#39;) || cursor.peek() == Some(&amp;&#39;E&#39;) {&#xA;        number.push(&#39;e&#39;);&#xA;        cursor.next();&#xA;&#xA;        if cursor.peek() == Some(&amp;&#39;-&#39;) {&#xA;            number.push(&#39;-&#39;);&#xA;            cursor.next();&#xA;        }&#xA;&#xA;        if cursor.peek() == Some(&amp;&#39;+&#39;) {&#xA;            cursor.next();&#xA;        }&#xA;&#xA;        readinteger(cursor, &amp;mut number)?;&#xA;    }&#xA;&#xA;    Ok(number.parse::f64()?)&#xA;}&#xA;&#xA;fn parsenumber(cursor: &amp;mut PeekableChars) -  ResultJSONValue, JSONParseError {&#xA;    Ok(JSONValue::Number(parsenumberraw(cursor)?))&#xA;}&#xA;&#xA;fn parseunicoderaw(&#xA;    cursor: &amp;mut PeekableChars,&#xA;    high: Optionu32,&#xA;) -  Resultchar, JSONParseError {&#xA;    expect(cursor, &#39;u&#39;)?;&#xA;&#xA;    let mut u = String::new();&#xA;&#xA;    for  in 0..4 {&#xA;        if let Some(c) = cursor.next() {&#xA;            if c.isasciihexdigit() {&#xA;                u.push(c);&#xA;            } else {&#xA;                return Err(JSONParseError::new(&#34;Unexpected character&#34;));&#xA;            }&#xA;        } else {&#xA;            return Err(JSONParseError::new(&#34;Unexpected EOF&#34;));&#xA;        }&#xA;    }&#xA;&#xA;    let numeric = u32::fromstrradix(&amp;u, 16)?;&#xA;&#xA;    if let Some(h) = high {&#xA;        if numeric   = 0xDC00 &amp;&amp; numeric &lt;= 0xDFFF {&#xA;            let mut code = 0x10000;&#xA;            code += (h &amp; 0x03FF) &lt;&lt; 10;&#xA;            code += numeric &amp; 0x03FF;&#xA;            char::fromu32(code).okor(JSONParseError::new(&#34;Invalid unicode character&#34;))&#xA;        } else {&#xA;            Err(JSONParseError::new(&#34;Invalid surrogate pair&#34;))&#xA;        }&#xA;    } else {&#xA;        if numeric   = 0xD800 &amp;&amp; numeric &lt;= 0xDBFF {&#xA;            expect(cursor, &#39;\\&#39;)?;&#xA;            parseunicoderaw(cursor, Some(numeric))&#xA;        } else {&#xA;            char::fromu32(numeric).okor(JSONParseError::new(&#34;Invalid unicode character&#34;))&#xA;        }&#xA;    }&#xA;}&#xA;&#xA;fn parsestringraw(cursor: &amp;mut PeekableChars) -  ResultString, JSONParseError {&#xA;    expect(cursor, &#39;&#34;&#39;)?;&#xA;&#xA;    let mut s = String::new();&#xA;    let mut done = false;&#xA;&#xA;    while let Some(c) = cursor.next() {&#xA;        if c == &#39;&#34;&#39; {&#xA;            done = true;&#xA;            break;&#xA;        } else if c == &#39;\\&#39; {&#xA;            if let Some(&#39;u&#39;) = cursor.peek() {&#xA;                s.push(parseunicoderaw(cursor, None)?)&#xA;            } else {&#xA;                match cursor.next() {&#xA;                    Some(&#39;&#34;&#39;) =  s.push(&#39;&#34;&#39;),&#xA;                    Some(&#39;\\&#39;) =  s.push(&#39;\\&#39;),&#xA;                    Some(&#39;/&#39;) =  s.push(&#39;/&#39;),&#xA;                    Some(&#39;b&#39;) =  s.push(&#39;\x08&#39;),&#xA;                    Some(&#39;f&#39;) =  s.push(&#39;\x0C&#39;),&#xA;                    Some(&#39;n&#39;) =  s.push(&#39;\n&#39;),&#xA;                    Some(&#39;r&#39;) =  s.push(&#39;\r&#39;),&#xA;                    Some(&#39;t&#39;) =  s.push(&#39;\t&#39;),&#xA;                    None =  return Err(JSONParseError::new(&#34;Unexpected EOF&#34;)),&#xA;                     =  return Err(JSONParseError::new(&#34;Unexpected character&#34;)),&#xA;                }&#xA;            }&#xA;        } else {&#xA;            match c {&#xA;                &#39;\u{0000}&#39;..=&#39;\x1F&#39; | &#39;\u{007F}&#39; | &#39;\u{0080}&#39;..=&#39;\u{009F}&#39; =  {&#xA;                    return Err(JSONParseError::new(&#34;Unexpected control character&#34;))&#xA;                }&#xA;                 =  s.push(c),&#xA;            }&#xA;        }&#xA;    }&#xA;&#xA;    if done {&#xA;        return Ok(s);&#xA;    }&#xA;&#xA;    return Err(JSONParseError::new(&#34;Unexpected EOF&#34;));&#xA;}&#xA;&#xA;fn parsestring(cursor: &amp;mut PeekableChars) -  ResultJSONValue, JSONParseError {&#xA;    Ok(JSONValue::String(parsestringraw(cursor)?))&#xA;}&#xA;&#xA;fn parsearray(cursor: &amp;mut PeekableChars, limit: usize) -  ResultJSONValue, JSONParseError {&#xA;    expect(cursor, &#39;[&#39;)?;&#xA;&#xA;    let mut array: VecJSONValue = vec![];&#xA;&#xA;    if cursor.peek() == Some(&amp;&#39;]&#39;) {&#xA;        cursor.next();&#xA;        return Ok(JSONValue::Array(array));&#xA;    }&#xA;&#xA;    loop {&#xA;        array.push(parsevalue(cursor, limit)?);&#xA;&#xA;        if cursor.peek() == Some(&amp;&#39;]&#39;) {&#xA;            cursor.next();&#xA;            break;&#xA;        }&#xA;&#xA;        expect(cursor, &#39;,&#39;)?;&#xA;    }&#xA;&#xA;    Ok(JSONValue::Array(array))&#xA;}&#xA;&#xA;fn parseobject(cursor: &amp;mut PeekableChars, limit: usize) -  ResultJSONValue, JSONParseError {&#xA;    expect(cursor, &#39;{&#39;)?;&#xA;&#xA;    let mut object = HashMap::new();&#xA;&#xA;    if cursor.peek() == Some(&amp;&#39;}&#39;) {&#xA;        cursor.next();&#xA;        return Ok(JSONValue::Object(object));&#xA;    }&#xA;&#xA;    loop {&#xA;        let key = parsestringraw(cursor)?;&#xA;&#xA;        expect(cursor, &#39;:&#39;)?;&#xA;        object.insert(key, parsevalue(cursor, limit)?);&#xA;&#xA;        if cursor.peek() == Some(&amp;&#39;}&#39;) {&#xA;            cursor.next();&#xA;            break;&#xA;        }&#xA;&#xA;        expect(cursor, &#39;,&#39;)?;&#xA;    }&#xA;&#xA;    Ok(JSONValue::Object(object))&#xA;}&#xA;&#xA;fn parsevalue(cursor: &amp;mut PeekableChars, limit: usize) -  ResultJSONValue, JSONParseError {&#xA;    if limit == 0 {&#xA;        return Err(JSONParseError::new(&#34;Exceeded nesting limit&#34;));&#xA;    }&#xA;&#xA;    let value = match cursor.peek() {&#xA;        Some(c) =  match c {&#xA;            &#39;&#34;&#39; =  parsestring(cursor),&#xA;            &#39;0&#39;..=&#39;9&#39; | &#39;-&#39; =  parsenumber(cursor),&#xA;            &#39;[&#39; =  parsearray(cursor, limit - 1),&#xA;            &#39;{&#39; =  parseobject(cursor, limit - 1),&#xA;            &#39;t&#39; =  parsetrue(cursor),&#xA;            &#39;f&#39; =  parsefalse(cursor),&#xA;            &#39;n&#39; =  parsenull(cursor),&#xA;             =  return Err(JSONParseError::new(&#34;Unexpected character&#34;)),&#xA;        },&#xA;        None =  return Err(JSONParseError::new(&#34;Unexpected EOF&#34;)),&#xA;    };&#xA;&#xA;    value&#xA;}&#xA;&#xA;pub fn parse(input: String) -  ResultJSONValue, JSONParseError {&#xA;    let mut cursor = input.chars().peekable();&#xA;&#xA;    let value = parsevalue(&amp;mut cursor, 1000)?;&#xA;&#xA;    if let Some() = cursor.peek() {&#xA;        return Err(JSONParseError::new(&#34;Expected EOF&#34;));&#xA;    }&#xA;&#xA;    Ok(value)&#xA;}&#xA;&#xA;Here are the new assumptions that were introduced during the implementation:&#xA;&#34;Control characters&#34; means &#34;Unicode control characters&#34;, which includes C0 controls ( 0x00–0x1F), C1 controls (0x80-0x9F), and delete (0x7F)&#xA;There should be some maximum recursion depth after which an error is returned. This isn&#39;t required to conform with the business card, but that paper tape isn&#39;t actually infinite so I&#39;m counting this as a necessary implementation detail.&#xA;&#xA;The Minefield&#xA;&#xA;So now it&#39;s time to see how my &#34;naive&#34; parser holds up against more rigorous test cases. In order to do this I added all the tests from the JSON Parsing Test Suite I mentioned before to my project and wrote the following build script to translate those into Rust tests:&#xA;&#xA;use std::fs;&#xA;use std::io;&#xA;&#xA;fn makename(name: &amp;String) -  String {&#xA;    name.replace(&#34;/&#34;, &#34;slash&#34;)&#xA;        .replace(&#34;-&#34;, &#34;dash&#34;)&#xA;        .replace(&#34;+&#34;, &#34;plus&#34;)&#xA;        .replace(&#34;.&#34;, &#34;dot&#34;)&#xA;        .replace(&#34;#&#34;, &#34;hash&#34;)&#xA;        .replace(&#34;&#34;, &#34;&#34;)&#xA;        .replace(&#34;.json&#34;, &#34;&#34;)&#xA;        .replace(&#34;minefieldslash&#34;, &#34;&#34;)&#xA;        .tolowercase()&#xA;}&#xA;&#xA;fn generateaccepttest(path: &amp;String) -  String {&#xA;    format!(&#xA;        r#&#34;&#xA;[test]&#xA;fn {}() -  std::io::Result() {{&#xA;    if let Err(e) = parse(std::fs::readtostring(&#34;{}&#34;)?) {{&#xA;        assert!(false, &#34;Parsing failed: {{:?}}&#34;, e);&#xA;    }}&#xA;&#xA;    Ok(())&#xA;}}&#34;#,&#xA;        makename(path),&#xA;        path&#xA;    )&#xA;}&#xA;&#xA;fn generaterejecttest(path: &amp;String) -  String {&#xA;    format!(&#xA;        r#&#34;&#xA;[test]&#xA;fn {}() {{&#xA;    if let Ok(data) = std::fs::readtostring(&#34;{}&#34;) {{&#xA;        if let Ok() = parse(data) {{&#xA;            assert!(false, &#34;Parsing should fail&#34;)&#xA;        }}&#xA;    }}&#xA;}}&#34;#,&#xA;        makename(path),&#xA;        path&#xA;    )&#xA;}&#xA;&#xA;fn generateundefinedtest(path: &amp;String) -  String {&#xA;    format!(&#xA;        r#&#34;&#xA;[test]&#xA;fn {}() {{&#xA;    if let Ok(data) = std::fs::readtostring(&#34;{}&#34;) {{&#xA;        let  = parse(data);&#xA;    }}&#xA;}}&#34;#,&#xA;        makename(path),&#xA;        path&#xA;    )&#xA;}&#xA;&#xA;fn generatetest(path: &amp;String) -  OptionString {&#xA;    if path.contains(&#34;/y&#34;) {&#xA;        return Some(generateaccepttest(path));&#xA;    } else if path.contains(&#34;/n&#34;) {&#xA;        return Some(generaterejecttest(path));&#xA;    } else if path.contains(&#34;/i&#34;) {&#xA;        return Some(generateundefinedtest(path));&#xA;    }&#xA;&#xA;    None&#xA;}&#xA;&#xA;fn generatemodule(tests: VecString) -  String {&#xA;    format!(&#xA;        r#&#34;#[cfg(test)]&#xA;use crate::parse;&#xA;{}&#34;#,&#xA;        tests.join(&#34;\n&#34;)&#xA;    )&#xA;}&#xA;&#xA;fn main() -  io::Result() {&#xA;    let mut tests = vec![];&#xA;&#xA;    for entry in fs::readdir(&#34;minefield&#34;)? {&#xA;        let entry = entry?;&#xA;        let path = entry.path().display().tostring();&#xA;&#xA;        if path.endswith(&#34;.json&#34;) {&#xA;            if let Some(test) = generatetest(&amp;path) {&#xA;                tests.push(test);&#xA;            }&#xA;        }&#xA;    }&#xA;&#xA;    fs::write(&#34;src/tests/minefield.rs&#34;, generatemodule(tests))?;&#xA;&#xA;    Ok(())&#xA;}&#xA;&#xA;These test suites are broken into three categories:&#xA;y tests which should be accepted&#xA;n tests which should be rejected&#xA;i tests which are undefined&#xA;&#xA;I decided that these should be handled as follows:&#xA;y tests should return a JSONValue&#xA;n tests should return a JSONParseError&#xA;i tests should just return without crashing&#xA;&#xA;So how did I fare?&#xA;&#xA;The Results&#xA;&#xA;The results at this point kind of surprised me. Out of 325 tests, only 18 failed. The thing is though, there&#39;s a bug I know about already. I knew from the start that some tests would fail because the business card grammar doesn&#39;t mention anything about white space.&#xA;&#xA;After updating the parser to allow whitespace there were only two failing tests. And both of these failed for the same reason: despite being a control character delete (0x7F) should be allowed in strings.&#xA;&#xA;After updating the parser to accept delete in strings 100% of the tests passed. A parser I wrote in an afternoon based on a business card and naive assumptions passed for all the defined behavior in the JSON Parsing Test Suite.&#xA;&#xA;Conclusion&#xA;&#xA;At the end of the day, parsing JSON in the real world is still a minefield. While I may have been able to easily craft a parser that passes tests for all of the defined behavior, it&#39;s those undefined tests that are the real problem. The cases where there isn&#39;t a correct answer will still provide interoperability issues when passing data between different libraries, especially when it&#39;s between different languages. Moreover, even for the defined y_ tests, there is no assertion for what a value parses into, just that it&#39;s accepted. As an example issue: my implementation can&#39;t safely handle integers greater than 2^53 because it treats all numbers as floats, but an implementation that supports parsing to an actual integer type could.&#xA;&#xA;Despite the real world challenges though, I think JSON parsing might be a navigable minefield. In an afternoon I was able to throw together a parser that&#39;s as correct as any mainstream JSON parser in use today (perhaps more correct than some), and I don&#39;t think that&#39;s something that would be possible for many data interchange formats.]]&gt;</description>
      <content:encoded><![CDATA[<p>There&#39;s a really great article titled <a href="http://seriot.ch/projects/parsing_json.html" rel="nofollow">Parsing JSON is a Minefield</a> that provides some pretty comprehensive information about the ambiguities and contradictions in the JSON “spec”. As is so often the case with software, it turns out JSON has more complexities than you may expect (or at the very least more than Douglas Crockford would tell you about).</p>

<p>The JSON grammar was famously printed on a business card in order to demonstrate how simple it was. This got me to thinking about an interesting experiment: how correct would a JSON parser implemented purely based on that business card be? Luckily the author of the “Parsing JSON is a Minefield” article also created a comprehensive suite of tests that cover all the edge cases discussed in the article. So of course I decided to write and test a JSON parser.</p>

<h2 id="baseline-assumptions">Baseline Assumptions</h2>

<p>The JSON business card is expectedly pretty vague, so I had to make several assumptions about how things should work. I did my best to make decisions based on what a typical software developer would expect, but they are still assumptions based on my own biases. Just keep that in mind going forward.</p>

<p>Before I could do anything I needed to define how the parser would work. Unlike serde which deserializes specific JSON into structs, I want to parse JSON into a more generic enum that can represent any JSON file.</p>

<pre><code class="language-rust">pub enum JSONValue {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec&lt;JSONValue&gt;),
    Object(HashMap&lt;String, JSONValue&gt;),
}
</code></pre>

<p>So already there were a few assumptions being made. “Number” was always represented as a 64-bit floating point number (the same way JavaScript does it), String was a Rust string (which is UTF-8 and satisfies the “any-Unicode-character” requirement), and Objects are HashMaps (meaning duplicate keys can&#39;t exist).</p>

<p>Next I needed to define the behavior of the parser. I decided it should take a Rust <code>String</code> as inpujt, which means I&#39;m assuming all JSON is encoded as UTF-8 (or can be easily translated into UTF-8). Combined with a custom <code>JSONParseError</code> type this resulted in the following function signature for the parser:</p>

<pre><code class="language-rust">pub fn parse(input: String) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    ...
}
</code></pre>

<p>Note: For brevity I&#39;ve omitted several boilerplate trait implementations like <code>PartialEq</code>, <code>Display</code>, <code>Debug</code>, and <code>From</code>.</p>

<p>Now that data types and a function signature existed, I could take the business card and translate it into a series of test that I felt covered the described behavior. I put these tests in a separate file called <code>business_card.rs</code>.</p>

<pre><code class="language-rust">use crate::*;

#[test]
fn parse_null() {
    assert_eq!(parse(String::from(&#34;null&#34;)), Ok(JSONValue::Null));
}

#[test]
fn parse_true() {
    assert_eq!(parse(String::from(&#34;true&#34;)), Ok(JSONValue::Boolean(true)));
}

#[test]
fn parse_false() {
    assert_eq!(parse(String::from(&#34;false&#34;)), Ok(JSONValue::Boolean(false)));
}

#[test]
fn parse_number() {
    assert_eq!(parse(String::from(&#34;1&#34;)), Ok(JSONValue::Number(1.0)));

    assert_eq!(parse(String::from(&#34;123&#34;)), Ok(JSONValue::Number(123.0)));

    assert_eq!(parse(String::from(&#34;123.45&#34;)), Ok(JSONValue::Number(123.45)));

    assert_eq!(parse(String::from(&#34;-1&#34;)), Ok(JSONValue::Number(-1.0)));

    assert_eq!(parse(String::from(&#34;-123&#34;)), Ok(JSONValue::Number(-123.0)));

    assert_eq!(
        parse(String::from(&#34;-123.45&#34;)),
        Ok(JSONValue::Number(-123.45))
    );

    assert_eq!(parse(String::from(&#34;123e8&#34;)), Ok(JSONValue::Number(123e8)));

    assert_eq!(parse(String::from(&#34;123e+8&#34;)), Ok(JSONValue::Number(123e8)));

    assert_eq!(parse(String::from(&#34;123e-8&#34;)), Ok(JSONValue::Number(123e-8)));

    assert_eq!(parse(String::from(&#34;123E8&#34;)), Ok(JSONValue::Number(123e8)));

    assert_eq!(parse(String::from(&#34;123E+8&#34;)), Ok(JSONValue::Number(123e8)));

    assert_eq!(parse(String::from(&#34;123E-8&#34;)), Ok(JSONValue::Number(123e-8)));

    assert_eq!(
        parse(String::from(&#34;-123.45e-8&#34;)),
        Ok(JSONValue::Number(-123.45e-8))
    );
}

#[test]
fn parse_string() {
    assert_eq!(
        parse(String::from(&#34;\&#34;\&#34;&#34;)),
        Ok(JSONValue::String(String::new()))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;a\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;a&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;Hello World!\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;Hello World!&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\\&#34;\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\&#34;&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\\\\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\\&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\/\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;/&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\b\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\x08&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\f\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\x0C&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\n\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\n&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\r\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\r&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\t\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\t&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\u2603\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\u{2603}&#34;)))
    );

    assert_eq!(
        parse(String::from(&#34;\&#34;\\uD83D\\uDE00\&#34;&#34;)),
        Ok(JSONValue::String(String::from(&#34;\u{1F600}&#34;)))
    );
}

#[test]
fn parse_array() {
    assert_eq!(parse(String::from(&#34;[]&#34;)), Ok(JSONValue::Array(vec![])));

    assert_eq!(
        parse(String::from(&#34;[1,2,3]&#34;)),
        Ok(JSONValue::Array(vec![
            JSONValue::Number(1.0),
            JSONValue::Number(2.0),
            JSONValue::Number(3.0)
        ]))
    );
}

#[test]
fn parse_object() {
    assert_eq!(
        parse(String::from(&#34;{}&#34;)),
        Ok(JSONValue::Object(HashMap::new()))
    );

    let mut o1: HashMap&lt;String, JSONValue&gt; = HashMap::new();
    o1.insert(
        String::from(&#34;name&#34;),
        JSONValue::String(String::from(&#34;John Doe&#34;)),
    );

    assert_eq!(
        parse(String::from(&#34;{\&#34;name\&#34;:\&#34;John Doe\&#34;}&#34;)),
        Ok(JSONValue::Object(o1))
    );

    let mut o2: HashMap&lt;String, JSONValue&gt; = HashMap::new();
    o2.insert(
        String::from(&#34;name&#34;),
        JSONValue::String(String::from(&#34;John Doe&#34;)),
    );
    o2.insert(String::from(&#34;age&#34;), JSONValue::Number(42.0));

    assert_eq!(
        parse(String::from(&#34;{\&#34;name\&#34;:\&#34;John Doe\&#34;,\&#34;age\&#34;:42}&#34;)),
        Ok(JSONValue::Object(o2))
    );
}
</code></pre>

<p>Most of these tests are pretty straightforward. The biggest assumptions were what each of the escape characters meant. Among those the major assumption was that <code>\u four-hex-digits</code> referred to UTF-16 encoded values and thus need to account for surrogate pairs.</p>

<h2 id="building-the-parser">Building the Parser</h2>

<p>With all  of that out of the way I was free to start hacking away at a hand crafted recursive descent parser. This parser has two primary goals:</p>

<p>1) Be correct (this includes not crashing)
2) Be easy to understand</p>

<p>Performance, extensibility, security, and real world usability are non-goals.</p>

<p>Here&#39;s the initial parser I came up with that conforms to the business card behavior:</p>

<pre><code class="language-rust">fn expect(cursor: &amp;mut Peekable&lt;Chars&gt;, c: char) -&gt; Result&lt;(), JSONParseError&gt; {
    if cursor.next() != Some(c) {
        return Err(JSONParseError::new(&amp;format!(&#34;Expected character: {}&#34;, c)));
    }

    Ok(())
}

fn parse_token(
    cursor: &amp;mut Peekable&lt;Chars&gt;,
    token: Chars,
    value: JSONValue,
) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    for c in token {
        expect(cursor, c)?;
    }

    Ok(value)
}

fn parse_null(cursor: &amp;mut Peekable&lt;Chars&gt;) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    parse_token(cursor, &#34;null&#34;.chars(), JSONValue::Null)
}

fn parse_true(cursor: &amp;mut Peekable&lt;Chars&gt;) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    parse_token(cursor, &#34;true&#34;.chars(), JSONValue::Boolean(true))
}

fn parse_false(cursor: &amp;mut Peekable&lt;Chars&gt;) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    parse_token(cursor, &#34;false&#34;.chars(), JSONValue::Boolean(false))
}

fn read_integer(cursor: &amp;mut Peekable&lt;Chars&gt;, number: &amp;mut String) -&gt; Result&lt;(), JSONParseError&gt; {
    let first = cursor.peek();

    if first &lt; Some(&amp;&#39;0&#39;) || first &gt; Some(&amp;&#39;9&#39;) {
        return Err(JSONParseError::new(&#34;Unexpected character&#34;));
    }

    while let Some(c) = cursor.peek() {
        if c &gt;= &amp;&#39;0&#39; &amp;&amp; c &lt;= &amp;&#39;9&#39; {
            number.push(*c);
            cursor.next();
        } else {
            break;
        }
    }

    Ok(())
}

fn parse_number_raw(cursor: &amp;mut Peekable&lt;Chars&gt;) -&gt; Result&lt;f64, JSONParseError&gt; {
    let mut number = String::new();

    if cursor.peek() == Some(&amp;&#39;-&#39;) {
        number.push(&#39;-&#39;);
        cursor.next();
    }

    if cursor.peek() == Some(&amp;&#39;0&#39;) {
        number.push(&#39;0&#39;);
        cursor.next();
    } else {
        read_integer(cursor, &amp;mut number)?;
    }

    if cursor.peek() == Some(&amp;&#39;.&#39;) {
        number.push(&#39;.&#39;);
        cursor.next();
        read_integer(cursor, &amp;mut number)?;
    }

    if cursor.peek() == Some(&amp;&#39;e&#39;) || cursor.peek() == Some(&amp;&#39;E&#39;) {
        number.push(&#39;e&#39;);
        cursor.next();

        if cursor.peek() == Some(&amp;&#39;-&#39;) {
            number.push(&#39;-&#39;);
            cursor.next();
        }

        if cursor.peek() == Some(&amp;&#39;+&#39;) {
            cursor.next();
        }

        read_integer(cursor, &amp;mut number)?;
    }

    Ok(number.parse::&lt;f64&gt;()?)
}

fn parse_number(cursor: &amp;mut Peekable&lt;Chars&gt;) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    Ok(JSONValue::Number(parse_number_raw(cursor)?))
}

fn parse_unicode_raw(
    cursor: &amp;mut Peekable&lt;Chars&gt;,
    high: Option&lt;u32&gt;,
) -&gt; Result&lt;char, JSONParseError&gt; {
    expect(cursor, &#39;u&#39;)?;

    let mut u = String::new();

    for _ in 0..4 {
        if let Some(c) = cursor.next() {
            if c.is_ascii_hexdigit() {
                u.push(c);
            } else {
                return Err(JSONParseError::new(&#34;Unexpected character&#34;));
            }
        } else {
            return Err(JSONParseError::new(&#34;Unexpected EOF&#34;));
        }
    }

    let numeric = u32::from_str_radix(&amp;u, 16)?;

    if let Some(h) = high {
        if numeric &gt;= 0xDC00 &amp;&amp; numeric &lt;= 0xDFFF {
            let mut code = 0x10000;
            code += (h &amp; 0x03FF) &lt;&lt; 10;
            code += numeric &amp; 0x03FF;
            char::from_u32(code).ok_or(JSONParseError::new(&#34;Invalid unicode character&#34;))
        } else {
            Err(JSONParseError::new(&#34;Invalid surrogate pair&#34;))
        }
    } else {
        if numeric &gt;= 0xD800 &amp;&amp; numeric &lt;= 0xDBFF {
            expect(cursor, &#39;\\&#39;)?;
            parse_unicode_raw(cursor, Some(numeric))
        } else {
            char::from_u32(numeric).ok_or(JSONParseError::new(&#34;Invalid unicode character&#34;))
        }
    }
}

fn parse_string_raw(cursor: &amp;mut Peekable&lt;Chars&gt;) -&gt; Result&lt;String, JSONParseError&gt; {
    expect(cursor, &#39;&#34;&#39;)?;

    let mut s = String::new();
    let mut done = false;

    while let Some(c) = cursor.next() {
        if c == &#39;&#34;&#39; {
            done = true;
            break;
        } else if c == &#39;\\&#39; {
            if let Some(&#39;u&#39;) = cursor.peek() {
                s.push(parse_unicode_raw(cursor, None)?)
            } else {
                match cursor.next() {
                    Some(&#39;&#34;&#39;) =&gt; s.push(&#39;&#34;&#39;),
                    Some(&#39;\\&#39;) =&gt; s.push(&#39;\\&#39;),
                    Some(&#39;/&#39;) =&gt; s.push(&#39;/&#39;),
                    Some(&#39;b&#39;) =&gt; s.push(&#39;\x08&#39;),
                    Some(&#39;f&#39;) =&gt; s.push(&#39;\x0C&#39;),
                    Some(&#39;n&#39;) =&gt; s.push(&#39;\n&#39;),
                    Some(&#39;r&#39;) =&gt; s.push(&#39;\r&#39;),
                    Some(&#39;t&#39;) =&gt; s.push(&#39;\t&#39;),
                    None =&gt; return Err(JSONParseError::new(&#34;Unexpected EOF&#34;)),
                    _ =&gt; return Err(JSONParseError::new(&#34;Unexpected character&#34;)),
                }
            }
        } else {
            match c {
                &#39;\u{0000}&#39;..=&#39;\x1F&#39; | &#39;\u{007F}&#39; | &#39;\u{0080}&#39;..=&#39;\u{009F}&#39; =&gt; {
                    return Err(JSONParseError::new(&#34;Unexpected control character&#34;))
                }
                _ =&gt; s.push(c),
            }
        }
    }

    if done {
        return Ok(s);
    }

    return Err(JSONParseError::new(&#34;Unexpected EOF&#34;));
}

fn parse_string(cursor: &amp;mut Peekable&lt;Chars&gt;) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    Ok(JSONValue::String(parse_string_raw(cursor)?))
}

fn parse_array(cursor: &amp;mut Peekable&lt;Chars&gt;, limit: usize) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    expect(cursor, &#39;[&#39;)?;

    let mut array: Vec&lt;JSONValue&gt; = vec![];

    if cursor.peek() == Some(&amp;&#39;]&#39;) {
        cursor.next();
        return Ok(JSONValue::Array(array));
    }

    loop {
        array.push(parse_value(cursor, limit)?);

        if cursor.peek() == Some(&amp;&#39;]&#39;) {
            cursor.next();
            break;
        }

        expect(cursor, &#39;,&#39;)?;
    }

    Ok(JSONValue::Array(array))
}

fn parse_object(cursor: &amp;mut Peekable&lt;Chars&gt;, limit: usize) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    expect(cursor, &#39;{&#39;)?;

    let mut object = HashMap::new();

    if cursor.peek() == Some(&amp;&#39;}&#39;) {
        cursor.next();
        return Ok(JSONValue::Object(object));
    }

    loop {
        let key = parse_string_raw(cursor)?;

        expect(cursor, &#39;:&#39;)?;
        object.insert(key, parse_value(cursor, limit)?);

        if cursor.peek() == Some(&amp;&#39;}&#39;) {
            cursor.next();
            break;
        }

        expect(cursor, &#39;,&#39;)?;
    }

    Ok(JSONValue::Object(object))
}

fn parse_value(cursor: &amp;mut Peekable&lt;Chars&gt;, limit: usize) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    if limit == 0 {
        return Err(JSONParseError::new(&#34;Exceeded nesting limit&#34;));
    }

    let value = match cursor.peek() {
        Some(c) =&gt; match c {
            &#39;&#34;&#39; =&gt; parse_string(cursor),
            &#39;0&#39;..=&#39;9&#39; | &#39;-&#39; =&gt; parse_number(cursor),
            &#39;[&#39; =&gt; parse_array(cursor, limit - 1),
            &#39;{&#39; =&gt; parse_object(cursor, limit - 1),
            &#39;t&#39; =&gt; parse_true(cursor),
            &#39;f&#39; =&gt; parse_false(cursor),
            &#39;n&#39; =&gt; parse_null(cursor),
            _ =&gt; return Err(JSONParseError::new(&#34;Unexpected character&#34;)),
        },
        None =&gt; return Err(JSONParseError::new(&#34;Unexpected EOF&#34;)),
    };

    value
}

pub fn parse(input: String) -&gt; Result&lt;JSONValue, JSONParseError&gt; {
    let mut cursor = input.chars().peekable();

    let value = parse_value(&amp;mut cursor, 1000)?;

    if let Some(_) = cursor.peek() {
        return Err(JSONParseError::new(&#34;Expected EOF&#34;));
    }

    Ok(value)
}
</code></pre>

<p>Here are the new assumptions that were introduced during the implementation:
– “Control characters” means “Unicode control characters”, which includes C0 controls ( 0x00–0x1F), C1 controls (0x80-0x9F), and delete (0x7F)
– There should be some maximum recursion depth after which an error is returned. This isn&#39;t required to conform with the business card, but that paper tape isn&#39;t actually infinite so I&#39;m counting this as a necessary implementation detail.</p>

<h2 id="the-minefield">The Minefield</h2>

<p>So now it&#39;s time to see how my “naive” parser holds up against more rigorous test cases. In order to do this I added all the tests from the <a href="https://github.com/nst/JSONTestSuite" rel="nofollow">JSON Parsing Test Suite</a> I mentioned before to my project and wrote the following build script to translate those into Rust tests:</p>

<pre><code class="language-rust">use std::fs;
use std::io;

fn make_name(name: &amp;String) -&gt; String {
    name.replace(&#34;/&#34;, &#34;_slash_&#34;)
        .replace(&#34;-&#34;, &#34;_dash_&#34;)
        .replace(&#34;+&#34;, &#34;_plus_&#34;)
        .replace(&#34;.&#34;, &#34;_dot_&#34;)
        .replace(&#34;#&#34;, &#34;_hash_&#34;)
        .replace(&#34;__&#34;, &#34;_&#34;)
        .replace(&#34;.json&#34;, &#34;&#34;)
        .replace(&#34;minefield_slash_&#34;, &#34;&#34;)
        .to_lowercase()
}

fn generate_accept_test(path: &amp;String) -&gt; String {
    format!(
        r#&#34;
#[test]
fn {}() -&gt; std::io::Result&lt;()&gt; {{
    if let Err(e) = parse(std::fs::read_to_string(&#34;{}&#34;)?) {{
        assert!(false, &#34;Parsing failed: {{:?}}&#34;, e);
    }}

    Ok(())
}}&#34;#,
        make_name(path),
        path
    )
}

fn generate_reject_test(path: &amp;String) -&gt; String {
    format!(
        r#&#34;
#[test]
fn {}() {{
    if let Ok(data) = std::fs::read_to_string(&#34;{}&#34;) {{
        if let Ok(_) = parse(data) {{
            assert!(false, &#34;Parsing should fail&#34;)
        }}
    }}
}}&#34;#,
        make_name(path),
        path
    )
}

fn generate_undefined_test(path: &amp;String) -&gt; String {
    format!(
        r#&#34;
#[test]
fn {}() {{
    if let Ok(data) = std::fs::read_to_string(&#34;{}&#34;) {{
        let _ = parse(data);
    }}
}}&#34;#,
        make_name(path),
        path
    )
}

fn generate_test(path: &amp;String) -&gt; Option&lt;String&gt; {
    if path.contains(&#34;/y_&#34;) {
        return Some(generate_accept_test(path));
    } else if path.contains(&#34;/n_&#34;) {
        return Some(generate_reject_test(path));
    } else if path.contains(&#34;/i_&#34;) {
        return Some(generate_undefined_test(path));
    }

    None
}

fn generate_module(tests: Vec&lt;String&gt;) -&gt; String {
    format!(
        r#&#34;#[cfg(test)]
use crate::parse;
{}&#34;#,
        tests.join(&#34;\n&#34;)
    )
}

fn main() -&gt; io::Result&lt;()&gt; {
    let mut tests = vec![];

    for entry in fs::read_dir(&#34;minefield&#34;)? {
        let entry = entry?;
        let path = entry.path().display().to_string();

        if path.ends_with(&#34;.json&#34;) {
            if let Some(test) = generate_test(&amp;path) {
                tests.push(test);
            }
        }
    }

    fs::write(&#34;src/tests/minefield.rs&#34;, generate_module(tests))?;

    Ok(())
}
</code></pre>

<p>These test suites are broken into three categories:
– <code>y_</code> tests which should be accepted
– <code>n_</code> tests which should be rejected
– <code>i_</code> tests which are undefined</p>

<p>I decided that these should be handled as follows:
– <code>y_</code> tests should return a <code>JSONValue</code>
– <code>n_</code> tests should return a <code>JSONParseError</code>
– <code>i_</code> tests should just return without crashing</p>

<p>So how did I fare?</p>

<h2 id="the-results">The Results</h2>

<p>The results at this point kind of surprised me. Out of 325 tests, only 18 failed. The thing is though, there&#39;s a bug I know about already. I knew from the start that some tests would fail because the business card grammar doesn&#39;t mention anything about white space.</p>

<p>After updating the parser to allow whitespace there were only two failing tests. And both of these failed for the same reason: despite being a control character <code>delete</code> (0x7F) should be allowed in strings.</p>

<p>After updating the parser to accept <code>delete</code> in strings 100% of the tests passed. A parser I wrote in an afternoon based on a business card and naive assumptions passed for all the defined behavior in the JSON Parsing Test Suite.</p>

<h2 id="conclusion">Conclusion</h2>

<p>At the end of the day, parsing JSON in the real world is still a minefield. While I may have been able to easily craft a parser that passes tests for all of the defined behavior, it&#39;s those undefined tests that are the real problem. The cases where there isn&#39;t a correct answer will still provide interoperability issues when passing data between different libraries, especially when it&#39;s between different languages. Moreover, even for the defined <code>y_</code> tests, there is no assertion for <em>what</em> a value parses into, just that it&#39;s accepted. As an example issue: my implementation can&#39;t safely handle integers greater than 2^53 because it treats all numbers as floats, but an implementation that supports parsing to an actual integer type could.</p>

<p>Despite the real world challenges though, I think JSON parsing might be a navigable minefield. In an afternoon I was able to throw together a parser that&#39;s as correct as any mainstream JSON parser in use today (perhaps more correct than some), and I don&#39;t think that&#39;s something that would be possible for many data interchange formats.</p>
]]></content:encoded>
      <guid>https://toast.zeroflag.net/is-parsing-json-a-minefield</guid>
      <pubDate>Wed, 29 Nov 2023 06:35:44 +0000</pubDate>
    </item>
    <item>
      <title>Expectations of Support in Open Source</title>
      <link>https://toast.zeroflag.net/expectations-of-support-in-open-source?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[I recently saw yet another exchange about developers being expected to provide support for open source software. The debate on this topic isn&#39;t anything new. It&#39;s gone on long before I got into open source and will probably be going on long after I&#39;m dead. I&#39;ve heard it all before, but this time one of the takes made me stop and ask a question.&#xA;&#xA;The basic idea was that it&#39;s irresponsible to your users to release software if you can&#39;t support it. It sounds pretty straightforward at first, but there&#39;s some very important assumptions you have to make in order to resolve that sentence. Specifically, what do you mean by &#34;release&#34; and &#34;support&#34;. I don&#39;t think any two people reading that sentence will insert the same definition for either of those terms and that&#39;s a recipe for misunderstanding. In reality, both release and support are sliding scales.&#xA;&#xA;First let&#39;s consider what we mean by support. Is support responding to an email from a user with a question? Is support acting as a maintainer for the project? Is support providing ongoing bug fixes with an SLA? Is support adding new features that get requested? Is support having an on call rotation? These are all valid definitions of support, but it&#39;s pretty clear that some of these would be an absurd standard to hold every open source developer to. But where do we draw the line? I think that varies from project to project and largely depends on what release means for that project.&#xA;&#xA;Just for example, let&#39;s take my text editor. I&#39;ve released this code by putting it on GitHub, but the intention isn&#39;t for people to actually use it as their text editor. The code is there for other developers to use as a reference for how to build terminal applications from scratch. The only documentation is a single README, and that begins with a note about how it exists for educational purposes. Now consider Helix. Helix has a logo, marketing page, nicely packaged binaries for multiple operating systems, and extensive end user documentation. Helix has a very different definition of release, and I think as a result the expectation of support is much higher than for my text editor.&#xA;&#xA;Beyond that there are also external factors that can influence what support should be offered. What if the user would rather the software exist without support than not exist at all? What if your software results in a critical CVE? The context of a specific situation can&#39;t be ignored.&#xA;&#xA;Ultimately, the point I&#39;m trying to make is that the level of support a developer owes their users is a sliding scale that&#39;s influenced by a variety of factors. Reasonable people can disagree about where exactly we draw the specific lines, but treating it as a yes or no binary is always going to be wrong.]]&gt;</description>
      <content:encoded><![CDATA[<p>I recently saw yet another exchange about developers being expected to provide support for open source software. The debate on this topic isn&#39;t anything new. It&#39;s gone on long before I got into open source and will probably be going on long after I&#39;m dead. I&#39;ve heard it all before, but this time one of the takes made me stop and ask a question.</p>

<p>The basic idea was that it&#39;s irresponsible to your users to release software if you can&#39;t support it. It sounds pretty straightforward at first, but there&#39;s some very important assumptions you have to make in order to resolve that sentence. Specifically, what do you mean by “release” and “support”. I don&#39;t think any two people reading that sentence will insert the same definition for either of those terms and that&#39;s a recipe for misunderstanding. In reality, both release and support are sliding scales.</p>

<p>First let&#39;s consider what we mean by support. Is support responding to an email from a user with a question? Is support acting as a maintainer for the project? Is support providing ongoing bug fixes with an SLA? Is support adding new features that get requested? Is support having an on call rotation? These are all valid definitions of support, but it&#39;s pretty clear that some of these would be an absurd standard to hold every open source developer to. But where do we draw the line? I think that varies from project to project and largely depends on what release means for that project.</p>

<p>Just for example, let&#39;s take <a href="https://github.com/campaul/takkun" rel="nofollow">my text editor</a>. I&#39;ve released this code by putting it on GitHub, but the intention isn&#39;t for people to actually use it as their text editor. The code is there for other developers to use as a reference for how to build terminal applications from scratch. The only documentation is a single README, and that begins with a note about how it exists for educational purposes. Now consider <a href="https://helix-editor.com/" rel="nofollow">Helix</a>. Helix has a logo, marketing page, nicely packaged binaries for multiple operating systems, and extensive end user documentation. Helix has a very different definition of release, and I think as a result the expectation of support is much higher than for my text editor.</p>

<p>Beyond that there are also external factors that can influence what support should be offered. What if the user would rather the software exist without support than not exist at all? What if your software results in a critical CVE? The context of a specific situation can&#39;t be ignored.</p>

<p>Ultimately, the point I&#39;m trying to make is that the level of support a developer owes their users is a sliding scale that&#39;s influenced by a variety of factors. Reasonable people can disagree about where exactly we draw the specific lines, but treating it as a yes or no binary is always going to be wrong.</p>
]]></content:encoded>
      <guid>https://toast.zeroflag.net/expectations-of-support-in-open-source</guid>
      <pubDate>Wed, 14 Jun 2023 20:22:02 +0000</pubDate>
    </item>
    <item>
      <title>Why I Chose the libc Crate for My Text Editor</title>
      <link>https://toast.zeroflag.net/why-i-chose-the-libc-crate-for-my-text-editor?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Lately I&#39;ve been working on a text editor as a learning exercise in how to build terminal apps from scratch. In doing so I had to draw a line somewhere as to what &#34;from scratch&#34; actually meant. After all, I don&#39;t intend to invent the universe.&#xA;&#xA;My initial plan was to restrict myself to Rust&#39;s standard library, but it quickly became clear that wasn&#39;t going to be sufficient. Specifically, this project requires the ability to change the terminal mode and handle signals, both of which require platform specific code to do manually. I think it&#39;s fair to say writing separate blocks of inline assembly for every architecture/operating system pair I want to support is totally out of scope for this project.&#xA;&#xA;Ultimately I decided that the libc crate was a reasonable foundation to build off. libc is the standard library for the C programming language, but it&#39;s also part of the POSIX specification. This means it&#39;s guaranteed to be available on pretty much any OS except for Windows, and is also going to be the lowest level of abstraction possible that doesn&#39;t require writing platform specific code.&#xA;&#xA;Another good choice would be the nix crate, which provides the same functionality as the libc crate, but without requiring the user to write any unsafe code. Ultimately I decided that dealing with the unsafe code myself was a reasonable part of the &#34;from scratch&#34; experience. Moreover, the libc crate is part of the rust-lang project, so it doesn&#39;t introduce a dependency on a third party.&#xA;&#xA;Unfortunately the POSIX specification has some wiggle room, so portions of libc still differ between operating systems. Up until today I had only tried compiling the editor on Linux, and I was curious if it would build under FreeBSD. Unfortunately I was greeted with errors.&#xA;&#xA;   Compiling takkun v0.1.0 (/usr/home/cameron/workspace/takkun)&#xA;error[E0308]: mismatched types&#xA;  --  src/terminal.rs:27:11&#xA;   |&#xA;27 |     ccc: [0; 32],&#xA;   |           ^^^^^^^ expected an array with a fixed size of 20 elements, found one with 32 elements&#xA;&#xA;error[E0560]: struct termios has no field named cline&#xA;  --  src/terminal.rs:30:5&#xA;   |&#xA;30 |     cline: 0,&#xA;   |     ^^^^^^ termios does not have this field&#xA;   |&#xA;   = note: available fields are: ciflag, coflag, ccflag, clflag, ccc ... and 2 others&#xA;&#xA;Some errors have detailed explanations: E0308, E0560.&#xA;For more information about an error, try rustc --explain E0308.&#xA;error: could not compile takkun due to 2 previous errors&#xA;&#xA;Right away it&#39;s clear that the termios struct isn&#39;t the same of FreeBSD as it is under Linux. Specifically, the ccc field seems to be a different size and the cline field only exists in Linux. So I&#39;m going to need a way to conditionally initialize the termios struct depending on what OS I&#39;m on.&#xA;&#xA;Luckily Rust has a way to do this that even works when initializing static items. The cfg attribute can be used to conditionally include code, allowing me to do the following:&#xA;&#xA;[cfg(any(targetos = &#34;linux&#34;))]&#xA;static mut TERMIOS: libc::termios = libc::termios {&#xA;    ciflag: 0,&#xA;    coflag: 0,&#xA;    ccflag: 0,&#xA;    clflag: 0,&#xA;    ccc: [0; 32],&#xA;    cispeed: 0,&#xA;    cospeed: 0,&#xA;    cline: 0,&#xA;};&#xA;&#xA;[cfg(any(targetos = &#34;freebsd&#34;))]&#xA;static mut TERMIOS: libc::termios = libc::termios {&#xA;    ciflag: 0,&#xA;    coflag: 0,&#xA;    ccflag: 0,&#xA;    clflag: 0,&#xA;    ccc: [0; 20],&#xA;    cispeed: 0,&#xA;    c_ospeed: 0,&#xA;};&#xA;&#xA;And that was all it took. If I had also needed to write some OS specific logic, Rust provides the cfg! macro that can be used in boolean expressions. It&#39;s not a perfect out-of-the-box solution, but it&#39;s trivial compared to triggering system calls in pure Rust.]]&gt;</description>
      <content:encoded><![CDATA[<p>Lately I&#39;ve been working on a text editor as a learning exercise in how to build terminal apps from scratch. In doing so I had to draw a line somewhere as to what “from scratch” actually meant. After all, I don&#39;t intend to invent the universe.</p>

<p>My initial plan was to restrict myself to Rust&#39;s standard library, but it quickly became clear that wasn&#39;t going to be sufficient. Specifically, this project requires the ability to change the terminal mode and handle signals, both of which require platform specific code to do manually. I think it&#39;s fair to say writing separate blocks of inline assembly for every architecture/operating system pair I want to support is totally out of scope for this project.</p>

<p>Ultimately I decided that the <a href="https://crates.io/crates/libc" rel="nofollow">libc crate</a> was a reasonable foundation to build off. libc is the standard library for the C programming language, but it&#39;s also part of the POSIX specification. This means it&#39;s guaranteed to be available on pretty much any OS except for Windows, and is also going to be the lowest level of abstraction possible that doesn&#39;t require writing platform specific code.</p>

<p>Another good choice would be the <a href="https://crates.io/crates/nix" rel="nofollow">nix crate</a>, which provides the same functionality as the libc crate, but without requiring the user to write any unsafe code. Ultimately I decided that dealing with the unsafe code myself was a reasonable part of the “from scratch” experience. Moreover, the libc crate is part of the rust-lang project, so it doesn&#39;t introduce a dependency on a third party.</p>

<p>Unfortunately the POSIX specification has some wiggle room, so portions of libc still differ between operating systems. Up until today I had only tried compiling the editor on Linux, and I was curious if it would build under FreeBSD. Unfortunately I was greeted with errors.</p>

<pre><code>   Compiling takkun v0.1.0 (/usr/home/cameron/workspace/takkun)
error[E0308]: mismatched types
  --&gt; src/terminal.rs:27:11
   |
27 |     c_cc: [0; 32],
   |           ^^^^^^^ expected an array with a fixed size of 20 elements, found one with 32 elements

error[E0560]: struct `termios` has no field named `c_line`
  --&gt; src/terminal.rs:30:5
   |
30 |     c_line: 0,
   |     ^^^^^^ `termios` does not have this field
   |
   = note: available fields are: `c_iflag`, `c_oflag`, `c_cflag`, `c_lflag`, `c_cc` ... and 2 others

Some errors have detailed explanations: E0308, E0560.
For more information about an error, try `rustc --explain E0308`.
error: could not compile `takkun` due to 2 previous errors
</code></pre>

<p>Right away it&#39;s clear that the <code>termios</code> struct isn&#39;t the same of FreeBSD as it is under Linux. Specifically, the <code>c_cc</code> field seems to be a different size and the <code>c_line</code> field only exists in Linux. So I&#39;m going to need a way to conditionally initialize the <code>termios</code> struct depending on what OS I&#39;m on.</p>

<p>Luckily Rust has a way to do this that even works when initializing static items. The <code>cfg</code> attribute can be used to conditionally include code, allowing me to do the following:</p>

<pre><code class="language-rust">#[cfg(any(target_os = &#34;linux&#34;))]
static mut TERMIOS: libc::termios = libc::termios {
    c_iflag: 0,
    c_oflag: 0,
    c_cflag: 0,
    c_lflag: 0,
    c_cc: [0; 32],
    c_ispeed: 0,
    c_ospeed: 0,
    c_line: 0,
};

#[cfg(any(target_os = &#34;freebsd&#34;))]
static mut TERMIOS: libc::termios = libc::termios {
    c_iflag: 0,
    c_oflag: 0,
    c_cflag: 0,
    c_lflag: 0,
    c_cc: [0; 20],
    c_ispeed: 0,
    c_ospeed: 0,
};
</code></pre>

<p>And that was all it took. If I had also needed to write some OS specific logic, Rust provides the <code>cfg!</code> macro that can be used in boolean expressions. It&#39;s not a perfect out-of-the-box solution, but it&#39;s trivial compared to triggering system calls in pure Rust.</p>
]]></content:encoded>
      <guid>https://toast.zeroflag.net/why-i-chose-the-libc-crate-for-my-text-editor</guid>
      <pubDate>Mon, 17 Apr 2023 19:46:57 +0000</pubDate>
    </item>
    <item>
      <title>Beyond Kilo: Some Improvements for My Text Editor</title>
      <link>https://toast.zeroflag.net/beyond-kilo-some-improvements-for-my-text-editor?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[I recently followed through the Build Your Own Text Editor tutorial that walks through all of antirez&#39;s kilo editor as a way to learn more about how terminals really work under the hood. In the process I ended up having so much fun that I decided to re-implement everything in Rust and expand what I had into a more capable editor.&#xA;&#xA;There are obvious features that I think anyone would want to add like an undo stack and support for multiple files, but I wanted to start with improving  some of the ways the editor interacted with the terminal. This post documents some improvements I&#39;ve made so far, but I&#39;m still learning about this stuff. Expect some stream of consciousness, unanswered questions, and potentially incorrect (or more likely incomplete) information.&#xA;&#xA;Alternate Screen Buffer&#xA;One of the first things that I found was that kilo leaves traces of itself behind in the terminal&#39;s scrollback. More complete editors like Vim, Emacs, and Nano all return the terminal to the state it was in before they were launched.&#xA;&#xA;The answer is a feature called alternate screen buffer. This StackOverflow answer describes how to switch into and out of the alternate screen buffer in a backwards compatible way by writing the escape sequences &#34;\033[?1049h\033[2J\033[H&#34; and &#34;\033[2J\033[H\033[?1049l&#34; respectively.&#xA;&#xA;That same answer also provides a function for writing to stdin that still confuses me. It&#39;s unclear to me from the answer why I would want to write to stdin instead of stdout. Writing these strings to stdout seems to work fine, but I need to investigate that further.&#xA;&#xA;Resizing the Terminal&#xA;The next thing I wanted to fix was handling the terminal being resized. There is a naive solution which is to resize the terminal between every key press. Kilo&#39;s input loop runs every tenth of a second, so this works relatively well, but it seems inefficient and could be more responsive.&#xA;&#xA;Unfortunately the only way to detect a terminal resize is handling the SIGWINCH signal. Signal handlers can be dangerous and are generally discouraged if not strictly necessary. They&#39;re essentially another thread that isn&#39;t allowed to do any locking, so ultimately the only way safely communicate back to the main application thread is writing to a named pipe. The main thread can then read data from that pipe and trigger a resize.&#xA;&#xA;If this were the only signal I needed to handle, I would probably forgo signal handling altogether and opt for the naive solution. Unfortunately there is one place I actually need one.&#xA;&#xA;ctrl+z Support&#xA;In a typical terminal application, hitting ctrl+z will cause the process to be sent to the background. This utilizes a feature the shell provides called job control and is typically handled automatically. Unfortunately there are two problems that prevent this from working as expected.&#xA;&#xA;The first problem is raw mode&#39;s handling of key presses. In raw mode, ctrl+z is sent along to the application instead of being handled by the terminal. Normally the terminal sends the SIGTSTP signal to the application when ctrl+z is pressed. I couldn&#39;t find any information on how to make a process background itself, so I guessed and tried making it send SIGTSTP to itself using libc&#39;s kill function.&#xA;&#xA;unsafe {&#xA;    libc::kill(std::process::id() as i32, libc::SIGTSTP);&#xA;}&#xA;&#xA;Thankfully that worked. The process was sent to the background with raw mode and the alternate screen buffer being exited as well.&#xA;&#xA;The second problem is returning to the foreground. Running fg does resume t