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 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}, 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} 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} 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}, 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} if the file contains the filter's magic number 235 * at the specified offset, {@code false} 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 }