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 |
|
80 |
1.1 |
|
84 |
1.1 |
|
96 |
1.1 |
|
97 |
1.1 |
|
99 |
1.1 |
|
100 |
1.1 |
|
102 |
1.1 |
|
107 |
1.1 |
|
111 |
1.1 |
|
112 |
1.1 2.2 |
|
114 |
1.1 |
|
116 |
1.1 |
|
121 |
1.1 |
|
126 |
1.1 |
|
130 |
1.1 2.2 |
|
134 |
1.1 2.2 |
|
136 |
1.1 |
|
137 |
1.1 |
|
156 |
1.1 |
|
157 |
1.1 |
|
159 |
1.1 |
|
168 |
1.1 |
|
169 |
1.1 2.2 |
|
180 |
1.1 |