    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     */
    017    package org.apache.commons.io.filefilter;
    018
    019    import java.io.File;
    020    import java.io.IOException;
    021    import java.io.RandomAccessFile;
    022    import java.io.Serializable;
    023    import java.util.Arrays;
    024
    025    import org.apache.commons.io.IOUtils;
    026
    027    /**
    028     * <p>
    029     * File filter for matching files containing a "magic number". A magic number
    030     * is a unique series of bytes common to all files of a specific file format.
    031     * For instance, all Java class files begin with the bytes
    032     * <code>0xCAFEBABE</code>.
    033     * </p>
    034     *
    035     * <code><pre>
    036     * File dir = new File(".");
    037     * MagicNumberFileFilter javaClassFileFilter =
    038     *     MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE,
    039     *       (byte) 0xBA, (byte) 0xBE});
    040     * String[] javaClassFiles = dir.list(javaClassFileFilter);
    041     * for (String javaClassFile : javaClassFiles) {
    042     *     System.out.println(javaClassFile);
    043     * }
    044     * </pre></code>
    045     *
    046     * <p>
    047     * Sometimes, such as in the case of TAR files, the
    048     * magic number will be offset by a certain number of bytes in the file. In the
    049     * case of TAR archive files, this offset is 257 bytes.
    050     * </p>
    051     *
    052     * <code><pre>
    053     * File dir = new File(".");
    054     * MagicNumberFileFilter tarFileFilter =
    055     *     MagicNumberFileFilter("ustar", 257);
    056     * String[] tarFiles = dir.list(tarFileFilter);
    057     * for (String tarFile : tarFiles) {
    058     *     System.out.println(tarFile);
    059     * }
    060     * </pre></code>
    061     * @since Commons IO 2.0
    062     * @see FileFilterUtils#magicNumberFileFilter(byte[])
    063     * @see FileFilterUtils#magicNumberFileFilter(String)
    064     * @see FileFilterUtils#magicNumberFileFilter(byte[], long)
    065     * @see FileFilterUtils#magicNumberFileFilter(String, long)
    066     */
    067    public class MagicNumberFileFilter extends AbstractFileFilter implements
    068            Serializable {
    069
    070        /**
    071         * The serialization version unique identifier.
    072         */
    073        private static final long serialVersionUID = -547733176983104172L;
    074
    075        /**
    076         * The magic number to compare against the file's bytes at the provided
    077         * offset.
    078         */
    079        private final byte[] magicNumbers;
    080
    081        /**
    082         * The offset (in bytes) within the files that the magic number's bytes
    083         * should appear.
    084         */
    085        private final long byteOffset;
    086
    087        /**
    088         * <p>
    089         * Constructs a new MagicNumberFileFilter and associates it with the magic
    090         * number to test for in files. This constructor assumes a starting offset
    091         * of <code>0</code>.
    092         * </p>
    093         *
    094         * <p>
    095         * It is important to note that <em>the array is not cloned</em> and that
    096         * any changes to the magic number array after construction will affect the
    097         * behavior of this file filter.
    098         * </p>
    099         *
    100         * <code><pre>
    101         * MagicNumberFileFilter javaClassFileFilter =
    102         *     MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE,
    103         *       (byte) 0xBA, (byte) 0xBE});
    104         * </pre></code>
    105         *
    106         * @param magicNumber the magic number to look for in the file.
    107         *
    108         * @throws IllegalArgumentException if <code>magicNumber</code> is
    109         *         <code>null</code>, or contains no bytes.
    110         */
    111        public MagicNumberFileFilter(byte[] magicNumber) {
    112            this(magicNumber, 0);
    113        }
    114
    115        /**
    116         * <p>
    117         * Constructs a new MagicNumberFileFilter and associates it with the magic
    118         * number to test for in files. This constructor assumes a starting offset
    119         * of <code>0</code>.
    120         * </p>
    121         *
    122         * Example usage:
    123         * <pre>
    124         * {@code
    125         * MagicNumberFileFilter xmlFileFilter =
    126         *     MagicNumberFileFilter("<?xml");
    127         * }
    128         * </pre>
    129         *
    130         * @param magicNumber the magic number to look for in the file.
    131         *        The string is converted to bytes using the platform default charset.
    132         *
    133         * @throws IllegalArgumentException if <code>magicNumber</code> is
    134         *         <code>null</code> or the empty String.
    135         */
    136        public MagicNumberFileFilter(String magicNumber) {
    137            this(magicNumber, 0);
    138        }
    139
    140        /**
    141         * <p>
    142         * Constructs a new MagicNumberFileFilter and associates it with the magic
    143         * number to test for in files and the byte offset location in the file to
    144         * to look for that magic number.
    145         * </p>
    146         *
    147         * <code><pre>
    148         * MagicNumberFileFilter tarFileFilter =
    149         *     MagicNumberFileFilter("ustar", 257);
    150         * </pre></code>
    151         *
    152         * @param magicNumber the magic number to look for in the file.
    153         *        The string is converted to bytes using the platform default charset.
    154         * @param offset the byte offset in the file to start comparing bytes.
    155         *
    156         * @throws IllegalArgumentException if <code>magicNumber</code> is
    157         *         <code>null</code> or the empty String, or <code>offset</code> is
    158         *         a negative number.
    159         */
    160        public MagicNumberFileFilter(String magicNumber, long offset) {
    161            if (magicNumber == null) {
    162                throw new IllegalArgumentException("The magic number cannot be null");
    163            }
    164            if (magicNumber.length() == 0) {
    165                throw new IllegalArgumentException("The magic number must contain at least one byte");
    166            }
    167            if (offset < 0) {
    168                throw new IllegalArgumentException("The offset cannot be negative");
    169            }
    170
    171            this.magicNumbers = magicNumber.getBytes(); // uses the platform default charset
    172            this.byteOffset = offset;
    173        }
    174
    175        /**
    176         * <p>
    177         * Constructs a new MagicNumberFileFilter and associates it with the magic
    178         * number to test for in files and the byte offset location in the file to
    179         * to look for that magic number.
    180         * </p>
    181         *
    182         * <p>
    183         * It is important to note that <em>the array is not cloned</em> and that
    184         * any changes to the magic number array after construction will affect the
    185         * behavior of this file filter.
    186         * </p>
    187         *
    188         * <code><pre>
    189         * MagicNumberFileFilter tarFileFilter =
    190         *     MagicNumberFileFilter(new byte[] {0x75, 0x73, 0x74, 0x61, 0x72}, 257);
    191         * </pre></code>
    192         *
    193         * <code><pre>
    194         * MagicNumberFileFilter javaClassFileFilter =
    195         *     MagicNumberFileFilter(new byte[] {0xCA, 0xFE, 0xBA, 0xBE}, 0);
    196         * </pre></code>
    197         *
    198         * @param magicNumber the magic number to look for in the file.
    199         * @param offset the byte offset in the file to start comparing bytes.
    200         *
    201         * @throws IllegalArgumentException if <code>magicNumber</code> is
    202         *         <code>null</code>, or contains no bytes, or <code>offset</code>
    203         *         is a negative number.
    204         */
    205        public MagicNumberFileFilter(byte[] magicNumber, long offset) {
    206            if (magicNumber == null) {
    207                throw new IllegalArgumentException("The magic number cannot be null");
    208            }
    209            if (magicNumber.length == 0) {
    210                throw new IllegalArgumentException("The magic number must contain at least one byte");
    211            }
    212            if (offset < 0) {
    213                throw new IllegalArgumentException("The offset cannot be negative");
    214            }
    215
    216            this.magicNumbers = new byte[magicNumber.length];
    217            System.arraycopy(magicNumber, 0, this.magicNumbers, 0, magicNumber.length);
    218            this.byteOffset = offset;
    219        }
    220
    221        /**
    222         * <p>
    223         * Accepts the provided file if the file contains the file filter's magic
    224         * number at the specified offset.
    225         * </p>
    226         *
    227         * <p>
    228         * If any {@link IOException}s occur while reading the file, the file will
    229         * be rejected.
    230         * </p>
    231         *
    232         * @param file the file to accept or reject.
    233         *
    234         * @return <code>true</code> if the file contains the filter's magic number
    235         *         at the specified offset, <code>false</code> otherwise.
    236         */
    237        @Override
    238        public boolean accept(File file) {
    239            if (file != null && file.isFile() && file.canRead()) {
    240                RandomAccessFile randomAccessFile = null;
    241                try {
    242                    byte[] fileBytes = new byte[this.magicNumbers.length];
    243                    randomAccessFile = new RandomAccessFile(file, "r");
    244                    randomAccessFile.seek(byteOffset);
    245                    int read = randomAccessFile.read(fileBytes);
    246                    if (read != magicNumbers.length) {
    247                        return false;
    248                    }
    249                    return Arrays.equals(this.magicNumbers, fileBytes);
    250                } catch (IOException ioe) {
    251                    // Do nothing, fall through and do not accept file
    252                } finally {
    253                    IOUtils.closeQuietly(randomAccessFile);
    254                }
    255            }
    256
    257            return false;
    258        }
    259
    260        /**
    261         * Returns a String representation of the file filter, which includes the
    262         * magic number bytes and byte offset.
    263         *
    264         * @return a String representation of the file filter.
    265         */
    266        @Override
    267        public String toString() {
    268            StringBuilder builder = new StringBuilder(super.toString());
    269            builder.append("(");
    270            builder.append(new String(magicNumbers));// TODO perhaps use hex if value is not printable
    271            builder.append(",");
    272            builder.append(this.byteOffset);
    273            builder.append(")");
    274            return builder.toString();
    275        }
    276    }