diff --git a/src/error.rs b/src/error.rs index 870d806..7f4b2d0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,6 +23,18 @@ pub enum Error { #[error("I/O: {0}")] Io(#[from] std::io::Error), + // JSON Bad Character + #[error("JSON bad character: {0} at position {1}, {2} was expected")] + JsonBadCharacter(char, usize, char), + + // JSON Bad (general) + #[error("JSON bad: {0} at position {1}")] + JsonBad(&'static str, usize), + + // JSON Bad Event + #[error("JSON bad event: {0} at position {1}")] + JsonBadEvent(&'static str, usize), + // JSON Bad String Character #[error("JSON string bad character: codepoint {0}")] JsonBadStringChar(u32), diff --git a/src/types/parse/json_parse.rs b/src/types/parse/json_parse.rs new file mode 100644 index 0000000..3c91520 --- /dev/null +++ b/src/types/parse/json_parse.rs @@ -0,0 +1,445 @@ +use super::json_escape::json_unescape; +use crate::Error; + +#[inline] +pub fn eat_whitespace(input: &[u8], inposp: &mut usize) { + while *inposp < input.len() && [0x20, 0x09, 0x0A, 0x0D].contains(&input[*inposp]) { + *inposp += 1; + } +} + +#[inline] +pub fn eat_whitespace_and_commas(input: &[u8], inposp: &mut usize) { + while *inposp < input.len() && [0x20, 0x09, 0x0A, 0x0D, b','].contains(&input[*inposp]) { + *inposp += 1; + } +} + +#[inline] +pub fn verify_char(input: &[u8], ch: u8, inposp: &mut usize) -> Result<(), Error> { + if *inposp >= input.len() { + Err(Error::JsonBad("Too Short or Missing Fields", *inposp)) + } else if input[*inposp] == ch { + *inposp += 1; + Ok(()) + } else { + Err(Error::JsonBadCharacter( + input[*inposp] as char, + *inposp, + ch as char, + )) + } +} + +pub fn eat_colon_with_whitespace(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + eat_whitespace(input, inposp); + verify_char(input, b':', inposp)?; + eat_whitespace(input, inposp); + Ok(()) +} + +pub fn next_object_field(input: &[u8], inposp: &mut usize) -> Result { + eat_whitespace(input, inposp); + // next comes either comma or end brace + if *inposp >= input.len() { + return Err(Error::JsonBad("Too short", *inposp)); + } + if input[*inposp] == b'}' { + *inposp += 1; + Ok(true) + } else if input[*inposp] == b',' { + *inposp += 1; + Ok(false) + } else { + Err(Error::JsonBad("Unexpected char", *inposp)) + } +} + +pub fn read_id(input: &[u8], inposp: &mut usize, output: &mut [u8]) -> Result<(), Error> { + verify_char(input, b'"', inposp)?; + if *inposp + 64 >= input.len() { + return Err(Error::JsonBad("Too short reading id", *inposp)); + } + // Read the hex ID and write the binary ID into the output event structure + read_hex!(&input[*inposp..*inposp + 64], &mut output[..32], 32)?; + *inposp += 64; + verify_char(input, b'"', inposp)?; + Ok(()) +} + +pub fn read_pubkey(input: &[u8], inposp: &mut usize, output: &mut [u8]) -> Result<(), Error> { + verify_char(input, b'"', inposp)?; + if *inposp + 64 >= input.len() { + return Err(Error::JsonBad("Too short reading pubkey", *inposp)); + } + // Read the hex pubkey and write the binary pubkey into the output event structure + read_hex!(&input[*inposp..*inposp + 64], &mut output[..32], 32)?; + *inposp += 64; + verify_char(input, b'"', inposp)?; + Ok(()) +} + +pub fn read_u64(input: &[u8], inposp: &mut usize) -> Result { + let mut value: u64 = 0; + let mut any: bool = false; + while *inposp < input.len() && b"0123456789".contains(&input[*inposp]) { + any = true; + value = (value * 10) + (input[*inposp] - 48) as u64; + *inposp += 1; + } + if !any { + return Err(Error::JsonBad( + "Created at must be a positive or zero valued number", + *inposp, + )); + } + Ok(value) +} + +pub fn read_kind(input: &[u8], inposp: &mut usize) -> Result { + let mut value: u32 = 0; + let mut any: bool = false; + while *inposp < input.len() && b"0123456789".contains(&input[*inposp]) { + any = true; + value = (value * 10) + (input[*inposp] - 48) as u32; + *inposp += 1; + } + if !any { + return Err(Error::JsonBad( + "Kind at must be a positive or zero valued number", + *inposp, + )); + } + if value > 65535 { + Err(Error::JsonBad("Kind larger than 65535", *inposp)) + } else { + Ok(value as u16) + } +} + +// HELP written for event only +// From the outer bracket through to the character after the close outer bracket +// returns the size of data written to the output +pub fn read_tags_array( + input: &[u8], + inposp: &mut usize, + output: &mut [u8], +) -> Result { + verify_char(input, b'[', inposp)?; // outer array open brace + eat_whitespace(input, inposp); + + // NOTE: we cannot write any tag strings until after we have counted the tags. + // (our tags structure is optimized for reading, not writing) + let num_tags: usize = count_tags(input, *inposp)?; + output[2..4].copy_from_slice((num_tags as u16).to_ne_bytes().as_slice()); + + // Case where we have no tags + if num_tags == 0 { + output[0..2].copy_from_slice(4_u16.to_ne_bytes().as_slice()); + return Ok(4); + } + + verify_char(input, b'[', inposp)?; // opening brace of first tag + eat_whitespace(input, inposp); + + let mut tag_num = 0; + let mut outpos: usize = 4 + num_tags * 2; + loop { + // Write the offset of this tag + let offset_slot = 4 + tag_num * 2; + output[offset_slot..offset_slot + 2] + .copy_from_slice((outpos as u16).to_ne_bytes().as_slice()); + + // Read the tag (bumps inpos and outpos) + read_tag(input, inposp, output, &mut outpos)?; + eat_whitespace(input, inposp); + + // Check what is next + match input[*inposp] { + b']' => { + *inposp += 1; + if tag_num != num_tags - 1 { + panic!("Tag count mismatch"); + } + break; + } + b',' => { + *inposp += 1; + eat_whitespace(input, inposp); + verify_char(input, b'[', inposp)?; + tag_num += 1; + if tag_num >= num_tags { + panic!("Tag count mismatch"); + } + eat_whitespace(input, inposp); + } + _ => return Err(Error::JsonBad("Tag array bad character", *inposp)), + } + } + + // Write length of tags section + output[0..2].copy_from_slice((outpos as u16).to_ne_bytes().as_slice()); + + Ok(outpos) +} + +// From the first inner tag bracket, ending after the outer bracket. +// This just counts the tags, it does not write output or modify the inpos +// This does a quicker pass over the content than actual tag parsing does. +pub fn count_tags(input: &[u8], mut inpos: usize) -> Result { + // First non-whitespace character after the opening brace + match input[inpos] { + b']' => return Ok(0), // no tags + b'[' => (), // expected + _ => return Err(Error::JsonBad("Tag array bad initial character", inpos)), + } + + let mut count = 1; + inpos += 1; + burn_tag(input, &mut inpos)?; + eat_whitespace(input, &mut inpos); + + loop { + match input[inpos] { + b']' => return Ok(count), + b',' => { + inpos += 1; + eat_whitespace(input, &mut inpos); + verify_char(input, b'[', &mut inpos)?; + count += 1; + burn_tag(input, &mut inpos)?; + eat_whitespace(input, &mut inpos); + } + _ => return Err(Error::JsonBad("Tag array bad character", inpos)), + } + } +} + +pub fn read_tag( + input: &[u8], + inposp: &mut usize, + output: &mut [u8], + outposp: &mut usize, +) -> Result<(), Error> { + verify_char(input, b'"', inposp)?; + + let countpos = *outposp; + *outposp += 2; + let mut num_strings: usize = 1; + loop { + // read string + let (inlen, outlen) = json_unescape(&input[*inposp..], &mut output[*outposp + 2..])?; + // write the length before it + output[*outposp..*outposp + 2].copy_from_slice((outlen as u16).to_ne_bytes().as_slice()); + // bump the outposp past it + *outposp += 2 + outlen; + // bump the inpos past the string (and the ending quote which isn't counted in the len) + *inposp += inlen + 1; + + eat_whitespace(input, inposp); + match input[*inposp] { + b',' => { + *inposp += 1; + eat_whitespace(input, inposp); + verify_char(input, b'"', inposp)?; + num_strings += 1; + continue; + } + b']' => { + *inposp += 1; + break; + } + _ => return Err(Error::JsonBad("Tag array bad character", *inposp)), + } + } + + // Write the count of strings at the very start + output[countpos..countpos + 2].copy_from_slice((num_strings as u16).to_ne_bytes().as_slice()); + + Ok(()) +} + +pub fn read_content( + input: &[u8], + inposp: &mut usize, + output: &mut [u8], + after_tags: usize, +) -> Result<(), Error> { + verify_char(input, b'"', inposp)?; + + // Place content 4 bytes beyond tags, to reserve space for content length + let (inlen, outlen) = json_unescape(&input[*inposp..], &mut output[after_tags + 4..])?; + *inposp += inlen + 1; // +1 to pass the end quote + + // Write content length + output[after_tags..after_tags + 4].copy_from_slice((outlen as u32).to_ne_bytes().as_slice()); + + // Write event size + let event_len = after_tags + 4 + outlen; + output[0..4].copy_from_slice((event_len as u32).to_ne_bytes().as_slice()); + + Ok(()) +} + +pub fn read_sig(input: &[u8], inposp: &mut usize, output: &mut [u8]) -> Result<(), Error> { + verify_char(input, b'"', inposp)?; + if *inposp + 128 >= input.len() { + return Err(Error::JsonBad("Too short reading sig", *inposp)); + } + // Read the hex sig and write the binary sig into the output event structure + read_hex!(&input[*inposp..*inposp + 128], &mut output[80..144], 64)?; + *inposp += 128; + verify_char(input, b'"', inposp)?; + Ok(()) +} + +// from the character after the start quote +// ending on the character following the end quote +pub fn burn_string(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + while *inposp < input.len() && input[*inposp] != b'"' { + if input[*inposp] == b'\\' && *inposp + 1 < input.len() { + *inposp += 2; + } else { + *inposp += 1; + } + } + if input[*inposp] == b'"' { + *inposp += 1; + Ok(()) + } else { + Err(Error::JsonBad("Unterminated string", *inposp)) + } +} + +// from the character after the open brace +// ending on the character following the close brace +pub fn burn_tag(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + eat_whitespace(input, inposp); + // assuming that every tag must have at least one string + verify_char(input, b'"', inposp)?; + burn_string(input, inposp)?; + eat_whitespace(input, inposp); + while input[*inposp] == b',' { + *inposp += 1; + eat_whitespace(input, inposp); + verify_char(input, b'"', inposp)?; + burn_string(input, inposp)?; + eat_whitespace(input, inposp); + } + verify_char(input, b']', inposp)?; + Ok(()) +} + +pub fn burn_key_and_value(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + verify_char(input, b'"', inposp)?; + burn_string(input, inposp)?; + eat_colon_with_whitespace(input, inposp)?; + burn_value(input, inposp)?; + Ok(()) +} + +// from the character after the open brace +// ending on the character following the close brace +pub fn burn_object(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + loop { + eat_whitespace_and_commas(input, inposp); + + // Check for the end + if input[*inposp] == b'}' { + *inposp += 1; + return Ok(()); + } + + burn_key_and_value(input, inposp)?; + } +} + +// from the character after the open bracket +// ending on the character following the close bracket +pub fn burn_array(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + loop { + eat_whitespace_and_commas(input, inposp); + + // Check for the end + if input[*inposp] == b']' { + *inposp += 1; + return Ok(()); + } + + burn_value(input, inposp)?; + } +} + +pub fn burn_value(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + if *inposp >= input.len() { + return Err(Error::JsonBad( + "Too short burning an unused JSON value", + *inposp, + )); + } + match input[*inposp] { + b'"' => { + *inposp += 1; + burn_string(input, inposp)? + } + b'[' => { + *inposp += 1; + burn_array(input, inposp)? + } + b'{' => { + *inposp += 1; + burn_object(input, inposp)? + } + b't' => burn_true(input, inposp)?, + b'f' => burn_false(input, inposp)?, + b'n' => burn_null(input, inposp)?, + b'-' => burn_number(input, inposp)?, + _ => { + if b"123456789".contains(&input[*inposp]) { + burn_number(input, inposp)? + } else { + return Err(Error::JsonBad( + "Too short burning an unused JSON value", + *inposp, + )); + } + } + } + + Ok(()) +} + +pub fn burn_null(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + if *inposp + 4 <= input.len() && &input[*inposp..*inposp + 4] == b"null" { + *inposp += 4; + Ok(()) + } else { + Err(Error::JsonBad("Expected null", *inposp)) + } +} + +pub fn burn_true(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + if *inposp + 4 <= input.len() && &input[*inposp..*inposp + 4] == b"true" { + *inposp += 4; + Ok(()) + } else { + Err(Error::JsonBad("Expected true", *inposp)) + } +} + +pub fn burn_false(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + if *inposp + 5 <= input.len() && &input[*inposp..*inposp + 5] == b"false" { + *inposp += 5; + Ok(()) + } else { + Err(Error::JsonBad("Expected false", *inposp)) + } +} + +pub fn burn_number(input: &[u8], inposp: &mut usize) -> Result<(), Error> { + // For burning, we don't check validity. + while *inposp < input.len() && b".+-0123456789abcdefABCDEF_oOxXn".contains(&input[*inposp]) { + *inposp += 1; + } + Ok(()) +} diff --git a/src/types/parse/mod.rs b/src/types/parse/mod.rs index 325e934..d159abd 100644 --- a/src/types/parse/mod.rs +++ b/src/types/parse/mod.rs @@ -1,3 +1,5 @@ pub mod json_escape; +pub mod json_parse; + pub mod utf8;