InputStreamUtils.java
package com.renomad.minum.web;
import com.renomad.minum.security.ForbiddenUseException;
import com.renomad.minum.utils.UtilsException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* Handy helpful utilities for working with input streams.
*/
final class InputStreamUtils implements IInputStreamUtils {
private final int maxReadLineSizeBytes;
public InputStreamUtils(int maxReadLineSizeBytes) {
this.maxReadLineSizeBytes = maxReadLineSizeBytes;
}
@Override
public String readLine(InputStream inputStream) throws IOException {
final int NEWLINE_DECIMAL = 10;
final int CARRIAGE_RETURN_DECIMAL = 13;
final var result = new ByteArrayOutputStream(maxReadLineSizeBytes / 3);
int bytesRead = 0;
for (int i = 0;; i++) {
if (i >= maxReadLineSizeBytes) {
inputStream.close();
throw new ForbiddenUseException("client sent more bytes than allowed for a single line. max: " + maxReadLineSizeBytes);
}
int a = inputStream.read();
if (a == -1) {
if (bytesRead > 0) {
return result.toString(StandardCharsets.UTF_8);
} else {
/*
it could be unclear whether we read a line that's an empty string, or we
reached the end of stream. With this code, if we get an empty string,
that means the line we read is just an empty string, and if we get a null,
that means we didn't have any characters read into our ByteArrayOutputStream,
and tried reading at the end of stream.
*/
return null;
}
}
if (a == CARRIAGE_RETURN_DECIMAL) continue;
if (a == NEWLINE_DECIMAL) break;
result.write(a);
bytesRead += 1;
}
return result.toString(StandardCharsets.UTF_8);
}
@Override
public byte[] read(int lengthToRead, InputStream inputStream) {
final int typicalBufferSize = 1024 * 8;
byte[] buf = new byte[Math.min(lengthToRead, typicalBufferSize)]; // 8k buffer is my understanding of a decent size. Fast, doesn't waste too much space.
byte[] data;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int read;
int totalRead = 0;
try {
while ((read = inputStream.read(buf)) >= 0) {
totalRead += read;
if (totalRead < lengthToRead) {
// if we haven't gotten everything we wanted, write this to the output and loop again
baos.write(buf, 0, read);
} else {
baos.write(buf, 0, read - (totalRead - lengthToRead));
break;
}
}
} catch (IOException ex) {
throw new UtilsException(ex);
}
data = baos.toByteArray();
if (data.length != lengthToRead) {
String message = String.format("length of bytes read (%d) must be what we expected (%d)", data.length, lengthToRead);
throw new ForbiddenUseException(message);
}
return data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InputStreamUtils that = (InputStreamUtils) o;
return maxReadLineSizeBytes == that.maxReadLineSizeBytes;
}
@Override
public int hashCode() {
return Objects.hash(maxReadLineSizeBytes);
}
}