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);
        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) return result.toString(StandardCharsets.UTF_8);
            if (a == CARRIAGE_RETURN_DECIMAL) continue;
            if (a == NEWLINE_DECIMAL) break;
            result.write(a);
        }
        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);
    }
}