summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pom.xml13
-rw-r--r--xtra4j-misc/pom.xml9
-rw-r--r--xtra4j-misc/src/main/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStream.java106
-rw-r--r--xtra4j-misc/src/test/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStreamTest.java149
4 files changed, 277 insertions, 0 deletions
diff --git a/pom.xml b/pom.xml
index 98e3167..ef52a5c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,6 +16,8 @@
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<dep.version.junit>4.13.2</dep.version.junit>
+ <dep.version.mockito>4.11.0</dep.version.mockito>
+ <dep.version.slf4j>2.0.6</dep.version.slf4j>
</properties>
<modules>
@@ -25,11 +27,22 @@
<dependencyManagement>
<dependencies>
<dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${dep.version.slf4j}</version>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${dep.version.junit}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>${dep.version.mockito}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/xtra4j-misc/pom.xml b/xtra4j-misc/pom.xml
index e93de16..85dca0b 100644
--- a/xtra4j-misc/pom.xml
+++ b/xtra4j-misc/pom.xml
@@ -17,10 +17,19 @@
<dependencies>
<dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/xtra4j-misc/src/main/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStream.java b/xtra4j-misc/src/main/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStream.java
new file mode 100644
index 0000000..2a44d01
--- /dev/null
+++ b/xtra4j-misc/src/main/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStream.java
@@ -0,0 +1,106 @@
+package ch.hiddenalpha.xtra4j.octetstream;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.slf4j.ILoggerFactory;
+import org.slf4j.Logger;
+
+
+/** Filters away broken newlines. */
+public class CRLFtoLFOutputStream extends FilterOutputStream {
+
+ private static final int EMPTY = -42;
+ private final Logger log;
+ private int previous = EMPTY;
+
+ /**
+ * @param dst
+ * Destination where the result will be written to.
+ */
+ public CRLFtoLFOutputStream( OutputStream dst ) {
+ this(dst, null);
+ }
+
+ /**
+ * @param dst
+ * Destination where the result will be written to.
+ */
+ public CRLFtoLFOutputStream( OutputStream dst, ILoggerFactory lf ) {
+ super(dst);
+ this.log = (lf == null) ? null : lf.getLogger(CRLFtoLFOutputStream.class.getName());
+ }
+
+ @Override
+ public void write( int current ) throws IOException {
+ // We're allowed to ignore the three high octets (See doc of
+ // "OutputStream#write"). This allows us to assign special meanings to
+ // those values internally (eg our 'EMPTY' value). For this to work, we
+ // clear the high bits to not get confused just in case someone really
+ // passes such values.
+ current &= 0xFF;
+
+ if( previous == '\r' && current == '\n' ){
+ // Ignore the CR and only write the LF.
+ super.write(current);
+ previous = EMPTY;
+ }else if( previous == EMPTY ){
+ // Just fill our "buffer".
+ previous = current;
+ }else{
+ // Not a CRLF sequence. So shift a byte forward.
+ super.write(previous);
+ previous = current;
+ }
+ }
+
+ // TODO impl this
+// @Override
+// public void write( byte[] buf, int off, int len ) throws IOException {
+// int wrOff = off;
+// if( len > 0 && previous == '\r' && buf[off] != '\n' ){
+// out.write('\r'); // CR but no LF, so pass-through CR from last turn.
+// previous = EMPTY;
+// }
+// int i = -1;
+// while( true ){
+// i += 1;
+// if( i >= len ){
+// if( len > wrOff ){
+// if( previous != '\r' && previous != EMPTY && buf[wrOff] != '\n' ){
+// out.write(previous); previous = EMPTY; }
+// assert previous == EMPTY || previous == '\r';
+// previous = EMPTY;
+// out.write(buf, wrOff, len - wrOff); // Last chunk
+// }
+// if( len > 0 && buf[off + len - 1] == '\r' ){
+// previous = buf[off + len - 1]; }
+// break;
+// }
+// int pos = i + off;
+// int current = (buf[pos] & 0xFF);
+// if( current == '\r' && pos - off > 1 ){
+// // Found an ugly octet. Write up to that octet.
+// assert previous == EMPTY;
+// out.write(buf, wrOff, pos - wrOff);
+// // then skip unwanted octet.
+// wrOff = pos + 1;
+// }
+// }
+// }
+
+ @Override
+ public void flush() throws IOException {
+ if( previous == '\r' ){
+ log.debug("Have to flush a CR byte without knowing if the next byte might be a LF");
+ }
+ if( previous != EMPTY ){
+ int tmp = previous;
+ previous = EMPTY;
+ out.write(tmp);
+ }
+ out.flush();
+ }
+
+}
diff --git a/xtra4j-misc/src/test/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStreamTest.java b/xtra4j-misc/src/test/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStreamTest.java
new file mode 100644
index 0000000..fefe843
--- /dev/null
+++ b/xtra4j-misc/src/test/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStreamTest.java
@@ -0,0 +1,149 @@
+package ch.hiddenalpha.xtra4j.octetstream;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.ILoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import static java.nio.charset.StandardCharsets.*;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+
+
+public class CRLFtoLFOutputStreamTest {
+
+ OutputStream dst;
+ ILoggerFactory loggerFactory;
+
+ CRLFtoLFOutputStream testTarget;
+
+ @Before
+ public void before(){
+ dst = mock(OutputStream.class);
+ loggerFactory = null;
+ }
+
+ @Test
+ public void removesCRViaWriteInt() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ byte[] input = ("Hello\r\nWorld").getBytes(UTF_8);
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ for (byte b : input) {
+ testTarget.write((int)b);
+ }
+ }
+ byte[] result = dst.toByteArray();
+ assertArrayEquals(("Hello\nWorld").getBytes(UTF_8), result);
+ }
+
+ @Test
+ public void removesCRViaWriteArr() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ testTarget.write(("Hello\r\nWorld").getBytes(UTF_8));
+ }
+ byte[] result = dst.toByteArray();
+ assertArrayEquals(("Hello\nWorld").getBytes(UTF_8), result);
+ }
+
+ @Test
+ public void keepCrIfNoLfFollowsViaWriteInt() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ testTarget.write(0x13);
+ }
+ byte[] result = dst.toByteArray();
+ assertEquals(1, result.length);
+ assertEquals(0x13, result[0]);
+ }
+
+ @Test
+ public void keepCrIfNoLfFollowsViaWriteArr() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ testTarget.write(new byte[]{ 0x13 });
+ }
+ byte[] result = dst.toByteArray();
+ assertEquals(1, result.length);
+ assertEquals(0x13, result[0]);
+ }
+
+ @Test
+ public void replacesCrLfAtEofViaWriteInt() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ byte[] input = ("Hello\r\nWorld\r\n").getBytes(UTF_8);
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ for (byte b : input) {
+ testTarget.write((int)b);
+ }
+ }
+ byte[] result = dst.toByteArray();
+ assertArrayEquals(("Hello\nWorld\n").getBytes(UTF_8), result);
+ }
+
+ @Test
+ public void replacesCrLfAtEofViaWriteArr() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ testTarget.write(("Hello\r\nWorld\r\n").getBytes(UTF_8));
+ }
+ byte[] result = dst.toByteArray();
+ assertArrayEquals(("Hello\nWorld\n").getBytes(UTF_8), result);
+ }
+
+ @Test
+ public void worksIfCrLfSplitOverCalls() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ testTarget.write(("Hello\r").getBytes(UTF_8));
+ testTarget.write(("\nWorld").getBytes(UTF_8));
+ }
+ byte[] result = dst.toByteArray();
+ assertArrayEquals(("Hello\nWorld").getBytes(UTF_8), result);
+ }
+
+ @Test
+ public void keepsCrAtBufEndIfNonLfFollows() throws IOException {
+ ByteArrayOutputStream dst = new ByteArrayOutputStream();
+ this.dst = dst;
+ initTestTarget();
+
+ try (CRLFtoLFOutputStream testTarget = this.testTarget) {
+ testTarget.write(("CR\r").getBytes(UTF_8));
+ testTarget.write(("without any LF").getBytes(UTF_8));
+ }
+ byte[] result = dst.toByteArray();
+ assertArrayEquals(("CR\rwithout any LF").getBytes(UTF_8), result);
+ }
+
+ private void initTestTarget(){
+ testTarget = new CRLFtoLFOutputStream(dst, loggerFactory);
+ }
+
+}