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 import org.apache.commons.io.IOUtils;
27
28 /**
29 * <p>
30 * File filter for matching files containing a "magic number". A magic number
31 * is a unique series of bytes common to all files of a specific file format.
32 * For instance, all Java class files begin with the bytes
33 * <code>0xCAFEBABE</code>.
34 * </p>
35 *
36 * <code><pre>
37 * File dir = new File(".");
38 * MagicNumberFileFilter javaClassFileFilter =
39 * MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE,
40 * (byte) 0xBA, (byte) 0xBE});
41 * String[] javaClassFiles = dir.list(javaClassFileFilter);
42 * for (String javaClassFile : javaClassFiles) {
43 * System.out.println(javaClassFile);
44 * }
45 * </pre></code>
46 *
47 * <p>
48 * Sometimes, such as in the case of TAR files, the
49 * magic number will be offset by a certain number of bytes in the file. In the
50 * case of TAR archive files, this offset is 257 bytes.
51 * </p>
52 *
53 * <code><pre>
54 * File dir = new File(".");
55 * MagicNumberFileFilter tarFileFilter =
56 * MagicNumberFileFilter("ustar", 257);
57 * String[] tarFiles = dir.list(tarFileFilter);
58 * for (String tarFile : tarFiles) {
59 * System.out.println(tarFile);
60 * }
61 * </pre></code>
62 * @since 2.0
63 * @see FileFilterUtils#magicNumberFileFilter(byte[])
64 * @see FileFilterUtils#magicNumberFileFilter(String)
65 * @see FileFilterUtils#magicNumberFileFilter(byte[], long)
66 * @see FileFilterUtils#magicNumberFileFilter(String, long)
67 */
68 public class MagicNumberFileFilter extends AbstractFileFilter implements
69 Serializable {
70
71 /**
72 * The serialization version unique identifier.
73 */
74 private static final long serialVersionUID = -547733176983104172L;
75
76 /**
77 * The magic number to compare against the file's bytes at the provided
78 * offset.
79 */
80 private final byte[] magicNumbers;
81
82 /**
83 * The offset (in bytes) within the files that the magic number's bytes
84 * should appear.
85 */
86 private final long byteOffset;
87
88 /**
89 * <p>
90 * Constructs a new MagicNumberFileFilter and associates it with the magic
91 * number to test for in files. This constructor assumes a starting offset
92 * of <code>0</code>.
93 * </p>
94 *
95 * <p>
96 * It is important to note that <em>the array is not cloned</em> and that
97 * any changes to the magic number array after construction will affect the
98 * behavior of this file filter.
99 * </p>
100 *
101 * <code><pre>
102 * MagicNumberFileFilter javaClassFileFilter =
103 * MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE,
104 * (byte) 0xBA, (byte) 0xBE});
105 * </pre></code>
106 *
107 * @param magicNumber the magic number to look for in the file.
108 *
109 * @throws IllegalArgumentException if <code>magicNumber</code> is
110 * {@code null}, or contains no bytes.
111 */
112 public MagicNumberFileFilter(final byte[] magicNumber) {
113 this(magicNumber, 0);
114 }
115
116 /**
117 * <p>
118 * Constructs a new MagicNumberFileFilter and associates it with the magic
119 * number to test for in files. This constructor assumes a starting offset
120 * of <code>0</code>.
121 * </p>
122 *
123 * Example usage:
124 * <pre>
125 * {@code
126 * MagicNumberFileFilter xmlFileFilter =
127 * MagicNumberFileFilter("<?xml");
128 * }
129 * </pre>
130 *
131 * @param magicNumber the magic number to look for in the file.
132 * The string is converted to bytes using the platform default charset.
133 *
134 * @throws IllegalArgumentException if <code>magicNumber</code> is
135 * {@code null} or the empty String.
136 */
137 public MagicNumberFileFilter(final String magicNumber) {
138 this(magicNumber, 0);
139 }
140
141 /**
142 * <p>
143 * Constructs a new MagicNumberFileFilter and associates it with the magic
144 * number to test for in files and the byte offset location in the file to
145 * to look for that magic number.
146 * </p>
147 *
148 * <code><pre>
149 * MagicNumberFileFilter tarFileFilter =
150 * MagicNumberFileFilter("ustar", 257);
151 * </pre></code>
152 *
153 * @param magicNumber the magic number to look for in the file.
154 * The string is converted to bytes using the platform default charset.
155 * @param offset the byte offset in the file to start comparing bytes.
156 *
157 * @throws IllegalArgumentException if <code>magicNumber</code> is
158 * {@code null} or the empty String, or <code>offset</code> is
159 * a negative number.
160 */
161 public MagicNumberFileFilter(final String magicNumber, final long offset) {
162 if (magicNumber == null) {
163 throw new IllegalArgumentException("The magic number cannot be null");
164 }
165 if (magicNumber.length() == 0) {
166 throw new IllegalArgumentException("The magic number must contain at least one byte");
167 }
168 if (offset < 0) {
169 throw new IllegalArgumentException("The offset cannot be negative");
170 }
171
172 this.magicNumbers = magicNumber.getBytes(Charset.defaultCharset()); // explicitly uses the platform default 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 * <code><pre>
184 * MagicNumberFileFilter tarFileFilter =
185 * MagicNumberFileFilter(new byte[] {0x75, 0x73, 0x74, 0x61, 0x72}, 257);
186 * </pre></code>
187 *
188 * <code><pre>
189 * MagicNumberFileFilter javaClassFileFilter =
190 * MagicNumberFileFilter(new byte[] {0xCA, 0xFE, 0xBA, 0xBE}, 0);
191 * </pre></code>
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 RandomAccessFile randomAccessFile = null;
236 try {
237 final byte[] fileBytes = new byte[this.magicNumbers.length];
238 randomAccessFile = new RandomAccessFile(file, "r");
239 randomAccessFile.seek(byteOffset);
240 final int read = randomAccessFile.read(fileBytes);
241 if (read != magicNumbers.length) {
242 return false;
243 }
244 return Arrays.equals(this.magicNumbers, fileBytes);
245 } catch (final IOException ioe) {
246 // Do nothing, fall through and do not accept file
247 } finally {
248 IOUtils.closeQuietly(randomAccessFile);
249 }
250 }
251
252 return false;
253 }
254
255 /**
256 * Returns a String representation of the file filter, which includes the
257 * magic number bytes and byte offset.
258 *
259 * @return a String representation of the file filter.
260 */
261 @Override
262 public String toString() {
263 final StringBuilder builder = new StringBuilder(super.toString());
264 builder.append("(");
265 builder.append(new String(magicNumbers, Charset.defaultCharset()));// TODO perhaps use hex if value is not printable
266 builder.append(",");
267 builder.append(this.byteOffset);
268 builder.append(")");
269 return builder.toString();
270 }
271 }