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.assertFalse;
021import static org.junit.jupiter.api.Assertions.assertNotNull;
022import static org.junit.jupiter.api.Assertions.assertThrows;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.ByteArrayOutputStream;
026import java.io.IOException;
027import java.io.ObjectOutputStream;
028import java.nio.file.FileVisitResult;
029import java.nio.file.Files;
030import java.nio.file.InvalidPathException;
031import java.nio.file.Path;
032import java.nio.file.attribute.BasicFileAttributes;
033
034import org.apache.commons.io.file.PathUtils;
035import org.apache.commons.io.file.SimplePathVisitor;
036import org.apache.commons.lang3.SerializationUtils;
037import org.junit.jupiter.api.AfterEach;
038import org.junit.jupiter.api.BeforeEach;
039import org.junit.jupiter.api.Test;
040
041/**
042 * Serialization Unit tests for {@link DiskFileItem}.
043 */
044public class DiskFileItemSerializeTest {
045
046    /**
047     * Use a private repository to catch any files left over by tests.
048     */
049    private static final Path REPOSITORY = PathUtils.getTempDirectory().resolve("DiskFileItemRepo");
050
051    /**
052     * Content type for regular form items.
053     */
054    private static final String TEXT_CONTENT_TYPE = "text/plain";
055
056    /**
057     * Very low threshold for testing memory versus disk options.
058     */
059    private static final int THRESHOLD = 16;
060
061    /**
062     * Compare content bytes.
063     */
064    private void compareBytes(final String text, final byte[] origBytes, final byte[] newBytes) {
065        assertNotNull(origBytes, "origBytes must not be null");
066        assertNotNull(newBytes, "newBytes must not be null");
067        assertEquals(origBytes.length, newBytes.length, text + " byte[] length");
068        for (var i = 0; i < origBytes.length; i++) {
069            assertEquals(origBytes[i], newBytes[i], text + " byte[" + i + "]");
070        }
071    }
072
073    /**
074     * Create content bytes of a specified size.
075     */
076    private byte[] createContentBytes(final int size) {
077        final var buffer = new StringBuilder(size);
078        byte count = 0;
079        for (var i = 0; i < size; i++) {
080            buffer.append(count + "");
081            count++;
082            if (count > 9) {
083                count = 0;
084            }
085        }
086        return buffer.toString().getBytes();
087    }
088
089    /**
090     * Create a FileItem with the specfied content bytes.
091     */
092    private DiskFileItem createFileItem(final byte[] contentBytes) throws IOException {
093        return createFileItem(contentBytes, REPOSITORY);
094    }
095
096    /**
097     * Create a FileItem with the specfied content bytes and repository.
098     */
099    private DiskFileItem createFileItem(final byte[] contentBytes, final Path repository) throws IOException {
100        // @formatter:off
101        final FileItemFactory<DiskFileItem> factory = DiskFileItemFactory.builder()
102                .setBufferSize(THRESHOLD)
103                .setPath(repository)
104                .get();
105        // @formatter:on
106        // @formatter:off
107        final var item = factory.fileItemBuilder()
108                .setFieldName("textField")
109                .setContentType(TEXT_CONTENT_TYPE)
110                .setFormField(true)
111                .setFileName("My File Name")
112                .get();
113        // @formatter:on
114
115        try (var os = item.getOutputStream()) {
116            os.write(contentBytes);
117        }
118        return item;
119    }
120
121    /**
122     * Deserializes.
123     */
124    private Object deserialize(final ByteArrayOutputStream baos) {
125        return SerializationUtils.deserialize(baos.toByteArray());
126    }
127
128    /**
129     * Serializes.
130     */
131    private ByteArrayOutputStream serialize(final Object target) throws IOException {
132        try (final var baos = new ByteArrayOutputStream();
133                final var oos = new ObjectOutputStream(baos)) {
134            oos.writeObject(target);
135            oos.flush();
136            return baos;
137        }
138    }
139
140    @BeforeEach
141    public void setUp() throws IOException {
142        if (Files.exists(REPOSITORY)) {
143            PathUtils.deleteDirectory(REPOSITORY);
144        } else {
145            Files.createDirectories(REPOSITORY);
146        }
147    }
148
149    @AfterEach
150    public void tearDown() throws IOException {
151        if (Files.exists(REPOSITORY)) {
152            PathUtils.visitFileTree(new SimplePathVisitor() {
153                @Override
154                public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
155                    System.out.println("Found leftover file " + file);
156                    return FileVisitResult.CONTINUE;
157                }
158
159            }, REPOSITORY);
160            PathUtils.deleteDirectory(REPOSITORY);
161        }
162    }
163
164    /**
165     * Test creation of a field for which the amount of data falls above the configured threshold.
166     *
167     * @throws IOException Test failure.
168     */
169    @Test
170    public void testAboveThreshold() throws IOException {
171        // Create the FileItem
172        final var testFieldValueBytes = createContentBytes(THRESHOLD + 1);
173        final var item = createFileItem(testFieldValueBytes);
174
175        // Check state is as expected
176        assertFalse(item.isInMemory(), "Initial: in memory");
177        assertEquals(item.getSize(), testFieldValueBytes.length, "Initial: size");
178        compareBytes("Initial", item.get(), testFieldValueBytes);
179
180        testWritingToFile(item, testFieldValueBytes);
181        item.delete();
182    }
183
184    /**
185     * Test creation of a field for which the amount of data falls below the configured threshold.
186     *
187     * @throws IOException Test failure.
188     */
189    @Test
190    public void testBelowThreshold() throws IOException {
191        // Create the FileItem
192        final var testFieldValueBytes = createContentBytes(THRESHOLD - 1);
193        testInMemoryObject(testFieldValueBytes);
194    }
195
196    @Test
197    public void testCheckFileName() {
198        assertThrows(InvalidPathException.class, () -> DiskFileItem.checkFileName("\0"));
199    }
200
201    /**
202     * Helper method to test creation of a field.
203     */
204    private void testInMemoryObject(final byte[] testFieldValueBytes) throws IOException {
205        testInMemoryObject(testFieldValueBytes, REPOSITORY);
206    }
207
208    /**
209     * Helper method to test creation of a field when a repository is used.
210     */
211    private void testInMemoryObject(final byte[] testFieldValueBytes, final Path repository) throws IOException {
212        final var item = createFileItem(testFieldValueBytes, repository);
213
214        // Check state is as expected
215        assertTrue(item.isInMemory(), "Initial: in memory");
216        assertEquals(item.getSize(), testFieldValueBytes.length, "Initial: size");
217        compareBytes("Initial", item.get(), testFieldValueBytes);
218        testWritingToFile(item, testFieldValueBytes);
219        item.delete();
220    }
221
222    /**
223     * Test deserialization fails when repository is not valid.
224     *
225     * @throws IOException Test failure.
226     */
227    @Test
228    public void testInvalidRepository() throws IOException {
229        // Create the FileItem
230        final var testFieldValueBytes = createContentBytes(THRESHOLD);
231        final var repository = PathUtils.getTempDirectory().resolve("file");
232        final var item = createFileItem(testFieldValueBytes, repository);
233        assertThrows(IOException.class, () -> deserialize(serialize(item)));
234    }
235
236    /**
237     * Test creation of a field for which the amount of data equals the configured threshold.
238     *
239     * @throws IOException Test failure.
240     */
241    @Test
242    public void testThreshold() throws IOException {
243        // Create the FileItem
244        final var testFieldValueBytes = createContentBytes(THRESHOLD);
245        testInMemoryObject(testFieldValueBytes);
246    }
247
248    /**
249     * Test serialization and deserialization when repository is not null.
250     *
251     * @throws IOException Test failure.
252     */
253    @Test
254    public void testValidRepository() throws IOException {
255        // Create the FileItem
256        final var testFieldValueBytes = createContentBytes(THRESHOLD);
257        testInMemoryObject(testFieldValueBytes, REPOSITORY);
258    }
259
260    /**
261     * Helper method to test writing item contents to a file.
262     */
263    private void testWritingToFile(final DiskFileItem item, final byte[] testFieldValueBytes) throws IOException {
264        final var temp = Files.createTempFile("fileupload", null);
265        // Note that the file exists and is initially empty;
266        // write() must be able to handle that.
267        item.write(temp);
268        compareBytes("Initial", Files.readAllBytes(temp), testFieldValueBytes);
269    }
270}