View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io.filefilter;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.RandomAccessFile;
22  import java.io.Serializable;
23  import java.nio.charset.Charset;
24  import java.util.Arrays;
25  
26  /**
27   * <p>
28   * File filter for matching files containing a "magic number". A magic number
29   * is a unique series of bytes common to all files of a specific file format.
30   * For instance, all Java class files begin with the bytes
31   * <code>0xCAFEBABE</code>.
32   * </p>
33   *
34   * <pre>
35   * File dir = new File(".");
36   * MagicNumberFileFilter javaClassFileFilter =
37   *     MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE,
38   *       (byte) 0xBA, (byte) 0xBE});
39   * String[] javaClassFiles = dir.list(javaClassFileFilter);
40   * for (String javaClassFile : javaClassFiles) {
41   *     System.out.println(javaClassFile);
42   * }
43   * </pre>
44   *
45   * <p>
46   * Sometimes, such as in the case of TAR files, the
47   * magic number will be offset by a certain number of bytes in the file. In the
48   * case of TAR archive files, this offset is 257 bytes.
49   * </p>
50   *
51   * <pre>
52   * File dir = new File(".");
53   * MagicNumberFileFilter tarFileFilter =
54   *     MagicNumberFileFilter("ustar", 257);
55   * String[] tarFiles = dir.list(tarFileFilter);
56   * for (String tarFile : tarFiles) {
57   *     System.out.println(tarFile);
58   * }
59   * </pre>
60   *
61   * @since 2.0
62   * @see FileFilterUtils#magicNumberFileFilter(byte[])
63   * @see FileFilterUtils#magicNumberFileFilter(String)
64   * @see FileFilterUtils#magicNumberFileFilter(byte[], long)
65   * @see FileFilterUtils#magicNumberFileFilter(String, long)
66   */
67  public class MagicNumberFileFilter extends AbstractFileFilter implements
68          Serializable {
69  
70      /**
71       * The serialization version unique identifier.
72       */
73      private static final long serialVersionUID = -547733176983104172L;
74  
75      /**
76       * The magic number to compare against the file's bytes at the provided
77       * offset.
78       */
79      private final byte[] magicNumbers;
80  
81      /**
82       * The offset (in bytes) within the files that the magic number's bytes
83       * should appear.
84       */
85      private final long byteOffset;
86  
87      /**
88       * <p>
89       * Constructs a new MagicNumberFileFilter and associates it with the magic
90       * number to test for in files. This constructor assumes a starting offset
91       * of <code>0</code>.
92       * </p>
93       *
94       * <p>
95       * It is important to note that <em>the array is not cloned</em> and that
96       * any changes to the magic number array after construction will affect the
97       * behavior of this file filter.
98       * </p>
99       *
100      * <pre>
101      * MagicNumberFileFilter javaClassFileFilter =
102      *     MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE,
103      *       (byte) 0xBA, (byte) 0xBE});
104      * </pre>
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}, or contains no bytes.
110      */
111     public MagicNumberFileFilter(final 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} or the empty String.
135      */
136     public MagicNumberFileFilter(final 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      * <pre>
148      * MagicNumberFileFilter tarFileFilter =
149      *     MagicNumberFileFilter("ustar", 257);
150      * </pre>
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} or the empty String, or <code>offset</code> is
158      *         a negative number.
159      */
160     public MagicNumberFileFilter(final String magicNumber, final long offset) {
161         if (magicNumber == null) {
162             throw new IllegalArgumentException("The magic number cannot be null");
163         }
164         if (magicNumber.isEmpty()) {
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(Charset.defaultCharset()); // explicitly uses the platform default
172                                                                             // charset
173         this.byteOffset = offset;
174     }
175 
176     /**
177      * <p>
178      * Constructs a new MagicNumberFileFilter and associates it with the magic
179      * number to test for in files and the byte offset location in the file to
180      * to look for that magic number.
181      * </p>
182      *
183      * <pre>
184      * MagicNumberFileFilter tarFileFilter =
185      *     MagicNumberFileFilter(new byte[] {0x75, 0x73, 0x74, 0x61, 0x72}, 257);
186      * </pre>
187      *
188      * <pre>
189      * MagicNumberFileFilter javaClassFileFilter =
190      *     MagicNumberFileFilter(new byte[] {0xCA, 0xFE, 0xBA, 0xBE}, 0);
191      * </pre>
192      *
193      * @param magicNumber the magic number to look for in the file.
194      * @param offset the byte offset in the file to start comparing bytes.
195      *
196      * @throws IllegalArgumentException if <code>magicNumber</code> is
197      *         {@code null}, or contains no bytes, or <code>offset</code>
198      *         is a negative number.
199      */
200     public MagicNumberFileFilter(final byte[] magicNumber, final long offset) {
201         if (magicNumber == null) {
202             throw new IllegalArgumentException("The magic number cannot be null");
203         }
204         if (magicNumber.length == 0) {
205             throw new IllegalArgumentException("The magic number must contain at least one byte");
206         }
207         if (offset < 0) {
208             throw new IllegalArgumentException("The offset cannot be negative");
209         }
210 
211         this.magicNumbers = new byte[magicNumber.length];
212         System.arraycopy(magicNumber, 0, this.magicNumbers, 0, magicNumber.length);
213         this.byteOffset = offset;
214     }
215 
216     /**
217      * <p>
218      * Accepts the provided file if the file contains the file filter's magic
219      * number at the specified offset.
220      * </p>
221      *
222      * <p>
223      * If any {@link IOException}s occur while reading the file, the file will
224      * be rejected.
225      * </p>
226      *
227      * @param file the file to accept or reject.
228      *
229      * @return {@code true} if the file contains the filter's magic number
230      *         at the specified offset, {@code false} otherwise.
231      */
232     @Override
233     public boolean accept(final File file) {
234         if (file != null && file.isFile() && file.canRead()) {
235             try {
236                 try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
237                     final byte[] fileBytes = new byte[this.magicNumbers.length];
238                     randomAccessFile.seek(byteOffset);
239                     final int read = randomAccessFile.read(fileBytes);
240                     if (read != magicNumbers.length) {
241                         return false;
242                     }
243                     return Arrays.equals(this.magicNumbers, fileBytes);
244                 }
245             }
246             catch (final IOException ioe) {
247                 // Do nothing, fall through and do not accept file
248             }
249         }
250 
251         return false;
252     }
253 
254     /**
255      * Returns a String representation of the file filter, which includes the
256      * magic number bytes and byte offset.
257      *
258      * @return a String representation of the file filter.
259      */
260     @Override
261     public String toString() {
262         final StringBuilder builder = new StringBuilder(super.toString());
263         builder.append("(");
264         builder.append(new String(magicNumbers, Charset.defaultCharset()));// TODO perhaps use hex if value is not
265                                                                            // printable
266         builder.append(",");
267         builder.append(this.byteOffset);
268         builder.append(")");
269         return builder.toString();
270     }
271 }