001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.fileupload2.core;
018
019import static org.junit.jupiter.api.Assertions.assertEquals;
020import static org.junit.jupiter.api.Assertions.assertTrue;
021import static org.junit.jupiter.api.Assertions.fail;
022
023import java.io.ByteArrayInputStream;
024import java.io.ByteArrayOutputStream;
025import java.io.FilterInputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStreamWriter;
029import java.nio.charset.StandardCharsets;
030import java.nio.file.InvalidPathException;
031import java.util.List;
032
033import org.junit.jupiter.api.Test;
034
035/**
036 * Unit test for items with varying sizes.
037 *
038 * @param <AFU> The subclass of FileUpload.
039 * @param <R>   The type of FileUpload request.
040 * @param <C>   The request context type.
041 * @param <I>   The FileItem type.
042 * @param <F>   The FileItemFactory type.
043 */
044public abstract class AbstractStreamingTest<AFU extends AbstractFileUpload<R, I, F>, R, C extends AbstractRequestContext<?>, I extends FileItem<I>, F extends FileItemFactory<I>>
045        extends AbstractTest<AFU, R, I, F> {
046
047    protected String getFooter() {
048        return "-----1234--\r\n";
049    }
050
051    protected String getHeader(final String value) {
052        // @formatter:off
053        return "-----1234\r\n"
054            + "Content-Disposition: form-data; name=\"" + value + "\"\r\n"
055            + "\r\n";
056        // @formatter:on
057    }
058
059    protected abstract F newDiskFileItemFactory();
060
061    protected byte[] newRequest() throws IOException {
062        final var baos = new ByteArrayOutputStream();
063        try (final var osw = new OutputStreamWriter(baos, StandardCharsets.US_ASCII)) {
064            var add = 16;
065            var num = 0;
066            for (var i = 0; i < 16384; i += add) {
067                if (++add == 32) {
068                    add = 16;
069                }
070                osw.write(getHeader("field" + num++));
071                osw.flush();
072                for (var j = 0; j < i; j++) {
073                    baos.write((byte) j);
074                }
075                osw.write("\r\n");
076            }
077            osw.write(getFooter());
078        }
079        return baos.toByteArray();
080    }
081
082    protected abstract C newServletRequestContext(final R request);
083
084    protected byte[] newShortRequest() throws IOException {
085        final var baos = new ByteArrayOutputStream();
086        try (final var osw = new OutputStreamWriter(baos, StandardCharsets.US_ASCII)) {
087            osw.write(getHeader("field"));
088            osw.write("123");
089            osw.write("\r\n");
090            osw.write(getFooter());
091        }
092        return baos.toByteArray();
093    }
094
095    protected List<I> parseUpload(final byte[] bytes) throws FileUploadException {
096        return parseUpload(new ByteArrayInputStream(bytes), bytes.length);
097    }
098
099    protected List<I> parseUpload(final InputStream inputStream, final int length) throws FileUploadException {
100        final var contentType = "multipart/form-data; boundary=---1234";
101
102        final var upload = newFileUpload();
103        upload.setFileItemFactory(newDiskFileItemFactory());
104        final var request = newMockHttpServletRequest(inputStream, length, contentType, -1);
105
106        return upload.parseRequest(newServletRequestContext(request));
107    }
108
109    protected FileItemInputIterator parseUpload(final int length, final InputStream inputStream) throws FileUploadException, IOException {
110        final var contentType = "multipart/form-data; boundary=---1234";
111
112        final var upload = newFileUpload();
113        upload.setFileItemFactory(newDiskFileItemFactory());
114        final var request = newMockHttpServletRequest(inputStream, length, contentType, -1);
115
116        return upload.getItemIterator(newServletRequestContext(request));
117    }
118
119    /**
120     * Tests a file upload with varying file sizes.
121     *
122     * @throws IOException Test failure.
123     */
124    @Test
125    public void testFileUpload() throws IOException {
126        final var request = newRequest();
127        final var fileItems = parseUpload(request);
128        final var fileIter = fileItems.iterator();
129        var add = 16;
130        var num = 0;
131        for (var i = 0; i < 16384; i += add) {
132            if (++add == 32) {
133                add = 16;
134            }
135            final var item = fileIter.next();
136            assertEquals("field" + num++, item.getFieldName());
137            final var bytes = item.get();
138            assertEquals(i, bytes.length);
139            for (var j = 0; j < i; j++) {
140                assertEquals((byte) j, bytes[j]);
141            }
142        }
143        assertTrue(!fileIter.hasNext());
144    }
145
146    /**
147     * Test for FILEUPLOAD-135
148     *
149     * @throws IOException Test failure.
150     */
151    @Test
152    public void testFILEUPLOAD135() throws IOException {
153        final var request = newShortRequest();
154        final var bais = new ByteArrayInputStream(request);
155        final var fileItems = parseUpload(new InputStream() {
156            @Override
157            public int read() throws IOException {
158                return bais.read();
159            }
160
161            @Override
162            public int read(final byte[] b, final int off, final int len) throws IOException {
163                return bais.read(b, off, Math.min(len, 3));
164            }
165
166        }, request.length);
167        final var fileIter = fileItems.iterator();
168        assertTrue(fileIter.hasNext());
169        final var item = fileIter.next();
170        assertEquals("field", item.getFieldName());
171        final var bytes = item.get();
172        assertEquals(3, bytes.length);
173        assertEquals((byte) '1', bytes[0]);
174        assertEquals((byte) '2', bytes[1]);
175        assertEquals((byte) '3', bytes[2]);
176        assertTrue(!fileIter.hasNext());
177    }
178
179    /**
180     * Tests, whether an invalid request throws a proper exception.
181     *
182     * @throws IOException Test failure.
183     */
184    @Test
185    public void testFileUploadException() throws IOException {
186        final var request = newRequest();
187        final var invalidRequest = new byte[request.length - 11];
188        System.arraycopy(request, 0, invalidRequest, 0, request.length - 11);
189        try {
190            parseUpload(invalidRequest);
191            fail("Expected EndOfStreamException");
192        } catch (final FileUploadException e) {
193            assertTrue(e.getSuppressed()[0] instanceof MultipartInput.MalformedStreamException, e.toString());
194        }
195    }
196
197    /**
198     * Tests, whether an {@link InvalidPathException} is thrown.
199     *
200     * @throws IOException Test failure.
201     */
202    @Test
203    public void testInvalidFileNameException() throws IOException {
204        final var fileName = "foo.exe\u0000.png";
205        // @formatter:off
206        final var request =
207            "-----1234\r\n" +
208            "Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"\r\n" +
209            "Content-Type: text/whatever\r\n" +
210            "\r\n" +
211            "This is the content of the file\n" +
212            "\r\n" +
213            "-----1234\r\n" +
214            "Content-Disposition: form-data; name=\"field\"\r\n" +
215            "\r\n" +
216            "fieldValue\r\n" +
217            "-----1234\r\n" +
218            "Content-Disposition: form-data; name=\"multi\"\r\n" +
219            "\r\n" +
220            "value1\r\n" +
221            "-----1234\r\n" +
222            "Content-Disposition: form-data; name=\"multi\"\r\n" +
223            "\r\n" +
224            "value2\r\n" +
225            "-----1234--\r\n";
226        // @formatter:on
227        final var reqBytes = request.getBytes(StandardCharsets.US_ASCII);
228
229        final var fileItemIter = parseUpload(reqBytes.length, new ByteArrayInputStream(reqBytes));
230        final var fileItemInput = fileItemIter.next();
231        try {
232            fileItemInput.getName();
233            fail("Expected exception");
234        } catch (final InvalidPathException e) {
235            assertEquals(fileName, e.getInput());
236            assertEquals(26, e.getMessage().indexOf(fileName));
237            assertEquals(7, e.getIndex());
238            assertTrue(e.getMessage().contains("foo.exe\\0.png"));
239        }
240
241        try {
242            parseUpload(reqBytes);
243            fail("Expected exception");
244        } catch (final InvalidPathException e) {
245            assertEquals(fileName, e.getInput());
246            assertEquals(26, e.getMessage().indexOf(fileName));
247            assertEquals(7, e.getIndex());
248            assertTrue(e.getMessage().contains("foo.exe\\0.png"));
249        }
250    }
251
252    /**
253     * Tests, whether an IOException is properly delegated.
254     *
255     * @throws IOException Test failure.
256     */
257    @Test
258    public void testIOException() throws IOException {
259        final var request = newRequest();
260        final InputStream stream = new FilterInputStream(new ByteArrayInputStream(request)) {
261            private int num;
262
263            @Override
264            public int read() throws IOException {
265                if (++num > 123) {
266                    throw new IOException("123");
267                }
268                return super.read();
269            }
270
271            @Override
272            public int read(final byte[] buffer, final int offset, final int length) throws IOException {
273                for (var i = 0; i < length; i++) {
274                    final var res = read();
275                    if (res == -1) {
276                        return i == 0 ? -1 : i;
277                    }
278                    buffer[offset + i] = (byte) res;
279                }
280                return length;
281            }
282        };
283        try {
284            parseUpload(stream, request.length);
285            fail("Expected IOException");
286        } catch (final FileUploadException e) {
287            assertTrue(e.getCause() instanceof IOException);
288            assertEquals("123", e.getCause().getMessage());
289        }
290    }
291
292}