1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package java.util.jar; 19 20 import java.io.File; 21 import java.io.FilterInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.util.ArrayList; 25 import java.util.Enumeration; 26 import java.util.HashMap; 27 import java.util.List; 28 import java.util.Locale; 29 import java.util.zip.ZipEntry; 30 import java.util.zip.ZipFile; 31 import libcore.io.Streams; 32 33 /** 34 * {@code JarFile} is used to read jar entries and their associated data from 35 * jar files. 36 * 37 * @see JarInputStream 38 * @see JarEntry 39 */ 40 public class JarFile extends ZipFile { 41 42 /** 43 * The MANIFEST file name. 44 */ 45 public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; 46 47 // The directory containing the manifest. 48 static final String META_DIR = "META-INF/"; 49 50 // The manifest after it has been read from the JAR. 51 private Manifest manifest; 52 53 // The entry for the MANIFEST.MF file before the first call to getManifest(). 54 private byte[] manifestBytes; 55 56 JarVerifier verifier; 57 58 private boolean closed = false; 59 60 static final class JarFileInputStream extends FilterInputStream { 61 private final JarVerifier.VerifierEntry entry; 62 63 private long count; 64 private boolean done = false; 65 66 JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) { 67 super(is); 68 entry = e; 69 70 count = size; 71 } 72 73 @Override 74 public int read() throws IOException { 75 if (done) { 76 return -1; 77 } 78 if (count > 0) { 79 int r = super.read(); 80 if (r != -1) { 81 entry.write(r); 82 count--; 83 } else { 84 count = 0; 85 } 86 if (count == 0) { 87 done = true; 88 entry.verify(); 89 } 90 return r; 91 } else { 92 done = true; 93 entry.verify(); 94 return -1; 95 } 96 } 97 98 @Override 99 public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { 100 if (done) { 101 return -1; 102 } 103 if (count > 0) { 104 int r = super.read(buffer, byteOffset, byteCount); 105 if (r != -1) { 106 int size = r; 107 if (count < size) { 108 size = (int) count; 109 } 110 entry.write(buffer, byteOffset, size); 111 count -= size; 112 } else { 113 count = 0; 114 } 115 if (count == 0) { 116 done = true; 117 entry.verify(); 118 } 119 return r; 120 } else { 121 done = true; 122 entry.verify(); 123 return -1; 124 } 125 } 126 127 @Override 128 public int available() throws IOException { 129 if (done) { 130 return 0; 131 } 132 return super.available(); 133 } 134 135 @Override 136 public long skip(long byteCount) throws IOException { 137 return Streams.skipByReading(this, byteCount); 138 } 139 } 140 141 static final class JarFileEnumerator implements Enumeration<JarEntry> { 142 final Enumeration<? extends ZipEntry> ze; 143 final JarFile jf; 144 145 JarFileEnumerator(Enumeration<? extends ZipEntry> zenum, JarFile jf) { 146 ze = zenum; 147 this.jf = jf; 148 } 149 150 public boolean hasMoreElements() { 151 return ze.hasMoreElements(); 152 } 153 154 public JarEntry nextElement() { 155 return new JarEntry(ze.nextElement(), jf /* parentJar */); 156 } 157 } 158 159 /** 160 * Create a new {@code JarFile} using the contents of the specified file. 161 * 162 * @param file 163 * the JAR file as {@link File}. 164 * @throws IOException 165 * If the file cannot be read. 166 */ 167 public JarFile(File file) throws IOException { 168 this(file, true); 169 } 170 171 /** 172 * Create a new {@code JarFile} using the contents of the specified file. 173 * 174 * @param file 175 * the JAR file as {@link File}. 176 * @param verify 177 * if this JAR file is signed whether it must be verified. 178 * @throws IOException 179 * If the file cannot be read. 180 */ 181 public JarFile(File file, boolean verify) throws IOException { 182 this(file, verify, ZipFile.OPEN_READ); 183 } 184 185 /** 186 * Create a new {@code JarFile} using the contents of file. 187 * 188 * @param file 189 * the JAR file as {@link File}. 190 * @param verify 191 * if this JAR filed is signed whether it must be verified. 192 * @param mode 193 * the mode to use, either {@link ZipFile#OPEN_READ OPEN_READ} or 194 * {@link ZipFile#OPEN_DELETE OPEN_DELETE}. 195 * @throws IOException 196 * If the file cannot be read. 197 */ 198 public JarFile(File file, boolean verify, int mode) throws IOException { 199 super(file, mode); 200 201 // Step 1: Scan the central directory for meta entries (MANIFEST.mf 202 // & possibly the signature files) and read them fully. 203 HashMap<String, byte[]> metaEntries = readMetaEntries(this, verify); 204 205 // Step 2: Construct a verifier with the information we have. 206 // Verification is possible *only* if the JAR file contains a manifest 207 // *AND* it contains signing related information (signature block 208 // files and the signature files). 209 // 210 // TODO: Is this really the behaviour we want if verify == true ? 211 // We silently skip verification for files that have no manifest or 212 // no signatures. 213 if (verify && metaEntries.containsKey(MANIFEST_NAME) && 214 metaEntries.size() > 1) { 215 // We create the manifest straight away, so that we can create 216 // the jar verifier as well. 217 manifest = new Manifest(metaEntries.get(MANIFEST_NAME), true); 218 verifier = new JarVerifier(getName(), manifest, metaEntries); 219 } else { 220 verifier = null; 221 manifestBytes = metaEntries.get(MANIFEST_NAME); 222 } 223 } 224 225 /** 226 * Create a new {@code JarFile} from the contents of the file specified by 227 * filename. 228 * 229 * @param filename 230 * the file name referring to the JAR file. 231 * @throws IOException 232 * if file name cannot be opened for reading. 233 */ 234 public JarFile(String filename) throws IOException { 235 this(filename, true); 236 } 237 238 /** 239 * Create a new {@code JarFile} from the contents of the file specified by 240 * {@code filename}. 241 * 242 * @param filename 243 * the file name referring to the JAR file. 244 * @param verify 245 * if this JAR filed is signed whether it must be verified. 246 * @throws IOException 247 * If file cannot be opened or read. 248 */ 249 public JarFile(String filename, boolean verify) throws IOException { 250 this(new File(filename), verify, ZipFile.OPEN_READ); 251 } 252 253 /** 254 * Return an enumeration containing the {@code JarEntrys} contained in this 255 * {@code JarFile}. 256 * 257 * @return the {@code Enumeration} containing the JAR entries. 258 * @throws IllegalStateException 259 * if this {@code JarFile} is closed. 260 */ 261 @Override 262 public Enumeration<JarEntry> entries() { 263 return new JarFileEnumerator(super.entries(), this); 264 } 265 266 /** 267 * Return the {@code JarEntry} specified by its name or {@code null} if no 268 * such entry exists. 269 * 270 * @param name 271 * the name of the entry in the JAR file. 272 * @return the JAR entry defined by the name. 273 */ 274 public JarEntry getJarEntry(String name) { 275 return (JarEntry) getEntry(name); 276 } 277 278 /** 279 * Returns the {@code Manifest} object associated with this {@code JarFile} 280 * or {@code null} if no MANIFEST entry exists. 281 * 282 * @return the MANIFEST. 283 * @throws IOException 284 * if an error occurs reading the MANIFEST file. 285 * @throws IllegalStateException 286 * if the jar file is closed. 287 * @see Manifest 288 */ 289 public Manifest getManifest() throws IOException { 290 if (closed) { 291 throw new IllegalStateException("JarFile has been closed"); 292 } 293 294 if (manifest != null) { 295 return manifest; 296 } 297 298 // If manifest == null && manifestBytes == null, there's no manifest. 299 if (manifestBytes == null) { 300 return null; 301 } 302 303 // We hit this code path only if the verification isn't necessary. If 304 // we did decide to verify this file, we'd have created the Manifest and 305 // the associated Verifier in the constructor itself. 306 manifest = new Manifest(manifestBytes, false); 307 manifestBytes = null; 308 309 return manifest; 310 } 311 312 /** 313 * Called by the JarFile constructors, Reads the contents of the 314 * file's META-INF/ directory and picks out the MANIFEST.MF file and 315 * verifier signature files if they exist. 316 * 317 * @throws IOException 318 * if there is a problem reading the jar file entries. 319 * @return a map of entry names to their {@code byte[]} content. 320 */ 321 static HashMap<String, byte[]> readMetaEntries(ZipFile zipFile, 322 boolean verificationRequired) throws IOException { 323 // Get all meta directory entries 324 List<ZipEntry> metaEntries = getMetaEntries(zipFile); 325 326 HashMap<String, byte[]> metaEntriesMap = new HashMap<String, byte[]>(); 327 328 for (ZipEntry entry : metaEntries) { 329 String entryName = entry.getName(); 330 // Is this the entry for META-INF/MANIFEST.MF ? 331 // 332 // TODO: Why do we need the containsKey check ? Shouldn't we discard 333 // files that contain duplicate entries like this as invalid ?. 334 if (entryName.equalsIgnoreCase(MANIFEST_NAME) && 335 !metaEntriesMap.containsKey(MANIFEST_NAME)) { 336 337 metaEntriesMap.put(MANIFEST_NAME, Streams.readFully( 338 zipFile.getInputStream(entry))); 339 340 // If there is no verifier then we don't need to look any further. 341 if (!verificationRequired) { 342 break; 343 } 344 } else if (verificationRequired) { 345 // Is this an entry that the verifier needs? 346 if (endsWithIgnoreCase(entryName, ".SF") 347 || endsWithIgnoreCase(entryName, ".DSA") 348 || endsWithIgnoreCase(entryName, ".RSA") 349 || endsWithIgnoreCase(entryName, ".EC")) { 350 InputStream is = zipFile.getInputStream(entry); 351 metaEntriesMap.put(entryName.toUpperCase(Locale.US), Streams.readFully(is)); 352 } 353 } 354 } 355 356 return metaEntriesMap; 357 } 358 359 private static boolean endsWithIgnoreCase(String s, String suffix) { 360 return s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length()); 361 } 362 363 /** 364 * Return an {@code InputStream} for reading the decompressed contents of 365 * ZIP entry. 366 * 367 * @param ze 368 * the ZIP entry to be read. 369 * @return the input stream to read from. 370 * @throws IOException 371 * if an error occurred while creating the input stream. 372 */ 373 @Override 374 public InputStream getInputStream(ZipEntry ze) throws IOException { 375 if (manifestBytes != null) { 376 getManifest(); 377 } 378 379 if (verifier != null) { 380 if (verifier.readCertificates()) { 381 verifier.removeMetaEntries(); 382 manifest.removeChunks(); 383 384 if (!verifier.isSignedJar()) { 385 verifier = null; 386 } 387 } 388 } 389 390 InputStream in = super.getInputStream(ze); 391 if (in == null) { 392 return null; 393 } 394 if (verifier == null || ze.getSize() == -1) { 395 return in; 396 } 397 JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName()); 398 if (entry == null) { 399 return in; 400 } 401 return new JarFileInputStream(in, ze.getSize(), entry); 402 } 403 404 /** 405 * Return the {@code JarEntry} specified by name or {@code null} if no such 406 * entry exists. 407 * 408 * @param name 409 * the name of the entry in the JAR file. 410 * @return the ZIP entry extracted. 411 */ 412 @Override 413 public ZipEntry getEntry(String name) { 414 ZipEntry ze = super.getEntry(name); 415 if (ze == null) { 416 return ze; 417 } 418 return new JarEntry(ze, this /* parentJar */); 419 } 420 421 /** 422 * Returns all the ZipEntry's that relate to files in the 423 * JAR's META-INF directory. 424 */ 425 private static List<ZipEntry> getMetaEntries(ZipFile zipFile) { 426 List<ZipEntry> list = new ArrayList<ZipEntry>(8); 427 428 Enumeration<? extends ZipEntry> allEntries = zipFile.entries(); 429 while (allEntries.hasMoreElements()) { 430 ZipEntry ze = allEntries.nextElement(); 431 if (ze.getName().startsWith(META_DIR) 432 && ze.getName().length() > META_DIR.length()) { 433 list.add(ze); 434 } 435 } 436 437 return list; 438 } 439 440 /** 441 * Closes this {@code JarFile}. 442 * 443 * @throws IOException 444 * if an error occurs. 445 */ 446 @Override 447 public void close() throws IOException { 448 super.close(); 449 closed = true; 450 } 451 } 452