diff --git a/src/impls/avx2/deser.rs b/src/impls/avx2/deser.rs index cf978d0f..1563c720 100644 --- a/src/impls/avx2/deser.rs +++ b/src/impls/avx2/deser.rs @@ -10,7 +10,7 @@ use arch::{ }; use crate::{ - Deserializer, Result, SillyWrapper, + Deserializer, InputView, Result, SillyWrapper, error::ErrorType, macros::static_cast_u32, safer_unchecked::GetSaferUnchecked, @@ -24,10 +24,10 @@ use crate::{ clippy::too_many_lines )] #[cfg_attr(not(feature = "no-inline"), inline)] -pub(crate) unsafe fn parse_str<'invoke, 'de>( +pub(crate) unsafe fn parse_str<'de>( input: SillyWrapper<'de>, - data: &'invoke [u8], - buffer: &'invoke mut [u8], + data: InputView, + buffer: &mut [u8], mut idx: usize, ) -> Result<&'de str> { unsafe { @@ -42,7 +42,7 @@ pub(crate) unsafe fn parse_str<'invoke, 'de>( // This is safe since we check sub's length in the range access above and only // create sub sliced form sub to `sub.len()`. - let src: &[u8] = data.get_kinda_unchecked(idx..); + let src: &[u8] = data.tail(idx); let mut src_i: usize = 0; let mut len = src_i; loop { diff --git a/src/impls/native/deser.rs b/src/impls/native/deser.rs index 1bfd47e3..81e810e9 100644 --- a/src/impls/native/deser.rs +++ b/src/impls/native/deser.rs @@ -1,30 +1,33 @@ use crate::{ - Deserializer, ErrorType, Result, SillyWrapper, + Deserializer, ErrorType, InputView, Result, SillyWrapper, safer_unchecked::GetSaferUnchecked, stringparse::{ESCAPE_MAP, get_unicode_codepoint}, }; +// `data` may alias the buffer written through `input` (the padded path), so reads +// interleaved with the escape writes below go through the raw pointer; the only +// slices materialized are transient and dead before the writes that follow them. #[allow(clippy::cast_possible_truncation)] -pub(crate) unsafe fn parse_str<'invoke, 'de>( +pub(crate) unsafe fn parse_str<'de>( input: SillyWrapper<'de>, - data: &'invoke [u8], - _buffer: &'invoke mut [u8], + data: InputView, + _buffer: &mut [u8], idx: usize, ) -> Result<&'de str> { use ErrorType::{InvalidEscape, InvalidUnicodeCodepoint}; let input = input.input; // skip leading `"` - let src: &[u8] = unsafe { data.get_kinda_unchecked(idx + 1..) }; + let src: *const u8 = unsafe { data.ptr.add(idx + 1) }; let input = unsafe { input.add(idx + 1) }; let mut src_i = 0; - let mut b = unsafe { *src.get_kinda_unchecked(src_i) }; + let mut b = unsafe { src.add(src_i).read() }; // quickly skip all the "good stuff" while b != b'"' && b != b'\\' { src_i += 1; - b = unsafe { *src.get_kinda_unchecked(src_i) }; + b = unsafe { src.add(src_i).read() }; } if b == b'"' { let v = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(input, src_i)) }; @@ -37,13 +40,13 @@ pub(crate) unsafe fn parse_str<'invoke, 'de>( while b != b'"' { if b == b'\\' { // don't advance i yet - let escape_char = unsafe { *src.get_kinda_unchecked(src_i + 1) }; + let escape_char = unsafe { src.add(src_i + 1).read() }; if escape_char == b'u' { // got to reduce by 1 since we have to include the '\\' for get_unicode_codepoint - let (cp, src_offset) = - unsafe { get_unicode_codepoint(src.get_kinda_unchecked(src_i..)) }.map_err( - |_| Deserializer::error_c(idx + 1 + src_i, 'u', InvalidUnicodeCodepoint), - )?; + let (cp, src_offset) = unsafe { get_unicode_codepoint(data.tail(idx + 1 + src_i)) } + .map_err(|_| { + Deserializer::error_c(idx + 1 + src_i, 'u', InvalidUnicodeCodepoint) + })?; // from codepoint_to_utf8 since we write directly to input unsafe { @@ -101,7 +104,7 @@ pub(crate) unsafe fn parse_str<'invoke, 'de>( dst_i += 1; } src_i += 1; - b = unsafe { *src.get_kinda_unchecked(src_i) }; + b = unsafe { src.add(src_i).read() }; } unsafe { Ok(std::str::from_utf8_unchecked(std::slice::from_raw_parts( @@ -121,7 +124,12 @@ mod test { let mut buffer = vec![0; 1024]; let r = unsafe { - super::parse_str(input.as_mut_ptr().into(), &input2, buffer.as_mut_slice(), 0)? + super::parse_str( + input.as_mut_ptr().into(), + crate::InputView::from_slice(&input2), + buffer.as_mut_slice(), + 0, + )? }; Ok(String::from(r)) } diff --git a/src/impls/neon/deser.rs b/src/impls/neon/deser.rs index e1932ded..dc9a10bd 100644 --- a/src/impls/neon/deser.rs +++ b/src/impls/neon/deser.rs @@ -1,4 +1,5 @@ use crate::Deserializer; +use crate::InputView; use crate::Result; use crate::SillyWrapper; use crate::error::ErrorType; @@ -42,10 +43,10 @@ fn find_bs_bits_and_quote_bits(v0: uint8x16_t, v1: uint8x16_t) -> (u32, u32) { #[allow(clippy::if_not_else, clippy::too_many_lines)] #[cfg_attr(not(feature = "no-inline"), inline)] -pub(crate) fn parse_str<'invoke, 'de>( +pub(crate) fn parse_str<'de>( input: SillyWrapper<'de>, - data: &'invoke [u8], - buffer: &'invoke mut [u8], + data: InputView, + buffer: &mut [u8], mut idx: usize, ) -> Result<&'de str> { use ErrorType::{InvalidEscape, InvalidUnicodeCodepoint}; @@ -59,7 +60,7 @@ pub(crate) fn parse_str<'invoke, 'de>( // This is safe since we check sub's length in the range access above and only // create sub sliced form sub to `sub.len()`. - let src: &[u8] = unsafe { data.get_kinda_unchecked(idx..) }; + let src: &[u8] = unsafe { data.tail(idx) }; let mut src_i: usize = 0; let mut len = src_i; loop { diff --git a/src/impls/portable/deser.rs b/src/impls/portable/deser.rs index 0323b31f..456e4ab4 100644 --- a/src/impls/portable/deser.rs +++ b/src/impls/portable/deser.rs @@ -1,16 +1,16 @@ use std::simd::{SimdPartialEq, ToBitMask, u8x32}; use crate::{ - Deserializer, ErrorType, Result, SillyWrapper, + Deserializer, ErrorType, InputView, Result, SillyWrapper, safer_unchecked::GetSaferUnchecked, stringparse::{ESCAPE_MAP, handle_unicode_codepoint}, }; #[cfg_attr(not(feature = "no-inline"), inline)] -pub(crate) unsafe fn parse_str<'invoke, 'de>( +pub(crate) unsafe fn parse_str<'de>( input: SillyWrapper<'de>, - data: &'invoke [u8], - buffer: &'invoke mut [u8], + data: InputView, + buffer: &mut [u8], mut idx: usize, ) -> Result<&'de str> { let input = input.input; @@ -26,7 +26,7 @@ pub(crate) unsafe fn parse_str<'invoke, 'de>( // This is safe since we check sub's length in the range access above and only // create sub sliced form sub to `sub.len()`. - let src: &[u8] = data.get_kinda_unchecked(idx..); + let src: &[u8] = data.tail(idx); let mut src_i: usize = 0; let mut len = src_i; loop { diff --git a/src/impls/simd128/deser.rs b/src/impls/simd128/deser.rs index cb9e3023..f4568d4b 100644 --- a/src/impls/simd128/deser.rs +++ b/src/impls/simd128/deser.rs @@ -1,7 +1,7 @@ use std::arch::wasm32::{u8x16_bitmask, u8x16_eq, u8x16_splat, v128, v128_load, v128_store}; use crate::{ - Deserializer, Result, SillyWrapper, + Deserializer, InputView, Result, SillyWrapper, error::ErrorType, safer_unchecked::GetSaferUnchecked, stringparse::{ESCAPE_MAP, handle_unicode_codepoint}, @@ -14,10 +14,10 @@ use crate::{ clippy::too_many_lines )] #[cfg_attr(not(feature = "no-inline"), inline)] -pub(crate) fn parse_str<'invoke, 'de>( +pub(crate) fn parse_str<'de>( input: SillyWrapper<'de>, - data: &'invoke [u8], - buffer: &'invoke mut [u8], + data: InputView, + buffer: &mut [u8], mut idx: usize, ) -> Result<&'de str> { use ErrorType::{InvalidEscape, InvalidUnicodeCodepoint}; @@ -29,7 +29,7 @@ pub(crate) fn parse_str<'invoke, 'de>( // This is safe since we check sub's length in the range access above and only // create sub sliced form sub to `sub.len()`. - let src = unsafe { data.get_kinda_unchecked(idx..) }; + let src = unsafe { data.tail(idx) }; let mut src_i = 0; let mut len = src_i; loop { diff --git a/src/impls/sse42/deser.rs b/src/impls/sse42/deser.rs index c169c5a2..46e9139a 100644 --- a/src/impls/sse42/deser.rs +++ b/src/impls/sse42/deser.rs @@ -5,7 +5,7 @@ use std::arch::x86 as arch; use std::arch::x86_64 as arch; use crate::{ - Deserializer, Result, SillyWrapper, + Deserializer, InputView, Result, SillyWrapper, error::ErrorType, safer_unchecked::GetSaferUnchecked, stringparse::{ESCAPE_MAP, handle_unicode_codepoint}, @@ -17,10 +17,10 @@ use arch::{ #[target_feature(enable = "sse4.2")] #[allow(clippy::if_not_else, clippy::cast_possible_wrap)] #[cfg_attr(not(feature = "no-inline"), inline)] -pub(crate) unsafe fn parse_str<'invoke, 'de>( +pub(crate) unsafe fn parse_str<'de>( input: SillyWrapper<'de>, - data: &'invoke [u8], - buffer: &'invoke mut [u8], + data: InputView, + buffer: &mut [u8], mut idx: usize, ) -> Result<&'de str> { unsafe { @@ -33,7 +33,7 @@ pub(crate) unsafe fn parse_str<'invoke, 'de>( // This is safe since we check sub's length in the range access above and only // create sub sliced form sub to `sub.len()`. - let src: &[u8] = data.get_kinda_unchecked(idx..); + let src: &[u8] = data.tail(idx); let mut src_i: usize = 0; let mut len = src_i; loop { diff --git a/src/lib.rs b/src/lib.rs index 2f4ab1de..43ed252a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,6 +162,38 @@ pub fn fill_tape<'de>(s: &'de mut [u8], buffers: &mut Buffers, tape: &mut Tape<' Deserializer::fill_tape(s, buffers, &mut tape.0) } +/// Padding bytes callers must provide beyond the logical input for +/// [`fill_tape_padded`]. +pub const INPUT_PADDING: usize = SIMDINPUT_LENGTH; + +/// Fills an already existing tape from a caller-padded input, skipping the padded copy +/// of the input that [`fill_tape`] makes into its internal buffer. +/// +/// `s[..len]` is the logical JSON document and `s[len..]` is the padding, which must be +/// at least [`INPUT_PADDING`] initialized bytes. A root-level number or atom is +/// terminated by the byte that follows it, so `s[len]` must be structural or whitespace; +/// filling the padding with `b' '` satisfies that, as [`fill_tape`] does internally. The +/// remaining padding bytes only absorb SIMD over-reads and their content is irrelevant. +/// +/// # Errors +/// +/// Will return `Err` if `s[..len]` is invalid JSON. +/// +/// # Safety +/// +/// The caller must guarantee `s.len() >= len + INPUT_PADDING`. Like [`fill_tape`], string +/// unescaping writes in place within the logical input. +#[cfg_attr(not(feature = "no-inline"), inline)] +pub unsafe fn fill_tape_padded<'de>( + s: &'de mut [u8], + len: usize, + buffers: &mut Buffers, + tape: &mut Tape<'de>, +) -> Result<()> { + tape.0.clear(); + unsafe { Deserializer::fill_tape_padded(s, len, buffers, &mut tape.0) } +} + pub(crate) trait Stage1Parse { type Utf8Validator: ChunkedUtf8Validator; type SimdRepresentation; @@ -331,6 +363,51 @@ impl From<*mut u8> for SillyWrapper<'_> { } } +/// Read-only stage-2 view of the (padded) input, carried as a raw pointer. +/// +/// In the padded path ([`fill_tape_padded`]) this aliases the buffer string +/// unescaping writes through the sibling `input` pointer. A `&[u8]` argument +/// spanning those bytes would be UB the moment a write lands during its call +/// (borrow-model protectors on reference arguments; LLVM marks them +/// `noalias readonly`), so reads go through this view instead, materializing +/// only transient slices that are dead before any write to their range. +#[derive(Debug, Clone, Copy)] +pub(crate) struct InputView { + pub(crate) ptr: *const u8, + pub(crate) len: usize, +} + +impl InputView { + #[cfg_attr(not(feature = "no-inline"), inline)] + pub(crate) fn from_slice(s: &[u8]) -> Self { + Self { + ptr: s.as_ptr(), + len: s.len(), + } + } + + /// # Safety + /// + /// `idx` must be in bounds of the view. + #[cfg_attr(not(feature = "no-inline"), inline)] + pub(crate) unsafe fn byte(self, idx: usize) -> u8 { + debug_assert!(idx < self.len); + unsafe { self.ptr.add(idx).read() } + } + + /// Transient shared slice of `[idx..len)`. + /// + /// # Safety + /// + /// `idx <= len`, and the returned slice must be dead before the next write + /// through the aliasing `input` pointer touches its range. + #[cfg_attr(not(feature = "no-inline"), inline)] + pub(crate) unsafe fn tail<'a>(self, idx: usize) -> &'a [u8] { + debug_assert!(idx <= self.len); + unsafe { core::slice::from_raw_parts(self.ptr.add(idx), self.len - idx) } + } +} + #[cfg(all( feature = "runtime-detection", any(target_arch = "x86_64", target_arch = "x86"), @@ -342,7 +419,7 @@ type FnRaw = *mut (); ))] type ParseStrFn = for<'invoke, 'de> unsafe fn( SillyWrapper<'de>, - &'invoke [u8], + InputView, &'invoke mut [u8], usize, ) -> std::result::Result<&'de str, error::Error>; @@ -499,7 +576,7 @@ impl<'de> Deserializer<'de> { #[allow(dead_code)] pub(crate) unsafe fn parse_str_<'invoke>( input: *mut u8, - data: &'invoke [u8], + data: InputView, buffer: &'invoke mut [u8], idx: usize, ) -> Result<&'de str> @@ -523,7 +600,7 @@ impl<'de> Deserializer<'de> { )))] pub(crate) unsafe fn parse_str_<'invoke>( input: *mut u8, - data: &'invoke [u8], + data: InputView, buffer: &'invoke mut [u8], idx: usize, ) -> Result<&'de str> @@ -537,7 +614,7 @@ impl<'de> Deserializer<'de> { #[cfg(all(feature = "portable", not(feature = "runtime-detection")))] pub(crate) unsafe fn parse_str_<'invoke>( input: *mut u8, - data: &'invoke [u8], + data: InputView, buffer: &'invoke mut [u8], idx: usize, ) -> Result<&'de str> @@ -556,7 +633,7 @@ impl<'de> Deserializer<'de> { ))] pub(crate) unsafe fn parse_str_<'invoke>( input: *mut u8, - data: &'invoke [u8], + data: InputView, buffer: &'invoke mut [u8], idx: usize, ) -> Result<&'de str> { @@ -573,7 +650,7 @@ impl<'de> Deserializer<'de> { ))] pub(crate) unsafe fn parse_str_<'invoke>( input: *mut u8, - data: &'invoke [u8], + data: InputView, buffer: &'invoke mut [u8], idx: usize, ) -> Result<&'de str> { @@ -585,7 +662,7 @@ impl<'de> Deserializer<'de> { #[cfg(all(target_arch = "aarch64", not(feature = "portable")))] pub(crate) unsafe fn parse_str_<'invoke>( input: *mut u8, - data: &'invoke [u8], + data: InputView, buffer: &'invoke mut [u8], idx: usize, ) -> Result<&'de str> { @@ -596,7 +673,7 @@ impl<'de> Deserializer<'de> { #[cfg(all(target_feature = "simd128", not(feature = "portable")))] pub(crate) unsafe fn parse_str_<'invoke>( input: *mut u8, - data: &'invoke [u8], + data: InputView, buffer: &'invoke mut [u8], idx: usize, ) -> Result<&'de str> { @@ -888,14 +965,66 @@ impl<'de> Deserializer<'de> { .map_err(Error::generic)?; }; - Self::build_tape( - input, - input_buffer, - &mut buffer.string_buffer, - &buffer.structural_indexes, - &mut buffer.stage2_stack, - tape, - ) + // SAFETY: the pointer spans the caller's exclusive borrow of `input`, which is + // not used again; reads go through `input_buffer`, a disjoint padded copy. + unsafe { + Self::build_tape( + input.as_mut_ptr(), + InputView::from_slice(input_buffer), + &mut buffer.string_buffer, + &buffer.structural_indexes, + &mut buffer.stage2_stack, + tape, + ) + } + } + + #[allow(clippy::uninit_vec)] + #[cfg_attr(not(feature = "no-inline"), inline)] + unsafe fn fill_tape_padded( + input: &'de mut [u8], + len: usize, + buffer: &mut Buffers, + tape: &mut Vec>, + ) -> Result<()> { + debug_assert!(input.len() >= len + SIMDINPUT_LENGTH); + if len > u32::MAX as usize { + return Err(Self::error(ErrorType::InputTooLarge)); + } + + buffer.string_buffer.clear(); + buffer.string_buffer.reserve(len + SIMDJSON_PADDING); + unsafe { + buffer.string_buffer.set_len(len + SIMDJSON_PADDING); + }; + + // The caller-provided padding plays the role fill_tape's internal copy plays: a + // region parse_str can over-read with SIMD loads. Unlike fill_tape, reads and + // writes share this one buffer, so stage 2 reads through a raw-pointer InputView + // (see its doc); no reference into the buffer is live once unescaping writes + // start. The stage-1 slice below is dead before the first write. + let ptr = input.as_mut_ptr(); + let input2 = InputView { + ptr, + len: input.len(), + }; + + unsafe { + let head: &[u8] = core::slice::from_raw_parts(ptr, len); + Self::find_structural_bits(head, &mut buffer.structural_indexes) + .map_err(Error::generic)?; + }; + + unsafe { + Self::build_tape( + ptr, + input2, + &mut buffer.string_buffer, + &buffer.structural_indexes, + &mut buffer.stage2_stack, + tape, + ) + } } /// Creates a serializer from a mutable slice of bytes using a temporary diff --git a/src/stage2.rs b/src/stage2.rs index a19c322e..5c0d198c 100644 --- a/src/stage2.rs +++ b/src/stage2.rs @@ -4,7 +4,7 @@ use crate::charutils::is_not_structural_or_whitespace; use crate::macros::unlikely; use crate::safer_unchecked::GetSaferUnchecked; use crate::value::tape::Node; -use crate::{Deserializer, Error, ErrorType, InternalError, Result}; +use crate::{Deserializer, Error, ErrorType, InputView, InternalError, Result}; use value_trait::StaticNode; #[cfg_attr(not(feature = "no-inline"), inline)] @@ -104,9 +104,16 @@ impl<'de> Deserializer<'de> { unused_unsafe, clippy::needless_continue )] - pub(crate) fn build_tape( - input: &'de mut [u8], - input2: &[u8], + /// # Safety + /// + /// `input` must be valid for writes over the parsed document (string unescaping + /// writes through it in place) and must not be aliased by a live `&mut`; reads go + /// only through `input2`, which either views a disjoint padded copy of the input + /// ([`Deserializer::fill_tape`]) or aliases `input` itself + /// ([`Deserializer::fill_tape_padded`]). + pub(crate) unsafe fn build_tape( + input: *mut u8, + input2: InputView, buffer: &mut [u8], structural_indexes: &[u32], stack: &mut Vec, @@ -121,7 +128,7 @@ impl<'de> Deserializer<'de> { // Safety: Must NOT advance input pointer as part of logic, since we only get the pointer once. // Use idx in order to advance through the input. - let input_ptr = input.as_mut_ptr(); + let input_ptr = input; // Resolve the per-ISA `parse_str` implementation once per document // instead of once per string (T6). #[cfg(all( @@ -185,7 +192,7 @@ impl<'de> Deserializer<'de> { if i < structural_indexes.len() { idx = *get!(structural_indexes, i) as usize; i += 1; - c = *get!(input2, idx); + c = unsafe { input2.byte(idx) }; } else { fail!(ErrorType::Syntax); } @@ -207,13 +214,13 @@ impl<'de> Deserializer<'de> { any(target_arch = "x86_64", target_arch = "x86"), ))] let s = s2try!(unsafe { - parse_str_fn(crate::SillyWrapper::from(input_ptr), &input2, buffer, idx) + parse_str_fn(crate::SillyWrapper::from(input_ptr), input2, buffer, idx) }); #[cfg(not(all( feature = "runtime-detection", any(target_arch = "x86_64", target_arch = "x86"), )))] - let s = s2try!(unsafe { Self::parse_str_(input_ptr, &input2, buffer, idx) }); + let s = s2try!(unsafe { Self::parse_str_(input_ptr, input2, buffer, idx) }); insert_res!(Node::String(s)); }}; } @@ -368,7 +375,7 @@ impl<'de> Deserializer<'de> { } b't' => { unsafe { - if !is_valid_true_atom(get!(input2, idx..)) { + if !is_valid_true_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedTrue); } }; @@ -380,7 +387,7 @@ impl<'de> Deserializer<'de> { } b'f' => { unsafe { - if !is_valid_false_atom(get!(input2, idx..)) { + if !is_valid_false_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedFalse); } }; @@ -392,7 +399,7 @@ impl<'de> Deserializer<'de> { } b'n' => { unsafe { - if !is_valid_null_atom(get!(input2, idx..)) { + if !is_valid_null_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedNull); } }; @@ -410,7 +417,11 @@ impl<'de> Deserializer<'de> { fail!(ErrorType::TrailingData); } b'-' => { - insert_res!(Node::Static(s2try!(Self::parse_number(idx, input2, true)))); + insert_res!(Node::Static(s2try!(Self::parse_number( + idx, + unsafe { input2.tail(0) }, + true + )))); if i == structural_indexes.len() { success!(); @@ -418,7 +429,11 @@ impl<'de> Deserializer<'de> { fail!(ErrorType::TrailingData); } b'0'..=b'9' => { - insert_res!(Node::Static(s2try!(Self::parse_number(idx, input2, false)))); + insert_res!(Node::Static(s2try!(Self::parse_number( + idx, + unsafe { input2.tail(0) }, + false + )))); if i == structural_indexes.len() { success!(); @@ -447,35 +462,39 @@ impl<'de> Deserializer<'de> { } b't' => { insert_res!(Node::Static(StaticNode::Bool(true))); - if !is_valid_true_atom(get!(input2, idx..)) { + if !is_valid_true_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedTrue); } object_continue!(); } b'f' => { insert_res!(Node::Static(StaticNode::Bool(false))); - if !is_valid_false_atom(get!(input2, idx..)) { + if !is_valid_false_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedFalse); } object_continue!(); } b'n' => { insert_res!(Node::Static(StaticNode::Null)); - if !is_valid_null_atom(get!(input2, idx..)) { + if !is_valid_null_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedNull); } object_continue!(); } b'-' => { insert_res!(Node::Static(s2try!(Self::parse_number( - idx, input2, true + idx, + unsafe { input2.tail(0) }, + true, )))); object_continue!(); } b'0'..=b'9' => { insert_res!(Node::Static(s2try!(Self::parse_number( - idx, input2, false + idx, + unsafe { input2.tail(0) }, + false, )))); object_continue!(); @@ -570,35 +589,39 @@ impl<'de> Deserializer<'de> { } b't' => { insert_res!(Node::Static(StaticNode::Bool(true))); - if !is_valid_true_atom(get!(input2, idx..)) { + if !is_valid_true_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedTrue); } array_continue!(); } b'f' => { insert_res!(Node::Static(StaticNode::Bool(false))); - if !is_valid_false_atom(get!(input2, idx..)) { + if !is_valid_false_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedFalse); } array_continue!(); } b'n' => { insert_res!(Node::Static(StaticNode::Null)); - if !is_valid_null_atom(get!(input2, idx..)) { + if !is_valid_null_atom(unsafe { input2.tail(idx) }) { fail!(ErrorType::ExpectedNull); } array_continue!(); } b'-' => { insert_res!(Node::Static(s2try!(Self::parse_number( - idx, input2, true + idx, + unsafe { input2.tail(0) }, + true, )))); array_continue!(); } b'0'..=b'9' => { insert_res!(Node::Static(s2try!(Self::parse_number( - idx, input2, false + idx, + unsafe { input2.tail(0) }, + false, )))); array_continue!(); @@ -716,7 +739,12 @@ mod test { let mut buffer = vec![0; 1024]; let s = unsafe { - Deserializer::parse_str_(input.as_mut_ptr(), &input2, buffer.as_mut_slice(), 0)? + Deserializer::parse_str_( + input.as_mut_ptr(), + InputView::from_slice(&input2), + buffer.as_mut_slice(), + 0, + )? }; assert_eq!(r#"{"arg":"test"}"#, s); Ok(()) diff --git a/tests/fill_tape_padded.rs b/tests/fill_tape_padded.rs new file mode 100644 index 00000000..125603f9 --- /dev/null +++ b/tests/fill_tape_padded.rs @@ -0,0 +1,311 @@ +//! Equivalence and soundness harness for `fill_tape_padded`. +//! +//! `fill_tape_padded` parses out of the caller's own buffer, so string unescaping writes +//! into the same bytes stage 2 reads from. That aliasing is the one thing this path has +//! that `fill_tape` does not, so every test here pins the padded result against the +//! copying `fill_tape` on the same document, and the layout mirrors the intended caller +//! shape: several rows packed into one padded scratch, `Buffers` reused across rows. +//! +//! Worth running under Miri after any change to stage 2 or the per-ISA `parse_str`, in +//! both borrow models and on a second architecture to cover the SIMD and scalar backends: +//! +//! ```text +//! cargo +nightly miri test --test fill_tape_padded +//! MIRIFLAGS=-Zmiri-tree-borrows cargo +nightly miri test --test fill_tape_padded +//! ``` + +use simd_json::{Buffers, INPUT_PADDING, Tape, fill_tape, fill_tape_padded}; + +/// Packs `rows` into one scratch buffer with a single trailing padding run, parses each +/// row through `fill_tape_padded`, and returns per-row `Ok(debug of nodes)` / +/// `Err(debug of error)`. +/// +/// Every row except the last is followed by the next row's bytes rather than by padding, +/// which is what makes this layout worth testing: row N's SIMD over-read reaches into row +/// N+1, and row N+1 is parsed after row N has already unescaped in place. +fn parse_rows(rows: &[&[u8]]) -> Vec { + let mut scratch: Vec = Vec::new(); + let mut offsets = vec![0_usize]; + for r in rows { + scratch.extend_from_slice(r); + offsets.push(scratch.len()); + } + // Spaces, not zeros: a root-level number or atom is terminated by the byte after it, + // and a NUL is not a valid terminator. + scratch.resize(scratch.len() + INPUT_PADDING, b' '); + + let mut buffers = Buffers::new(256); + let mut out = Vec::new(); + for row in 0..rows.len() { + let row_len = offsets[row + 1] - offsets[row]; + let padded = &mut scratch[offsets[row]..]; + let mut tape = Tape::null(); + let res = unsafe { fill_tape_padded(padded, row_len, &mut buffers, &mut tape) }; + out.push(match res { + Ok(()) => format!("{:?}", tape.0), + Err(e) => format!("ERR {e:?}"), + }); + } + out +} + +/// The reference result: the same document through the copying `fill_tape`. +fn parse_reference(doc: &[u8]) -> String { + let mut buf = doc.to_vec(); + let mut buffers = Buffers::new(256); + let mut tape = Tape::null(); + match fill_tape(&mut buf, &mut buffers, &mut tape) { + Ok(()) => format!("{:?}", tape.0), + Err(e) => format!("ERR {e:?}"), + } +} + +/// Asserts every document parses identically through both entry points, each as its own +/// single-row scratch. +fn assert_matches_fill_tape(docs: &[&[u8]]) { + for doc in docs { + let got = &parse_rows(&[doc])[0]; + assert_eq!( + got, + &parse_reference(doc), + "fill_tape_padded diverged from fill_tape on {:?}", + String::from_utf8_lossy(doc) + ); + } +} + +#[test] +fn plain_documents() { + assert_matches_fill_tape(&[ + br#"{"a":"plain","b":"also plain","c":7}"#, + br#"{"a":1,"b":-2.5,"c":true,"d":false,"e":null}"#, + br#"[1,"two",{"three":3},[4,[5]]]"#, + br#""just a string""#, + br#"42"#, + br#"{}"#, + br#"[]"#, + ]); +} + +/// A root-level number or atom runs to the end of the logical input, so the byte that +/// terminates it is the caller's first padding byte. +#[test] +fn root_scalars_terminated_by_padding() { + assert_matches_fill_tape(&[ + br#"123"#, + br#"-2374611873366417043"#, + br#"-1.5e3"#, + br#"1.7976931348623157e308"#, + br#"true"#, + br#"false"#, + br#"null"#, + // and one nesting level in, where the terminator is still real input + br#"[123]"#, + br#"{"a":true}"#, + ]); +} + +#[test] +fn escapes_everywhere() { + assert_matches_fill_tape(&[ + br#"{"a":"has\nescape","b":2}"#, + br#"{"a":"\n","b":"\t\r\b\f\\\/\"","c":"end\\"}"#, + // embedded JSON: the escape-dense real-world shape + br#"{"payload":"{\"inner\":{\"k\":\"v\",\"n\":1},\"list\":[\"a\",\"b\"]}","after":9}"#, + // unicode escapes incl. surrogate pair and multi-byte output + br#"{"u":"\u00e9\u4e2d\ud83d\ude00","v":"x"}"#, + "{\"u\":\"\u{e9}\u{4e2d} literal\",\"w\":\"then\\nescaped\"}".as_bytes(), + // escaped key, value read after an unescaped write + br#"{"k\ney":123,"z":"tail"}"#, + // atoms and numbers after escape-heavy strings + br#"{"s":"a\\b\\c\\d","t":true,"n":null,"f":-1.25e3}"#, + // multi-byte literal content + "{\"k\":\"caf\u{e9} \u{4e2d}\u{6587} \u{1f642}\",\"n\":1}".as_bytes(), + ]); +} + +/// One escape at every position of strings sized around the 16/32/64-byte lanes, so +/// in-place write-back and lane loads get exercised at each relative offset. +#[test] +fn escape_positions_cross_simd_boundaries() { + let sizes: &[usize] = if cfg!(miri) { + &[31, 32, 33] + } else { + &[ + 1, 2, 15, 16, 17, 30, 31, 32, 33, 34, 47, 48, 49, 63, 64, 65, 66, 96, + ] + }; + for &size in sizes { + for pos in 0..size.saturating_sub(1) { + let mut val = vec![b'a'; size]; + val[pos] = b'\\'; + val[pos + 1] = b'n'; + let mut doc = Vec::from(&br#"{"k":""#[..]); + doc.extend_from_slice(&val); + doc.extend_from_slice(br#"","m":7}"#); + assert_matches_fill_tape(&[&doc]); + } + } +} + +/// Last row in the scratch: the unescape write-back lands directly against the padding. +#[test] +fn escape_at_end_of_logical_input() { + assert_matches_fill_tape(&[br#"{"k":"tail\n"}"#, br#"{"k":"tail\\"}"#, br#"["aA"]"#]); +} + +#[test] +fn multi_row_scratch_shares_padding() { + let rows: &[&[u8]] = &[ + br#"{"a":"x\ny","b":1}"#, + br#"{"a":"plain","b":2}"#, + "{\"a\":\"\u{e9}\u{e8}\",\"b\":3}".as_bytes(), + br#"{"a":"end\\"}"#, + // trailing row whose value runs into the padding + br#"{"a":42}"#, + ]; + let got = parse_rows(rows); + for (doc, got) in rows.iter().zip(&got) { + assert_eq!( + got, + &parse_reference(doc), + "row {:?} diverged", + String::from_utf8_lossy(doc) + ); + } +} + +#[test] +fn invalid_documents_match_fill_tape() { + assert_matches_fill_tape(&[ + br#"{"a":"bad\qescape"}"#, + br#"{"a":"\ud83d"}"#, // unpaired surrogate + br#"{"a":tru}"#, + br#"{"a":1"#, + br#"{"a" 1}"#, + br#"{"a":}"#, + br#"[1,]"#, + br#""unterminated"#, + br#""#, + ]); +} + +/// Invalid UTF-8 in the logical input must be rejected, same as `fill_tape`. +#[test] +fn invalid_utf8_matches_fill_tape() { + assert_matches_fill_tape(&[ + &[b'"', 0xff, 0xfe, b'"'], + &[b'{', b'"', b'k', b'"', b':', b'"', 0x80, b'"', b'}'], + ]); +} + +/// Deterministic xorshift so the corpus is reproducible (no rand dev-dependency). +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } +} + +fn random_string(rng: &mut Rng, out: &mut Vec) { + out.push(b'"'); + for _ in 0..rng.below(80) { + match rng.below(12) { + 0 => out.extend_from_slice(b"\\n"), + 1 => out.extend_from_slice(b"\\\""), + 2 => out.extend_from_slice(b"\\\\"), + 3 => out.extend_from_slice(b"\\u00e9"), + 4 => out.extend_from_slice(b"\\ud83d\\ude00"), + 5 => out.extend_from_slice("\u{4e2d}".as_bytes()), + _ => { + let run = 1 + rng.below(37); + let c = b'a' + (rng.below(26) as u8); + out.extend(std::iter::repeat_n(c, run)); + } + } + } + out.push(b'"'); +} + +fn random_value(rng: &mut Rng, depth: usize, out: &mut Vec) { + let pick = if depth >= 3 { + rng.below(4) + } else { + rng.below(6) + }; + match pick { + 0 => random_string(rng, out), + 1 => out.extend_from_slice(rng.next().to_string().as_bytes()), + 2 => { + out.extend_from_slice( + format!("-{}.{}e-2", rng.below(1000), rng.below(1000)).as_bytes(), + ); + } + 3 => out.extend_from_slice([&b"true"[..], b"false", b"null"][rng.below(3)]), + 4 => { + out.push(b'['); + for i in 0..rng.below(4) { + if i > 0 { + out.push(b','); + } + random_value(rng, depth + 1, out); + } + out.push(b']'); + } + _ => { + out.push(b'{'); + for i in 0..rng.below(4) { + if i > 0 { + out.push(b','); + } + random_string(rng, out); + out.push(b':'); + random_value(rng, depth + 1, out); + } + out.push(b'}'); + } + } +} + +#[test] +fn randomized_equivalence_with_fill_tape() { + let iterations = if cfg!(miri) { 20 } else { 500 }; + let mut rng = Rng(0x5eed_cafe_f00d_0001); + for _ in 0..iterations { + // Several rows per scratch, so most rows are followed by real input. + let mut rows: Vec> = Vec::new(); + for _ in 0..1 + rng.below(4) { + let mut doc = Vec::new(); + doc.push(b'{'); + for i in 0..1 + rng.below(5) { + if i > 0 { + doc.push(b','); + } + random_string(&mut rng, &mut doc); + doc.push(b':'); + random_value(&mut rng, 1, &mut doc); + } + doc.push(b'}'); + rows.push(doc); + } + let row_refs: Vec<&[u8]> = rows.iter().map(Vec::as_slice).collect(); + let got = parse_rows(&row_refs); + for (doc, got) in row_refs.iter().zip(&got) { + assert_eq!( + got, + &parse_reference(doc), + "diverged on {:?}", + String::from_utf8_lossy(doc) + ); + } + } +}