Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/FoundationEssentials/CodableUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ struct BufferReader {
if nextIndex < readIndex && fullBuffer[unchecked: nextIndex] == ._newline {
p = nextIndex
}
} else if fullBuffer[offset: 1] == ._newline {
} else if fullBuffer[unchecked: p] == ._newline {
count += 1
}
fullBuffer.formIndex(&p, offsetBy: 1)
Expand Down
104 changes: 104 additions & 0 deletions Tests/FoundationEssentialsTests/BufferViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,107 @@ private struct BufferViewTests {
}
}
}

@Suite("BufferReader")
private struct BufferReaderTests {

@Test("Line number counting with standalone newlines")
func lineNumberCounting() {
// Test case designed to expose the bug: second byte is 'A' (not a newline),
// but there are newlines at positions 3, 5, and 7.
// With the bug, fullBuffer[offset: 1] would check byte 1 ('A') and never count
// the newlines at positions 3, 5, and 7.
let testString = "A\nB\nC\nD"
let bytes = Array(testString.utf8)

bytes.withUnsafeBufferPointer {
let bufferView = BufferView(unsafeBufferPointer: $0)
#expect(bufferView != nil)
guard let bufferView else { return }

var reader = BufferReader(bytes: bufferView)

// Advance reader to the end to test lineNumber calculation
while !reader.isAtEnd {
_ = reader.read()
}

// Should count 4 lines: initial line + 3 newlines
#expect(reader.lineNumber == 4, "Expected 4 lines, got \(reader.lineNumber)")
}
}

@Test("Line number counting with CRLF sequences")
func lineNumberCountingWithCRLF() {
// Test CRLF handling: \r\n should count as one line break
let testString = "Line1\r\nLine2\r\nLine3"
let bytes = Array(testString.utf8)

bytes.withUnsafeBufferPointer {
let bufferView = BufferView(unsafeBufferPointer: $0)
#expect(bufferView != nil)
guard let bufferView else { return }

var reader = BufferReader(bytes: bufferView)

while !reader.isAtEnd {
_ = reader.read()
}

// Should count 3 lines: initial line + 2 CRLF sequences
#expect(reader.lineNumber == 3, "Expected 3 lines, got \(reader.lineNumber)")
}
}

@Test("Line number counting with mixed newlines")
func lineNumberCountingMixed() {
// Test mixed \n and \r\n sequences
// Second byte is 'a' (not a newline) to ensure bug would be exposed
let testString = "a\nb\r\nc\nd"
let bytes = Array(testString.utf8)

bytes.withUnsafeBufferPointer {
let bufferView = BufferView(unsafeBufferPointer: $0)
#expect(bufferView != nil)
guard let bufferView else { return }

var reader = BufferReader(bytes: bufferView)

while !reader.isAtEnd {
_ = reader.read()
}

// Should count 4 lines: initial line + 3 line breaks (one \n, one \r\n, one \n)
#expect(reader.lineNumber == 4, "Expected 4 lines, got \(reader.lineNumber)")
}
}

@Test("Line number at different read positions")
func lineNumberAtPosition() {
let testString = "Line1\nLine2\nLine3"
let bytes = Array(testString.utf8)

bytes.withUnsafeBufferPointer {
let bufferView = BufferView(unsafeBufferPointer: $0)
#expect(bufferView != nil)
guard let bufferView else { return }

var reader = BufferReader(bytes: bufferView)

// Read up to first newline
while reader.readIndex < bufferView.endIndex {
let byte = reader.read()
if byte == UInt8(ascii: "\n") {
break
}
}
#expect(reader.lineNumber == 2, "After first newline, expected 2 lines, got \(reader.lineNumber)")

// Read to end
while !reader.isAtEnd {
_ = reader.read()
}
#expect(reader.lineNumber == 3, "At end, expected 3 lines, got \(reader.lineNumber)")
}
}
}