StreamingMultipartPartition.java

1
package com.renomad.minum.web;
2
3
4
import com.renomad.minum.utils.RingBuffer;
5
6
import java.io.ByteArrayOutputStream;
7
import java.io.IOException;
8
import java.io.InputStream;
9
import java.nio.charset.StandardCharsets;
10
import java.util.List;
11
import java.util.stream.IntStream;
12
13
/**
14
 * This class represents a single partition in a multipart/form
15
 * Request body, when read as an InputStream.  This enables the
16
 * developer to pull data incrementally, rather than reading it
17
 * all into memory at once.
18
 */
19
public class StreamingMultipartPartition extends InputStream {
20
21
    private final Headers headers;
22
    private final InputStream inputStream;
23
    private final ContentDisposition contentDisposition;
24
    private final int contentLength;
25
    /**
26
     * After we hit the boundary, we will set this flag to true, and all
27
     * subsequent reads will return -1.
28
     */
29
    private boolean isFinished = false;
30
31
    /**
32
     * This buffer follows along with what we are reading, so we can
33
     * easily compare against our boundary value.  There are four extra
34
     * bytes included, since multipart splits the content by two
35
     * dashes, followed by the boundary value, and then two dashes afterwards
36
     * on the last boundary.
37
     * <pre>
38
     * That is,
39
     * for a typical boundary:
40
     *
41
     *   --boundary_value
42
     *
43
     * and for the last boundary:
44
     *
45
     *   --boundary_value--
46
     *</pre>
47
     */
48
    private final RingBuffer<Byte> recentBytesBuffer;
49
    private final CountBytesRead countBytesRead;
50
    private final List<Byte> boundaryValueList;
51
    private boolean hasFilledBuffer;
52
53
    StreamingMultipartPartition(Headers headers,
54
                                       InputStream inputStream,
55
                                       ContentDisposition contentDisposition,
56
                                       String boundaryValue,
57
                                       CountBytesRead countBytesRead,
58
                                       int contentLength) {
59
60
        this.headers = headers;
61
        this.inputStream = inputStream;
62
        this.contentDisposition = contentDisposition;
63
        this.contentLength = contentLength;
64
        String boundaryValue1 = "\r\n--" + boundaryValue;
65
        byte[] bytes = boundaryValue1.getBytes(StandardCharsets.US_ASCII);
66 1 1. lambda$new$0 : replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::lambda$new$0 → KILLED
        boundaryValueList = IntStream.range(0, bytes.length).mapToObj(i -> bytes[i]).toList();
67
68
        /*
69
         * To explain the numbers here: we add one at the beginning to represent
70
         * the single character at the far left that is what we will actually return.
71
         * We have to fill the cache before we start sending anything.  The number
72
         * at the end represents the extra characters of the boundary - dashes,
73
         * carriage return, newline.
74
         */
75
        recentBytesBuffer = new RingBuffer<>(boundaryValue1.length(), Byte.class);
76
        this.countBytesRead = countBytesRead;
77
    }
78
79
    public Headers getHeaders() {
80 1 1. getHeaders : replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::getHeaders → KILLED
        return headers;
81
    }
82
83
    public ContentDisposition getContentDisposition() {
84 1 1. getContentDisposition : replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::getContentDisposition → KILLED
        return contentDisposition;
85
    }
86
87
88
    /**
89
     * Reads from the inputstream using a buffer for checking whether we've
90
     * hit the end of a multpart partition.
91
     * @return -1 if we're at the end of a partition
92
     * @throws IOException if the inputstream is closed unexpectedly while reading.
93
     */
94
    @Override
95
    public int read() throws IOException {
96 1 1. read : negated conditional → KILLED
        if (isFinished) {
97 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → TIMED_OUT
            return -1;
98
        }
99 1 1. read : negated conditional → KILLED
        if (!hasFilledBuffer) {
100 1 1. read : removed call to com/renomad/minum/web/StreamingMultipartPartition::fillBuffer → KILLED
            fillBuffer();
101
            boolean atTheEnd = recentBytesBuffer.containsAt(boundaryValueList, 0);
102 1 1. read : negated conditional → KILLED
            if (atTheEnd) {
103
                // don't really do anything with this, it's just to collect the
104
                // last characters to have a clean finish.
105
                byte[] unused = inputStream.readNBytes(2);
106
                isFinished = true;
107 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED
                return -1;
108
            }
109
        } else {
110
            int result = inputStream.read();
111 1 1. read : removed call to com/renomad/minum/web/CountBytesRead::increment → KILLED
            countBytesRead.increment();
112 2 1. read : changed conditional boundary → SURVIVED
2. read : negated conditional → KILLED
            if (countBytesRead.getCount() >= contentLength) {
113
                isFinished = true;
114 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → SURVIVED
                return -1;
115
            }
116 1 1. read : negated conditional → KILLED
            if (result == -1) {
117
                throw new IOException("Error: The inputstream has closed unexpectedly while reading");
118
            }
119
            byte byteValue = (byte) result;
120
            boolean isAtEndOfPartition = updateRecentBytesBufferAndCheck(byteValue);
121 1 1. read : negated conditional → KILLED
            if (isAtEndOfPartition) {
122
                // don't really do anything with this, it's just to collect the
123
                // last characters to have a clean finish.
124
                byte[] unused = inputStream.readNBytes(2);
125
                isFinished = true;
126 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED
                return -1;
127
            }
128
129
        }
130 2 1. read : Replaced bitwise AND with OR → KILLED
2. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED
        return ((int)recentBytesBuffer.atNextIndex()) & 0xff;
131
    }
132
133
    private void fillBuffer() throws IOException {
134 2 1. fillBuffer : changed conditional boundary → KILLED
2. fillBuffer : negated conditional → KILLED
        for (int i = 0; i < recentBytesBuffer.getLimit(); i++) {
135
            int result = inputStream.read();
136 1 1. fillBuffer : removed call to com/renomad/minum/web/CountBytesRead::increment → KILLED
            countBytesRead.increment();
137 1 1. fillBuffer : negated conditional → KILLED
            if (result == -1) {
138
                throw new IOException("Error: The inputstream has closed unexpectedly while reading");
139
            }
140
            byte byteValue = (byte) result;
141
            updateRecentBytesBufferAndCheck(byteValue);
142
        }
143
        hasFilledBuffer = true;
144
    }
145
146
    @Override
147
    public byte[] readAllBytes()  {
148
        var baos = new ByteArrayOutputStream();
149
        while (true) {
150
            int result = 0;
151
            try {
152
                result = read();
153
            } catch (IOException e) {
154
                throw new WebServerException(e);
155
            }
156 1 1. readAllBytes : negated conditional → TIMED_OUT
            if (result == -1) {
157 1 1. readAllBytes : replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::readAllBytes → KILLED
                return baos.toByteArray();
158
            }
159 1 1. readAllBytes : removed call to java/io/ByteArrayOutputStream::write → KILLED
            baos.write((byte)result);
160
        }
161
    }
162
163
    /**
164
     * Updates the buffer with the last characters read, and returns
165
     * true if we have encountered the end of this partition.
166
     */
167
    private boolean updateRecentBytesBufferAndCheck(byte newByte) {
168 1 1. updateRecentBytesBufferAndCheck : removed call to com/renomad/minum/utils/RingBuffer::add → KILLED
        recentBytesBuffer.add(newByte);
169 2 1. updateRecentBytesBufferAndCheck : replaced boolean return with true for com/renomad/minum/web/StreamingMultipartPartition::updateRecentBytesBufferAndCheck → KILLED
2. updateRecentBytesBufferAndCheck : replaced boolean return with false for com/renomad/minum/web/StreamingMultipartPartition::updateRecentBytesBufferAndCheck → KILLED
        return recentBytesBuffer.containsAt(boundaryValueList, 0);
170
    }
171
172
    /**
173
     * By "close", we will read from the {@link InputStream} until we have finished the body,
174
     * so that our InputStream has been read until the start of the next partition.
175
     */
176
    @Override
177
    public void close() throws IOException {
178
        while (true) {
179
            int result = read();
180 1 1. close : negated conditional → TIMED_OUT
            if (result == -1) {
181
                return;
182
            }
183
        }
184
    }
185
186
}

Mutations

66

1.1
Location : lambda$new$0
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::lambda$new$0 → KILLED

80

1.1
Location : getHeaders
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::getHeaders → KILLED

84

1.1
Location : getContentDisposition
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::getContentDisposition → KILLED

96

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

97

1.1
Location : read
Killed by : none
replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → TIMED_OUT

99

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

100

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
removed call to com/renomad/minum/web/StreamingMultipartPartition::fillBuffer → KILLED

102

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

107

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED

111

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.testReadingStreamingMultipart(com.renomad.minum.web.RequestTests)
removed call to com/renomad/minum/web/CountBytesRead::increment → KILLED

112

1.1
Location : read
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

2.2
Location : read
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

114

1.1
Location : read
Killed by : none
replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → SURVIVED
Covering tests

116

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

121

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

126

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED

130

1.1
Location : read
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
Replaced bitwise AND with OR → KILLED

2.2
Location : read
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED

134

1.1
Location : fillBuffer
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
changed conditional boundary → KILLED

2.2
Location : fillBuffer
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

136

1.1
Location : fillBuffer
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
removed call to com/renomad/minum/web/CountBytesRead::increment → KILLED

137

1.1
Location : fillBuffer
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

156

1.1
Location : readAllBytes
Killed by : none
negated conditional → TIMED_OUT

157

1.1
Location : readAllBytes
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::readAllBytes → KILLED

159

1.1
Location : readAllBytes
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
removed call to java/io/ByteArrayOutputStream::write → KILLED

168

1.1
Location : updateRecentBytesBufferAndCheck
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
removed call to com/renomad/minum/utils/RingBuffer::add → KILLED

169

1.1
Location : updateRecentBytesBufferAndCheck
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
replaced boolean return with true for com/renomad/minum/web/StreamingMultipartPartition::updateRecentBytesBufferAndCheck → KILLED

2.2
Location : updateRecentBytesBufferAndCheck
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
replaced boolean return with false for com/renomad/minum/web/StreamingMultipartPartition::updateRecentBytesBufferAndCheck → KILLED

180

1.1
Location : close
Killed by : none
negated conditional → TIMED_OUT

Active mutators

Tests examined


Report generated by PIT 1.17.0