    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}