src/main/java/org/takes/rq/ChunkedInputStream.java
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2024 Yegor Bugayenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.takes.rq;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import org.cactoos.Text;
import org.cactoos.scalar.Ternary;
import org.cactoos.scalar.Unchecked;
import org.cactoos.text.Sub;
import org.cactoos.text.TextOf;
import org.cactoos.text.Trimmed;
import org.cactoos.text.UncheckedText;
/**
* Input stream from chunked coded http request body.
*
* @since 0.31.2
* @link <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1">Chunked Transfer Coding</a>
*/
final class ChunkedInputStream extends InputStream {
/**
* The inputstream that we're wrapping.
*/
private final InputStream origin;
/**
* The chunk size.
*/
private int size;
/**
* The current position within the current chunk.
*/
private int pos;
/**
* True if we'are at the beginning of stream.
*/
private boolean bof;
/**
* True if we've reached the end of stream.
*/
private boolean eof;
/**
* Ctor.
* @param stream The raw input stream
*/
ChunkedInputStream(final InputStream stream) {
super();
this.bof = true;
this.origin = stream;
}
@Override
public int read() throws IOException {
if (!this.eof && this.pos >= this.size) {
this.nextChunk();
}
final int result;
if (this.eof) {
result = -1;
} else {
++this.pos;
result = this.origin.read();
}
return result;
}
@Override
public int read(final byte[] buf, final int off, final int len)
throws IOException {
if (!this.eof && this.pos >= this.size) {
this.nextChunk();
}
final int result;
if (this.eof) {
result = -1;
} else {
final int shift = Math.min(len, this.size - this.pos);
final int count = this.origin.read(buf, off, shift);
this.pos += count;
if (shift == len) {
result = len;
} else {
result = shift + this.read(buf, off + shift, len - shift);
}
}
return result;
}
@Override
public int read(final byte[] buf) throws IOException {
return this.read(buf, 0, buf.length);
}
/**
* Read the CRLF terminator.
* @throws IOException If an IO error occurs.
*/
private void readCrlf() throws IOException {
final int crsymbol = this.origin.read();
final int lfsymbol = this.origin.read();
if (crsymbol != '\r' || lfsymbol != '\n') {
throw new IOException(
String.format(
"%s %d%s%d",
"CRLF expected at end of chunk: ",
crsymbol,
"/",
lfsymbol
)
);
}
}
/**
* Read the next chunk.
* @throws IOException If an IO error occurs.
*/
private void nextChunk() throws IOException {
if (!this.bof) {
this.readCrlf();
}
this.size = ChunkedInputStream.chunkSize(this.origin);
this.bof = false;
this.pos = 0;
if (this.size == 0) {
this.eof = true;
}
}
/**
* Expects the stream to start with a chunksize in hex with optional
* comments after a semicolon. The line must end with a CRLF: "a3; some
* comment\r\n" Positions the stream at the start of the next line.
* @param stream The new input stream.
* @return The chunk size as integer
* @throws IOException when the chunk size could not be parsed
*/
private static int chunkSize(final InputStream stream)
throws IOException {
final ByteArrayOutputStream baos = ChunkedInputStream.sizeLine(stream);
final String data = baos.toString(Charset.defaultCharset().name());
final int separator = data.indexOf(';');
final Text number = new Trimmed(
new Unchecked<>(
new Ternary<>(
separator > 0,
new Sub(data, 0, separator),
new TextOf(data)
)
).value()
);
try {
return Integer.parseInt(
new UncheckedText(
number
).asString(),
16
);
} catch (final NumberFormatException ex) {
throw new IOException(
String.format(
"Bad chunk size: %s",
baos.toString(Charset.defaultCharset().name())
),
ex
);
}
}
/**
* Possible states of FSM that used to find chunk size.
*/
private enum State {
/**
* Normal.
*/
NORMAL,
/**
* If \r was scanned.
*/
R,
/**
* Inside quoted string.
*/
QUOTED_STRING,
/**
* End.
*/
END;
}
/**
* Extract line with chunk size from stream.
* @param stream Input stream.
* @return Line with chunk size.
* @throws IOException If fails.
*/
private static ByteArrayOutputStream sizeLine(final InputStream stream)
throws IOException {
State state = State.NORMAL;
final ByteArrayOutputStream result = new ByteArrayOutputStream();
while (state != State.END) {
state = next(stream, state, result);
}
return result;
}
/**
* Get next state for FSM.
* @param stream Input stream.
* @param state Current state.
* @param line Current chunk size line.
* @return New state.
* @throws IOException If fails.
*/
private static State next(final InputStream stream, final State state,
final ByteArrayOutputStream line) throws IOException {
final int next = stream.read();
if (next == -1) {
throw new IOException("chunked stream ended unexpectedly");
}
final State result;
switch (state) {
case NORMAL:
result = nextNormal(state, line, next);
break;
case R:
if (next == '\n') {
result = State.END;
} else {
throw new IOException(
String.format(
"%s%s",
"Protocol violation: Unexpected",
" single newline character in chunk size"
)
);
}
break;
case QUOTED_STRING:
result = nextQuoted(stream, state, line, next);
break;
default:
throw new IllegalStateException("Bad state");
}
return result;
}
/**
* Maintain next symbol for current state = State.NORMAL.
* @param state Current state.
* @param line Current chunk size line.
* @param next Next symbol.
* @return New state.
*/
private static State nextNormal(final State state,
final ByteArrayOutputStream line, final int next) {
final State result;
switch (next) {
case '\r':
result = State.R;
break;
case '\"':
result = State.QUOTED_STRING;
break;
default:
result = state;
line.write(next);
break;
}
return result;
}
/**
* Maintain next symbol for current state = State.QUOTED_STRING.
* @param stream Input stream.
* @param state Current state.
* @param line Current chunk size line.
* @param next Next symbol.
* @return New state.
* @throws IOException If fails.
* @checkstyle ParameterNumberCheck (3 lines)
*/
private static State nextQuoted(final InputStream stream, final State state,
final ByteArrayOutputStream line, final int next)
throws IOException {
final State result;
switch (next) {
case '\\':
result = state;
line.write(stream.read());
break;
case '\"':
result = State.NORMAL;
break;
default:
result = state;
line.write(next);
break;
}
return result;
}
}