001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2018 Richard "Shred" K├Ârber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j.connector;
015
016import java.io.BufferedInputStream;
017import java.io.IOException;
018import java.io.InputStream;
019
020/**
021 * Normalizes line separators in an InputStream. Converts all line separators to '\n'.
022 * Multiple line separators are compressed to a single line separator. Leading line
023 * separators are removed. Trailing line separators are compressed to a single separator.
024 */
025public class TrimmingInputStream extends InputStream {
026    private final BufferedInputStream in;
027    private boolean startOfFile = true;
028
029    /**
030     * Creates a new {@link TrimmingInputStream}.
031     *
032     * @param in
033     *            {@link InputStream} to read from. Will be closed when this stream is
034     *            closed.
035     */
036    public TrimmingInputStream(InputStream in) {
037        this.in = new BufferedInputStream(in, 1024);
038    }
039
040    @Override
041    public int read() throws IOException {
042        var ch = in.read();
043
044        if (!isLineSeparator(ch)) {
045            startOfFile = false;
046            return ch;
047        }
048
049        in.mark(1);
050        ch = in.read();
051        while (isLineSeparator(ch)) {
052            in.mark(1);
053            ch = in.read();
054        }
055
056        if (startOfFile) {
057            startOfFile = false;
058            return ch;
059        } else {
060            in.reset();
061            return '\n';
062        }
063    }
064
065    @Override
066    public int available() throws IOException {
067        // Workaround for https://github.com/google/conscrypt/issues/1068. Conscrypt
068        // requires the stream to have at least one non-blocking byte available for
069        // reading, otherwise generateCertificates() will not read the stream, but
070        // immediately returns an empty list. This workaround pre-fills the buffer
071        // of the BufferedInputStream by reading 1 byte ahead.
072        if (in.available() == 0) {
073            in.mark(1);
074            var read = in.read();
075            in.reset();
076            if (read < 0) {
077                return 0;
078            }
079        }
080        return in.available();
081    }
082
083    @Override
084    public void close() throws IOException {
085        in.close();
086        super.close();
087    }
088
089    /**
090     * Checks if the character is a line separator.
091     */
092    private static boolean isLineSeparator(int ch) {
093        return ch == '\n' || ch == '\r';
094    }
095
096}