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 * <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> 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 * <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> 062 * 063 * @since 2.0 064 * @see FileFilterUtils#magicNumberFileFilter(byte[]) 065 * @see FileFilterUtils#magicNumberFileFilter(String) 066 * @see FileFilterUtils#magicNumberFileFilter(byte[], long) 067 * @see FileFilterUtils#magicNumberFileFilter(String, long) 068 */ 069public class MagicNumberFileFilter extends AbstractFileFilter implements 070 Serializable { 071 072 /** 073 * The serialization version unique identifier. 074 */ 075 private static final long serialVersionUID = -547733176983104172L; 076 077 /** 078 * The magic number to compare against the file's bytes at the provided 079 * offset. 080 */ 081 private final byte[] magicNumbers; 082 083 /** 084 * The offset (in bytes) within the files that the magic number's bytes 085 * should appear. 086 */ 087 private final long byteOffset; 088 089 /** 090 * <p> 091 * Constructs a new MagicNumberFileFilter and associates it with the magic 092 * number to test for in files. This constructor assumes a starting offset 093 * of <code>0</code>. 094 * </p> 095 * 096 * <p> 097 * It is important to note that <em>the array is not cloned</em> and that 098 * any changes to the magic number array after construction will affect the 099 * behavior of this file filter. 100 * </p> 101 * 102 * <pre> 103 * MagicNumberFileFilter javaClassFileFilter = 104 * MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE, 105 * (byte) 0xBA, (byte) 0xBE}); 106 * </pre> 107 * 108 * @param magicNumber the magic number to look for in the file. 109 * 110 * @throws IllegalArgumentException if <code>magicNumber</code> is 111 * {@code null}, or contains no bytes. 112 */ 113 public MagicNumberFileFilter(final byte[] magicNumber) { 114 this(magicNumber, 0); 115 } 116 117 /** 118 * <p> 119 * Constructs a new MagicNumberFileFilter and associates it with the magic 120 * number to test for in files. This constructor assumes a starting offset 121 * of <code>0</code>. 122 * </p> 123 * 124 * Example usage: 125 * <pre> 126 * {@code 127 * MagicNumberFileFilter xmlFileFilter = 128 * MagicNumberFileFilter("<?xml"); 129 * } 130 * </pre> 131 * 132 * @param magicNumber the magic number to look for in the file. 133 * The string is converted to bytes using the platform default charset. 134 * 135 * @throws IllegalArgumentException if <code>magicNumber</code> is 136 * {@code null} or the empty String. 137 */ 138 public MagicNumberFileFilter(final String magicNumber) { 139 this(magicNumber, 0); 140 } 141 142 /** 143 * <p> 144 * Constructs a new MagicNumberFileFilter and associates it with the magic 145 * number to test for in files and the byte offset location in the file to 146 * to look for that magic number. 147 * </p> 148 * 149 * <pre> 150 * MagicNumberFileFilter tarFileFilter = 151 * MagicNumberFileFilter("ustar", 257); 152 * </pre> 153 * 154 * @param magicNumber the magic number to look for in the file. 155 * The string is converted to bytes using the platform default charset. 156 * @param offset the byte offset in the file to start comparing bytes. 157 * 158 * @throws IllegalArgumentException if <code>magicNumber</code> is 159 * {@code null} or the empty String, or <code>offset</code> is 160 * a negative number. 161 */ 162 public MagicNumberFileFilter(final String magicNumber, final long offset) { 163 if (magicNumber == null) { 164 throw new IllegalArgumentException("The magic number cannot be null"); 165 } 166 if (magicNumber.isEmpty()) { 167 throw new IllegalArgumentException("The magic number must contain at least one byte"); 168 } 169 if (offset < 0) { 170 throw new IllegalArgumentException("The offset cannot be negative"); 171 } 172 173 this.magicNumbers = magicNumber.getBytes(Charset.defaultCharset()); // explicitly uses the platform default 174 // charset 175 this.byteOffset = offset; 176 } 177 178 /** 179 * <p> 180 * Constructs a new MagicNumberFileFilter and associates it with the magic 181 * number to test for in files and the byte offset location in the file to 182 * to look for that magic number. 183 * </p> 184 * 185 * <pre> 186 * MagicNumberFileFilter tarFileFilter = 187 * MagicNumberFileFilter(new byte[] {0x75, 0x73, 0x74, 0x61, 0x72}, 257); 188 * </pre> 189 * 190 * <pre> 191 * MagicNumberFileFilter javaClassFileFilter = 192 * MagicNumberFileFilter(new byte[] {0xCA, 0xFE, 0xBA, 0xBE}, 0); 193 * </pre> 194 * 195 * @param magicNumber the magic number to look for in the file. 196 * @param offset the byte offset in the file to start comparing bytes. 197 * 198 * @throws IllegalArgumentException if <code>magicNumber</code> is 199 * {@code null}, or contains no bytes, or <code>offset</code> 200 * is a negative number. 201 */ 202 public MagicNumberFileFilter(final byte[] magicNumber, final long offset) { 203 if (magicNumber == null) { 204 throw new IllegalArgumentException("The magic number cannot be null"); 205 } 206 if (magicNumber.length == 0) { 207 throw new IllegalArgumentException("The magic number must contain at least one byte"); 208 } 209 if (offset < 0) { 210 throw new IllegalArgumentException("The offset cannot be negative"); 211 } 212 213 this.magicNumbers = new byte[magicNumber.length]; 214 System.arraycopy(magicNumber, 0, this.magicNumbers, 0, magicNumber.length); 215 this.byteOffset = offset; 216 } 217 218 /** 219 * <p> 220 * Accepts the provided file if the file contains the file filter's magic 221 * number at the specified offset. 222 * </p> 223 * 224 * <p> 225 * If any {@link IOException}s occur while reading the file, the file will 226 * be rejected. 227 * </p> 228 * 229 * @param file the file to accept or reject. 230 * 231 * @return {@code true} if the file contains the filter's magic number 232 * at the specified offset, {@code false} otherwise. 233 */ 234 @Override 235 public boolean accept(final File file) { 236 if (file != null && file.isFile() && file.canRead()) { 237 RandomAccessFile randomAccessFile = null; 238 try { 239 final byte[] fileBytes = new byte[this.magicNumbers.length]; 240 randomAccessFile = new RandomAccessFile(file, "r"); 241 randomAccessFile.seek(byteOffset); 242 final int read = randomAccessFile.read(fileBytes); 243 if (read != magicNumbers.length) { 244 return false; 245 } 246 return Arrays.equals(this.magicNumbers, fileBytes); 247 } catch (final IOException ioe) { 248 // Do nothing, fall through and do not accept file 249 } finally { 250 IOUtils.closeQuietly(randomAccessFile); 251 } 252 } 253 254 return false; 255 } 256 257 /** 258 * Returns a String representation of the file filter, which includes the 259 * magic number bytes and byte offset. 260 * 261 * @return a String representation of the file filter. 262 */ 263 @Override 264 public String toString() { 265 final StringBuilder builder = new StringBuilder(super.toString()); 266 builder.append("("); 267 builder.append(new String(magicNumbers, Charset.defaultCharset()));// TODO perhaps use hex if value is not 268 // printable 269 builder.append(","); 270 builder.append(this.byteOffset); 271 builder.append(")"); 272 return builder.toString(); 273 } 274}