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
    /**
54
     * @param contentLength the length specified by the Content-Length header, it is
55
     *                      the overall length of the body
56
     */
57
    StreamingMultipartPartition(Headers headers,
58
                                       InputStream inputStream,
59
                                       ContentDisposition contentDisposition,
60
                                       String boundaryValue,
61
                                       CountBytesRead countBytesRead,
62
                                       int contentLength) {
63
64
        this.headers = headers;
65
        this.inputStream = inputStream;
66
        this.contentDisposition = contentDisposition;
67
        this.contentLength = contentLength;
68
        String boundaryValue1 = "\r\n--" + boundaryValue;
69
        byte[] bytes = boundaryValue1.getBytes(StandardCharsets.US_ASCII);
70 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();
71
72
        /*
73
         * To explain the numbers here: we add one at the beginning to represent
74
         * the single character at the far left that is what we will actually return.
75
         * We have to fill the cache before we start sending anything.  The number
76
         * at the end represents the extra characters of the boundary - dashes,
77
         * carriage return, newline.
78
         */
79
        recentBytesBuffer = new RingBuffer<>(boundaryValue1.length(), Byte.class);
80
        this.countBytesRead = countBytesRead;
81
    }
82
83
    public Headers getHeaders() {
84 1 1. getHeaders : replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::getHeaders → KILLED
        return headers;
85
    }
86
87
    public ContentDisposition getContentDisposition() {
88 1 1. getContentDisposition : replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::getContentDisposition → KILLED
        return contentDisposition;
89
    }
90
91
92
    /**
93
     * Reads from the inputstream using a buffer for checking whether we've
94
     * hit the end of a multpart partition.
95
     * @return -1 if we're at the end of a partition
96
     * @throws IOException if the inputstream is closed unexpectedly while reading.
97
     */
98
    @Override
99
    public int read() throws IOException {
100 1 1. read : negated conditional → KILLED
        if (isFinished) {
101 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → TIMED_OUT
            return -1;
102
        }
103 1 1. read : negated conditional → KILLED
        if (!hasFilledBuffer) {
104 1 1. read : removed call to com/renomad/minum/web/StreamingMultipartPartition::fillBuffer → KILLED
            fillBuffer();
105
            boolean atTheEnd = recentBytesBuffer.containsAt(boundaryValueList, 0);
106 1 1. read : negated conditional → KILLED
            if (atTheEnd) {
107
                // don't really do anything with this, it's just to collect the
108
                // last characters to have a clean finish.
109
                byte[] unused = inputStream.readNBytes(2);
110
                isFinished = true;
111 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED
                return -1;
112
            }
113
        } else {
114
            int result = inputStream.read();
115 1 1. read : removed call to com/renomad/minum/web/CountBytesRead::increment → KILLED
            countBytesRead.increment();
116 2 1. read : changed conditional boundary → KILLED
2. read : negated conditional → KILLED
            if (countBytesRead.getCount() >= contentLength) {
117
                isFinished = true;
118 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → SURVIVED
                return -1;
119
            }
120 1 1. read : negated conditional → KILLED
            if (result == -1) {
121
                throw new IOException("Error: The inputstream has closed unexpectedly while reading");
122
            }
123
            byte byteValue = (byte) result;
124
            boolean isAtEndOfPartition = updateRecentBytesBufferAndCheck(byteValue);
125 1 1. read : negated conditional → KILLED
            if (isAtEndOfPartition) {
126
                // don't really do anything with this, it's just to collect the
127
                // last characters to have a clean finish.
128
                byte[] unused = inputStream.readNBytes(2);
129
                isFinished = true;
130 1 1. read : replaced int return with 0 for com/renomad/minum/web/StreamingMultipartPartition::read → KILLED
                return -1;
131
            }
132
133
        }
134 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;
135
    }
136
137
    private void fillBuffer() throws IOException {
138 2 1. fillBuffer : changed conditional boundary → KILLED
2. fillBuffer : negated conditional → KILLED
        for (int i = 0; i < recentBytesBuffer.getLimit(); i++) {
139
            int result = inputStream.read();
140 1 1. fillBuffer : removed call to com/renomad/minum/web/CountBytesRead::increment → KILLED
            countBytesRead.increment();
141 1 1. fillBuffer : negated conditional → KILLED
            if (result == -1) {
142
                throw new IOException("Error: The inputstream has closed unexpectedly while reading");
143
            }
144
            byte byteValue = (byte) result;
145
            updateRecentBytesBufferAndCheck(byteValue);
146
        }
147
        hasFilledBuffer = true;
148
    }
149
150
    @Override
151
    public byte[] readAllBytes()  {
152
        var baos = new ByteArrayOutputStream();
153
        while (true) {
154
            int result = 0;
155
            try {
156
                result = read();
157
            } catch (IOException e) {
158
                throw new WebServerException(e);
159
            }
160 1 1. readAllBytes : negated conditional → KILLED
            if (result == -1) {
161 1 1. readAllBytes : replaced return value with null for com/renomad/minum/web/StreamingMultipartPartition::readAllBytes → KILLED
                return baos.toByteArray();
162
            }
163 1 1. readAllBytes : removed call to java/io/ByteArrayOutputStream::write → KILLED
            baos.write((byte)result);
164
        }
165
    }
166
167
    /**
168
     * Updates the buffer with the last characters read, and returns
169
     * true if we have encountered the end of this partition.
170
     */
171
    private boolean updateRecentBytesBufferAndCheck(byte newByte) {
172 1 1. updateRecentBytesBufferAndCheck : removed call to com/renomad/minum/utils/RingBuffer::add → KILLED
        recentBytesBuffer.add(newByte);
173 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);
174
    }
175
176
    /**
177
     * By "close", we will read from the {@link InputStream} until we have finished the body,
178
     * so that our InputStream has been read until the start of the next partition.
179
     */
180
    @Override
181
    public void close() throws IOException {
182
        while (true) {
183
            int result = read();
184 1 1. close : negated conditional → TIMED_OUT
            if (result == -1) {
185
                return;
186
            }
187
        }
188
    }
189
190
}

Mutations

70

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

84

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

88

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

100

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

101

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

103

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

104

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

106

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

111

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

115

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

116

1.1
Location : read
Killed by : com.renomad.minum.web.WebTests
changed conditional boundary → KILLED

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

118

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

120

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

125

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

130

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

134

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

138

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

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

140

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

141

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

160

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

161

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

163

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

172

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

173

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

184

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

Active mutators

Tests examined


Report generated by PIT 1.17.0