Skip to content

Commit 6f66138

Browse files
authored
Fix BufferReader.lineNumber to correctly count newlines (#1628)
The `BufferReader.lineNumber` property had a bug where it used `fullBuffer[offset: 1]` to check for newline characters. Since `offset:` is relative to the start of the buffer, this always checked the same byte (the second byte) regardless of the current cursor position `p`. This caused incorrect line counting: - If the second byte happened to be `\n`, it would increment the count on every iteration, overcounting lines - If the second byte was not `\n`, standalone newlines (without preceding `\r`) would never be counted, undercounting lines.
1 parent e4df0e8 commit 6f66138

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

Sources/FoundationEssentials/CodableUtilities.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ struct BufferReader {
599599
if nextIndex < readIndex && fullBuffer[unchecked: nextIndex] == ._newline {
600600
p = nextIndex
601601
}
602-
} else if fullBuffer[offset: 1] == ._newline {
602+
} else if fullBuffer[unchecked: p] == ._newline {
603603
count += 1
604604
}
605605
fullBuffer.formIndex(&p, offsetBy: 1)

Tests/FoundationEssentialsTests/BufferViewTests.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,107 @@ private struct BufferViewTests {
362362
}
363363
}
364364
}
365+
366+
@Suite("BufferReader")
367+
private struct BufferReaderTests {
368+
369+
@Test("Line number counting with standalone newlines")
370+
func lineNumberCounting() {
371+
// Test case designed to expose the bug: second byte is 'A' (not a newline),
372+
// but there are newlines at positions 3, 5, and 7.
373+
// With the bug, fullBuffer[offset: 1] would check byte 1 ('A') and never count
374+
// the newlines at positions 3, 5, and 7.
375+
let testString = "A\nB\nC\nD"
376+
let bytes = Array(testString.utf8)
377+
378+
bytes.withUnsafeBufferPointer {
379+
let bufferView = BufferView(unsafeBufferPointer: $0)
380+
#expect(bufferView != nil)
381+
guard let bufferView else { return }
382+
383+
var reader = BufferReader(bytes: bufferView)
384+
385+
// Advance reader to the end to test lineNumber calculation
386+
while !reader.isAtEnd {
387+
_ = reader.read()
388+
}
389+
390+
// Should count 4 lines: initial line + 3 newlines
391+
#expect(reader.lineNumber == 4, "Expected 4 lines, got \(reader.lineNumber)")
392+
}
393+
}
394+
395+
@Test("Line number counting with CRLF sequences")
396+
func lineNumberCountingWithCRLF() {
397+
// Test CRLF handling: \r\n should count as one line break
398+
let testString = "Line1\r\nLine2\r\nLine3"
399+
let bytes = Array(testString.utf8)
400+
401+
bytes.withUnsafeBufferPointer {
402+
let bufferView = BufferView(unsafeBufferPointer: $0)
403+
#expect(bufferView != nil)
404+
guard let bufferView else { return }
405+
406+
var reader = BufferReader(bytes: bufferView)
407+
408+
while !reader.isAtEnd {
409+
_ = reader.read()
410+
}
411+
412+
// Should count 3 lines: initial line + 2 CRLF sequences
413+
#expect(reader.lineNumber == 3, "Expected 3 lines, got \(reader.lineNumber)")
414+
}
415+
}
416+
417+
@Test("Line number counting with mixed newlines")
418+
func lineNumberCountingMixed() {
419+
// Test mixed \n and \r\n sequences
420+
// Second byte is 'a' (not a newline) to ensure bug would be exposed
421+
let testString = "a\nb\r\nc\nd"
422+
let bytes = Array(testString.utf8)
423+
424+
bytes.withUnsafeBufferPointer {
425+
let bufferView = BufferView(unsafeBufferPointer: $0)
426+
#expect(bufferView != nil)
427+
guard let bufferView else { return }
428+
429+
var reader = BufferReader(bytes: bufferView)
430+
431+
while !reader.isAtEnd {
432+
_ = reader.read()
433+
}
434+
435+
// Should count 4 lines: initial line + 3 line breaks (one \n, one \r\n, one \n)
436+
#expect(reader.lineNumber == 4, "Expected 4 lines, got \(reader.lineNumber)")
437+
}
438+
}
439+
440+
@Test("Line number at different read positions")
441+
func lineNumberAtPosition() {
442+
let testString = "Line1\nLine2\nLine3"
443+
let bytes = Array(testString.utf8)
444+
445+
bytes.withUnsafeBufferPointer {
446+
let bufferView = BufferView(unsafeBufferPointer: $0)
447+
#expect(bufferView != nil)
448+
guard let bufferView else { return }
449+
450+
var reader = BufferReader(bytes: bufferView)
451+
452+
// Read up to first newline
453+
while reader.readIndex < bufferView.endIndex {
454+
let byte = reader.read()
455+
if byte == UInt8(ascii: "\n") {
456+
break
457+
}
458+
}
459+
#expect(reader.lineNumber == 2, "After first newline, expected 2 lines, got \(reader.lineNumber)")
460+
461+
// Read to end
462+
while !reader.isAtEnd {
463+
_ = reader.read()
464+
}
465+
#expect(reader.lineNumber == 3, "At end, expected 3 lines, got \(reader.lineNumber)")
466+
}
467+
}
468+
}

0 commit comments

Comments
 (0)