// Original File 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.
//
 
 // Modifications: syntax, formatting, and making pesky addPatchFile public...

package com.flyingsoftgames.xapkreader;

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 XAPKZipResourceFile {
 // 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 number 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 XAPKZipResourceFile (String zipFileName) throws IOException {addPatchFile(zipFileName);}
 
 ZipEntryRO[] getEntriesAt (String path) {
  Vector<ZipEntryRO> zev = new Vector<ZipEntryRO>();
  Collection<ZipEntryRO> values = mHashMap.values();
  if (path == null) path = "";
  int length = path.length();
  for (ZipEntryRO ze : values) {
   if (!ze.mFileName.startsWith(path)) continue;
   if (ze.mFileName.indexOf('/', length) != -1) continue;
   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 (entry != null) {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 (entry != null) {
   if (entry.isUncompressed()) {
    return entry.getAssetFileDescriptor().createInputStream();
   } else {
    ZipFile zf = mZipFiles.get(entry.getZipFile());
    // Read compressed files.
    if (zf == null) {
     zf = new ZipFile(entry.getZipFile(), ZipFile.OPEN_READ);
     mZipFiles.put(entry.getZipFile(), zf);
    }
    ZipEntry zi = zf.getEntry(assetPath);
    if (zi != null) 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.
 public 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 archiv.e");
   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 a zip file.");
  
  // 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 the file into the 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.");
 }
}
