    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}