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

kaidokert / picojson-rs / 16095334159

06 Jul 2025 04:35AM UTC coverage: 93.459% (-1.2%) from 94.616%
16095334159

Pull #39

github

kaidokert
I think we won
Pull Request #39: Streambuffer debug

445 of 512 new or added lines in 5 files covered. (86.91%)

9 existing lines in 1 file now uncovered.

4101 of 4388 relevant lines covered (93.46%)

523.52 hits per line

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

93.89
/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
    /// Invalid buffer state or operation
13
    InvalidState(&'static str),
14
}
15

16
impl From<StreamBufferError> for ParseError {
17
    fn from(err: StreamBufferError) -> Self {
×
18
        match err {
×
19
            StreamBufferError::BufferFull => ParseError::ScratchBufferFull,
×
20
            StreamBufferError::EndOfData => ParseError::EndOfData,
×
21
            StreamBufferError::InvalidState(msg) => ParseError::UnexpectedState(msg),
×
22
        }
23
    }
×
24
}
25

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

44
impl<'a> StreamBuffer<'a> {
45
    /// Create a new StreamBuffer with the given buffer slice
46
    pub fn new(buffer: &'a mut [u8]) -> Self {
1,046✔
47
        Self {
1,046✔
48
            buffer,
1,046✔
49
            tokenize_pos: 0,
1,046✔
50
            data_end: 0,
1,046✔
51
            unescaped_len: 0,
1,046✔
52
        }
1,046✔
53
    }
1,046✔
54

55
    /// Get the current byte at tokenize position
56
    pub fn current_byte(&self) -> Result<u8, StreamBufferError> {
17,381✔
57
        if self.tokenize_pos >= self.data_end {
17,381✔
58
            return Err(StreamBufferError::EndOfData);
1✔
59
        }
17,380✔
60
        self.buffer
17,380✔
61
            .get(self.tokenize_pos)
17,380✔
62
            .copied()
17,380✔
63
            .ok_or(StreamBufferError::EndOfData)
17,380✔
64
    }
17,381✔
65

66
    /// Advance the tokenize position by one byte
67
    pub fn advance(&mut self) -> Result<(), StreamBufferError> {
17,410✔
68
        if self.tokenize_pos >= self.data_end {
17,410✔
69
            return Err(StreamBufferError::EndOfData);
1✔
70
        }
17,409✔
71
        self.tokenize_pos = self.tokenize_pos.wrapping_add(1);
17,409✔
72
        Ok(())
17,409✔
73
    }
17,410✔
74

75
    /// Get remaining bytes available for reading
76
    pub fn remaining_bytes(&self) -> usize {
278✔
77
        self.data_end.saturating_sub(self.tokenize_pos)
278✔
78
    }
278✔
79

80
    /// Get slice for Reader to fill with new data
81
    /// Returns None if no space available
82
    pub fn get_fill_slice(&mut self) -> Option<&mut [u8]> {
7,927✔
83
        if self.data_end >= self.buffer.len() {
7,927✔
84
            return None;
1,793✔
85
        }
6,134✔
86
        self.buffer.get_mut(self.data_end..)
6,134✔
87
    }
7,927✔
88

89
    /// Compact buffer by moving unprocessed data from a given start offset to the beginning.
90
    ///
91
    /// # Arguments
92
    /// * `start_offset` - The position from which to preserve data.
93
    ///
94
    /// Returns the offset by which data was moved.
95
    pub fn compact_from(&mut self, start_offset: usize) -> Result<usize, StreamBufferError> {
1,798✔
96
        if start_offset == 0 {
1,798✔
97
            // Already at start, no compaction possible
98
            return Ok(0);
530✔
99
        }
1,268✔
100

101
        let offset = start_offset;
1,268✔
102

103
        if start_offset >= self.data_end {
1,268✔
104
            // All data has been processed, reset to start
105
            self.tokenize_pos = 0;
649✔
106
            self.data_end = 0;
649✔
107
            return Ok(offset);
649✔
108
        }
619✔
109

110
        // Move unprocessed data to start of buffer
111
        let remaining_data = self.data_end - start_offset;
619✔
112
        self.buffer.copy_within(start_offset..self.data_end, 0);
619✔
113

114
        // Update positions
115
        self.tokenize_pos = self.tokenize_pos.saturating_sub(offset);
619✔
116
        self.data_end = remaining_data;
619✔
117

118
        Ok(offset)
619✔
119
    }
1,798✔
120

121
    /// Mark that Reader filled `bytes_read` bytes
122
    pub fn mark_filled(&mut self, bytes_read: usize) -> Result<(), StreamBufferError> {
6,131✔
123
        let new_data_end = self.data_end.wrapping_add(bytes_read);
6,131✔
124
        if new_data_end > self.buffer.len() {
6,131✔
125
            return Err(StreamBufferError::InvalidState(
×
126
                "Attempted to mark more bytes than buffer space",
×
127
            ));
×
128
        }
6,131✔
129
        self.data_end = new_data_end;
6,131✔
130
        Ok(())
6,131✔
131
    }
6,131✔
132

133
    /// Start unescaping and copy existing content from a range in the buffer
134
    /// This handles the common case of starting escape processing partway through a string
135
    pub fn start_unescaping_with_copy(
269✔
136
        &mut self,
269✔
137
        max_escaped_len: usize,
269✔
138
        copy_start: usize,
269✔
139
        copy_end: usize,
269✔
140
    ) -> Result<(), StreamBufferError> {
269✔
141
        // Clear any previous unescaped content
142
        self.unescaped_len = 0;
269✔
143

144
        // Ensure we have space at the start for unescaping
145
        if max_escaped_len > self.buffer.len() {
269✔
146
            return Err(StreamBufferError::BufferFull);
×
147
        }
269✔
148

149
        // Copy existing content if there is any
150
        if copy_end > copy_start && copy_start < self.data_end {
269✔
151
            let span_len = copy_end.saturating_sub(copy_start);
269✔
152

153
            // Ensure the span fits in the buffer - return error instead of silent truncation
154
            if span_len > self.buffer.len() {
269✔
155
                return Err(StreamBufferError::BufferFull);
1✔
156
            }
268✔
157

158
            let src_range = copy_start..copy_start.wrapping_add(span_len);
268✔
159
            if src_range.end > self.buffer.len() {
268✔
160
                return Err(StreamBufferError::InvalidState(
×
161
                    "Source range out of bounds",
×
162
                ));
×
163
            }
268✔
164

165
            // Copy within the same buffer: move data from [copy_start..copy_end] to [0..span_len]
166
            // Use copy_within to handle overlapping ranges safely
167
            self.buffer.copy_within(src_range, 0);
268✔
168
            self.unescaped_len = span_len;
268✔
169
        }
×
170

171
        Ok(())
268✔
172
    }
269✔
173

174
    /// Get the unescaped content slice
175
    pub fn get_unescaped_slice(&self) -> Result<&[u8], StreamBufferError> {
155✔
176
        if self.unescaped_len == 0 {
155✔
177
            return Err(StreamBufferError::InvalidState(
1✔
178
                "No unescaped content available",
1✔
179
            ));
1✔
180
        }
154✔
181
        self.buffer
154✔
182
            .get(0..self.unescaped_len)
154✔
183
            .ok_or(StreamBufferError::InvalidState(
154✔
184
                "Unescaped length exceeds buffer size",
154✔
185
            ))
154✔
186
    }
155✔
187

188
    /// Clear unescaped content (call after yielding unescaped string)
189
    pub fn clear_unescaped(&mut self) {
126✔
190
        self.unescaped_len = 0;
126✔
191
    }
126✔
192

193
    /// Get current tokenize position (for string start tracking)
194
    pub fn current_position(&self) -> usize {
2,700✔
195
        self.tokenize_pos
2,700✔
196
    }
2,700✔
197

198
    /// Check if buffer is empty (no more data to process)
199
    pub fn is_empty(&self) -> bool {
36,190✔
200
        self.tokenize_pos >= self.data_end
36,190✔
201
    }
36,190✔
202

203
    /// Check if we have unescaped content ready
204
    pub fn has_unescaped_content(&self) -> bool {
7,179✔
205
        self.unescaped_len > 0
7,179✔
206
    }
7,179✔
207

208
    /// Append a single byte to the unescaped content
209
    pub fn append_unescaped_byte(&mut self, byte: u8) -> Result<(), StreamBufferError> {
1,285✔
210
        if let Some(b) = self.buffer.get_mut(self.unescaped_len) {
1,285✔
211
            *b = byte;
1,284✔
212
            self.unescaped_len = self.unescaped_len.wrapping_add(1);
1,284✔
213
            Ok(())
1,284✔
214
        } else {
215
            Err(StreamBufferError::BufferFull)
1✔
216
        }
217
    }
1,285✔
218

219
    /// Get a string slice from the buffer (zero-copy)
220
    /// Used for strings without escapes
221
    pub fn get_string_slice(&self, start: usize, end: usize) -> Result<&[u8], StreamBufferError> {
750✔
222
        if start > end || end > self.data_end {
750✔
223
            return Err(StreamBufferError::InvalidState("Invalid slice bounds"));
×
224
        }
750✔
225
        self.buffer
750✔
226
            .get(start..end)
750✔
227
            .ok_or(StreamBufferError::InvalidState("Invalid slice bounds"))
750✔
228
    }
750✔
229
}
230

231
#[cfg(test)]
232
mod tests {
233
    use super::*;
234

235
    #[test]
236
    fn test_lifetime_expectations() {
1✔
237
        // This test demonstrates how StreamBuffer lifetimes should work
238
        let mut buffer = [0u8; 100];
1✔
239
        let mut stream_buffer = StreamBuffer::new(&mut buffer);
1✔
240

241
        // Simulate some data being in the buffer
242
        let test_data = b"hello world";
1✔
243
        stream_buffer.buffer[0..test_data.len()].copy_from_slice(test_data);
1✔
244
        stream_buffer.data_end = test_data.len();
1✔
245

246
        // Test that we can get buffer data
247

248
        // Test unescaped content - add some unescaped data
249
        stream_buffer.unescaped_len = 3;
1✔
250
        stream_buffer.buffer[0..3].copy_from_slice(b"abc");
1✔
251

252
        let unescaped_slice = stream_buffer.get_unescaped_slice().unwrap();
1✔
253
        assert_eq!(unescaped_slice, b"abc");
1✔
254

255
        // The key expectation: these slices should live as long as the original buffer
256
        // and be usable to create String::Borrowed(&'buffer str) and String::Unescaped(&'buffer str)
257
    }
1✔
258

259
    #[test]
260
    fn test_new_stream_buffer() {
1✔
261
        let mut buffer = [0u8; 100];
1✔
262
        let db = StreamBuffer::new(&mut buffer);
1✔
263

264
        assert_eq!(db.tokenize_pos, 0);
1✔
265
        assert_eq!(db.data_end, 0);
1✔
266
        assert_eq!(db.unescaped_len, 0);
1✔
267
        assert!(db.is_empty());
1✔
268
    }
1✔
269

270
    #[test]
271
    fn test_fill_and_advance() {
1✔
272
        let mut buffer = [0u8; 100];
1✔
273
        let mut db = StreamBuffer::new(&mut buffer);
1✔
274

275
        // Fill with some data
276
        {
1✔
277
            let fill_slice = db.get_fill_slice().unwrap();
1✔
278
            fill_slice[0..5].copy_from_slice(b"hello");
1✔
279
        }
1✔
280
        db.mark_filled(5).unwrap();
1✔
281

282
        assert_eq!(db.data_end, 5);
1✔
283
        assert_eq!(db.remaining_bytes(), 5);
1✔
284

285
        // Read bytes
286
        assert_eq!(db.current_byte().unwrap(), b'h');
1✔
287
        db.advance().unwrap();
1✔
288
        assert_eq!(db.current_byte().unwrap(), b'e');
1✔
289
        assert_eq!(db.remaining_bytes(), 4);
1✔
290
    }
1✔
291

292
    #[test]
293
    fn test_error_conditions() {
1✔
294
        let mut buffer = [0u8; 10];
1✔
295
        let mut db = StreamBuffer::new(&mut buffer);
1✔
296

297
        // EndOfData errors
298
        assert_eq!(db.current_byte().unwrap_err(), StreamBufferError::EndOfData);
1✔
299
        assert_eq!(db.advance().unwrap_err(), StreamBufferError::EndOfData);
1✔
300

301
        // No unescaped content
302
        assert!(db.get_unescaped_slice().is_err());
1✔
303
    }
1✔
304

305
    #[test]
306
    fn test_buffer_full_scenario() {
1✔
307
        // Test what happens when buffer gets completely full
308
        let mut buffer = [0u8; 10];
1✔
309
        let mut db = StreamBuffer::new(&mut buffer);
1✔
310

311
        // Fill buffer completely
312
        {
1✔
313
            let fill_slice = db.get_fill_slice().unwrap();
1✔
314
            fill_slice.copy_from_slice(b"0123456789");
1✔
315
        }
1✔
316
        db.mark_filled(10).unwrap();
1✔
317

318
        // No more space for filling
319
        assert!(db.get_fill_slice().is_none());
1✔
320

321
        // We can still read from buffer
322
        assert_eq!(db.current_byte().unwrap(), b'0');
1✔
323
        assert_eq!(db.remaining_bytes(), 10);
1✔
324
    }
1✔
325

326
    #[test]
327
    fn test_minimal_buffer_with_long_token() {
1✔
328
        // Test very small buffer with a token that doesn't fit
329
        let mut buffer = [0u8; 8]; // Very small buffer
1✔
330
        let mut db = StreamBuffer::new(&mut buffer);
1✔
331

332
        // Try to put a string that's almost as big as the buffer
333
        {
1✔
334
            let fill_slice = db.get_fill_slice().unwrap();
1✔
335
            fill_slice[0..6].copy_from_slice(b"\"hello"); // Start of a long string, no closing quote
1✔
336
        }
1✔
337
        db.mark_filled(6).unwrap();
1✔
338

339
        // Advance through the data
340
        for _ in 0..6 {
7✔
341
            db.advance().unwrap();
6✔
342
        }
6✔
343

344
        // Now buffer is exhausted but we don't have a complete token
345
        assert!(db.is_empty());
1✔
346
        assert_eq!(db.remaining_bytes(), 0);
1✔
347

348
        // This simulates the scenario where we need more data but can't fit it
349
        // The parser would need to handle this by buffering the incomplete token
350
    }
1✔
351

352
    #[test]
353
    fn test_reader_returns_zero_bytes() {
1✔
354
        let mut buffer = [0u8; 20];
1✔
355
        let mut db = StreamBuffer::new(&mut buffer);
1✔
356

357
        // Simulate Reader returning 0 bytes (EOF)
358
        {
359
            let fill_slice = db.get_fill_slice().unwrap();
1✔
360
            assert_eq!(fill_slice.len(), 20);
1✔
361
            // Reader returns 0 bytes - simulating EOF or no data available
362
        }
363
        db.mark_filled(0).unwrap(); // Reader returned 0
1✔
364

365
        assert!(db.is_empty());
1✔
366
        assert_eq!(db.data_end, 0);
1✔
367
        assert_eq!(db.remaining_bytes(), 0);
1✔
368

369
        // Should still be able to get fill slice for next attempt
370
        let fill_slice = db.get_fill_slice().unwrap();
1✔
371
        assert_eq!(fill_slice.len(), 20);
1✔
372
    }
1✔
373

374
    #[test]
375
    fn test_boundary_conditions() {
1✔
376
        let mut buffer = [0u8; 3]; // Absolute minimum
1✔
377
        let mut db = StreamBuffer::new(&mut buffer);
1✔
378

379
        // Can't even hold a proper JSON token, but should not crash
380
        {
1✔
381
            let fill_slice = db.get_fill_slice().unwrap();
1✔
382
            fill_slice.copy_from_slice(b"\"a\"");
1✔
383
        }
1✔
384
        db.mark_filled(3).unwrap();
1✔
385

386
        // Should be able to read through it
387
        assert_eq!(db.current_byte().unwrap(), b'"');
1✔
388
        db.advance().unwrap();
1✔
389
        assert_eq!(db.current_byte().unwrap(), b'a');
1✔
390
        db.advance().unwrap();
1✔
391
        assert_eq!(db.current_byte().unwrap(), b'"');
1✔
392
        db.advance().unwrap();
1✔
393

394
        assert!(db.is_empty());
1✔
395
    }
1✔
396

397
    #[test]
398
    fn test_start_unescaping_with_copy_span_too_large() {
1✔
399
        let mut buffer = [0u8; 10]; // Small buffer
1✔
400
        let mut db = StreamBuffer::new(&mut buffer);
1✔
401

402
        // Fill buffer with some data
403
        {
1✔
404
            let fill_slice = db.get_fill_slice().unwrap();
1✔
405
            fill_slice.copy_from_slice(b"0123456789");
1✔
406
        }
1✔
407
        db.mark_filled(10).unwrap();
1✔
408

409
        // Try to copy a span that's larger than the entire buffer
410
        let copy_start = 0;
1✔
411
        let copy_end = 15; // This span (15 bytes) is larger than buffer (10 bytes)
1✔
412
        let max_escaped_len = 5; // This is fine
1✔
413

414
        // Should return BufferFull error instead of silently truncating
415
        let result = db.start_unescaping_with_copy(max_escaped_len, copy_start, copy_end);
1✔
416
        assert_eq!(result.unwrap_err(), StreamBufferError::BufferFull);
1✔
417

418
        // Test boundary case: span exactly equals buffer size should work
419
        let copy_end_exact = 10; // Span of exactly 10 bytes (buffer size)
1✔
420
        let result = db.start_unescaping_with_copy(max_escaped_len, 0, copy_end_exact);
1✔
421
        assert!(result.is_ok());
1✔
422
        assert_eq!(db.unescaped_len, 10);
1✔
423

424
        // Test valid smaller span should work
425
        db.clear_unescaped();
1✔
426
        let result = db.start_unescaping_with_copy(max_escaped_len, 2, 6); // 4 byte span
1✔
427
        assert!(result.is_ok());
1✔
428
        assert_eq!(db.unescaped_len, 4);
1✔
429
        assert_eq!(db.get_unescaped_slice().unwrap(), b"2345");
1✔
430
    }
1✔
431

432
    #[test]
433
    fn test_append_unescaped_byte_uses_full_buffer() {
1✔
434
        let mut buffer = [0u8; 10]; // 10 byte buffer
1✔
435
        let mut db = StreamBuffer::new(&mut buffer);
1✔
436

437
        // Should be able to append up to buffer_len bytes (no more escape reserve!)
438
        for i in 0..10 {
11✔
439
            let result = db.append_unescaped_byte(b'A');
10✔
440
            assert!(result.is_ok(), "Failed at byte {}", i);
10✔
441
        }
442

443
        assert_eq!(db.unescaped_len, 10);
1✔
444

445
        // One more byte should fail because buffer is full
446
        let result = db.append_unescaped_byte(b'B');
1✔
447
        assert_eq!(result.unwrap_err(), StreamBufferError::BufferFull);
1✔
448
    }
1✔
449

450
    #[test]
451
    fn test_compact_basic() {
1✔
452
        let mut buffer = [0u8; 10];
1✔
453
        let mut db = StreamBuffer::new(&mut buffer);
1✔
454

455
        // Fill buffer with data: "0123456789"
456
        {
1✔
457
            let fill_slice = db.get_fill_slice().unwrap();
1✔
458
            fill_slice.copy_from_slice(b"0123456789");
1✔
459
        }
1✔
460
        db.mark_filled(10).unwrap();
1✔
461

462
        // Process some data (advance tokenize_pos to position 4)
463
        for _ in 0..4 {
5✔
464
            db.advance().unwrap();
4✔
465
        }
4✔
466

467
        // Before compact: tokenize_pos=4, data_end=10, remaining="456789"
468
        assert_eq!(db.tokenize_pos, 4);
1✔
469
        assert_eq!(db.data_end, 10);
1✔
470
        assert_eq!(db.remaining_bytes(), 6);
1✔
471

472
        // Compact the buffer
473
        let offset = db.compact_from(4).unwrap();
1✔
474
        assert_eq!(offset, 4); // Data was moved by 4 positions
1✔
475

476
        // After compact: tokenize_pos=0, data_end=6, buffer starts with "456789"
477
        assert_eq!(db.tokenize_pos, 0);
1✔
478
        assert_eq!(db.data_end, 6);
1✔
479
        assert_eq!(db.remaining_bytes(), 6);
1✔
480

481
        // Verify the data was moved correctly
482
        assert_eq!(db.current_byte().unwrap(), b'4');
1✔
483
        db.advance().unwrap();
1✔
484
        assert_eq!(db.current_byte().unwrap(), b'5');
1✔
485
        db.advance().unwrap();
1✔
486
        assert_eq!(db.current_byte().unwrap(), b'6');
1✔
487
    }
1✔
488

489
    #[test]
490
    fn test_compact_from_preserves_number() {
1✔
491
        let mut buffer = [0u8; 10];
1✔
492
        let mut db = StreamBuffer::new(&mut buffer);
1✔
493
        db.buffer.copy_from_slice(b"0123456789");
1✔
494
        db.data_end = 10;
1✔
495
        db.tokenize_pos = 5;
1✔
496
        let number_start_pos = 3;
1✔
497

498
        let offset = db.compact_from(number_start_pos).unwrap();
1✔
499
        assert_eq!(offset, 3);
1✔
500
        assert_eq!(db.tokenize_pos, 2); // 5 - 3
1✔
501
        assert_eq!(db.data_end, 7); // 10 - 3
1✔
502
        assert_eq!(&db.buffer[..db.data_end], b"3456789");
1✔
503
    }
1✔
504

505
    #[test]
506
    fn test_compact_no_op_when_at_start() {
1✔
507
        let mut buffer = [0u8; 10];
1✔
508
        let mut db = StreamBuffer::new(&mut buffer);
1✔
509

510
        // Fill buffer with data
511
        {
1✔
512
            let fill_slice = db.get_fill_slice().unwrap();
1✔
513
            fill_slice[0..5].copy_from_slice(b"hello");
1✔
514
        }
1✔
515
        db.mark_filled(5).unwrap();
1✔
516

517
        // Don't advance tokenize_pos (stays at 0)
518
        assert_eq!(db.tokenize_pos, 0);
1✔
519
        assert_eq!(db.data_end, 5);
1✔
520

521
        // Compact should be no-op
522
        let offset = db.compact_from(0).unwrap();
1✔
523
        assert_eq!(offset, 0); // No movement occurred
1✔
524

525
        // Should be unchanged
526
        assert_eq!(db.tokenize_pos, 0);
1✔
527
        assert_eq!(db.data_end, 5);
1✔
528
        assert_eq!(db.current_byte().unwrap(), b'h');
1✔
529
    }
1✔
530

531
    #[test]
532
    fn test_compact_all_data_processed() {
1✔
533
        let mut buffer = [0u8; 10];
1✔
534
        let mut db = StreamBuffer::new(&mut buffer);
1✔
535

536
        // Fill buffer with data
537
        {
1✔
538
            let fill_slice = db.get_fill_slice().unwrap();
1✔
539
            fill_slice[0..5].copy_from_slice(b"hello");
1✔
540
        }
1✔
541
        db.mark_filled(5).unwrap();
1✔
542

543
        // Process all data
544
        for _ in 0..5 {
6✔
545
            db.advance().unwrap();
5✔
546
        }
5✔
547

548
        // All data processed
549
        assert_eq!(db.tokenize_pos, 5);
1✔
550
        assert_eq!(db.data_end, 5);
1✔
551
        assert!(db.is_empty());
1✔
552

553
        // Compact should reset to start
554
        let offset = db.compact_from(5).unwrap();
1✔
555
        assert_eq!(offset, 5); // All data was processed, moved by 5
1✔
556

557
        // Should be reset to empty state
558
        assert_eq!(db.tokenize_pos, 0);
1✔
559
        assert_eq!(db.data_end, 0);
1✔
560
        assert!(db.is_empty());
1✔
561
    }
1✔
562

563
    #[test]
564
    fn test_compact_enables_new_data_fill() {
1✔
565
        let mut buffer = [0u8; 10];
1✔
566
        let mut db = StreamBuffer::new(&mut buffer);
1✔
567

568
        // Fill buffer completely
569
        {
1✔
570
            let fill_slice = db.get_fill_slice().unwrap();
1✔
571
            fill_slice.copy_from_slice(b"0123456789");
1✔
572
        }
1✔
573
        db.mark_filled(10).unwrap();
1✔
574

575
        // Process half the data
576
        for _ in 0..5 {
6✔
577
            db.advance().unwrap();
5✔
578
        }
5✔
579

580
        // Buffer is full, can't get fill slice
581
        assert!(db.get_fill_slice().is_none());
1✔
582

583
        // Compact to make space
584
        let offset = db.compact_from(5).unwrap();
1✔
585
        assert_eq!(offset, 5); // Data moved by 5 positions
1✔
586

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

591
        // Fill with new data
592
        fill_slice[0..5].copy_from_slice(b"ABCDE");
1✔
593
        db.mark_filled(5).unwrap();
1✔
594

595
        // Verify combined data: "56789ABCDE"
596
        assert_eq!(db.data_end, 10);
1✔
597
        assert_eq!(db.current_byte().unwrap(), b'5');
1✔
598
        db.advance().unwrap();
1✔
599
        assert_eq!(db.current_byte().unwrap(), b'6');
1✔
600
        db.advance().unwrap();
1✔
601
        assert_eq!(db.current_byte().unwrap(), b'7');
1✔
602
        db.advance().unwrap();
1✔
603
        assert_eq!(db.current_byte().unwrap(), b'8');
1✔
604
        db.advance().unwrap();
1✔
605
        assert_eq!(db.current_byte().unwrap(), b'9');
1✔
606
        db.advance().unwrap();
1✔
607
        assert_eq!(db.current_byte().unwrap(), b'A');
1✔
608
    }
1✔
609

610
    #[test]
611
    fn test_compact_with_single_byte_remaining() {
1✔
612
        let mut buffer = [0u8; 5];
1✔
613
        let mut db = StreamBuffer::new(&mut buffer);
1✔
614

615
        // Fill buffer: "abcde"
616
        {
1✔
617
            let fill_slice = db.get_fill_slice().unwrap();
1✔
618
            fill_slice.copy_from_slice(b"abcde");
1✔
619
        }
1✔
620
        db.mark_filled(5).unwrap();
1✔
621

622
        // Process almost all data (leave one byte)
623
        for _ in 0..4 {
5✔
624
            db.advance().unwrap();
4✔
625
        }
4✔
626

627
        // One byte remaining
628
        assert_eq!(db.remaining_bytes(), 1);
1✔
629
        assert_eq!(db.current_byte().unwrap(), b'e');
1✔
630

631
        // Compact
632
        let offset = db.compact_from(4).unwrap();
1✔
633
        assert_eq!(offset, 4); // Moved by 4 positions
1✔
634

635
        // Should have moved the last byte to start
636
        assert_eq!(db.tokenize_pos, 0);
1✔
637
        assert_eq!(db.data_end, 1);
1✔
638
        assert_eq!(db.current_byte().unwrap(), b'e');
1✔
639
        assert_eq!(db.remaining_bytes(), 1);
1✔
640

641
        // Should have space for 4 more bytes
642
        let fill_slice = db.get_fill_slice().unwrap();
1✔
643
        assert_eq!(fill_slice.len(), 4);
1✔
644
    }
1✔
645

646
    #[test]
647
    fn test_compact_buffer_wall_scenario() {
1✔
648
        // Simulate hitting the buffer wall during token processing
649
        // This tests the "always compact when buffer full" strategy
650

651
        let mut buffer = [0u8; 10];
1✔
652
        let mut db = StreamBuffer::new(&mut buffer);
1✔
653

654
        // Fill buffer completely with: `{"hello_wo` (10 bytes, fills buffer exactly)
655
        {
1✔
656
            let fill_slice = db.get_fill_slice().unwrap();
1✔
657
            fill_slice.copy_from_slice(b"{\"hello_wo");
1✔
658
        }
1✔
659
        db.mark_filled(10).unwrap();
1✔
660

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

665
        // Advance to simulate tokenizer processing
666
        for _ in 0..10 {
11✔
667
            db.advance().unwrap();
10✔
668
        }
10✔
669

670
        // Buffer is now empty, we hit the wall
671
        assert!(db.is_empty());
1✔
672
        assert!(db.get_fill_slice().is_none()); // No space to read more
1✔
673

674
        // ALWAYS compact when hitting buffer wall
675
        let offset = db.compact_from(10).unwrap();
1✔
676
        assert_eq!(offset, 10); // Moved by 10 positions (everything was processed)
1✔
677

678
        // Parser updates state: string_start_pos = 2 - 10 = -8
679
        // Since string_start_pos < 0, the original string start was discarded!
680
        // Parser must now switch to escape/copy mode for the continuation
681
        if _string_start_pos < offset {
1✔
682
            // Original string start was discarded - must use escape/copy mode
1✔
683
            // In real implementation, parser would copy what it had processed to unescaped buffer
1✔
684
            println!("String start was discarded, switching to escape mode");
1✔
685
            _string_start_pos = 0; // Reset for escape mode
1✔
686
        } else {
1✔
NEW
687
            _string_start_pos -= offset; // Normal position update
×
NEW
688
        }
×
689

690
        // After compaction, buffer is reset and ready for new data
691
        assert_eq!(db.tokenize_pos, 0);
1✔
692
        assert_eq!(db.data_end, 0);
1✔
693

694
        // Now we can read more data
695
        {
696
            let fill_slice = db.get_fill_slice().unwrap();
1✔
697
            assert_eq!(fill_slice.len(), 10); // Full buffer available
1✔
698
            fill_slice[0..3].copy_from_slice(b"rld");
1✔
699
        }
700
        db.mark_filled(3).unwrap();
1✔
701

702
        // Continue processing the string continuation
703
        assert_eq!(db.current_byte().unwrap(), b'r');
1✔
704
        assert_eq!(db.remaining_bytes(), 3);
1✔
705
    }
1✔
706

707
    #[test]
708
    fn test_compact_saves_partial_token() {
1✔
709
        // Test case where compaction saves partial token at end of buffer
710
        let mut buffer = [0u8; 8];
1✔
711
        let mut db = StreamBuffer::new(&mut buffer);
1✔
712

713
        // Fill buffer: {"hel|lo"} where we process up to 'l' and hit wall with "lo\"}" remaining
714
        {
1✔
715
            let fill_slice = db.get_fill_slice().unwrap();
1✔
716
            fill_slice.copy_from_slice(b"{\"hello\"");
1✔
717
        }
1✔
718
        db.mark_filled(8).unwrap();
1✔
719

720
        // Process: { " h e l - stop here with "lo\"" remaining
721
        for _ in 0..5 {
6✔
722
            db.advance().unwrap();
5✔
723
        }
5✔
724

725
        // Current state: parser at position 5, with "lo\"" remaining (3 bytes)
726
        let mut _string_start_pos = 2; // Parser state: string started at position 2
1✔
727
        assert_eq!(db.current_byte().unwrap(), b'l');
1✔
728
        assert_eq!(db.remaining_bytes(), 3);
1✔
729

730
        // Hit buffer wall, compact
731
        let offset = db.compact_from(5).unwrap();
1✔
732
        assert_eq!(offset, 5); // Moved data by 5 positions
1✔
733

734
        // Update parser state
735
        _string_start_pos = if _string_start_pos < offset {
1✔
736
            0 // Switch to escape mode - original start was discarded
1✔
737
        } else {
NEW
738
            _string_start_pos - offset // Normal position update: 2 - 5 = -3, so switch to escape mode
×
739
        };
740

741
        // After compaction: "lo\"" is now at start of buffer
742
        assert_eq!(db.tokenize_pos, 0);
1✔
743
        assert_eq!(db.data_end, 3);
1✔
744
        assert_eq!(db.current_byte().unwrap(), b'l');
1✔
745
        assert_eq!(db.remaining_bytes(), 3);
1✔
746

747
        // We saved 3 bytes, gained 5 bytes of space
748
        let fill_slice = db.get_fill_slice().unwrap();
1✔
749
        assert_eq!(fill_slice.len(), 5);
1✔
750
    }
1✔
751

752
    #[test]
753
    fn test_position_update_after_compaction_normal_case() {
1✔
754
        // Test normal position updates where positions are preserved
755

756
        // Case 1: String position preserved after compaction
757
        let _state = crate::shared::State::String(10);
1✔
758
        let offset = 5;
1✔
759

760
        // Simulate the position update logic
761
        let updated_pos = if 10 < offset {
1✔
NEW
762
            0 // Would need escape mode
×
763
        } else {
764
            10 - offset // Normal position update: 10 - 5 = 5
1✔
765
        };
766

767
        assert_eq!(updated_pos, 5);
1✔
768

769
        // Case 2: Key position preserved after compaction
770
        let key_pos = 8;
1✔
771
        let offset = 3;
1✔
772

773
        let updated_key_pos = if key_pos < offset {
1✔
NEW
774
            0 // Would need escape mode
×
775
        } else {
776
            key_pos - offset // Normal position update: 8 - 3 = 5
1✔
777
        };
778

779
        assert_eq!(updated_key_pos, 5);
1✔
780

781
        // Case 3: Number position preserved after compaction
782
        let number_pos = 15;
1✔
783
        let offset = 7;
1✔
784

785
        let updated_number_pos = if number_pos < offset {
1✔
786
            // Numbers should not normally lose their start position
NEW
787
            panic!("Number position discarded - buffer too small");
×
788
        } else {
789
            number_pos - offset // Normal position update: 15 - 7 = 8
1✔
790
        };
791

792
        assert_eq!(updated_number_pos, 8);
1✔
793
    }
1✔
794

795
    #[test]
796
    fn test_position_update_after_compaction_escape_mode_case() {
1✔
797
        // Test position updates where original positions are discarded (need escape mode)
798

799
        // Case 1: String position discarded - needs escape mode
800
        let string_pos = 3;
1✔
801
        let offset = 7; // Offset is larger than string position
1✔
802

803
        let needs_escape_mode = string_pos < offset;
1✔
804
        assert!(needs_escape_mode);
1✔
805

806
        let updated_string_pos = if needs_escape_mode {
1✔
807
            0 // Reset for escape mode
1✔
808
        } else {
NEW
809
            string_pos - offset
×
810
        };
811

812
        assert_eq!(updated_string_pos, 0);
1✔
813

814
        // Case 2: Key position discarded - needs escape mode
815
        let key_pos = 2;
1✔
816
        let offset = 8;
1✔
817

818
        let needs_escape_mode = key_pos < offset;
1✔
819
        assert!(needs_escape_mode);
1✔
820

821
        let updated_key_pos = if needs_escape_mode {
1✔
822
            0 // Reset for escape mode
1✔
823
        } else {
NEW
824
            key_pos - offset
×
825
        };
826

827
        assert_eq!(updated_key_pos, 0);
1✔
828

829
        // Case 3: Number position discarded - should be an error
830
        let number_pos = 1;
1✔
831
        let offset = 5;
1✔
832

833
        let should_error = number_pos < offset;
1✔
834
        assert!(should_error); // Numbers spanning compaction boundaries should error
1✔
835
    }
1✔
836

837
    #[test]
838
    fn test_position_update_boundary_conditions() {
1✔
839
        // Test exact boundary conditions for position updates
840

841
        // Case 1: Position exactly equals offset
842
        let pos = 5;
1✔
843
        let offset = 5;
1✔
844

845
        let needs_escape_mode = pos < offset; // false, pos == offset
1✔
846
        assert!(!needs_escape_mode);
1✔
847

848
        let updated_pos = pos - offset; // 5 - 5 = 0
1✔
849
        assert_eq!(updated_pos, 0);
1✔
850

851
        // Case 2: Position one less than offset (boundary case)
852
        let pos = 4;
1✔
853
        let offset = 5;
1✔
854

855
        let needs_escape_mode = pos < offset; // true, pos < offset
1✔
856
        assert!(needs_escape_mode);
1✔
857

858
        // Case 3: Position one more than offset (boundary case)
859
        let pos = 6;
1✔
860
        let offset = 5;
1✔
861

862
        let needs_escape_mode = pos < offset; // false, pos > offset
1✔
863
        assert!(!needs_escape_mode);
1✔
864

865
        let updated_pos = pos - offset; // 6 - 5 = 1
1✔
866
        assert_eq!(updated_pos, 1);
1✔
867

868
        // Case 4: Zero offset (no compaction occurred)
869
        let pos = 10;
1✔
870
        let offset = 0;
1✔
871

872
        let needs_escape_mode = pos < offset; // false, 10 < 0
1✔
873
        assert!(!needs_escape_mode);
1✔
874

875
        let updated_pos = pos - offset; // 10 - 0 = 10 (unchanged)
1✔
876
        assert_eq!(updated_pos, 10);
1✔
877
    }
1✔
878

879
    #[test]
880
    fn test_position_update_state_transitions() {
1✔
881
        // Test the complete state transition logic for different parser states
882

883
        // Mock the State enum variants and position update logic
884
        use crate::shared::State;
885

886
        // Case 1: State::None - no position to update
887
        let state = State::None;
1✔
888
        // No position updates needed for None state
889
        match state {
1✔
890
            State::None => {
1✔
891
                // No action needed - test passes
1✔
892
            }
1✔
NEW
893
            _ => panic!("Expected State::None"),
×
894
        }
895

896
        // Case 2: String state position updates
897
        let mut string_state = State::String(12);
1✔
898
        let offset = 8;
1✔
899

900
        match &mut string_state {
1✔
901
            State::String(pos) => {
1✔
902
                if *pos < offset {
1✔
NEW
903
                    // Would need escape mode
×
NEW
904
                    *pos = 0;
×
905
                } else {
1✔
906
                    *pos -= offset; // 12 - 8 = 4
1✔
907
                }
1✔
908
            }
NEW
909
            _ => panic!("Expected State::String"),
×
910
        }
911

912
        match string_state {
1✔
913
            State::String(pos) => assert_eq!(pos, 4),
1✔
NEW
914
            _ => panic!("Expected State::String"),
×
915
        }
916

917
        // Case 3: Key state needing escape mode
918
        let mut key_state = State::Key(3);
1✔
919
        let offset = 10;
1✔
920

921
        match &mut key_state {
1✔
922
            State::Key(pos) => {
1✔
923
                if *pos < offset {
1✔
924
                    // Needs escape mode
1✔
925
                    *pos = 0;
1✔
926
                } else {
1✔
NEW
927
                    *pos -= offset;
×
NEW
928
                }
×
929
            }
NEW
930
            _ => panic!("Expected State::Key"),
×
931
        }
932

933
        match key_state {
1✔
934
            State::Key(pos) => assert_eq!(pos, 0), // Reset for escape mode
1✔
NEW
935
            _ => panic!("Expected State::Key"),
×
936
        }
937

938
        // Case 4: Number state normal update
939
        let mut number_state = State::Number(20);
1✔
940
        let offset = 6;
1✔
941

942
        match &mut number_state {
1✔
943
            State::Number(pos) => {
1✔
944
                if *pos < offset {
1✔
945
                    // This should not happen for numbers in normal operation
NEW
946
                    panic!("Number position discarded - buffer too small");
×
947
                } else {
1✔
948
                    *pos -= offset; // 20 - 6 = 14
1✔
949
                }
1✔
950
            }
NEW
951
            _ => panic!("Expected State::Number"),
×
952
        }
953

954
        match number_state {
1✔
955
            State::Number(pos) => assert_eq!(pos, 14),
1✔
NEW
956
            _ => panic!("Expected State::Number"),
×
957
        }
958
    }
1✔
959
}
960

961
impl crate::number_parser::NumberExtractor for StreamBuffer<'_> {
962
    fn get_number_slice(
227✔
963
        &self,
227✔
964
        start: usize,
227✔
965
        end: usize,
227✔
966
    ) -> Result<&[u8], crate::shared::ParseError> {
227✔
967
        self.get_string_slice(start, end)
227✔
968
            .map_err(|_| crate::shared::ParseError::UnexpectedState("Invalid number slice bounds"))
227✔
969
    }
227✔
970

971
    fn current_position(&self) -> usize {
227✔
972
        self.tokenize_pos
227✔
973
    }
227✔
974

975
    fn is_empty(&self) -> bool {
21✔
976
        self.tokenize_pos >= self.data_end
21✔
977
    }
21✔
978
}
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