package com.android.vending.expansion.zipfile; /* * Copyright (C) 2012 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. */ import android.content.res.AssetFileDescriptor; import android.os.ParcelFileDescriptor; import android.util.Log; import java.io.EOFException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.util.Collection; import java.util.HashMap; import java.util.Vector; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class ZipResourceFile { // // Read-only access to Zip archives, with minimal heap allocation. // static final String LOG_TAG = "zipro"; static final boolean LOGV = false; // 4-byte number static private int swapEndian(int i) { return ((i & 0xff) << 24) + ((i & 0xff00) << 8) + ((i & 0xff0000) >>> 8) + ((i >>> 24) & 0xff); } // 2-byte number static private int swapEndian(short i) { return ((i & 0x00FF) << 8 | (i & 0xFF00) >>> 8); } /* * Zip file constants. */ static final int kEOCDSignature = 0x06054b50; static final int kEOCDLen = 22; static final int kEOCDNumEntries = 8; // offset to #of entries in file static final int kEOCDSize = 12; // size of the central directory static final int kEOCDFileOffset = 16; // offset to central directory static final int kMaxCommentLen = 65535; // longest possible in ushort static final int kMaxEOCDSearch = (kMaxCommentLen + kEOCDLen); static final int kLFHSignature = 0x04034b50; static final int kLFHLen = 30; // excluding variable-len fields static final int kLFHNameLen = 26; // offset to filename length static final int kLFHExtraLen = 28; // offset to extra length static final int kCDESignature = 0x02014b50; static final int kCDELen = 46; // excluding variable-len fields static final int kCDEMethod = 10; // offset to compression method static final int kCDEModWhen = 12; // offset to modification timestamp static final int kCDECRC = 16; // offset to entry CRC static final int kCDECompLen = 20; // offset to compressed length static final int kCDEUncompLen = 24; // offset to uncompressed length static final int kCDENameLen = 28; // offset to filename length static final int kCDEExtraLen = 30; // offset to extra length static final int kCDECommentLen = 32; // offset to comment length static final int kCDELocalOffset = 42; // offset to local hdr static final int kCompressStored = 0; // no compression static final int kCompressDeflated = 8; // standard deflate /* * The values we return for ZipEntryRO use 0 as an invalid value, so we want * to adjust the hash table index by a fixed amount. Using a large value * helps insure that people don't mix & match arguments, e.g. to * findEntryByIndex(). */ static final int kZipEntryAdj = 10000; static public final class ZipEntryRO { public ZipEntryRO(final String zipFileName, final File file, final String fileName) { mFileName = fileName; mZipFileName = zipFileName; mFile = file; } public final File mFile; public final String mFileName; public final String mZipFileName; public long mLocalHdrOffset; // offset of local file header /* useful stuff from the directory entry */ public int mMethod; public long mWhenModified; public long mCRC32; public long mCompressedLength; public long mUncompressedLength; public long mOffset = -1; public void setOffsetFromFile(RandomAccessFile f, ByteBuffer buf) throws IOException { long localHdrOffset = mLocalHdrOffset; try { f.seek(localHdrOffset); f.readFully(buf.array()); if (buf.getInt(0) != kLFHSignature) { Log.w(LOG_TAG, "didn't find signature at start of lfh"); throw new IOException(); } int nameLen = buf.getShort(kLFHNameLen) & 0xFFFF; int extraLen = buf.getShort(kLFHExtraLen) & 0xFFFF; mOffset = localHdrOffset + kLFHLen + nameLen + extraLen; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException ioe) { ioe.printStackTrace(); } } /** * Calculates the offset of the start of the Zip file entry within the * Zip file. * * @return the offset, in bytes from the start of the file of the entry */ public long getOffset() { return mOffset; } /** * isUncompressed * * @return true if the file is stored in uncompressed form */ public boolean isUncompressed() { return mMethod == kCompressStored; } public AssetFileDescriptor getAssetFileDescriptor() { if (mMethod == kCompressStored) { ParcelFileDescriptor pfd; try { pfd = ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_READ_ONLY); return new AssetFileDescriptor(pfd, getOffset(), mUncompressedLength); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return null; } public String getZipFileName() { return mZipFileName; } public File getZipFile() { return mFile; } } private HashMap<String, ZipEntryRO> mHashMap = new HashMap<String, ZipEntryRO>(); /* for reading compressed files */ public HashMap<File, ZipFile> mZipFiles = new HashMap<File, ZipFile>(); public ZipResourceFile(String zipFileName) throws IOException { addPatchFile(zipFileName); } ZipEntryRO[] getEntriesAt(String path) { Vector<ZipEntryRO> zev = new Vector<ZipEntryRO>(); Collection<ZipEntryRO> values = mHashMap.values(); if (null == path) path = ""; int length = path.length(); for (ZipEntryRO ze : values) { if (ze.mFileName.startsWith(path)) { if (-1 == ze.mFileName.indexOf('/', length)) { zev.add(ze); } } } ZipEntryRO[] entries = new ZipEntryRO[zev.size()]; return zev.toArray(entries); } public ZipEntryRO[] getAllEntries() { Collection<ZipEntryRO> values = mHashMap.values(); return values.toArray(new ZipEntryRO[values.size()]); } /** * getAssetFileDescriptor allows for ZipResourceFile to directly feed * Android API's that want an fd, offset, and length such as the * MediaPlayer. It also allows for the class to be used in a content * provider that can feed video players. The file must be stored * (non-compressed) in the Zip file for this to work. * * @param assetPath * @return the asset file descriptor for the file, or null if the file isn't * present or is stored compressed */ public AssetFileDescriptor getAssetFileDescriptor(String assetPath) { ZipEntryRO entry = mHashMap.get(assetPath); if (null != entry) { return entry.getAssetFileDescriptor(); } return null; } /** * getInputStream returns an AssetFileDescriptor.AutoCloseInputStream * associated with the asset that is contained in the Zip file, or a * standard ZipInputStream if necessary to uncompress the file * * @param assetPath * @return an input stream for the named asset path, or null if not found * @throws IOException */ public InputStream getInputStream(String assetPath) throws IOException { ZipEntryRO entry = mHashMap.get(assetPath); if (null != entry) { if (entry.isUncompressed()) { return entry.getAssetFileDescriptor().createInputStream(); } else { ZipFile zf = mZipFiles.get(entry.getZipFile()); /** read compressed files **/ if (null == zf) { zf = new ZipFile(entry.getZipFile(), ZipFile.OPEN_READ); mZipFiles.put(entry.getZipFile(), zf); } ZipEntry zi = zf.getEntry(assetPath); if (null != zi) return zf.getInputStream(zi); } } return null; } ByteBuffer mLEByteBuffer = ByteBuffer.allocate(4); static private int read4LE(RandomAccessFile f) throws EOFException, IOException { return swapEndian(f.readInt()); } /* * Opens the specified file read-only. We memory-map the entire thing and * close the file before returning. */ void addPatchFile(String zipFileName) throws IOException { File file = new File(zipFileName); RandomAccessFile f = new RandomAccessFile(file, "r"); long fileLength = f.length(); if (fileLength < kEOCDLen) { throw new java.io.IOException(); } long readAmount = kMaxEOCDSearch; if (readAmount > fileLength) readAmount = fileLength; /* * Make sure this is a Zip archive. */ f.seek(0); int header = read4LE(f); if (header == kEOCDSignature) { Log.i(LOG_TAG, "Found Zip archive, but it looks empty"); throw new IOException(); } else if (header != kLFHSignature) { Log.v(LOG_TAG, "Not a Zip archive"); throw new IOException(); } /* * Perform the traditional EOCD snipe hunt. We're searching for the End * of Central Directory magic number, which appears at the start of the * EOCD block. It's followed by 18 bytes of EOCD stuff and up to 64KB of * archive comment. We need to read the last part of the file into a * buffer, dig through it to find the magic number, parse some values * out, and use those to determine the extent of the CD. We start by * pulling in the last part of the file. */ long searchStart = fileLength - readAmount; f.seek(searchStart); ByteBuffer bbuf = ByteBuffer.allocate((int) readAmount); byte[] buffer = bbuf.array(); f.readFully(buffer); bbuf.order(ByteOrder.LITTLE_ENDIAN); /* * Scan backward for the EOCD magic. In an archive without a trailing * comment, we'll find it on the first try. (We may want to consider * doing an initial minimal read; if we don't find it, retry with a * second read as above.) */ // EOCD == 0x50, 0x4b, 0x05, 0x06 int eocdIdx; for (eocdIdx = buffer.length - kEOCDLen; eocdIdx >= 0; eocdIdx--) { if (buffer[eocdIdx] == 0x50 && bbuf.getInt(eocdIdx) == kEOCDSignature) { if (LOGV) { Log.v(LOG_TAG, "+++ Found EOCD at index: " + eocdIdx); } break; } } if (eocdIdx < 0) { Log.d(LOG_TAG, "Zip: EOCD not found, " + zipFileName + " is not zip"); } /* * Grab the CD offset and size, and the number of entries in the * archive. After that, we can release our EOCD hunt buffer. */ int numEntries = bbuf.getShort(eocdIdx + kEOCDNumEntries); long dirSize = bbuf.getInt(eocdIdx + kEOCDSize) & 0xffffffffL; long dirOffset = bbuf.getInt(eocdIdx + kEOCDFileOffset) & 0xffffffffL; // Verify that they look reasonable. if (dirOffset + dirSize > fileLength) { Log.w(LOG_TAG, "bad offsets (dir " + dirOffset + ", size " + dirSize + ", eocd " + eocdIdx + ")"); throw new IOException(); } if (numEntries == 0) { Log.w(LOG_TAG, "empty archive?"); throw new IOException(); } if (LOGV) { Log.v(LOG_TAG, "+++ numEntries=" + numEntries + " dirSize=" + dirSize + " dirOffset=" + dirOffset); } MappedByteBuffer directoryMap = f.getChannel() .map(FileChannel.MapMode.READ_ONLY, dirOffset, dirSize); directoryMap.order(ByteOrder.LITTLE_ENDIAN); byte[] tempBuf = new byte[0xffff]; /* * Walk through the central directory, adding entries to the hash table. */ int currentOffset = 0; /* * Allocate the local directory information */ ByteBuffer buf = ByteBuffer.allocate(kLFHLen); buf.order(ByteOrder.LITTLE_ENDIAN); for (int i = 0; i < numEntries; i++) { if (directoryMap.getInt(currentOffset) != kCDESignature) { Log.w(LOG_TAG, "Missed a central dir sig (at " + currentOffset + ")"); throw new IOException(); } /* useful stuff from the directory entry */ int fileNameLen = directoryMap.getShort(currentOffset + kCDENameLen) & 0xffff; int extraLen = directoryMap.getShort(currentOffset + kCDEExtraLen) & 0xffff; int commentLen = directoryMap.getShort(currentOffset + kCDECommentLen) & 0xffff; /* get the CDE filename */ directoryMap.position(currentOffset + kCDELen); directoryMap.get(tempBuf, 0, fileNameLen); directoryMap.position(0); /* UTF-8 on Android */ String str = new String(tempBuf, 0, fileNameLen); if (LOGV) { Log.v(LOG_TAG, "Filename: " + str); } ZipEntryRO ze = new ZipEntryRO(zipFileName, file, str); ze.mMethod = directoryMap.getShort(currentOffset + kCDEMethod) & 0xffff; ze.mWhenModified = directoryMap.getInt(currentOffset + kCDEModWhen) & 0xffffffffL; ze.mCRC32 = directoryMap.getLong(currentOffset + kCDECRC) & 0xffffffffL; ze.mCompressedLength = directoryMap.getLong(currentOffset + kCDECompLen) & 0xffffffffL; ze.mUncompressedLength = directoryMap.getLong(currentOffset + kCDEUncompLen) & 0xffffffffL; ze.mLocalHdrOffset = directoryMap.getInt(currentOffset + kCDELocalOffset) & 0xffffffffL; // set the offsets buf.clear(); ze.setOffsetFromFile(f, buf); // put file into hash mHashMap.put(str, ze); // go to next directory entry currentOffset += kCDELen + fileNameLen + extraLen + commentLen; } if (LOGV) { Log.v(LOG_TAG, "+++ zip good scan " + numEntries + " entries"); } } }