diff --git a/docs/cpp.md b/docs/cpp.md index 776e11d..f66fed1 100644 --- a/docs/cpp.md +++ b/docs/cpp.md @@ -103,7 +103,7 @@ A struct representing a parser. It has the following fields: - `autodetect` (`bool`): If the parser should autodetect requests and responses. Enabled by default. - `is_request` (`bool`): The configured or detected message type. Set this when `autodetect` is `false`. - `paused` (`bool`): If the parser is paused. -- `manage_unconsumed` (`bool`): If the parser should automatically copy and prepend unconsumed data. +- `manage_unconsumed` (`bool`): If the parser should automatically retain and prepend unconsumed data. Retention grows geometrically and incomplete lines resume scanning at the previous endpoint, so byte-fragmented input performs linear aggregate copying and scanning. - `suspend_after_headers` (`bool`): If parsing should stop after headers have completed. Disabled by default. - `continue_without_data` (`bool`): If the next execution of the parse loop should execute even if there is no more data. - `is_connect` (`bool`): If the current request used `CONNECT` method. diff --git a/docs/js.md b/docs/js.md index d43df97..bcba67d 100644 --- a/docs/js.md +++ b/docs/js.md @@ -375,7 +375,7 @@ Returns `true` if the parser is paused. #### `shouldManageUnconsumed(parser)` -Returns `true` if the parser should automatically copy and prepend unconsumed data. +Returns `true` if the parser should automatically retain and prepend unconsumed data. Retention grows geometrically and incomplete lines resume scanning at the previous endpoint, so byte-fragmented input performs linear aggregate copying and scanning. #### `getMaxStartLineLength(parser)` diff --git a/docs/rust.md b/docs/rust.md index 1ad19ae..f5136bf 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -104,7 +104,7 @@ A struct representing a parser. It has the following fields: - `autodetect` (`bool`): If the parser should autodetect requests and responses. Enabled by default. - `is_request` (`bool`): The configured or detected message type. Set this when `autodetect` is `false`. - `paused` (`bool`): If the parser is paused. -- `manage_unconsumed` (`bool`): If the parser should automatically copy and prepend unconsumed data. +- `manage_unconsumed` (`bool`): If the parser should automatically retain and prepend unconsumed data. Retention grows geometrically and incomplete lines resume scanning at the previous endpoint, so byte-fragmented input performs linear aggregate copying and scanning. - `suspend_after_headers` (`bool`): If parsing should stop after headers have completed. Disabled by default. - `continue_without_data` (`bool`): If the next execution of the parse loop should execute even if there is no more data. - `is_connect` (`bool`): If the current request used `CONNECT` method. diff --git a/parser/src/lib.rs b/parser/src/lib.rs index f5f0b50..08d9043 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -2,6 +2,7 @@ extern crate alloc; +use alloc::alloc::{Layout, alloc as allocate, dealloc as deallocate, handle_alloc_error, realloc}; use alloc::ffi::CString; use alloc::vec::Vec; use alloc::{boxed::Box, format}; @@ -12,6 +13,16 @@ use core::str; use core::{mem, ptr}; use core::{slice, slice::from_raw_parts}; +#[repr(C)] +struct RetainedInput { + capacity: usize, + scanned: usize, + copied: usize, + scan_work: usize, +} + +const RETAINED_INPUT_MIN_CAPACITY: usize = 64; + use milo_macros::generate; #[repr(C)] @@ -182,15 +193,156 @@ impl Parser { } } + #[inline(always)] + fn retained_header(&self) -> *mut RetainedInput { + unsafe { self.unconsumed.cast_mut().cast::().sub(1) } + } + + #[inline(always)] + fn retained_layout(capacity: usize) -> Layout { + Layout::from_size_align( + mem::size_of::() + .checked_add(capacity) + .expect("retained input capacity overflow"), + mem::align_of::(), + ) + .expect("invalid retained input layout") + } + + fn allocate_retained(&mut self, data: &[u8], scanned: usize, scan_work: usize) { + let capacity = data.len().max(RETAINED_INPUT_MIN_CAPACITY).next_power_of_two(); + let layout = Self::retained_layout(capacity); + let allocation = unsafe { allocate(layout) }; + if allocation.is_null() { + handle_alloc_error(layout); + } + + let header = allocation.cast::(); + let retained = unsafe { header.add(1).cast::() }; + unsafe { + header.write(RetainedInput { + capacity, + scanned, + copied: data.len(), + scan_work, + }); + ptr::copy_nonoverlapping(data.as_ptr(), retained, data.len()); + } + self.unconsumed = retained; + self.unconsumed_len = data.len(); + } + + pub(crate) fn append_unconsumed(&mut self, data: &[u8]) { + if data.is_empty() { + return; + } + if self.unconsumed_len == 0 { + self.allocate_retained(data, 0, 0); + return; + } + + let required = self + .unconsumed_len + .checked_add(data.len()) + .expect("retained input length overflow"); + let retained_start = self.unconsumed.addr(); + let retained_end = retained_start + .checked_add(self.unconsumed_len) + .expect("retained input address overflow"); + let data_start = data.as_ptr().addr(); + let data_end = data_start.checked_add(data.len()); + let aliased_offset = data_end + .filter(|end| data_start >= retained_start && *end <= retained_end) + .map(|_| data_start - retained_start); + let mut header = self.retained_header(); + let mut capacity = unsafe { (*header).capacity }; + if required > capacity { + while capacity < required { + capacity = capacity.checked_mul(2).expect("retained input capacity overflow"); + } + let old_capacity = unsafe { (*header).capacity }; + let old_layout = Self::retained_layout(old_capacity); + let allocation = unsafe { realloc(header.cast::(), old_layout, Self::retained_layout(capacity).size()) }; + if allocation.is_null() { + handle_alloc_error(Self::retained_layout(capacity)); + } + header = allocation.cast::(); + unsafe { + (*header).capacity = capacity; + (*header).copied += self.unconsumed_len; + } + self.unconsumed = unsafe { header.add(1).cast::() }; + } + + unsafe { + let source = aliased_offset.map_or(data.as_ptr(), |offset| self.unconsumed.add(offset)); + ptr::copy(source, self.unconsumed.cast_mut().add(self.unconsumed_len), data.len()); + (*header).copied += data.len(); + } + self.unconsumed_len = required; + } + + pub(crate) fn retain_unconsumed_suffix(&mut self, data: &[u8], scanned: usize, scan_work: usize) { + if data.is_empty() { + self.clear_unconsumed(); + return; + } + if self.unconsumed_len == 0 { + self.allocate_retained(data, scanned, scan_work); + return; + } + + let header = self.retained_header(); + if data.as_ptr() != self.unconsumed { + unsafe { + ptr::copy(data.as_ptr(), self.unconsumed.cast_mut(), data.len()); + (*header).copied += data.len(); + } + } + self.unconsumed_len = data.len(); + unsafe { + (*header).scanned = scanned.min(data.len()); + (*header).scan_work = scan_work; + } + } + + pub(crate) fn unconsumed_scan_state(&self) -> (usize, usize) { + if self.unconsumed_len == 0 { + (0, 0) + } else { + let header = self.retained_header(); + unsafe { ((*header).scanned.min(self.unconsumed_len), (*header).scan_work) } + } + } + + /// Returns deterministic counters for the managed-input implementation. + /// + /// This is intended for regression instrumentation. The tuple contains the + /// retained allocation capacity, the line-scan resume offset, cumulative + /// bytes copied, and cumulative bytes inspected while looking for line ends. + #[doc(hidden)] + pub fn managed_unconsumed_stats(&self) -> (usize, usize, usize, usize) { + if self.unconsumed_len == 0 { + (0, 0, 0, 0) + } else { + let header = self.retained_header(); + unsafe { + ( + (*header).capacity, + (*header).scanned.min(self.unconsumed_len), + (*header).copied, + (*header).scan_work, + ) + } + } + } + pub(crate) fn clear_unconsumed(&mut self) { if self.unconsumed_len > 0 { - // SAFETY: Retained input is allocated as a boxed slice with exactly this - // length, and the parser owns it until replacement, reset, or destruction. + let header = self.retained_header(); + let capacity = unsafe { (*header).capacity }; unsafe { - drop(Box::from_raw(ptr::slice_from_raw_parts_mut( - self.unconsumed as *mut c_uchar, - self.unconsumed_len, - ))); + deallocate(header.cast::(), Self::retained_layout(capacity)); } } self.unconsumed = ptr::null(); diff --git a/parser/src/matchers.rs b/parser/src/matchers.rs index 0687d26..c221fd5 100644 --- a/parser/src/matchers.rs +++ b/parser/src/matchers.rs @@ -24,12 +24,36 @@ pub fn ensure_valid_line(data: &[u8], cr: usize, available: usize) -> MatchResul } #[inline(always)] -pub fn find_cr(data: &[u8], available: usize) -> Option { - if available == 0 { +pub fn find_cr(data: &[u8], available: usize, cursor: &mut usize, scan_work: &mut usize) -> Option { + let start = (*cursor).min(available); + let found = if start == available { None } else { - find_char(data, 0, available - 1, b'\r') - } + find_char(data, start, available - 1, b'\r') + }; + let end = found.unwrap_or(available); + *scan_work += end - start + usize::from(found.is_some()); + *cursor = end; + found +} + +#[inline(always)] +pub fn find_header_line_end_from( + ptr: *const u8, + len: usize, + cursor: &mut usize, + scan_work: &mut usize, +) -> HeaderLineScanResult { + let start = (*cursor).min(len); + let result = unsafe { find_header_line_end(ptr.add(start), len - start) }; + let (result, end, found) = match result { + HeaderLineScanResult::Cr(index) => (HeaderLineScanResult::Cr(start + index), start + index, true), + HeaderLineScanResult::Invalid(index) => (HeaderLineScanResult::Invalid(start + index), start + index, true), + HeaderLineScanResult::Incomplete => (HeaderLineScanResult::Incomplete, len, false), + }; + *scan_work += end - start + usize::from(found); + *cursor = end; + result } #[inline(always)] diff --git a/parser/src/parse.rs b/parser/src/parse.rs index 80105a0..5337460 100644 --- a/parser/src/parse.rs +++ b/parser/src/parse.rs @@ -2,8 +2,7 @@ extern crate alloc; -use alloc::vec::Vec; -use alloc::{boxed::Box, format}; +use alloc::format; use core::cell::{Cell, RefCell}; use core::ffi::{c_char, c_uchar, c_void}; use core::fmt::Debug; @@ -49,22 +48,20 @@ impl Parser { let input = unsafe { from_raw_parts(input, limit) }; - // Set the data to analyze, prepending unconsumed data from previous iteration - // if needed + // Set the data to analyze, appending to the amortized retained allocation + // when a previous call ended in the middle of a line. let mut limit = limit; - let aggregate: Vec; - let unconsumed_len = self.unconsumed_len; - - let mut data = if self.manage_unconsumed && unconsumed_len > 0 { - unsafe { - limit += unconsumed_len; - let unconsumed = from_raw_parts(self.unconsumed, unconsumed_len); + let had_unconsumed = self.manage_unconsumed && self.unconsumed_len > 0; + let (mut line_scan_cursor, mut line_scan_work) = if had_unconsumed { + self.unconsumed_scan_state() + } else { + (0, 0) + }; - // Rebuild a contiguous view so state handlers can scan across parse-call - // boundaries. - aggregate = [unconsumed, input].concat(); - &aggregate[..] - } + let mut data = if had_unconsumed { + self.append_unconsumed(input); + limit = self.unconsumed_len; + unsafe { from_raw_parts(self.unconsumed, limit) } } else { input }; @@ -200,7 +197,7 @@ impl Parser { } STATE_REQUEST_LINE => { - match find_cr(data, available) { + match find_cr(data, available, &mut line_scan_cursor, &mut line_scan_work) { // // RFC 9112 section 3 Some(cr) => { match ensure_valid_line(data, cr, available) { @@ -348,7 +345,7 @@ impl Parser { // RFC 9112 section 4 STATE_STATUS_LINE => { - match find_cr(data, available) { + match find_cr(data, available, &mut line_scan_cursor, &mut line_scan_work) { Some(cr) => { match ensure_valid_line(data, cr, available) { MatchResult::Continue => {} @@ -457,7 +454,7 @@ impl Parser { } STATE_HEADER => { - match find_header_line_end(data.as_ptr(), available) { + match find_header_line_end_from(data.as_ptr(), available, &mut line_scan_cursor, &mut line_scan_work) { HeaderLineScanResult::Cr(cr) => { match ensure_valid_line(data, cr, available) { MatchResult::Continue => {} @@ -1009,7 +1006,7 @@ impl Parser { // RFC 9112 section 7.1 STATE_CHUNK_HEADER => { - match find_cr(data, available) { + match find_cr(data, available, &mut line_scan_cursor, &mut line_scan_work) { Some(cr) => { match ensure_valid_line(data, cr, available) { MatchResult::Continue => {} @@ -1091,7 +1088,7 @@ impl Parser { } STATE_CHUNK_EXTENSIONS => { - match find_cr(data, available) { + match find_cr(data, available, &mut line_scan_cursor, &mut line_scan_work) { Some(cr) => { match ensure_valid_line(data, cr, available) { MatchResult::Continue => {} @@ -1291,7 +1288,7 @@ impl Parser { // RFC 9112 section 7.1.2 STATE_TRAILER => { - match find_header_line_end(data.as_ptr(), available) { + match find_header_line_end_from(data.as_ptr(), available, &mut line_scan_cursor, &mut line_scan_work) { HeaderLineScanResult::Cr(cr) => { match ensure_valid_line(data, cr, available) { MatchResult::Continue => {} @@ -1397,6 +1394,7 @@ impl Parser { self.position += advanced; data = &data[advanced..]; available -= advanced; + line_scan_cursor = 0; #[cfg(any(debug_assertions, feature = "debug"))] if self.debug { @@ -1451,14 +1449,10 @@ impl Parser { self.parsed += consumed as u64; if self.manage_unconsumed { - self.clear_unconsumed(); - - // Use a boxed slice so the length also determines the allocation layout - // when releasing retained input; Vec capacity is not stored in the ABI. if consumed < limit { - let retained = data.to_vec().into_boxed_slice(); - self.unconsumed_len = retained.len(); - self.unconsumed = Box::into_raw(retained) as *const c_uchar; + self.retain_unconsumed_suffix(data, line_scan_cursor, line_scan_work); + } else { + self.clear_unconsumed(); } } diff --git a/parser/src/wasm.rs b/parser/src/wasm.rs index 73d380c..33eefbe 100644 --- a/parser/src/wasm.rs +++ b/parser/src/wasm.rs @@ -187,6 +187,18 @@ pub fn get_state(parser: *const c_void) -> u8 { unsafe { (*(parser as *const Par #[unsafe(no_mangle)] pub fn get_position(parser: *const c_void) -> usize { unsafe { (*(parser as *const Parser)).position } } +// Get deterministic managed-input copy instrumentation. +#[unsafe(no_mangle)] +pub fn get_managed_unconsumed_copy_bytes(parser: *const c_void) -> usize { + unsafe { (*(parser as *const Parser)).managed_unconsumed_stats().2 } +} + +// Get deterministic managed-line scan instrumentation. +#[unsafe(no_mangle)] +pub fn get_managed_unconsumed_scan_bytes(parser: *const c_void) -> usize { + unsafe { (*(parser as *const Parser)).managed_unconsumed_stats().3 } +} + // Get the parser parsed property. #[unsafe(no_mangle)] pub fn get_parsed(parser: *const c_void) -> u64 { unsafe { (*(parser as *const Parser)).parsed } } diff --git a/parser/tests/issues.rs b/parser/tests/issues.rs index e287557..f5d808f 100644 --- a/parser/tests/issues.rs +++ b/parser/tests/issues.rs @@ -35,9 +35,12 @@ unsafe impl GlobalAlloc for TrackingAllocator { if TRACK_MEMORY.try_with(Cell::get).unwrap_or(false) { let _ = LIVE_BYTES.try_with(|bytes| bytes.set(bytes.get() - layout.size() as isize)); } - if WATCHED_POINTER - .compare_exchange(pointer as usize, 0, Ordering::SeqCst, Ordering::SeqCst) - .is_ok() + let watched = WATCHED_POINTER.load(Ordering::SeqCst); + if watched >= pointer as usize + && watched < pointer as usize + layout.size() + && WATCHED_POINTER + .compare_exchange(watched, 0, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() { FREED_SIZE.store(layout.size(), Ordering::SeqCst); } @@ -137,15 +140,17 @@ fn issue_16__release_retained_input() { assert_eq!(parser.parse(prefix.as_ptr(), prefix.len()), 16); assert_eq!(parser.unconsumed_len, 8); - // Replacing an incomplete header must free the previous allocation, even + // Appending an incomplete header reuses the retained allocation, including // when no new input arrives. + let retained = parser.unconsumed; for input in [b"a".as_slice(), b"", b"b"] { let previous_len = parser.unconsumed_len; FREED_SIZE.store(0, Ordering::SeqCst); WATCHED_POINTER.store(parser.unconsumed as usize, Ordering::SeqCst); assert_eq!(parser.parse(input.as_ptr(), input.len()), 0); - assert_eq!(WATCHED_POINTER.load(Ordering::SeqCst), 0); - assert_eq!(FREED_SIZE.load(Ordering::SeqCst), previous_len); + assert_eq!(WATCHED_POINTER.load(Ordering::SeqCst), retained as usize); + assert_eq!(FREED_SIZE.load(Ordering::SeqCst), 0); + assert_eq!(parser.unconsumed, retained); assert_eq!(parser.unconsumed_len, previous_len + input.len()); } @@ -154,7 +159,7 @@ fn issue_16__release_retained_input() { let suffix = b"\r\n\r\n"; assert_eq!(parser.parse(suffix.as_ptr(), suffix.len()), previous_len + suffix.len()); assert_eq!(WATCHED_POINTER.load(Ordering::SeqCst), 0); - assert_eq!(FREED_SIZE.load(Ordering::SeqCst), previous_len); + assert!(FREED_SIZE.load(Ordering::SeqCst) >= previous_len); assert!(parser.unconsumed.is_null()); assert_eq!(parser.unconsumed_len, 0); assert_eq!(parser.error_code, ERROR_NONE); @@ -182,8 +187,74 @@ fn issue_16__release_retained_input() { parser = Parser::new(); } assert_eq!(WATCHED_POINTER.load(Ordering::SeqCst), 0); - assert_eq!(FREED_SIZE.load(Ordering::SeqCst), previous_len); + assert!(FREED_SIZE.load(Ordering::SeqCst) >= previous_len); parser.manage_unconsumed = true; } } } + +#[test] +#[allow(non_snake_case)] +fn sec_10__retained_input_may_be_reused_as_input_across_growth() { + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.manage_unconsumed = true; + + let prefix = b"GET / HTTP/1.1\r\nX: aaaaaaaa"; + assert_eq!(parser.parse(prefix.as_ptr(), prefix.len()), 16); + + while parser.unconsumed_len < 128 { + let retained = parser.unconsumed; + let retained_len = parser.unconsumed_len; + assert_eq!(parser.parse(retained, retained_len), 0); + assert_eq!(parser.unconsumed_len, retained_len * 2); + assert_eq!(parser.error_code, ERROR_NONE); + } +} + +#[test] +#[allow(non_snake_case)] +fn sec_10__byte_fragmented_managed_line_has_linear_work() { + let mut parser = Parser::new(); + parser.autodetect = false; + parser.is_request = true; + parser.manage_unconsumed = true; + + let prefix = b"GET / HTTP/1.1\r\nX-Long: "; + assert_eq!(parser.parse(prefix.as_ptr(), prefix.len()), 16); + let initial_len = parser.unconsumed_len; + let (_, initial_scanned, initial_copied, initial_scan_work) = parser.managed_unconsumed_stats(); + assert_eq!(initial_scanned, initial_len); + + const FRAGMENTS: usize = 4096; + for _ in 0..FRAGMENTS { + assert_eq!(parser.parse(b"a".as_ptr(), 1), 0); + } + + let retained_len = initial_len + FRAGMENTS; + let (capacity, scanned, copied, scan_work) = parser.managed_unconsumed_stats(); + assert_eq!(parser.unconsumed_len, retained_len); + assert_eq!(scanned, retained_len); + assert_eq!(scan_work - initial_scan_work, FRAGMENTS); + assert!(capacity >= retained_len); + assert!(capacity < retained_len * 2); + assert!(copied - initial_copied < FRAGMENTS * 3); + + let cr = b"\r"; + assert_eq!(parser.parse(cr.as_ptr(), cr.len()), 0); + assert_eq!(parser.unconsumed_len, retained_len + 1); + + // Resume from the saved CR when LF arrives in a later invocation. + let suffix = b"\n\r\n"; + assert_eq!( + parser.parse(suffix.as_ptr(), suffix.len()), + retained_len + 1 + suffix.len() + ); + assert_eq!(parser.error_code, ERROR_NONE); + assert_eq!( + parser.parsed, + (prefix.len() + FRAGMENTS + cr.len() + suffix.len()) as u64 + ); + assert!(parser.unconsumed.is_null()); +} diff --git a/parser/wasm/test/issues.test.js b/parser/wasm/test/issues.test.js index 3ee83c1..3af7253 100644 --- a/parser/wasm/test/issues.test.js +++ b/parser/wasm/test/issues.test.js @@ -57,6 +57,57 @@ it('issue-16 - error_retries_do_not_accumulate_input', () => { } }) +it('sec-10 - byte_fragmented_managed_line_has_linear_work', () => { + const milo = setup() + const parser = milo.create() + const prefix = Buffer.from('GET / HTTP/1.1\r\nX-Long: ') + const suffix = Buffer.from('\n\r\n') + const ptr = milo.alloc(prefix.length) + try { + milo.setShouldAutodetect(parser, false) + milo.setIsRequest(parser, true) + milo.setShouldManageUnconsumed(parser, true) + + let memory = new Uint8Array(milo.memory.buffer) + memory.set(prefix, ptr) + assert.equal(milo.parse(parser, ptr, prefix.length), 16) + const view = new DataView(milo.memory.buffer) + const initialLength = view.getUint32(parser + milo.ParserFields.UNCONSUMED_LEN, true) + const initialCopied = milo.getManagedUnconsumedCopyBytes(parser) + const initialScanWork = milo.getManagedUnconsumedScanBytes(parser) + + const fragments = 4096 + for (let i = 0; i < fragments; i++) { + memory = new Uint8Array(milo.memory.buffer) + memory[ptr] = 0x61 + assert.equal(milo.parse(parser, ptr, 1), 0) + } + + const retainedLength = initialLength + fragments + assert.equal( + new DataView(milo.memory.buffer).getUint32(parser + milo.ParserFields.UNCONSUMED_LEN, true), + retainedLength + ) + assert.equal(milo.getManagedUnconsumedScanBytes(parser) - initialScanWork, fragments) + assert.ok(milo.getManagedUnconsumedCopyBytes(parser) - initialCopied < fragments * 3) + + memory = new Uint8Array(milo.memory.buffer) + memory[ptr] = 0x0d + assert.equal(milo.parse(parser, ptr, 1), 0) + + // Resume from the saved CR when LF arrives in a later invocation. + memory = new Uint8Array(milo.memory.buffer) + memory.set(suffix, ptr) + assert.equal(milo.parse(parser, ptr, suffix.length), retainedLength + 1 + suffix.length) + assert.equal(milo.getErrorCode(parser), milo.ERROR_NONE) + assert.equal(milo.getParsed(parser), BigInt(prefix.length + fragments + 1 + suffix.length)) + assert.equal(new DataView(milo.memory.buffer).getUint32(parser + milo.ParserFields.UNCONSUMED_LEN, true), 0) + } finally { + milo.dealloc(ptr, prefix.length) + milo.destroy(parser) + } +}) + it('issue-26 - preserve_events_before_error', () => { const suffix = 'HTTP/9.9 garbage\r\n\r\n' for (const chunked of [false, true]) { diff --git a/scripts/src/bin/postbuild-wasm.rs b/scripts/src/bin/postbuild-wasm.rs index 33a2482..d7fa4ed 100644 --- a/scripts/src/bin/postbuild-wasm.rs +++ b/scripts/src/bin/postbuild-wasm.rs @@ -45,6 +45,16 @@ const GETTERS: &[(&str, GetterType, &str)] = &[ ("shouldSkipBody", GetterType::Bool, "should_skip_body"), ("getState", GetterType::Number, "get_state"), ("getPosition", GetterType::Number, "get_position"), + ( + "getManagedUnconsumedCopyBytes", + GetterType::Number, + "get_managed_unconsumed_copy_bytes", + ), + ( + "getManagedUnconsumedScanBytes", + GetterType::Number, + "get_managed_unconsumed_scan_bytes", + ), ("getParsed", GetterType::BigInt, "get_parsed"), ("getErrorCode", GetterType::Number, "get_error_code"), ("getMethod", GetterType::Number, "get_method"),