FileItemInputImpl.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.fileupload2.core;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.InvalidPathException;

import org.apache.commons.io.input.BoundedInputStream;

/**
 * Default implementation of {@link FileItemInput}.
 */
class FileItemInputImpl implements FileItemInput {

    /**
     * The File Item iterator implementation.
     *
     * @see FileItemInputIteratorImpl
     */
    private final FileItemInputIteratorImpl fileItemInputIteratorImpl;

    /**
     * The file items content type.
     */
    private final String contentType;

    /**
     * The file items field name.
     */
    private final String fieldName;

    /**
     * The file items file name.
     */
    private final String fileName;

    /**
     * Whether the file item is a form field.
     */
    private final boolean formField;

    /**
     * The file items input stream.
     */
    private final InputStream inputStream;

    /**
     * The file items input stream closed flag.
     */
    private boolean inputStreamClosed;

    /**
     * The headers, if any.
     */
    private FileItemHeaders headers;

    /**
     * Creates a new instance.
     *
     * @param fileItemIterator The {@link FileItemInputIteratorImpl iterator}, which returned this file item.
     * @param fileName         The items file name, or null.
     * @param fieldName        The items field name.
     * @param contentType      The items content type, or null.
     * @param formField        Whether the item is a form field.
     * @param contentLength    The items content length, if known, or -1
     * @throws IOException         Creating the file item failed.
     * @throws FileUploadException Parsing the incoming data stream failed.
     */
    FileItemInputImpl(final FileItemInputIteratorImpl fileItemIterator, final String fileName, final String fieldName, final String contentType,
            final boolean formField, final long contentLength) throws FileUploadException, IOException {
        this.fileItemInputIteratorImpl = fileItemIterator;
        this.fileName = fileName;
        this.fieldName = fieldName;
        this.contentType = contentType;
        this.formField = formField;
        final var fileSizeMax = fileItemInputIteratorImpl.getFileSizeMax();
        if (fileSizeMax != -1 && contentLength != -1 && contentLength > fileSizeMax) {
            throw new FileUploadByteCountLimitException(String.format("The field %s exceeds its maximum permitted size of %s bytes.", fieldName, fileSizeMax),
                    contentLength, fileSizeMax, fileName, fieldName);
        }
        // OK to construct stream now
        final var itemInputStream = fileItemInputIteratorImpl.getMultiPartInput().newInputStream();
        InputStream istream = itemInputStream;
        if (fileSizeMax != -1) {
            // onMaxLength will be called when the length is greater than _or equal to_ the supplied maxLength.
            // Because we only want to throw an exception when the length is greater than fileSizeMax, we
            // increment fileSizeMax by 1.
            // @formatter:off
            istream = BoundedInputStream.builder()
                .setInputStream(istream)
                .setMaxCount(fileSizeMax + 1)
                .setOnMaxCount((max, count) -> {
                    itemInputStream.close(true);
                    throw new FileUploadByteCountLimitException(
                            String.format("The field %s exceeds its maximum permitted size of %s bytes.", fieldName, fileSizeMax), count, fileSizeMax, fileName,
                            fieldName);
                })
                .get();
            // @formatter:on

        }
        this.inputStream = istream;
    }

    /**
     * Closes the file item.
     *
     * @throws IOException An I/O error occurred.
     */
    public void close() throws IOException {
        inputStream.close();
        inputStreamClosed = true;
    }

    /**
     * Gets the content type, or null.
     *
     * @return Content type, if known, or null.
     */
    @Override
    public String getContentType() {
        return contentType;
    }

    /**
     * Gets the items field name.
     *
     * @return Field name.
     */
    @Override
    public String getFieldName() {
        return fieldName;
    }

    /**
     * Gets the headers.
     *
     * @return The items header object
     */
    @Override
    public FileItemHeaders getHeaders() {
        return headers;
    }

    /**
     * Gets the input stream, which may be used to read the items contents.
     *
     * @return Opened input stream.
     * @throws IOException An I/O error occurred.
     */
    @Override
    public InputStream getInputStream() throws IOException {
        if (inputStreamClosed) {
            throw new FileItemInput.ItemSkippedException("getInputStream()");
        }
        return inputStream;
    }

    /**
     * Gets the file name.
     *
     * @return File name, if known, or null.
     * @throws InvalidPathException The file name is invalid, for example it contains a NUL character, which might be an indicator of a security attack. If you
     *                              intend to use the file name anyways, catch the exception and use InvalidPathException#getInput().
     */
    @Override
    public String getName() {
        return DiskFileItem.checkFileName(fileName);
    }

    /**
     * Tests whether this is a form field.
     *
     * @return True, if the item is a form field, otherwise false.
     */
    @Override
    public boolean isFormField() {
        return formField;
    }

    /**
     * Sets the file item headers.
     *
     * @param headers The items header object
     */
    @Override
    public FileItemInputImpl setHeaders(final FileItemHeaders headers) {
        this.headers = headers;
        return this;
    }

}