Skip to content

Commit

Permalink
Merge 0aa2ce0 into b6dce24
Browse files Browse the repository at this point in the history
  • Loading branch information
RutledgePaulV committed Jul 17, 2021
2 parents b6dce24 + 0aa2ce0 commit 38869e5
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.github.rutledgepaulv.injectingstreams;

import java.io.InputStream;
import java.io.OutputStream;

public final class InjectingStreams {
private InjectingStreams() {
}

public static OutputStream injectBeforeOutput(OutputStream out, String delimiter, String injection) {
return new PreDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectBeforeOutput(OutputStream out, String delimiter, byte[] injection) {
return new PreDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectBeforeOutput(OutputStream out, String delimiter, InputStream injection) {
return new PreDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectBeforeOutput(OutputStream out, byte[] delimiter, String injection) {
return new PreDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectBeforeOutput(OutputStream out, byte[] delimiter, byte[] injection) {
return new PreDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectBeforeOutput(OutputStream out, byte[] delimiter, InputStream injection) {
return new PreDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectAfterOutput(OutputStream out, String delimiter, String injection) {
return new PostDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectAfterOutput(OutputStream out, String delimiter, byte[] injection) {
return new PostDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectAfterOutput(OutputStream out, String delimiter, InputStream injection) {
return new PostDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectAfterOutput(OutputStream out, byte[] delimiter, String injection) {
return new PostDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectAfterOutput(OutputStream out, byte[] delimiter, byte[] injection) {
return new PostDelimiterInjectingOutputStream(out, delimiter, injection);
}

public static OutputStream injectAfterOutput(OutputStream out, byte[] delimiter, InputStream injection) {
return new PostDelimiterInjectingOutputStream(out, delimiter, injection);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,38 @@

/**
* An output stream that injects a stream of bytes immediately following the first series of
* delimiter bytes. Useful for appending content to a stream following a known delimiter.
* delimiter bytes.
*
* Assumes a single writer (no synchronization)
*/
public class InjectingOutputStream extends FilterOutputStream {
public class PostDelimiterInjectingOutputStream extends FilterOutputStream {

private final InputStream injection;
private boolean injected = false;
private int bufferPos = 0;
private final byte[] delimiter;

public InjectingOutputStream(OutputStream out, String delimiter, String injection) {
public PostDelimiterInjectingOutputStream(OutputStream out, String delimiter, String injection) {
this(out, delimiter.getBytes(), injection.getBytes());
}

public InjectingOutputStream(OutputStream out, String delimiter, byte[] injection) {
public PostDelimiterInjectingOutputStream(OutputStream out, String delimiter, byte[] injection) {
this(out, delimiter.getBytes(), injection);
}

public InjectingOutputStream(OutputStream out, String delimiter, InputStream injection) {
public PostDelimiterInjectingOutputStream(OutputStream out, String delimiter, InputStream injection) {
this(out, delimiter.getBytes(), injection);
}

public InjectingOutputStream(OutputStream out, byte[] delimiter, String injection) {
public PostDelimiterInjectingOutputStream(OutputStream out, byte[] delimiter, String injection) {
this(out, delimiter, injection.getBytes());
}

public InjectingOutputStream(OutputStream out, byte[] delimiter, byte[] injection) {
public PostDelimiterInjectingOutputStream(OutputStream out, byte[] delimiter, byte[] injection) {
this(out, delimiter, new ByteArrayInputStream(injection));
}

public InjectingOutputStream(OutputStream out, byte[] delimiter, InputStream injection) {
public PostDelimiterInjectingOutputStream(OutputStream out, byte[] delimiter, InputStream injection) {
super(out);
this.delimiter = delimiter;
this.injection = injection;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.github.rutledgepaulv.injectingstreams;

import java.io.*;


/**
* An output stream that injects a stream of bytes immediately preceding the first series of
* delimiter bytes.
* <p>
* Assumes a single writer (no synchronization)
*/
public class PreDelimiterInjectingOutputStream extends FilterOutputStream {

private final InputStream injection;
private boolean injected = false;
private final byte[] delimiter;
private int bufferOffset = 0;

public PreDelimiterInjectingOutputStream(OutputStream out, String delimiter, String injection) {
this(out, delimiter.getBytes(), injection.getBytes());
}

public PreDelimiterInjectingOutputStream(OutputStream out, String delimiter, byte[] injection) {
this(out, delimiter.getBytes(), injection);
}

public PreDelimiterInjectingOutputStream(OutputStream out, String delimiter, InputStream injection) {
this(out, delimiter.getBytes(), injection);
}

public PreDelimiterInjectingOutputStream(OutputStream out, byte[] delimiter, String injection) {
this(out, delimiter, injection.getBytes());
}

public PreDelimiterInjectingOutputStream(OutputStream out, byte[] delimiter, byte[] injection) {
this(out, delimiter, new ByteArrayInputStream(injection));
}

public PreDelimiterInjectingOutputStream(OutputStream out, byte[] delimiter, InputStream injection) {
super(out);
this.delimiter = delimiter;
this.injection = injection;
}

private void inject() throws IOException {
try (InputStream in = this.injection) {
byte[] buffer = new byte[4096];
int n;
while (-1 != (n = in.read(buffer))) {
out.write(buffer, 0, n);
}
} finally {
injected = true;
bufferOffset = 0;
}
}

private void drainBuffer() throws IOException {
if (bufferOffset > 0) {
out.write(delimiter, 0, bufferOffset);
bufferOffset = 0;
}
}

@Override
public void write(int b) throws IOException {
if (!injected) {
// if this byte is the next element of the delimiter
if (b == delimiter[bufferOffset]) {
// increment the buffer offset
bufferOffset++;
// if we've reached the end of the delimiter
if (bufferOffset == delimiter.length) {
// inject the content
inject();
// inject the buffer
out.write(delimiter);
}
} else {
// flush the pending buffer
this.drainBuffer();
// if this byte is the first element of another potential delimiter sequence
if (b == delimiter[bufferOffset]) {
bufferOffset++;
} else {
out.write(b);
}
}
} else {
out.write(b);
}
}

@Override
public void write(byte[] bytes) throws IOException {
this.write(bytes, 0, bytes.length);
}

@Override
public void write(byte[] bytes, int off, int len) throws IOException {
if (!injected) {
for (int i = off; i < len + off; i++) {
byte b = bytes[i];
// b matches next position in buffer
if (b == delimiter[bufferOffset]) {
bufferOffset++;
// buffer became full
if (bufferOffset == delimiter.length) {
int length = i - off - delimiter.length + 1;
if (length > 0) {
out.write(bytes, off, length);
}
inject();
out.write(delimiter);
int nextIndex = i + 1;
if (nextIndex < len) {
int remaining = len - nextIndex - off;
out.write(bytes, nextIndex, remaining);
}
return;
}
} else {
// we read a byte that negates the current match, go ahead and flush
// the imaginary buffer if it might contain bytes from the prior write
if (i - bufferOffset < off) {
this.drainBuffer();
}
bufferOffset = 0;
// check if this byte is the beginning of the next sequence too
if (b == delimiter[bufferOffset]) {
bufferOffset++;
}
}
}

// still not injected, need to take care of earlier bytes up to buffer
if (!injected) {
int length = (len - bufferOffset);
if (length > 0) {
out.write(bytes, off, length);
}
}
} else {
out.write(bytes, off, len);
}
}

@Override
public void close() throws IOException {
this.drainBuffer();
try {
injection.close();
} finally {
super.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,35 @@
import static java.nio.charset.Charset.defaultCharset;
import static org.junit.Assert.assertEquals;

public class InjectingOutputStreamTest {
public class PostDelimiterInjectingOutputStreamTest {

@Test
public void constructors() {
new InjectingOutputStream(new ByteArrayOutputStream(), "Test", "Test".getBytes());
new InjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), "Test");
new InjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), "Test".getBytes());
new InjectingOutputStream(new ByteArrayOutputStream(), "Test", new ByteArrayInputStream("Test".getBytes()));
new InjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), new ByteArrayInputStream("Test".getBytes()));
new PostDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test", "Test".getBytes());
new PostDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), "Test");
new PostDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), "Test".getBytes());
new PostDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test", new ByteArrayInputStream("Test".getBytes()));
new PostDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), new ByteArrayInputStream("Test".getBytes()));
}

@Test
public void fuzzing() throws IOException {
public void fuzzingSingleCharacterDelimiter() throws IOException {

for (int i = 0; i < 10000; i++) {
ByteArrayOutputStream rawOut = new ByteArrayOutputStream();
fuzzyWrite(new InjectingOutputStream(rawOut, "hello ", "world "), "before hello after");
fuzzyWrite(new PostDelimiterInjectingOutputStream(rawOut, "h", "world "), "before hello after");
String finalOutput = new String(rawOut.toByteArray(), defaultCharset());
assertEquals("before hworld ello after", finalOutput);
}

}

@Test
public void fuzzingLongString() throws IOException {

for (int i = 0; i < 10000; i++) {
ByteArrayOutputStream rawOut = new ByteArrayOutputStream();
fuzzyWrite(new PostDelimiterInjectingOutputStream(rawOut, "hello ", "world "), "before hello after");
String finalOutput = new String(rawOut.toByteArray(), defaultCharset());
assertEquals("before hello world after", finalOutput);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.github.rutledgepaulv.injectingstreams;

import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

import static java.nio.charset.Charset.defaultCharset;
import static org.junit.Assert.assertEquals;

public class PreDelimiterInjectingOutputStreamTest {

@Test
public void constructors() {
new PreDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test", "Test".getBytes());
new PreDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), "Test");
new PreDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), "Test".getBytes());
new PreDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test", new ByteArrayInputStream("Test".getBytes()));
new PreDelimiterInjectingOutputStream(new ByteArrayOutputStream(), "Test".getBytes(), new ByteArrayInputStream("Test".getBytes()));
}

@Test
public void fuzzingFirstCharacter() throws IOException {

for (int i = 0; i < 10000; i++) {
ByteArrayOutputStream rawOut = new ByteArrayOutputStream();
fuzzyWrite(new PreDelimiterInjectingOutputStream(rawOut, "b", "world "), "before hello after");
String finalOutput = new String(rawOut.toByteArray(), defaultCharset());
assertEquals("world before hello after", finalOutput);
}

}

@Test
public void fuzzingSingleCharacterDelimiter() throws IOException {

for (int i = 0; i < 100000; i++) {
ByteArrayOutputStream rawOut = new ByteArrayOutputStream();
fuzzyWrite(new PreDelimiterInjectingOutputStream(rawOut, "h", "world "), "before hello after");
String finalOutput = new String(rawOut.toByteArray(), defaultCharset());
assertEquals("before world hello after", finalOutput);
}

}

@Test
public void fuzzing() throws IOException {

for (int i = 0; i < 10000; i++) {
ByteArrayOutputStream rawOut = new ByteArrayOutputStream();
fuzzyWrite(new PreDelimiterInjectingOutputStream(rawOut, "hello ", "world "), "before hello after");
String finalOutput = new String(rawOut.toByteArray(), defaultCharset());
assertEquals("before world hello after", finalOutput);
}

}

public void fuzzyWrite(OutputStream stream, String content) throws IOException {
List<byte[]> parts = partitions(content);
String combined = parts.stream().map(String::new).reduce((s1, s2) -> s1 + s2).get();
assertEquals(content, combined);
try (OutputStream out = stream) {
for (byte[] bites : parts) {
if (bites.length == 1) {
out.write(bites[0]);
} else {
out.write(bites);
}
}
}
}

public List<byte[]> partitions(String content) {
Random random = new Random(System.nanoTime());
int offset = 0;
List<String> partitions = new ArrayList<>();
while (offset < content.length()) {
int length = Math.min(random.nextInt(content.length() - offset) + 1, content.length());
partitions.add(content.substring(offset, offset + length));
offset += length;
}
return partitions.stream().map(String::getBytes).collect(Collectors.toList());
}
}

0 comments on commit 38869e5

Please sign in to comment.