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 */ 017package org.apache.commons.io.filefilter; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.RandomAccessFile; 022import java.io.Serializable; 023import java.nio.charset.Charset; 024import java.util.Arrays; 025 026import org.apache.commons.io.IOUtils; 027 028/** 029 * <p> 030 * File filter for matching files containing a "magic number". A magic number 031 * is a unique series of bytes common to all files of a specific file format. 032 * For instance, all Java class files begin with the bytes 033 * <code>0xCAFEBABE</code>. 034 * </p> 035 * 036 * <code><pre> 037 * File dir = new File("."); 038 * MagicNumberFileFilter javaClassFileFilter = 039 * MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE, 040 * (byte) 0xBA, (byte) 0xBE}); 041 * String[] javaClassFiles = dir.list(javaClassFileFilter); 042 * for (String javaClassFile : javaClassFiles) { 043 * System.out.println(javaClassFile); 044 * } 045 * </pre></code> 046 * 047 * <p> 048 * Sometimes, such as in the case of TAR files, the 049 * magic number will be offset by a certain number of bytes in the file. In the 050 * case of TAR archive files, this offset is 257 bytes. 051 * </p> 052 * 053 * <code><pre> 054 * File dir = new File("."); 055 * MagicNumberFileFilter tarFileFilter = 056 * MagicNumberFileFilter("ustar", 257); 057 * String[] tarFiles = dir.list(tarFileFilter); 058 * for (String tarFile : tarFiles) { 059 * System.out.println(tarFile); 060 * } 061 * </pre></code> 062 * @since 2.0 063 * @see FileFilterUtils#magicNumberFileFilter(byte[]) 064 * @see FileFilterUtils#magicNumberFileFilter(String) 065 * @see FileFilterUtils#magicNumberFileFilter(byte[], long) 066 * @see FileFilterUtils#magicNumberFileFilter(String, long) 067 */ 068public class MagicNumberFileFilter extends AbstractFileFilter implements 069 Serializable { 070 071 /** 072 * The serialization version unique identifier. 073 */ 074 private static final long serialVersionUID = -547733176983104172L; 075 076 /** 077 * The magic number to compare against the file's bytes at the provided 078 * offset. 079 */ 080 private final byte[] magicNumbers; 081 082 /** 083 * The offset (in bytes) within the files that the magic number's bytes 084 * should appear. 085 */ 086 private final long byteOffset; 087 088 /** 089 * <p> 090 * Constructs a new MagicNumberFileFilter and associates it with the magic 091 * number to test for in files. This constructor assumes a starting offset 092 * of <code>0</code>. 093 * </p> 094 * 095 * <p> 096 * It is important to note that <em>the array is not cloned</em> and that 097 * any changes to the magic number array after construction will affect the 098 * behavior of this file filter. 099 * </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}