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}