Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Carriage return handling #16

Merged
merged 8 commits into from
Sep 9, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@ class SnapshotFile {
fun parse(valueReader: SnapshotValueReader): SnapshotFile {
try {
val result = SnapshotFile()
result.unixNewlines =
TODO("""add internal method to SnapshotValueReader to query if newline is \n or \r\n""")
result.unixNewlines = valueReader.unixNewlines
val reader = SnapshotReader(valueReader)
// only if the first value starts with 📷
if (reader.peekKey()?.startsWith(HEADER_PREFIX) == true) {
Expand Down Expand Up @@ -220,6 +219,7 @@ class SnapshotReader(val valueReader: SnapshotValueReader) {
/** Provides the ability to parse a snapshot file incrementally. */
class SnapshotValueReader(val lineReader: LineReader) {
var line: String? = null
val unixNewlines = lineReader.unixNewlines()

/** The key of the next value, does not increment anything about the reader's state. */
fun peekKey(): String? {
Expand Down Expand Up @@ -324,6 +324,7 @@ class SnapshotValueReader(val lineReader: LineReader) {
expect class LineReader {
fun getLineNumber(): Int
fun readLine(): String?
fun unixNewlines(): Boolean

companion object {
fun forString(content: String): LineReader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ actual class LineReader {
}
actual fun getLineNumber(): Int = TODO()
actual fun readLine(): String? = TODO()
actual fun unixNewlines(): Boolean = TODO()
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,74 @@
*/
package com.diffplug.selfie

import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.LineNumberReader
import java.io.Reader
import java.io.StringReader
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets

actual class LineReader(reader: Reader) : LineNumberReader(reader) {
actual class LineReader(reader: Reader) {
private val reader = LineTerminatorAware(LineTerminatorReader(reader))

actual companion object {
actual fun forString(content: String) = LineReader(StringReader(content))
actual fun forBinary(content: ByteArray) =
LineReader(InputStreamReader(content.inputStream(), StandardCharsets.UTF_8))
}
actual fun getLineNumber(): Int = reader.lineNumber
actual fun readLine(): String? = reader.readLine()
actual fun unixNewlines(): Boolean = reader.lineTerminator.unixNewlines()
}

/**
* Keep track of carriage return char to figure it out if we need unix new line or not. The first
* line is kept in memory until we require the next line.
*/
private open class LineTerminatorAware(val lineTerminator: LineTerminatorReader) :
LineNumberReader(lineTerminator) {
/** First line is initialized as soon as possible. */
private var firstLine: String? = super.readLine()
override fun readLine(): String? {
if (this.firstLine != null) {
val result = this.firstLine
this.firstLine = null
return result
}
return super.readLine()
}
}

/**
* Override all read operations to find the carriage return. We want to keep lazy/incremental reads.
*/
private class LineTerminatorReader(reader: Reader) : BufferedReader(reader) {
private val CR: Int = '\r'.code
private var unixNewlines = System.lineSeparator().equals("\n")
nedtwigg marked this conversation as resolved.
Show resolved Hide resolved
override fun read(cbuf: CharArray): Int {
val result = super.read(cbuf)
unixNewlines = cbuf.indexOf(CR.toChar()) == -1
return result
}
override fun read(target: CharBuffer): Int {
val result = super.read(target)
unixNewlines = target.indexOf(CR.toChar()) == -1
return result
}
override fun read(cbuf: CharArray, off: Int, len: Int): Int {
val result = super.read(cbuf, off, len)
unixNewlines = cbuf.indexOf(CR.toChar()) == -1
return result
}
override fun read(): Int {
val ch = super.read()
if (ch == CR) {
unixNewlines = false
}
return ch
}
fun unixNewlines(): Boolean {
return unixNewlines
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.selfie

import io.kotest.matchers.shouldBe
import kotlin.test.Test

class LineReaderTest {

@Test
fun shouldFindUnixSeparatorFromBinary() {
val reader = LineReader.forBinary("This is a new line\n".encodeToByteArray())
reader.unixNewlines() shouldBe true
reader.readLine() shouldBe "This is a new line"
}

@Test
fun shouldFindWindowsSeparatorFromBinary() {
val reader = LineReader.forBinary("This is a new line\r\n".encodeToByteArray())
reader.unixNewlines() shouldBe false
reader.readLine() shouldBe "This is a new line"
}

@Test
fun shouldFindUnixSeparatorFromString() {
val reader = LineReader.forString("This is a new line\n")
reader.unixNewlines() shouldBe true
reader.readLine() shouldBe "This is a new line"
}

@Test
fun shouldFindWindowsSeparatorFromString() {
val reader = LineReader.forString("This is a new line\r\n")
reader.unixNewlines() shouldBe false
reader.readLine() shouldBe "This is a new line"
}

@Test
fun shouldGetOSLineSeparatorWhenThereIsNone() {
val reader = LineReader.forBinary("This is a new line".encodeToByteArray())
reader.unixNewlines() shouldBe System.lineSeparator().equals("\n")
reader.readLine() shouldBe "This is a new line"
}

@Test
fun shouldReadNextLineWithoutProblem() {
val reader = LineReader.forBinary("First\r\nSecond\r\n".encodeToByteArray())
reader.unixNewlines() shouldBe false
reader.readLine() shouldBe "First"
reader.unixNewlines() shouldBe false
reader.readLine() shouldBe "Second"
reader.unixNewlines() shouldBe false
}

@Test
fun shouldUseFirstLineSeparatorAndIgnoreNext() {
val reader = LineReader.forBinary("First\r\nAnother separator\n".encodeToByteArray())
reader.unixNewlines() shouldBe false
reader.readLine() shouldBe "First"
reader.unixNewlines() shouldBe false
reader.readLine() shouldBe "Another separator"
reader.unixNewlines() shouldBe false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,16 @@ internal class SnapshotFileLayout(
val snapshotRootFolder = rootFolder(properties.getProperty("output-dir"))
// it's pretty easy to preserve the line endings of existing snapshot files, but it's
// a bit harder to create a fresh snapshot file with the correct line endings.
val unixNewlines: Boolean =
TODO("find the first file in the snapshot folder and check if it has unix newlines")
return SnapshotFileLayout(snapshotRootFolder, snapshotFolderName, unixNewlines)
val candidate =
snapshotRootFolder
.resolve(snapshotFolderName!!)
.toFile()
.walkTopDown()
.maxDepth(1)
nedtwigg marked this conversation as resolved.
Show resolved Hide resolved
.filter { it.isFile }
.firstOrNull()
val cr = candidate?.readText()?.contains('\r') ?: System.lineSeparator().equals("\r\n")
nedtwigg marked this conversation as resolved.
Show resolved Hide resolved
return SnapshotFileLayout(snapshotRootFolder, snapshotFolderName, !cr)
}
private fun snapshotFolderName(snapshotDir: String?): String? {
if (snapshotDir == null) {
Expand Down