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 }