/* ZipFile.java --
   Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006
   Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

GNU Classpath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */


package java.util.zip;

import gnu.java.util.EmptyEnumeration;

import java.io.BufferedInputStream;
import java.io.DataInput;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;

/**
 * This class represents a Zip archive.  You can ask for the contained
 * entries, or get an input stream for a file entry.  The entry is
 * automatically decompressed.
 *
 * This class is thread safe:  You can open input streams for arbitrary
 * entries in different threads.
 *
 * @author Jochen Hoenicke
 * @author Artur Biesiadowski
 */
public class ZipFile implements ZipConstants
{
  static {
	System.loadLibrary("javautil");
  }
  
  /**
   * Mode flag to open a zip file for reading.
   */
  public static final int OPEN_READ = 0x1;

  /**
   * Mode flag to delete a zip file after reading.
   */
  public static final int OPEN_DELETE = 0x4;

  // Name of this zip file.
  private final String name;

  // ネイティブライブラリ側のファイル情報
  private int nativeDataAddress;
 
  private boolean closed = false;
  
  /**
   * Opens a Zip file with the given name for reading.
   * @exception IOException if a i/o error occured.
   * @exception ZipException if the file doesn't contain a valid zip
   * archive.  
   */
  public ZipFile(String name) throws ZipException, IOException
  {
    this(new File(name));
  }

  /**
   * Opens a Zip file reading the given File.
   * @exception IOException if a i/o error occured.
   * @exception ZipException if the file doesn't contain a valid zip
   * archive.  
   */
  public ZipFile(File file) throws ZipException, IOException
  {
    this(file, OPEN_READ);
  }

  /**
   * Opens a Zip file reading the given File in the given mode.
   *
   * If the OPEN_DELETE mode is specified, the zip file will be deleted at
   * some time moment after it is opened. It will be deleted before the zip
   * file is closed or the Virtual Machine exits.
   * 
   * The contents of the zip file will be accessible until it is closed.
   *
   * @since JDK1.3
   * @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE
   *
   * @exception IOException if a i/o error occured.
   * @exception ZipException if the file doesn't contain a valid zip
   * archive.  
   */
  public ZipFile(File file, int mode) throws ZipException, IOException
  {
    if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE))
      throw new IllegalArgumentException("invalid mode");
    if ((mode & OPEN_DELETE) != 0)
      file.deleteOnExit();
    this.nativeDataAddress = openNative(file.getAbsolutePath());
    this.name = file.getPath();
  }

  /**
   * Checks if file is closed and throws an exception.
   */
  private void checkClosed()
  {
    if (closed)
      throw new IllegalStateException("ZipFile has closed: " + name);
  }

  /**
   * Closes the ZipFile.  This also closes all input streams given by
   * this class.  After this is called, no further method should be
   * called.
   * 
   * @exception IOException if a i/o error occured.
   */
  public void close() throws IOException
  {
    if (! closed) {
      closed = true;
      closeNative(this.nativeDataAddress);
      this.nativeDataAddress = 0;
    }
  }

  /**
   * Calls the <code>close()</code> method when this ZipFile has not yet
   * been explicitly closed.
   */
  protected void finalize() throws IOException
  {
    if (! closed) {
      close();
    }
  }

  /**
   * Returns an enumeration of all Zip entries in this Zip file.
   *
   * @exception IllegalStateException when the ZipFile has already been closed
   */
  public Enumeration entries()
  {
    checkClosed();
    return new ZipEntryEnumeration();
  }

  /**
   * Searches for a zip entry in this archive with the given name.
   *
   * @param name the name. May contain directory components separated by
   * slashes ('/').
   * @return the zip entry, or null if no entry with that name exists.
   *
   * @exception IllegalStateException when the ZipFile has already been closed
   */
  public ZipEntry getEntry(String name)
  {
    checkClosed();

    try
      {
	return findEntry(this.nativeDataAddress,
	                 name);
      }
    catch (IOException ioe)
      {
	return null;
      }
  }
  
  /**
   * 指定されたインデックス位置にあるZipEntryを返す
   */
  ZipEntry getEntryByIndex(int index) throws ZipException 
  {
    return getEntryByIndex(this.nativeDataAddress,
                              index);
  }

  /**
   * Checks, if the local header of the entry at index i matches the
   * central directory, and returns the offset to the data.
   * 
   * @param entry to check.
   * @return the start offset of the (compressed) data.
   *
   * @exception IOException if a i/o error occured.
   * @exception ZipException if the local header doesn't match the 
   * central directory header
   */
  private long checkLocalHeader(ZipEntry entry) throws IOException
  {
    return checkNativeLocalHeader(this.nativeDataAddress,
                                  entry.offset,
                                  entry.getMethod(),
                                  entry.getName().length());
  }

  /**
   * Creates an input stream reading the given zip entry as
   * uncompressed data.  Normally zip entry should be an entry
   * returned by getEntry() or entries().
   *
   * This implementation returns null if the requested entry does not
   * exist.  This decision is not obviously correct, however, it does
   * appear to mirror Sun's implementation, and it is consistant with
   * their javadoc.  On the other hand, the old JCL book, 2nd Edition,
   * claims that this should return a "non-null ZIP entry".  We have
   * chosen for now ignore the old book, as modern versions of Ant (an
   * important application) depend on this behaviour.  See discussion
   * in this thread:
   * http://gcc.gnu.org/ml/java-patches/2004-q2/msg00602.html
   *
   * @param entry the entry to create an InputStream for.
   * @return the input stream, or null if the requested entry does not exist.
   *
   * @exception IllegalStateException when the ZipFile has already been closed
   * @exception IOException if a i/o error occured.
   * @exception ZipException if the Zip archive is malformed.  
   */
  public InputStream getInputStream(ZipEntry entry) throws IOException
  {
    checkClosed();

    final String name = entry.getName();
    ZipEntry zipEntry = findEntry(this.nativeDataAddress, name);
    if (zipEntry == null)
      return null;

    long start = checkLocalHeader(zipEntry);
    int method = zipEntry.getMethod();
    
    // available()が「圧縮前のファイルサイズ」を返すようにオーバーライドした
    // InputStreamのインスタンスを返す
    InputStream is = new PartialInputStream(start,
    					    zipEntry.getCompressedSize());
    switch (method)
      {
      case ZipOutputStream.STORED:
	return is;
      case ZipOutputStream.DEFLATED:
      {
	final ZipEntry ent = zipEntry;
	return new InflaterInputStream(is, new Inflater(true)) {
	  public int available() {
	    return (int) (ent.getSize() - inf.getTotalOut());
	  }
	  
	  /**
	   * read(byte[])が「すべてのデータを読み込む」ことを前提にしている
	   * アプリケーションがあるため、オーバーライドする
	   */
	  public int read(byte[] buff) throws IOException {
	    final int size = available();
	    if (size <= 0) {
		return -1;
	    }
	    int length = buff.length < size ? buff.length : size;
	    int offset = 0;
	    while (length > 0)
	    {
	    // in.read will block until some data is available.
	      int numread = read (buff, offset, length);
	      if (numread <= 0)
	        break;
	      length -= numread;
              offset += numread;
            }
            return offset;
          }
	};
      }
      default:
	throw new ZipException("Unknown compression method " + method);
      }
  }
  
  /**
   * Returns the (path) name of this zip file.
   */
  public String getName()
  {
    return name;
  }

  /**
   * Returns the number of entries in this zip file.
   *
   * @exception IllegalStateException when the ZipFile has already been closed
   */
  public int size()
  {
    checkClosed();
    return getEntryCount(this.nativeDataAddress);
  }

  /**
   * ZIPファイルをオープンする
   */
  private native int openNative(String path) throws ZipException;
  
  /**
   * ZIPファイルをクローズする
   */
  private native void closeNative(int nativeAddress) throws ZipException;

  /**
   * 指定されたインデックス位置にあるZipEntryを返す
   */
  private native ZipEntry getEntryByIndex(int nativeAddress,
                                             int index) throws ZipException;
  /**
   * 指定された名前を持つZipEntryを返す。
   *
   * @param	nativeAddress	openNative()が返した値
   * @param	name		検索対象となるファイル名
   * @param	一致するエントリ。見つからなかった場合はnull。
   * @throws	ZipException	エラーが発生した場合
   */
  private native ZipEntry findEntry(int nativeAddress,
                                    String name) throws ZipException;

  /**
   * Central Directoryの数を返す
   */
  private native int getEntryCount(int nativeAddress);
  
  /**
   * 指定されたローカルヘッダをチェックし、内容が正しければ
   * ファイルデータ開始オフセットを返す。
   *
   * @param	nativeAddress	openNative()の戻り値を指定する
   * @param	localHeaderOffset	ローカルヘッダの開始位置。
   * @param	method			Central Directoryから取得した圧縮メソッド
   * @param	nameLength		Central Directoryから取得したファイル名の長さ
   * @throws	ZipException		エラーが発生した場合
   */
  private native long checkNativeLocalHeader(int nativeAddress,
                                             int localHeaderPosition,
                                             int method,
                                             int nameLength) throws ZipException;

  /**
   * 1バイト読み込む
   */
  private native int readNative(int nativeAddress,
                                long position) throws ZipException;
  
  /**
   * データをバッファに読み込む
   */
  private native int readNative(int nativeDataAddress,
                                long position,
                                byte[] buff,
                                int off,
                                int length) throws ZipException;

  private class ZipEntryEnumeration implements Enumeration
  {
    private int index;
    
    public ZipEntryEnumeration()
    {
    }

    public boolean hasMoreElements()
    {
      return index < size();
    }

    public Object nextElement()
    {
      try
      {
	return getEntryByIndex(index++);
      }
      catch (ZipException ze)
      {
	ze.printStackTrace();
	return null;
      }
    }
  }

  private class PartialInputStream extends InputStream
  {
    long filepos, end;

    public PartialInputStream(long start, long len)
    {
      filepos = start;
      end = start + len;
    }
    
    public int available()
    {
      long amount = end - filepos;
      if (amount > Integer.MAX_VALUE)
	return Integer.MAX_VALUE;
      return (int) amount;
    }
    
    public int read() throws IOException
    {
      if (filepos >= end)
	return -1;
      return readNative(nativeDataAddress,
                        filepos++);
    }

    public int read(byte[] b, int off, int len) throws IOException
    {
      if (len > end - filepos)
	{
	  len = (int) (end - filepos);
	  if (len == 0)
	    return -1;
	}
      int count = readNative(nativeDataAddress,
                             filepos,
                             b,
                             off,
                             len);
      if (count > 0)
        filepos += len;
      return count;
    }

    public long skip(long amount)
    {
      if (amount < 0)
	throw new IllegalArgumentException();
      if (amount > end - filepos)
	amount = end - filepos;
      filepos += amount;
      return amount;
    }
  }
}
