diff options
-rw-r--r-- | pom.xml | 13 | ||||
-rw-r--r-- | xtra4j-misc/pom.xml | 9 | ||||
-rw-r--r-- | xtra4j-misc/src/main/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStream.java | 106 | ||||
-rw-r--r-- | xtra4j-misc/src/test/java/ch/hiddenalpha/xtra4j/octetstream/CRLFtoLFOutputStreamTest.java | 149 |
4 files changed, 277 insertions, 0 deletions
@@ -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); + } + +} |