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.Serializable; 022import java.nio.ByteBuffer; 023import java.nio.channels.FileChannel; 024import java.nio.charset.Charset; 025import java.nio.file.FileVisitResult; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.attribute.BasicFileAttributes; 029import java.util.Arrays; 030import java.util.Objects; 031 032import org.apache.commons.io.RandomAccessFileMode; 033import org.apache.commons.io.RandomAccessFiles; 034 035/** 036 * <p> 037 * File filter for matching files containing a "magic number". A magic number 038 * is a unique series of bytes common to all files of a specific file format. 039 * For instance, all Java class files begin with the bytes 040 * {@code 0xCAFEBABE}. 041 * </p> 042 * <h2>Using Classic IO</h2> 043 * <pre> 044 * File dir = FileUtils.current(); 045 * MagicNumberFileFilter javaClassFileFilter = 046 * MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE, 047 * (byte) 0xBA, (byte) 0xBE}); 048 * String[] javaClassFiles = dir.list(javaClassFileFilter); 049 * for (String javaClassFile : javaClassFiles) { 050 * System.out.println(javaClassFile); 051 * } 052 * </pre> 053 * 054 * <p> 055 * Sometimes, such as in the case of TAR files, the 056 * magic number will be offset by a certain number of bytes in the file. In the 057 * case of TAR archive files, this offset is 257 bytes. 058 * </p> 059 * 060 * <pre> 061 * File dir = FileUtils.current(); 062 * MagicNumberFileFilter tarFileFilter = 063 * MagicNumberFileFilter("ustar", 257); 064 * String[] tarFiles = dir.list(tarFileFilter); 065 * for (String tarFile : tarFiles) { 066 * System.out.println(tarFile); 067 * } 068 * </pre> 069 * <h2>Using NIO</h2> 070 * <pre> 071 * final Path dir = PathUtils.current(); 072 * final AccumulatorPathVisitor visitor = AccumulatorPathVisitor.withLongCounters(MagicNumberFileFilter("ustar", 257)); 073 * // 074 * // Walk one directory 075 * Files.<strong>walkFileTree</strong>(dir, Collections.emptySet(), 1, visitor); 076 * System.out.println(visitor.getPathCounters()); 077 * System.out.println(visitor.getFileList()); 078 * // 079 * visitor.getPathCounters().reset(); 080 * // 081 * // Walk directory tree 082 * Files.<strong>walkFileTree</strong>(dir, visitor); 083 * System.out.println(visitor.getPathCounters()); 084 * System.out.println(visitor.getDirList()); 085 * System.out.println(visitor.getFileList()); 086 * </pre> 087 * <h2>Deprecating Serialization</h2> 088 * <p> 089 * <em>Serialization is deprecated and will be removed in 3.0.</em> 090 * </p> 091 * 092 * <h2>Deprecating Serialization</h2> 093 * <p> 094 * <em>Serialization is deprecated and will be removed in 3.0.</em> 095 * </p> 096 * 097 * @since 2.0 098 * @see FileFilterUtils#magicNumberFileFilter(byte[]) 099 * @see FileFilterUtils#magicNumberFileFilter(String) 100 * @see FileFilterUtils#magicNumberFileFilter(byte[], long) 101 * @see FileFilterUtils#magicNumberFileFilter(String, long) 102 */ 103public class MagicNumberFileFilter extends AbstractFileFilter implements Serializable { 104 105 /** 106 * The serialization version unique identifier. 107 */ 108 private static final long serialVersionUID = -547733176983104172L; 109 110 /** 111 * The magic number to compare against the file's bytes at the provided 112 * offset. 113 */ 114 private final byte[] magicNumbers; 115 116 /** 117 * The offset (in bytes) within the files that the magic number's bytes 118 * should appear. 119 */ 120 private final long byteOffset; 121 122 /** 123 * <p> 124 * Constructs a new MagicNumberFileFilter and associates it with the magic 125 * number to test for in files. This constructor assumes a starting offset 126 * of {@code 0}. 127 * </p> 128 * 129 * <p> 130 * It is important to note that <em>the array is not cloned</em> and that 131 * any changes to the magic number array after construction will affect the 132 * behavior of this file filter. 133 * </p> 134 * 135 * <pre> 136 * MagicNumberFileFilter javaClassFileFilter = 137 * MagicNumberFileFilter(new byte[] {(byte) 0xCA, (byte) 0xFE, 138 * (byte) 0xBA, (byte) 0xBE}); 139 * </pre> 140 * 141 * @param magicNumber the magic number to look for in the file. 142 * 143 * @throws IllegalArgumentException if {@code magicNumber} is 144 * {@code null}, or contains no bytes. 145 */ 146 public MagicNumberFileFilter(final byte[] magicNumber) { 147 this(magicNumber, 0); 148 } 149 150 /** 151 * <p> 152 * Constructs a new MagicNumberFileFilter and associates it with the magic 153 * number to test for in files and the byte offset location in the file to 154 * to look for that magic number. 155 * </p> 156 * 157 * <pre> 158 * MagicNumberFileFilter tarFileFilter = 159 * MagicNumberFileFilter(new byte[] {0x75, 0x73, 0x74, 0x61, 0x72}, 257); 160 * </pre> 161 * 162 * <pre> 163 * MagicNumberFileFilter javaClassFileFilter = 164 * MagicNumberFileFilter(new byte[] {0xCA, 0xFE, 0xBA, 0xBE}, 0); 165 * </pre> 166 * 167 * @param magicNumbers the magic number to look for in the file. 168 * @param offset the byte offset in the file to start comparing bytes. 169 * 170 * @throws IllegalArgumentException if {@code magicNumber} 171 * contains no bytes, or {@code offset} 172 * is a negative number. 173 */ 174 public MagicNumberFileFilter(final byte[] magicNumbers, final long offset) { 175 Objects.requireNonNull(magicNumbers, "magicNumbers"); 176 if (magicNumbers.length == 0) { 177 throw new IllegalArgumentException("The magic number must contain at least one byte"); 178 } 179 if (offset < 0) { 180 throw new IllegalArgumentException("The offset cannot be negative"); 181 } 182 183 this.magicNumbers = magicNumbers.clone(); 184 this.byteOffset = offset; 185 } 186 187 /** 188 * <p> 189 * Constructs a new MagicNumberFileFilter and associates it with the magic 190 * number to test for in files. This constructor assumes a starting offset 191 * of {@code 0}. 192 * </p> 193 * 194 * Example usage: 195 * <pre> 196 * {@code 197 * MagicNumberFileFilter xmlFileFilter = 198 * MagicNumberFileFilter("<?xml"); 199 * } 200 * </pre> 201 * 202 * @param magicNumber the magic number to look for in the file. 203 * The string is converted to bytes using the platform default charset. 204 * 205 * @throws IllegalArgumentException if {@code magicNumber} is 206 * {@code null} or the empty String. 207 */ 208 public MagicNumberFileFilter(final String magicNumber) { 209 this(magicNumber, 0); 210 } 211 212 /** 213 * <p> 214 * Constructs a new MagicNumberFileFilter and associates it with the magic 215 * number to test for in files and the byte offset location in the file to 216 * to look for that magic number. 217 * </p> 218 * 219 * <pre> 220 * MagicNumberFileFilter tarFileFilter = 221 * MagicNumberFileFilter("ustar", 257); 222 * </pre> 223 * 224 * @param magicNumber the magic number to look for in the file. 225 * The string is converted to bytes using the platform default charset. 226 * @param offset the byte offset in the file to start comparing bytes. 227 * 228 * @throws IllegalArgumentException if {@code magicNumber} is 229 * the empty String, or {@code offset} is 230 * a negative number. 231 */ 232 public MagicNumberFileFilter(final String magicNumber, final long offset) { 233 Objects.requireNonNull(magicNumber, "magicNumber"); 234 if (magicNumber.isEmpty()) { 235 throw new IllegalArgumentException("The magic number must contain at least one byte"); 236 } 237 if (offset < 0) { 238 throw new IllegalArgumentException("The offset cannot be negative"); 239 } 240 241 this.magicNumbers = magicNumber.getBytes(Charset.defaultCharset()); // explicitly uses the platform default charset 242 this.byteOffset = offset; 243 } 244 245 /** 246 * <p> 247 * Accepts the provided file if the file contains the file filter's magic 248 * number at the specified offset. 249 * </p> 250 * 251 * <p> 252 * If any {@link IOException}s occur while reading the file, the file will 253 * be rejected. 254 * </p> 255 * 256 * @param file the file to accept or reject. 257 * 258 * @return {@code true} if the file contains the filter's magic number 259 * at the specified offset, {@code false} otherwise. 260 */ 261 @Override 262 public boolean accept(final File file) { 263 if (file != null && file.isFile() && file.canRead()) { 264 try { 265 return RandomAccessFileMode.READ_ONLY.apply(file.toPath(), 266 raf -> Arrays.equals(magicNumbers, RandomAccessFiles.read(raf, byteOffset, magicNumbers.length))); 267 } catch (final IOException ignored) { 268 // Do nothing, fall through and do not accept file 269 } 270 } 271 return false; 272 } 273 274 /** 275 * <p> 276 * Accepts the provided file if the file contains the file filter's magic 277 * number at the specified offset. 278 * </p> 279 * <p> 280 * If any {@link IOException}s occur while reading the file, the file will 281 * be rejected. 282 * 283 * </p> 284 * @param file the file to accept or reject. 285 * @param attributes the path's basic attributes (may be null). 286 * @return {@code true} if the file contains the filter's magic number 287 * at the specified offset, {@code false} otherwise. 288 * @since 2.9.0 289 */ 290 @Override 291 public FileVisitResult accept(final Path file, final BasicFileAttributes attributes) { 292 if (file != null && Files.isRegularFile(file) && Files.isReadable(file)) { 293 try { 294 try (FileChannel fileChannel = FileChannel.open(file)) { 295 final ByteBuffer byteBuffer = ByteBuffer.allocate(this.magicNumbers.length); 296 fileChannel.position(byteOffset); 297 final int read = fileChannel.read(byteBuffer); 298 if (read != magicNumbers.length) { 299 return FileVisitResult.TERMINATE; 300 } 301 return toFileVisitResult(Arrays.equals(this.magicNumbers, byteBuffer.array())); 302 } 303 } 304 catch (final IOException ignored) { 305 // Do nothing, fall through and do not accept file 306 } 307 } 308 return FileVisitResult.TERMINATE; 309 } 310 311 /** 312 * Returns a String representation of the file filter, which includes the 313 * magic number bytes and byte offset. 314 * 315 * @return a String representation of the file filter. 316 */ 317 @Override 318 public String toString() { 319 final StringBuilder builder = new StringBuilder(super.toString()); 320 builder.append("("); 321 // TODO perhaps use hex if value is not printable 322 builder.append(new String(magicNumbers, Charset.defaultCharset())); 323 builder.append(","); 324 builder.append(this.byteOffset); 325 builder.append(")"); 326 return builder.toString(); 327 } 328}