• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kaidokert / picojson-rs / 16211723222

11 Jul 2025 04:20AM UTC coverage: 93.666% (+0.3%) from 93.391%
16211723222

Pull #49

github

kaidokert
Implement surrogate pairs
Pull Request #49: Implement surrogate pairs

214 of 222 new or added lines in 3 files covered. (96.4%)

123 existing lines in 2 files now uncovered.

4481 of 4784 relevant lines covered (93.67%)

565.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.7
/picojson/src/stream_buffer.rs
1
// SPDX-License-Identifier: Apache-2.0
2

3
use crate::ParseError;
4

5
/// Error types for StreamBuffer operations
6
#[derive(Debug, PartialEq)]
7
pub enum StreamBufferError {
8
    /// Buffer is full and cannot accommodate more data
9
    BufferFull,
10
    /// Attempted to read beyond available data
11
    EndOfData,
12
    /// An unexpected error occurred.
13
    Unexpected,
14
    /// Invalid slice bounds provided for string extraction
15
    InvalidSliceBounds,
16
}
17

18
/// StreamBuffer manages a single buffer for both input and escape processing
19
///
20
/// Key design principles:
21
/// - Reader fills unused portions of buffer directly
22
/// - Unescaped content is copied to buffer start when needed
23
/// - Zero-copy string extraction when no escapes are present
24
/// - Guaranteed space for escape processing (unescaped ≤ escaped)
25
pub struct StreamBuffer<'a> {
26
    /// The entire buffer slice
27
    buffer: &'a mut [u8],
28
    /// Current position where tokenizer is reading
29
    tokenize_pos: usize,
30
    /// End of valid data from Reader (buffer[0..data_end] contains valid data)
31
    data_end: usize,
32
    /// Length of unescaped content at buffer start (0 if no unescaping active)
33
    unescaped_len: usize,
34
}
35

36
impl<'a> StreamBuffer<'a> {
37
    /// Panic-free copy_within implementation that handles overlapping ranges
38
    /// Based on memmove behavior but without panic machinery
39
    fn safe_copy_within(&mut self, src_start: usize, src_end: usize, dest: usize) {
619✔
40
        let count = src_end.saturating_sub(src_start);
619✔
41

42
        // Early return if nothing to copy or bounds are invalid
43
        if count == 0
619✔
44
            || src_start >= self.buffer.len()
619✔
45
            || src_end > self.buffer.len()
619✔
46
            || dest >= self.buffer.len()
619✔
47
        {
48
            return;
×
49
        }
619✔
50

51
        // Ensure dest + count doesn't exceed buffer
52
        let max_copy = (self.buffer.len().saturating_sub(dest)).min(count);
619✔
53
        if max_copy == 0 {
619✔
54
            return;
×
55
        }
619✔
56

57
        let iterator: &mut dyn Iterator<Item = usize> = if dest <= src_start {
619✔
58
            &mut (0..max_copy)
619✔
59
        } else {
60
            &mut (0..max_copy).rev()
×
61
        };
62

63
        for i in iterator {
7,271✔
64
            match (
65
                self.buffer.get(src_start.wrapping_add(i)).copied(),
6,652✔
66
                self.buffer.get_mut(dest.wrapping_add(i)),
6,652✔
67
            ) {
68
                (Some(src_byte), Some(dest_slot)) => {
6,652✔
69
                    *dest_slot = src_byte;
6,652✔
70
                }
6,652✔
UNCOV
71
                _ => {}
×
72
            }
73
        }
74
    }
619✔
75
    /// Create a new StreamBuffer with the given buffer slice
76
    pub fn new(buffer: &'a mut [u8]) -> Self {
1,095✔
77
        Self {
1,095✔
78
            buffer,
1,095✔
79
            tokenize_pos: 0,
1,095✔
80
            data_end: 0,
1,095✔
81
            unescaped_len: 0,
1,095✔
82
        }
1,095✔
83
    }
1,095✔
84

85
    /// Get the current byte at tokenize position
86
    pub fn current_byte(&self) -> Result<u8, StreamBufferError> {
18,315✔
87
        if self.tokenize_pos >= self.data_end {
18,315✔
88
            return Err(StreamBufferError::EndOfData);
1✔
89
        }
18,314✔
90
        self.buffer
18,314✔
91
            .get(self.tokenize_pos)
18,314✔
92
            .copied()
18,314✔
93
            .ok_or(StreamBufferError::EndOfData)
18,314✔
94
    }
18,315✔
95

96
    /// Advance the tokenize position by one byte
97
    pub fn advance(&mut self) -> Result<(), StreamBufferError> {
18,344✔
98
        if self.tokenize_pos >= self.data_end {
18,344✔
99
            return Err(StreamBufferError::EndOfData);
1✔
100
        }
18,343✔
101
        self.tokenize_pos = self.tokenize_pos.wrapping_add(1);
18,343✔
102
        Ok(())
18,343✔
103
    }
18,344✔
104

105
    /// Get remaining bytes available for reading
106
    pub fn remaining_bytes(&self) -> usize {
356✔
107
        self.data_end.saturating_sub(self.tokenize_pos)
356✔
108
    }
356✔
109

110
    /// Get slice for Reader to fill with new data
111
    /// Returns None if no space available
112
    pub fn get_fill_slice(&mut self) -> Option<&mut [u8]> {
8,178✔
113
        if self.data_end >= self.buffer.len() {
8,178✔
114
            return None;
1,793✔
115
        }
6,385✔
116
        self.buffer.get_mut(self.data_end..)
6,385✔
117
    }
8,178✔
118

119
    /// Compact buffer by moving unprocessed data from a given start offset to the beginning.
120
    ///
121
    /// # Arguments
122
    /// * `start_offset` - The position from which to preserve data.
123
    ///
124
    /// Returns the offset by which data was moved.
125
    pub fn compact_from(&mut self, start_offset: usize) -> Result<usize, StreamBufferError> {
1,798✔
126
        if start_offset == 0 {
1,798✔
127
            // Already at start, no compaction possible
128
            return Ok(0);
530✔
129
        }
1,268✔
130

131
        let offset = start_offset;
1,268✔
132

133
        if start_offset >= self.data_end {
1,268✔
134
            // All data has been processed, reset to start
135
            self.tokenize_pos = 0;
649✔
136
            self.data_end = 0;
649✔
137
            return Ok(offset);
649✔
138
        }
619✔
139

140
        // Move unprocessed data to start of buffer
141
        let remaining_data = self.data_end.saturating_sub(start_offset);
619✔
142

143
        // Copy existing content if there is any - EXACT same pattern as start_unescaping_with_copy
144
        if self.data_end > start_offset && start_offset < self.data_end {
619✔
145
            let span_len = remaining_data;
619✔
146

147
            // Ensure the span fits in the buffer - return error instead of silent truncation
148
            if span_len > self.buffer.len() {
619✔
UNCOV
149
                return Err(StreamBufferError::BufferFull);
×
150
            }
619✔
151

152
            let src_range = start_offset..start_offset.wrapping_add(span_len);
619✔
153
            if src_range.end > self.buffer.len() {
619✔
UNCOV
154
                return Err(StreamBufferError::InvalidSliceBounds);
×
155
            }
619✔
156

157
            // Copy within the same buffer: move data from [start_offset..end] to [0..span_len]
158
            // Use our panic-free copy implementation
159
            self.safe_copy_within(src_range.start, src_range.end, 0);
619✔
UNCOV
160
        }
×
161

162
        // Update positions
163
        self.tokenize_pos = self.tokenize_pos.saturating_sub(offset);
619✔
164
        self.data_end = remaining_data;
619✔
165

166
        Ok(offset)
619✔
167
    }
1,798✔
168

169
    /// Mark that Reader filled `bytes_read` bytes
170
    pub fn mark_filled(&mut self, bytes_read: usize) -> Result<(), StreamBufferError> {
6,382✔
171
        let new_data_end = self.data_end.wrapping_add(bytes_read);
6,382✔
172
        if new_data_end > self.buffer.len() {
6,382✔
UNCOV
173
            return Err(StreamBufferError::Unexpected);
×
174
        }
6,382✔
175
        self.data_end = new_data_end;
6,382✔
176
        Ok(())
6,382✔
177
    }
6,382✔
178

179
    /// Start unescaping and copy existing content from a range in the buffer
180
    /// This handles the common case of starting escape processing partway through a string
181
    pub fn start_unescaping_with_copy(
347✔
182
        &mut self,
347✔
183
        max_escaped_len: usize,
347✔
184
        copy_start: usize,
347✔
185
        copy_end: usize,
347✔
186
    ) -> Result<(), StreamBufferError> {
347✔
187
        // Clear any previous unescaped content
188
        self.unescaped_len = 0;
347✔
189

190
        // Ensure we have space at the start for unescaping
191
        if max_escaped_len > self.buffer.len() {
347✔
UNCOV
192
            return Err(StreamBufferError::BufferFull);
×
193
        }
347✔
194

195
        // Copy existing content if there is any
196
        if copy_end > copy_start && copy_start < self.data_end {
347✔
197
            let span_len = copy_end.saturating_sub(copy_start);
272✔
198

199
            // Ensure the span fits in the buffer - return error instead of silent truncation
200
            if span_len > self.buffer.len() {
272✔
201
                return Err(StreamBufferError::BufferFull);
1✔
202
            }
271✔
203

204
            let src_range = copy_start..copy_start.wrapping_add(span_len);
271✔
205
            if src_range.end > self.buffer.len() {
271✔
UNCOV
206
                return Err(StreamBufferError::InvalidSliceBounds);
×
207
            }
271✔
208

209
            // Copy within the same buffer: move data from [copy_start..copy_end] to [0..span_len]
210
            // Use copy_within to handle overlapping ranges safely
211
            self.buffer.copy_within(src_range, 0);
271✔
212
            self.unescaped_len = span_len;
271✔
213
        }
75✔
214

215
        Ok(())
346✔
216
    }
347✔
217

218
    /// Get the unescaped content slice
219
    pub fn get_unescaped_slice(&self) -> Result<&[u8], StreamBufferError> {
181✔
220
        if self.unescaped_len == 0 {
181✔
221
            return Err(StreamBufferError::InvalidSliceBounds);
1✔
222
        }
180✔
223
        self.buffer
180✔
224
            .get(0..self.unescaped_len)
180✔
225
            .ok_or(StreamBufferError::Unexpected)
180✔
226
    }
181✔
227

228
    /// Clear unescaped content (call after yielding unescaped string)
229
    pub fn clear_unescaped(&mut self) {
150✔
230
        self.unescaped_len = 0;
150✔
231
    }
150✔
232

233
    /// Get current tokenize position (for string start tracking)
234
    pub fn current_position(&self) -> usize {
3,214✔
235
        self.tokenize_pos
3,214✔
236
    }
3,214✔
237

238
    /// Check if buffer is empty (no more data to process)
239
    pub fn is_empty(&self) -> bool {
38,116✔
240
        self.tokenize_pos >= self.data_end
38,116✔
241
    }
38,116✔
242

243
    /// Check if we have unescaped content ready
244
    pub fn has_unescaped_content(&self) -> bool {
12,932✔
245
        self.unescaped_len > 0
12,932✔
246
    }
12,932✔
247

248
    /// Append a single byte to the unescaped content
249
    pub fn append_unescaped_byte(&mut self, byte: u8) -> Result<(), StreamBufferError> {
1,422✔
250
        if let Some(b) = self.buffer.get_mut(self.unescaped_len) {
1,422✔
251
            *b = byte;
1,421✔
252
            self.unescaped_len = self.unescaped_len.wrapping_add(1);
1,421✔
253
            Ok(())
1,421✔
254
        } else {
255
            Err(StreamBufferError::BufferFull)
1✔
256
        }
257
    }
1,422✔
258

259
    /// Get a string slice from the buffer (zero-copy)
260
    /// Used for strings without escapes
261
    pub fn get_string_slice(&self, start: usize, end: usize) -> Result<&[u8], StreamBufferError> {
875✔
262
        if start > end || end > self.data_end {
875✔
UNCOV
263
            return Err(StreamBufferError::InvalidSliceBounds);
×
264
        }
875✔
265
        self.buffer
875✔
266
            .get(start..end)
875✔
267
            .ok_or(StreamBufferError::InvalidSliceBounds)
875✔
268
    }
875✔
269
}
270

271
#[cfg(test)]
272
mod tests {
273
    use super::*;
274

275
    #[test]
276
    fn test_lifetime_expectations() {
1✔
277
        // This test demonstrates how StreamBuffer lifetimes should work
278
        let mut buffer = [0u8; 100];
1✔
279
        let mut stream_buffer = StreamBuffer::new(&mut buffer);
1✔
280

281
        // Simulate some data being in the buffer
282
        let test_data = b"hello world";
1✔
283
        stream_buffer.buffer[0..test_data.len()].copy_from_slice(test_data);
1✔
284
        stream_buffer.data_end = test_data.len();
1✔
285

286
        // Test that we can get buffer data
287

288
        // Test unescaped content - add some unescaped data
289
        stream_buffer.unescaped_len = 3;
1✔
290
        stream_buffer.buffer[0..3].copy_from_slice(b"abc");
1✔
291

292
        let unescaped_slice = stream_buffer.get_unescaped_slice().unwrap();
1✔
293
        assert_eq!(unescaped_slice, b"abc");
1✔
294

295
        // The key expectation: these slices should live as long as the original buffer
296
        // and be usable to create String::Borrowed(&'buffer str) and String::Unescaped(&'buffer str)
297
    }
1✔
298

299
    #[test]
300
    fn test_new_stream_buffer() {
1✔
301
        let mut buffer = [0u8; 100];
1✔
302
        let db = StreamBuffer::new(&mut buffer);
1✔
303

304
        assert_eq!(db.tokenize_pos, 0);
1✔
305
        assert_eq!(db.data_end, 0);
1✔
306
        assert_eq!(db.unescaped_len, 0);
1✔
307
        assert!(db.is_empty());
1✔
308
    }
1✔
309

310
    #[test]
311
    fn test_fill_and_advance() {
1✔
312
        let mut buffer = [0u8; 100];
1✔
313
        let mut db = StreamBuffer::new(&mut buffer);
1✔
314

315
        // Fill with some data
316
        {
1✔
317
            let fill_slice = db.get_fill_slice().unwrap();
1✔
318
            fill_slice[0..5].copy_from_slice(b"hello");
1✔
319
        }
1✔
320
        db.mark_filled(5).unwrap();
1✔
321

322
        assert_eq!(db.data_end, 5);
1✔
323
        assert_eq!(db.remaining_bytes(), 5);
1✔
324

325
        // Read bytes
326
        assert_eq!(db.current_byte().unwrap(), b'h');
1✔
327
        db.advance().unwrap();
1✔
328
        assert_eq!(db.current_byte().unwrap(), b'e');
1✔
329
        assert_eq!(db.remaining_bytes(), 4);
1✔
330
    }
1✔
331

332
    #[test]
333
    fn test_error_conditions() {
1✔
334
        let mut buffer = [0u8; 10];
1✔
335
        let mut db = StreamBuffer::new(&mut buffer);
1✔
336

337
        // EndOfData errors
338
        assert_eq!(db.current_byte().unwrap_err(), StreamBufferError::EndOfData);
1✔
339
        assert_eq!(db.advance().unwrap_err(), StreamBufferError::EndOfData);
1✔
340

341
        // No unescaped content
342
        assert!(db.get_unescaped_slice().is_err());
1✔
343
    }
1✔
344

345
    #[test]
346
    fn test_buffer_full_scenario() {
1✔
347
        // Test what happens when buffer gets completely full
348
        let mut buffer = [0u8; 10];
1✔
349
        let mut db = StreamBuffer::new(&mut buffer);
1✔
350

351
        // Fill buffer completely
352
        {
1✔
353
            let fill_slice = db.get_fill_slice().unwrap();
1✔
354
            fill_slice.copy_from_slice(b"0123456789");
1✔
355
        }
1✔
356
        db.mark_filled(10).unwrap();
1✔
357

358
        // No more space for filling
359
        assert!(db.get_fill_slice().is_none());
1✔
360

361
        // We can still read from buffer
362
        assert_eq!(db.current_byte().unwrap(), b'0');
1✔
363
        assert_eq!(db.remaining_bytes(), 10);
1✔
364
    }
1✔
365

366
    #[test]
367
    fn test_minimal_buffer_with_long_token() {
1✔
368
        // Test very small buffer with a token that doesn't fit
369
        let mut buffer = [0u8; 8]; // Very small buffer
1✔
370
        let mut db = StreamBuffer::new(&mut buffer);
1✔
371

372
        // Try to put a string that's almost as big as the buffer
373
        {
1✔
374
            let fill_slice = db.get_fill_slice().unwrap();
1✔
375
            fill_slice[0..6].copy_from_slice(b"\"hello"); // Start of a long string, no closing quote
1✔
376
        }
1✔
377
        db.mark_filled(6).unwrap();
1✔
378

379
        // Advance through the data
380
        for _ in 0..6 {
7✔
381
            db.advance().unwrap();
6✔
382
        }
6✔
383

384
        // Now buffer is exhausted but we don't have a complete token
385
        assert!(db.is_empty());
1✔
386
        assert_eq!(db.remaining_bytes(), 0);
1✔
387

388
        // This simulates the scenario where we need more data but can't fit it
389
        // The parser would need to handle this by buffering the incomplete token
390
    }
1✔
391

392
    #[test]
393
    fn test_reader_returns_zero_bytes() {
1✔
394
        let mut buffer = [0u8; 20];
1✔
395
        let mut db = StreamBuffer::new(&mut buffer);
1✔
396

397
        // Simulate Reader returning 0 bytes (EOF)
398
        {
399
            let fill_slice = db.get_fill_slice().unwrap();
1✔
400
            assert_eq!(fill_slice.len(), 20);
1✔
401
            // Reader returns 0 bytes - simulating EOF or no data available
402
        }
403
        db.mark_filled(0).unwrap(); // Reader returned 0
1✔
404

405
        assert!(db.is_empty());
1✔
406
        assert_eq!(db.data_end, 0);
1✔
407
        assert_eq!(db.remaining_bytes(), 0);
1✔
408

409
        // Should still be able to get fill slice for next attempt
410
        let fill_slice = db.get_fill_slice().unwrap();
1✔
411
        assert_eq!(fill_slice.len(), 20);
1✔
412
    }
1✔
413

414
    #[test]
415
    fn test_boundary_conditions() {
1✔
416
        let mut buffer = [0u8; 3]; // Absolute minimum
1✔
417
        let mut db = StreamBuffer::new(&mut buffer);
1✔
418

419
        // Can't even hold a proper JSON token, but should not crash
420
        {
1✔
421
            let fill_slice = db.get_fill_slice().unwrap();
1✔
422
            fill_slice.copy_from_slice(b"\"a\"");
1✔
423
        }
1✔
424
        db.mark_filled(3).unwrap();
1✔
425

426
        // Should be able to read through it
427
        assert_eq!(db.current_byte().unwrap(), b'"');
1✔
428
        db.advance().unwrap();
1✔
429
        assert_eq!(db.current_byte().unwrap(), b'a');
1✔
430
        db.advance().unwrap();
1✔
431
        assert_eq!(db.current_byte().unwrap(), b'"');
1✔
432
        db.advance().unwrap();
1✔
433

434
        assert!(db.is_empty());
1✔
435
    }
1✔
436

437
    #[test]
438
    fn test_start_unescaping_with_copy_span_too_large() {
1✔
439
        let mut buffer = [0u8; 10]; // Small buffer
1✔
440
        let mut db = StreamBuffer::new(&mut buffer);
1✔
441

442
        // Fill buffer with some data
443
        {
1✔
444
            let fill_slice = db.get_fill_slice().unwrap();
1✔
445
            fill_slice.copy_from_slice(b"0123456789");
1✔
446
        }
1✔
447
        db.mark_filled(10).unwrap();
1✔
448

449
        // Try to copy a span that's larger than the entire buffer
450
        let copy_start = 0;
1✔
451
        let copy_end = 15; // This span (15 bytes) is larger than buffer (10 bytes)
1✔
452
        let max_escaped_len = 5; // This is fine
1✔
453

454
        // Should return BufferFull error instead of silently truncating
455
        let result = db.start_unescaping_with_copy(max_escaped_len, copy_start, copy_end);
1✔
456
        assert_eq!(result.unwrap_err(), StreamBufferError::BufferFull);
1✔
457

458
        // Test boundary case: span exactly equals buffer size should work
459
        let copy_end_exact = 10; // Span of exactly 10 bytes (buffer size)
1✔
460
        let result = db.start_unescaping_with_copy(max_escaped_len, 0, copy_end_exact);
1✔
461
        assert!(result.is_ok());
1✔
462
        assert_eq!(db.unescaped_len, 10);
1✔
463

464
        // Test valid smaller span should work
465
        db.clear_unescaped();
1✔
466
        let result = db.start_unescaping_with_copy(max_escaped_len, 2, 6); // 4 byte span
1✔
467
        assert!(result.is_ok());
1✔
468
        assert_eq!(db.unescaped_len, 4);
1✔
469
        assert_eq!(db.get_unescaped_slice().unwrap(), b"2345");
1✔
470
    }
1✔
471

472
    #[test]
473
    fn test_append_unescaped_byte_uses_full_buffer() {
1✔
474
        let mut buffer = [0u8; 10]; // 10 byte buffer
1✔
475
        let mut db = StreamBuffer::new(&mut buffer);
1✔
476

477
        // Should be able to append up to buffer_len bytes (no more escape reserve!)
478
        for i in 0..10 {
11✔
479
            let result = db.append_unescaped_byte(b'A');
10✔
480
            assert!(result.is_ok(), "Failed at byte {}", i);
10✔
481
        }
482

483
        assert_eq!(db.unescaped_len, 10);
1✔
484

485
        // One more byte should fail because buffer is full
486
        let result = db.append_unescaped_byte(b'B');
1✔
487
        assert_eq!(result.unwrap_err(), StreamBufferError::BufferFull);
1✔
488
    }
1✔
489

490
    #[test]
491
    fn test_compact_basic() {
1✔
492
        let mut buffer = [0u8; 10];
1✔
493
        let mut db = StreamBuffer::new(&mut buffer);
1✔
494

495
        // Fill buffer with data: "0123456789"
496
        {
1✔
497
            let fill_slice = db.get_fill_slice().unwrap();
1✔
498
            fill_slice.copy_from_slice(b"0123456789");
1✔
499
        }
1✔
500
        db.mark_filled(10).unwrap();
1✔
501

502
        // Process some data (advance tokenize_pos to position 4)
503
        for _ in 0..4 {
5✔
504
            db.advance().unwrap();
4✔
505
        }
4✔
506

507
        // Before compact: tokenize_pos=4, data_end=10, remaining="456789"
508
        assert_eq!(db.tokenize_pos, 4);
1✔
509
        assert_eq!(db.data_end, 10);
1✔
510
        assert_eq!(db.remaining_bytes(), 6);
1✔
511

512
        // Compact the buffer
513
        let offset = db.compact_from(4).unwrap();
1✔
514
        assert_eq!(offset, 4); // Data was moved by 4 positions
1✔
515

516
        // After compact: tokenize_pos=0, data_end=6, buffer starts with "456789"
517
        assert_eq!(db.tokenize_pos, 0);
1✔
518
        assert_eq!(db.data_end, 6);
1✔
519
        assert_eq!(db.remaining_bytes(), 6);
1✔
520

521
        // Verify the data was moved correctly
522
        assert_eq!(db.current_byte().unwrap(), b'4');
1✔
523
        db.advance().unwrap();
1✔
524
        assert_eq!(db.current_byte().unwrap(), b'5');
1✔
525
        db.advance().unwrap();
1✔
526
        assert_eq!(db.current_byte().unwrap(), b'6');
1✔
527
    }
1✔
528

529
    #[test]
530
    fn test_compact_from_preserves_number() {
1✔
531
        let mut buffer = [0u8; 10];
1✔
532
        let mut db = StreamBuffer::new(&mut buffer);
1✔
533
        db.buffer.copy_from_slice(b"0123456789");
1✔
534
        db.data_end = 10;
1✔
535
        db.tokenize_pos = 5;
1✔
536
        let number_start_pos = 3;
1✔
537

538
        let offset = db.compact_from(number_start_pos).unwrap();
1✔
539
        assert_eq!(offset, 3);
1✔
540
        assert_eq!(db.tokenize_pos, 2); // 5 - 3
1✔
541
        assert_eq!(db.data_end, 7); // 10 - 3
1✔
542
        assert_eq!(&db.buffer[..db.data_end], b"3456789");
1✔
543
    }
1✔
544

545
    #[test]
546
    fn test_compact_no_op_when_at_start() {
1✔
547
        let mut buffer = [0u8; 10];
1✔
548
        let mut db = StreamBuffer::new(&mut buffer);
1✔
549

550
        // Fill buffer with data
551
        {
1✔
552
            let fill_slice = db.get_fill_slice().unwrap();
1✔
553
            fill_slice[0..5].copy_from_slice(b"hello");
1✔
554
        }
1✔
555
        db.mark_filled(5).unwrap();
1✔
556

557
        // Don't advance tokenize_pos (stays at 0)
558
        assert_eq!(db.tokenize_pos, 0);
1✔
559
        assert_eq!(db.data_end, 5);
1✔
560

561
        // Compact should be no-op
562
        let offset = db.compact_from(0).unwrap();
1✔
563
        assert_eq!(offset, 0); // No movement occurred
1✔
564

565
        // Should be unchanged
566
        assert_eq!(db.tokenize_pos, 0);
1✔
567
        assert_eq!(db.data_end, 5);
1✔
568
        assert_eq!(db.current_byte().unwrap(), b'h');
1✔
569
    }
1✔
570

571
    #[test]
572
    fn test_compact_all_data_processed() {
1✔
573
        let mut buffer = [0u8; 10];
1✔
574
        let mut db = StreamBuffer::new(&mut buffer);
1✔
575

576
        // Fill buffer with data
577
        {
1✔
578
            let fill_slice = db.get_fill_slice().unwrap();
1✔
579
            fill_slice[0..5].copy_from_slice(b"hello");
1✔
580
        }
1✔
581
        db.mark_filled(5).unwrap();
1✔
582

583
        // Process all data
584
        for _ in 0..5 {
6✔
585
            db.advance().unwrap();
5✔
586
        }
5✔
587

588
        // All data processed
589
        assert_eq!(db.tokenize_pos, 5);
1✔
590
        assert_eq!(db.data_end, 5);
1✔
591
        assert!(db.is_empty());
1✔
592

593
        // Compact should reset to start
594
        let offset = db.compact_from(5).unwrap();
1✔
595
        assert_eq!(offset, 5); // All data was processed, moved by 5
1✔
596

597
        // Should be reset to empty state
598
        assert_eq!(db.tokenize_pos, 0);
1✔
599
        assert_eq!(db.data_end, 0);
1✔
600
        assert!(db.is_empty());
1✔
601
    }
1✔
602

603
    #[test]
604
    fn test_compact_enables_new_data_fill() {
1✔
605
        let mut buffer = [0u8; 10];
1✔
606
        let mut db = StreamBuffer::new(&mut buffer);
1✔
607

608
        // Fill buffer completely
609
        {
1✔
610
            let fill_slice = db.get_fill_slice().unwrap();
1✔
611
            fill_slice.copy_from_slice(b"0123456789");
1✔
612
        }
1✔
613
        db.mark_filled(10).unwrap();
1✔
614

615
        // Process half the data
616
        for _ in 0..5 {
6✔
617
            db.advance().unwrap();
5✔
618
        }
5✔
619

620
        // Buffer is full, can't get fill slice
621
        assert!(db.get_fill_slice().is_none());
1✔
622

623
        // Compact to make space
624
        let offset = db.compact_from(5).unwrap();
1✔
625
        assert_eq!(offset, 5); // Data moved by 5 positions
1✔
626

627
        // Now should be able to get fill slice again
628
        let fill_slice = db.get_fill_slice().unwrap();
1✔
629
        assert_eq!(fill_slice.len(), 5); // 5 bytes available (10 - 5 remaining)
1✔
630

631
        // Fill with new data
632
        fill_slice[0..5].copy_from_slice(b"ABCDE");
1✔
633
        db.mark_filled(5).unwrap();
1✔
634

635
        // Verify combined data: "56789ABCDE"
636
        assert_eq!(db.data_end, 10);
1✔
637
        assert_eq!(db.current_byte().unwrap(), b'5');
1✔
638
        db.advance().unwrap();
1✔
639
        assert_eq!(db.current_byte().unwrap(), b'6');
1✔
640
        db.advance().unwrap();
1✔
641
        assert_eq!(db.current_byte().unwrap(), b'7');
1✔
642
        db.advance().unwrap();
1✔
643
        assert_eq!(db.current_byte().unwrap(), b'8');
1✔
644
        db.advance().unwrap();
1✔
645
        assert_eq!(db.current_byte().unwrap(), b'9');
1✔
646
        db.advance().unwrap();
1✔
647
        assert_eq!(db.current_byte().unwrap(), b'A');
1✔
648
    }
1✔
649

650
    #[test]
651
    fn test_compact_with_single_byte_remaining() {
1✔
652
        let mut buffer = [0u8; 5];
1✔
653
        let mut db = StreamBuffer::new(&mut buffer);
1✔
654

655
        // Fill buffer: "abcde"
656
        {
1✔
657
            let fill_slice = db.get_fill_slice().unwrap();
1✔
658
            fill_slice.copy_from_slice(b"abcde");
1✔
659
        }
1✔
660
        db.mark_filled(5).unwrap();
1✔
661

662
        // Process almost all data (leave one byte)
663
        for _ in 0..4 {
5✔
664
            db.advance().unwrap();
4✔
665
        }
4✔
666

667
        // One byte remaining
668
        assert_eq!(db.remaining_bytes(), 1);
1✔
669
        assert_eq!(db.current_byte().unwrap(), b'e');
1✔
670

671
        // Compact
672
        let offset = db.compact_from(4).unwrap();
1✔
673
        assert_eq!(offset, 4); // Moved by 4 positions
1✔
674

675
        // Should have moved the last byte to start
676
        assert_eq!(db.tokenize_pos, 0);
1✔
677
        assert_eq!(db.data_end, 1);
1✔
678
        assert_eq!(db.current_byte().unwrap(), b'e');
1✔
679
        assert_eq!(db.remaining_bytes(), 1);
1✔
680

681
        // Should have space for 4 more bytes
682
        let fill_slice = db.get_fill_slice().unwrap();
1✔
683
        assert_eq!(fill_slice.len(), 4);
1✔
684
    }
1✔
685

686
    #[test]
687
    fn test_compact_buffer_wall_scenario() {
1✔
688
        // Simulate hitting the buffer wall during token processing
689
        // This tests the "always compact when buffer full" strategy
690

691
        let mut buffer = [0u8; 10];
1✔
692
        let mut db = StreamBuffer::new(&mut buffer);
1✔
693

694
        // Fill buffer completely with: `{"hello_wo` (10 bytes, fills buffer exactly)
695
        {
1✔
696
            let fill_slice = db.get_fill_slice().unwrap();
1✔
697
            fill_slice.copy_from_slice(b"{\"hello_wo");
1✔
698
        }
1✔
699
        db.mark_filled(10).unwrap();
1✔
700

701
        // Process tokens: { " h e l l o _ w o
702
        // Parser is in State::String(2) tracking string start at position 2
703
        let mut _string_start_pos = 2; // Parser's state: string started at pos 2
1✔
704

705
        // Advance to simulate tokenizer processing
706
        for _ in 0..10 {
11✔
707
            db.advance().unwrap();
10✔
708
        }
10✔
709

710
        // Buffer is now empty, we hit the wall
711
        assert!(db.is_empty());
1✔
712
        assert!(db.get_fill_slice().is_none()); // No space to read more
1✔
713

714
        // ALWAYS compact when hitting buffer wall
715
        let offset = db.compact_from(10).unwrap();
1✔
716
        assert_eq!(offset, 10); // Moved by 10 positions (everything was processed)
1✔
717

718
        // Parser updates state: string_start_pos = 2 - 10 = -8
719
        // Since string_start_pos < 0, the original string start was discarded!
720
        // Parser must now switch to escape/copy mode for the continuation
721
        if _string_start_pos < offset {
1✔
722
            // Original string start was discarded - must use escape/copy mode
1✔
723
            // In real implementation, parser would copy what it had processed to unescaped buffer
1✔
724
            println!("String start was discarded, switching to escape mode");
1✔
725
            _string_start_pos = 0; // Reset for escape mode
1✔
726
        } else {
1✔
UNCOV
727
            _string_start_pos = _string_start_pos.saturating_sub(offset); // Normal position update
×
UNCOV
728
        }
×
729

730
        // After compaction, buffer is reset and ready for new data
731
        assert_eq!(db.tokenize_pos, 0);
1✔
732
        assert_eq!(db.data_end, 0);
1✔
733

734
        // Now we can read more data
735
        {
736
            let fill_slice = db.get_fill_slice().unwrap();
1✔
737
            assert_eq!(fill_slice.len(), 10); // Full buffer available
1✔
738
            fill_slice[0..3].copy_from_slice(b"rld");
1✔
739
        }
740
        db.mark_filled(3).unwrap();
1✔
741

742
        // Continue processing the string continuation
743
        assert_eq!(db.current_byte().unwrap(), b'r');
1✔
744
        assert_eq!(db.remaining_bytes(), 3);
1✔
745
    }
1✔
746

747
    #[test]
748
    fn test_compact_saves_partial_token() {
1✔
749
        // Test case where compaction saves partial token at end of buffer
750
        let mut buffer = [0u8; 8];
1✔
751
        let mut db = StreamBuffer::new(&mut buffer);
1✔
752

753
        // Fill buffer: {"hel|lo"} where we process up to 'l' and hit wall with "lo\"}" remaining
754
        {
1✔
755
            let fill_slice = db.get_fill_slice().unwrap();
1✔
756
            fill_slice.copy_from_slice(b"{\"hello\"");
1✔
757
        }
1✔
758
        db.mark_filled(8).unwrap();
1✔
759

760
        // Process: { " h e l - stop here with "lo\"" remaining
761
        for _ in 0..5 {
6✔
762
            db.advance().unwrap();
5✔
763
        }
5✔
764

765
        // Current state: parser at position 5, with "lo\"" remaining (3 bytes)
766
        let mut _string_start_pos = 2; // Parser state: string started at position 2
1✔
767
        assert_eq!(db.current_byte().unwrap(), b'l');
1✔
768
        assert_eq!(db.remaining_bytes(), 3);
1✔
769

770
        // Hit buffer wall, compact
771
        let offset = db.compact_from(5).unwrap();
1✔
772
        assert_eq!(offset, 5); // Moved data by 5 positions
1✔
773

774
        // Update parser state
775
        _string_start_pos = if _string_start_pos < offset {
1✔
776
            0 // Switch to escape mode - original start was discarded
1✔
777
        } else {
UNCOV
778
            _string_start_pos - offset // Normal position update: 2 - 5 = -3, so switch to escape mode
×
779
        };
780

781
        // After compaction: "lo\"" is now at start of buffer
782
        assert_eq!(db.tokenize_pos, 0);
1✔
783
        assert_eq!(db.data_end, 3);
1✔
784
        assert_eq!(db.current_byte().unwrap(), b'l');
1✔
785
        assert_eq!(db.remaining_bytes(), 3);
1✔
786

787
        // We saved 3 bytes, gained 5 bytes of space
788
        let fill_slice = db.get_fill_slice().unwrap();
1✔
789
        assert_eq!(fill_slice.len(), 5);
1✔
790
    }
1✔
791

792
    #[test]
793
    fn test_position_update_after_compaction_normal_case() {
1✔
794
        // Test normal position updates where positions are preserved
795

796
        // Case 1: String position preserved after compaction
797
        let _state = crate::shared::State::String(10);
1✔
798
        let offset = 5;
1✔
799

800
        // Simulate the position update logic
801
        let updated_pos = if 10 < offset {
1✔
UNCOV
802
            0 // Would need escape mode
×
803
        } else {
804
            10 - offset // Normal position update: 10 - 5 = 5
1✔
805
        };
806

807
        assert_eq!(updated_pos, 5);
1✔
808

809
        // Case 2: Key position preserved after compaction
810
        let key_pos = 8;
1✔
811
        let offset = 3;
1✔
812

813
        let updated_key_pos = if key_pos < offset {
1✔
UNCOV
814
            0 // Would need escape mode
×
815
        } else {
816
            key_pos - offset // Normal position update: 8 - 3 = 5
1✔
817
        };
818

819
        assert_eq!(updated_key_pos, 5);
1✔
820

821
        // Case 3: Number position preserved after compaction
822
        let number_pos = 15;
1✔
823
        let offset = 7;
1✔
824

825
        let updated_number_pos = if number_pos < offset {
1✔
826
            // Numbers should not normally lose their start position
UNCOV
827
            panic!("Number position discarded - buffer too small");
×
828
        } else {
829
            number_pos - offset // Normal position update: 15 - 7 = 8
1✔
830
        };
831

832
        assert_eq!(updated_number_pos, 8);
1✔
833
    }
1✔
834

835
    #[test]
836
    fn test_position_update_after_compaction_escape_mode_case() {
1✔
837
        // Test position updates where original positions are discarded (need escape mode)
838

839
        // Case 1: String position discarded - needs escape mode
840
        let string_pos = 3;
1✔
841
        let offset = 7; // Offset is larger than string position
1✔
842

843
        let needs_escape_mode = string_pos < offset;
1✔
844
        assert!(needs_escape_mode);
1✔
845

846
        let updated_string_pos = if needs_escape_mode {
1✔
847
            0 // Reset for escape mode
1✔
848
        } else {
UNCOV
849
            string_pos - offset
×
850
        };
851

852
        assert_eq!(updated_string_pos, 0);
1✔
853

854
        // Case 2: Key position discarded - needs escape mode
855
        let key_pos = 2;
1✔
856
        let offset = 8;
1✔
857

858
        let needs_escape_mode = key_pos < offset;
1✔
859
        assert!(needs_escape_mode);
1✔
860

861
        let updated_key_pos = if needs_escape_mode {
1✔
862
            0 // Reset for escape mode
1✔
863
        } else {
UNCOV
864
            key_pos - offset
×
865
        };
866

867
        assert_eq!(updated_key_pos, 0);
1✔
868

869
        // Case 3: Number position discarded - should be an error
870
        let number_pos = 1;
1✔
871
        let offset = 5;
1✔
872

873
        let should_error = number_pos < offset;
1✔
874
        assert!(should_error); // Numbers spanning compaction boundaries should error
1✔
875
    }
1✔
876

877
    #[test]
878
    fn test_position_update_boundary_conditions() {
1✔
879
        // Test exact boundary conditions for position updates
880

881
        // Case 1: Position exactly equals offset
882
        let pos = 5;
1✔
883
        let offset = 5;
1✔
884

885
        let needs_escape_mode = pos < offset; // false, pos == offset
1✔
886
        assert!(!needs_escape_mode);
1✔
887

888
        let updated_pos = pos - offset; // 5 - 5 = 0
1✔
889
        assert_eq!(updated_pos, 0);
1✔
890

891
        // Case 2: Position one less than offset (boundary case)
892
        let pos = 4;
1✔
893
        let offset = 5;
1✔
894

895
        let needs_escape_mode = pos < offset; // true, pos < offset
1✔
896
        assert!(needs_escape_mode);
1✔
897

898
        // Case 3: Position one more than offset (boundary case)
899
        let pos = 6;
1✔
900
        let offset = 5;
1✔
901

902
        let needs_escape_mode = pos < offset; // false, pos > offset
1✔
903
        assert!(!needs_escape_mode);
1✔
904

905
        let updated_pos = pos - offset; // 6 - 5 = 1
1✔
906
        assert_eq!(updated_pos, 1);
1✔
907

908
        // Case 4: Zero offset (no compaction occurred)
909
        let pos = 10;
1✔
910
        let offset = 0;
1✔
911

912
        let needs_escape_mode = pos < offset; // false, 10 < 0
1✔
913
        assert!(!needs_escape_mode);
1✔
914

915
        let updated_pos = pos - offset; // 10 - 0 = 10 (unchanged)
1✔
916
        assert_eq!(updated_pos, 10);
1✔
917
    }
1✔
918

919
    #[test]
920
    fn test_position_update_state_transitions() {
1✔
921
        // Test the complete state transition logic for different parser states
922

923
        // Mock the State enum variants and position update logic
924
        use crate::shared::State;
925

926
        // Case 1: State::None - no position to update
927
        let state = State::None;
1✔
928
        // No position updates needed for None state
929
        match state {
1✔
930
            State::None => {
1✔
931
                // No action needed - test passes
1✔
932
            }
1✔
UNCOV
933
            _ => panic!("Expected State::None"),
×
934
        }
935

936
        // Case 2: String state position updates
937
        let mut string_state = State::String(12);
1✔
938
        let offset = 8;
1✔
939

940
        match &mut string_state {
1✔
941
            State::String(pos) => {
1✔
942
                if *pos < offset {
1✔
UNCOV
943
                    // Would need escape mode
×
UNCOV
944
                    *pos = 0;
×
945
                } else {
1✔
946
                    *pos = pos.saturating_sub(offset); // 12 - 8 = 4
1✔
947
                }
1✔
948
            }
UNCOV
949
            _ => panic!("Expected State::String"),
×
950
        }
951

952
        match string_state {
1✔
953
            State::String(pos) => assert_eq!(pos, 4),
1✔
UNCOV
954
            _ => panic!("Expected State::String"),
×
955
        }
956

957
        // Case 3: Key state needing escape mode
958
        let mut key_state = State::Key(3);
1✔
959
        let offset = 10;
1✔
960

961
        match &mut key_state {
1✔
962
            State::Key(pos) => {
1✔
963
                if *pos < offset {
1✔
964
                    // Needs escape mode
1✔
965
                    *pos = 0;
1✔
966
                } else {
1✔
967
                    *pos = pos.saturating_sub(offset);
×
UNCOV
968
                }
×
969
            }
UNCOV
970
            _ => panic!("Expected State::Key"),
×
971
        }
972

973
        match key_state {
1✔
974
            State::Key(pos) => assert_eq!(pos, 0), // Reset for escape mode
1✔
UNCOV
975
            _ => panic!("Expected State::Key"),
×
976
        }
977

978
        // Case 4: Number state normal update
979
        let mut number_state = State::Number(20);
1✔
980
        let offset = 6;
1✔
981

982
        match &mut number_state {
1✔
983
            State::Number(pos) => {
1✔
984
                if *pos < offset {
1✔
985
                    // This should not happen for numbers in normal operation
UNCOV
986
                    panic!("Number position discarded - buffer too small");
×
987
                } else {
1✔
988
                    *pos = pos.saturating_sub(offset); // 20 - 6 = 14
1✔
989
                }
1✔
990
            }
UNCOV
991
            _ => panic!("Expected State::Number"),
×
992
        }
993

994
        match number_state {
1✔
995
            State::Number(pos) => assert_eq!(pos, 14),
1✔
UNCOV
996
            _ => panic!("Expected State::Number"),
×
997
        }
998
    }
1✔
999
}
1000

1001
impl crate::number_parser::NumberExtractor for StreamBuffer<'_> {
1002
    fn get_number_slice(&self, start: usize, end: usize) -> Result<&[u8], ParseError> {
251✔
1003
        self.get_string_slice(start, end).map_err(Into::into)
251✔
1004
    }
251✔
1005

UNCOV
1006
    fn current_position(&self) -> usize {
×
1007
        self.tokenize_pos
×
1008
    }
×
1009

UNCOV
1010
    fn is_empty(&self) -> bool {
×
UNCOV
1011
        self.tokenize_pos >= self.data_end
×
UNCOV
1012
    }
×
1013
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc