content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  
  package android.util.jar;
  
  import android.system.ErrnoException;
  import android.system.Os;
  import android.system.OsConstants;
  
  import dalvik.system.CloseGuard;
  import java.io.FileDescriptor;
  import java.io.FilterInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.security.cert.Certificate;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.Set;
  import java.util.jar.JarFile;
  import java.util.zip.Inflater;
  import java.util.zip.InflaterInputStream;
  import java.util.zip.ZipEntry;
  import libcore.io.IoBridge;
  import libcore.io.IoUtils;
  import libcore.io.Streams;
  
  /**
   * A subset of the JarFile API implemented as a thin wrapper over
   * system/core/libziparchive.
   *
   * @hide for internal use only. Not API compatible (or as forgiving) as
   *        {@link java.util.jar.JarFile}
   */
  public final class StrictJarFile {
  
      private final long nativeHandle;
  
      // NOTE: It's possible to share a file descriptor with the native
      // code, at the cost of some additional complexity.
      private final FileDescriptor fd;
  
      private final StrictJarManifest manifest;
      private final StrictJarVerifier verifier;
  
      private final boolean isSigned;
  
      private final CloseGuard guard = CloseGuard.get();
      private boolean closed;
  
      public StrictJarFile(String fileName)
              throws IOException, SecurityException {
          this(fileName, true, true);
      }
  
      public StrictJarFile(FileDescriptor fd)
              throws IOException, SecurityException {
          this(fd, true, true);
      }
  
      public StrictJarFile(FileDescriptor fd,
              boolean verify,
              boolean signatureSchemeRollbackProtectionsEnforced)
                      throws IOException, SecurityException {
          this("[fd:" + fd.getInt$() + "]", fd, verify,
                  signatureSchemeRollbackProtectionsEnforced);
      }
  
      public StrictJarFile(String fileName,
              boolean verify,
              boolean signatureSchemeRollbackProtectionsEnforced)
                      throws IOException, SecurityException {
          this(fileName, IoBridge.open(fileName, OsConstants.O_RDONLY),
                  verify, signatureSchemeRollbackProtectionsEnforced);
      }
  
      /**
       * @param name of the archive (not necessarily a path).
       * @param fd seekable file descriptor for the JAR file.
       * @param verify whether to verify the file's JAR signatures and collect the corresponding
       *        signer certificates.
       * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against
       *        stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or
       *        {@code false} to ignore any such protections. This parameter is ignored when
       *        {@code verify} is {@code false}.
       */
      private StrictJarFile(String name,
              FileDescriptor fd,
              boolean verify,
              boolean signatureSchemeRollbackProtectionsEnforced)
                      throws IOException, SecurityException {
          this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
          this.fd = fd;
  
          try {
              // Read the MANIFEST and signature files up front and try to
              // parse them. We never want to accept a JAR File with broken signatures
              // or manifests, so it's best to throw as early as possible.
              if (verify) {
                  HashMap<String, byte[]> metaEntries = getMetaEntries();
                  this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
                  this.verifier =
                          new StrictJarVerifier(
                                  name,
                                  manifest,
                                  metaEntries,
                                  signatureSchemeRollbackProtectionsEnforced);
                  Set<String> files = manifest.getEntries().keySet();
                  for (String file : files) {
                      if (findEntry(file) == null) {
                          throw new SecurityException("File " + file + " in manifest does not exist");
                      }
                  }
  
                  isSigned = verifier.readCertificates() && verifier.isSignedJar();
              } else {
                  isSigned = false;
                  this.manifest = null;
                  this.verifier = null;
              }
          } catch (IOException | SecurityException e) {
              nativeClose(this.nativeHandle);
              IoUtils.closeQuietly(fd);
              closed = true;
              throw e;
          }
  
          guard.open("close");
      }
  
      public StrictJarManifest getManifest() {
          return manifest;
      }
  
      public Iterator<ZipEntry> iterator() throws IOException {
          return new EntryIterator(nativeHandle, "");
      }
  
      public ZipEntry findEntry(String name) {
          return nativeFindEntry(nativeHandle, name);
      }
  
      /**
       * Return all certificate chains for a given {@link ZipEntry} belonging to this jar.
       * This method MUST be called only after fully exhausting the InputStream belonging
       * to this entry.
       *
       * Returns {@code null} if this jar file isn't signed or if this method is
       * called before the stream is processed.
       */
      public Certificate[][] getCertificateChains(ZipEntry ze) {
          if (isSigned) {
              return verifier.getCertificateChains(ze.getName());
          }
  
          return null;
      }
  
      /**
       * Return all certificates for a given {@link ZipEntry} belonging to this jar.
       * This method MUST be called only after fully exhausting the InputStream belonging
       * to this entry.
       *
       * Returns {@code null} if this jar file isn't signed or if this method is
       * called before the stream is processed.
       *
       * @deprecated Switch callers to use getCertificateChains instead
       */
      @Deprecated
      public Certificate[] getCertificates(ZipEntry ze) {
          if (isSigned) {
              Certificate[][] certChains = verifier.getCertificateChains(ze.getName());
  
              // Measure number of certs.
              int count = 0;
              for (Certificate[] chain : certChains) {
                  count += chain.length;
              }
  
              // Create new array and copy all the certs into it.
              Certificate[] certs = new Certificate[count];
              int i = 0;
              for (Certificate[] chain : certChains) {
                  System.arraycopy(chain, 0, certs, i, chain.length);
                  i += chain.length;
              }
  
              return certs;
          }
  
          return null;
      }
  
      public InputStream getInputStream(ZipEntry ze) {
          final InputStream is = getZipInputStream(ze);
  
          if (isSigned) {
              StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
              if (entry == null) {
                  return is;
              }
  
              return new JarFileInputStream(is, ze.getSize(), entry);
          }
  
          return is;
      }
  
      public void close() throws IOException {
          if (!closed) {
              if (guard != null) {
                  guard.close();
              }
  
              nativeClose(nativeHandle);
              IoUtils.closeQuietly(fd);
              closed = true;
          }
      }
  
      @Override
      protected void finalize() throws Throwable {
          try {
              if (guard != null) {
                  guard.warnIfOpen();
              }
              close();
          } finally {
              super.finalize();
          }
      }
  
      private InputStream getZipInputStream(ZipEntry ze) {
          if (ze.getMethod() == ZipEntry.STORED) {
              return new FDStream(fd, ze.getDataOffset(),
                      ze.getDataOffset() + ze.getSize());
          } else {
              final FDStream wrapped = new FDStream(
                      fd, ze.getDataOffset(), ze.getDataOffset() + ze.getCompressedSize());
  
              int bufSize = Math.max(1024, (int) Math.min(ze.getSize(), 65535L));
              return new ZipInflaterInputStream(wrapped, new Inflater(true), bufSize, ze);
          }
      }
  
      static final class EntryIterator implements Iterator<ZipEntry> {
          private final long iterationHandle;
          private ZipEntry nextEntry;
  
          EntryIterator(long nativeHandle, String prefix) throws IOException {
              iterationHandle = nativeStartIteration(nativeHandle, prefix);
          }
  
          public ZipEntry next() {
              if (nextEntry != null) {
                  final ZipEntry ze = nextEntry;
                  nextEntry = null;
                  return ze;
              }
  
              return nativeNextEntry(iterationHandle);
          }
  
          public boolean hasNext() {
              if (nextEntry != null) {
                  return true;
              }
  
              final ZipEntry ze = nativeNextEntry(iterationHandle);
              if (ze == null) {
                  return false;
              }
  
              nextEntry = ze;
              return true;
          }
  
          public void remove() {
              throw new UnsupportedOperationException();
          }
      }
  
      private HashMap<String, byte[]> getMetaEntries() throws IOException {
          HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>();
  
          Iterator<ZipEntry> entryIterator = new EntryIterator(nativeHandle, "META-INF/");
          while (entryIterator.hasNext()) {
              final ZipEntry entry = entryIterator.next();
              metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry)));
          }
  
          return metaEntries;
      }
  
      static final class JarFileInputStream extends FilterInputStream {
          private final StrictJarVerifier.VerifierEntry entry;
  
          private long count;
          private boolean done = false;
  
          JarFileInputStream(InputStream is, long size, StrictJarVerifier.VerifierEntry e) {
              super(is);
              entry = e;
  
              count = size;
          }
  
          @Override
          public int read() throws IOException {
              if (done) {
                  return -1;
              }
              if (count > 0) {
                  int r = super.read();
                  if (r != -1) {
                      entry.write(r);
                      count--;
                  } else {
                      count = 0;
                  }
                  if (count == 0) {
                      done = true;
                      entry.verify();
                  }
                  return r;
              } else {
                  done = true;
                  entry.verify();
                  return -1;
              }
          }
  
          @Override
          public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
              if (done) {
                  return -1;
              }
              if (count > 0) {
                  int r = super.read(buffer, byteOffset, byteCount);
                  if (r != -1) {
                      int size = r;
                      if (count < size) {
                          size = (int) count;
                      }
                      entry.write(buffer, byteOffset, size);
                      count -= size;
                  } else {
                      count = 0;
                  }
                  if (count == 0) {
                      done = true;
                      entry.verify();
                  }
                  return r;
              } else {
                  done = true;
                  entry.verify();
                  return -1;
              }
          }
  
          @Override
          public int available() throws IOException {
              if (done) {
                  return 0;
              }
              return super.available();
          }
  
          @Override
          public long skip(long byteCount) throws IOException {
              return Streams.skipByReading(this, byteCount);
          }
      }
  
      /** @hide */
      public static class ZipInflaterInputStream extends InflaterInputStream {
          private final ZipEntry entry;
          private long bytesRead = 0;
          private boolean closed;
  
          public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
              super(is, inf, bsize);
              this.entry = entry;
          }
  
          @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
              final int i;
              try {
                  i = super.read(buffer, byteOffset, byteCount);
              } catch (IOException e) {
                  throw new IOException("Error reading data for " + entry.getName() + " near offset "
                          + bytesRead, e);
              }
              if (i == -1) {
                  if (entry.getSize() != bytesRead) {
                      throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs "
                              + entry.getSize());
                  }
              } else {
                  bytesRead += i;
              }
              return i;
          }
  
          @Override public int available() throws IOException {
              if (closed) {
                  // Our superclass will throw an exception, but there's a jtreg test that
                  // explicitly checks that the InputStream returned from ZipFile.getInputStream
                  // returns 0 even when closed.
                  return 0;
              }
              return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
          }
  
          @Override
          public void close() throws IOException {
              super.close();
              closed = true;
          }
      }
  
      /**
       * Wrap a stream around a FileDescriptor.  The file descriptor is shared
       * among all streams returned by getInputStream(), so we have to synchronize
       * access to it.  (We can optimize this by adding buffering here to reduce
       * collisions.)
       *
       * <p>We could support mark/reset, but we don't currently need them.
       *
       * @hide
       */
      public static class FDStream extends InputStream {
          private final FileDescriptor fd;
          private long endOffset;
          private long offset;
  
          public FDStream(FileDescriptor fd, long initialOffset, long endOffset) {
              this.fd = fd;
              offset = initialOffset;
              this.endOffset = endOffset;
          }
  
          @Override public int available() throws IOException {
              return (offset < endOffset ? 1 : 0);
          }
  
          @Override public int read() throws IOException {
              return Streams.readSingleByte(this);
          }
  
          @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
              synchronized (this.fd) {
                  final long length = endOffset - offset;
                  if (byteCount > length) {
                      byteCount = (int) length;
                  }
                  try {
                      Os.lseek(fd, offset, OsConstants.SEEK_SET);
                  } catch (ErrnoException e) {
                      throw new IOException(e);
                  }
                  int count = IoBridge.read(fd, buffer, byteOffset, byteCount);
                  if (count > 0) {
                      offset += count;
                      return count;
                  } else {
                      return -1;
                  }
              }
          }
  
          @Override public long skip(long byteCount) throws IOException {
              if (byteCount > endOffset - offset) {
                  byteCount = endOffset - offset;
              }
              offset += byteCount;
              return byteCount;
          }
      }
  
      private static native long nativeOpenJarFile(String name, int fd)
              throws IOException;
      private static native long nativeStartIteration(long nativeHandle, String prefix);
      private static native ZipEntry nativeNextEntry(long iterationHandle);
      private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName);
      private static native void nativeClose(long nativeHandle);
  }
  