/* 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 com.classpath.zip;

import java.io.*;
import java.util.*;

import com.vmx.*;
import com.one.*;
import com.one.file.Connector;
import com.one.file.FileConnection;

/**
 * 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 extends ArchiveContainer
{
	/**
	 * Our Zip version is hard coded to 1.0 resp. 2.0
	 */
	public final static int ZIP_STORED_VERSION = 10;
	public final static int ZIP_DEFLATED_VERSION = 20;

	/**
	 * Compression method.  This method doesn't compress at all.
	 */
	public final static int STORED = 0;
	
	/**
	 * Compression method.  This method uses the Deflater.
	 */
	public final static int DEFLATED = 8;
	
	private static int complevel = Deflater.BEST_COMPRESSION;
	
	public static int getCompressionLevel()
	{
		return complevel;
	}

	public static void setCompressionLevel(int level)
	{
		if(level < Deflater.NO_COMPRESSION)
		{
			complevel = Deflater.NO_COMPRESSION;
		}
		else if(level > Deflater.BEST_COMPRESSION)
		{
			complevel = Deflater.BEST_COMPRESSION;
		}
		else
		{
			complevel = level;
		}
	}
	
	private CentralDirectoryEndRecord cder = new CentralDirectoryEndRecord();

	/*
	public void checkFormat() throws IOException
	{
		boolean valid = false;

		try
		{
			byte[] buf = new byte[4];
			rais.read(buf);
			int sig = buf[0] & 0xFF | ((buf[1] & 0xFF) << 8) | ((buf[2] & 0xFF) << 16) | ((buf[3] & 0xFF) << 24);
			valid = (sig == LOCSIG) || (sig == ENDSIG);
		}
		catch(IOException _)
		{
		}

		if(!valid)
		{
			try
			{
				rais.close();
			}
			catch(IOException _)
			{
			}
			
			throw new ZipException("Not a valid zip file");
		}
	}
	*/

	protected void readEntries() throws IOException
	{
		try
		{
			readCentralDirectory();
		}
		catch(IOException e1)
		{
			checkReadOnly();

			finished = false;
			
			try
			{
				readLocalHeaders();
			}
			catch(IOException e2)
			{
				throw new IOException(e1.toString() + ", " + e2.toString());
			}
		}
	}
	
	/**
	 * Read the central directory of a zip file and fill the entries
	 * array.  This is called exactly once when first needed. It is called
	 * while holding the lock on <code>rais</code>.
	 *
	 * @exception IOException if a i/o error occured.
	 * @exception ZipException if the central directory is malformed 
	 */
	protected void readCentralDirectory() throws IOException
	{
		/* 
		 * Search for the End Of Central Directory.  When a zip comment is 
		 * present the directory may start earlier.
		 * Note that a comment has a maximum length of 64K, so that is the
		 * maximum we search backwards.
		 */
		
		rais.setPosition(0);
		BufDataInputStream inp = new BufDataInputStream(rais);
		 
		int pos = rais.getCapacity() - ZipConstants.ENDHDR;
		int top = Math.max(0, pos - 0xFFFF);
		
		inp.setPosition(pos);
		
		if(!cder.read(inp))
		{
			byte[] sig = new byte[4];
			sig[0] = (byte)(ZipConstants.ENDSIG & 0xFF);
			sig[1] = (byte)((ZipConstants.ENDSIG >> 8) & 0xFF);
			sig[2] = (byte)((ZipConstants.ENDSIG >> 16) & 0xFF);
			sig[3] = (byte)((ZipConstants.ENDSIG >> 24) & 0xFF);
			
			pos = inp.indexOf(sig, 0, 4, top, pos);
			
			if(pos >= 0)
			{
				inp.setPosition(pos);
				cder.read(inp);
			}
			else
			{
				entries = new Hashtable();
				throw new ZipException("Central Directory not found, probably not a zip file");
			}
		}
		
		entries = new Hashtable(cder.numEntries + cder.numEntries / 2 + 1);
		ZipEntry entry;
		
		inp.setPosition(cder.centralOffset);
		
		for(int i = 0; i < cder.numEntries; i++)
		{
			entry = new ZipEntry();
			
			if(!entry.readCentralHeader(inp))
			{
				throw new ZipException("Wrong Central Directory signature");
			}
			
			entries.put(entry.getName(), entry);
			addFile(entry.getName());
		}
	}

	/**
	 * If for some reason Central Directory is either corrupted or not present at all,
	 * we can still try to recover files throug local headers.
	 */
	protected void readLocalHeaders() throws IOException
	{
		rais.setPosition(0);
		BufDataInputStream inp = new BufDataInputStream(rais);

		byte[] sig = new byte[4];
		sig[0] = (byte)(ZipConstants.LOCSIG & 0xFF);
		sig[1] = (byte)((ZipConstants.LOCSIG >> 8) & 0xFF);
		sig[2] = (byte)((ZipConstants.LOCSIG >> 16) & 0xFF);
		sig[3] = (byte)((ZipConstants.LOCSIG >> 24) & 0xFF);

		entries = new Hashtable();
		ZipEntry entry;
		int pos;

		while(true)
		{
			pos = inp.indexOf(sig, 0, 4, -1, -1);

			if(pos >= 0)
			{
				inp.setPosition(pos);

				entry = new ZipEntry();
				entry.readLocalHeader(inp);
				entry.offset = pos;

				entries.put(entry.getName(), entry);
				addFile(entry.getName());
			}
			else
			{
				sig[0] = (byte)(ZipConstants.ENDSIG & 0xFF);
				sig[1] = (byte)((ZipConstants.ENDSIG >> 8) & 0xFF);
				sig[2] = (byte)((ZipConstants.ENDSIG >> 16) & 0xFF);
				sig[3] = (byte)((ZipConstants.ENDSIG >> 24) & 0xFF);

				pos = inp.indexOf(sig, 0, 4, -1, -1);

				if(pos >= 0)
				{
					cder.centralOffset = pos;
				}
				else
				{
					cder.centralOffset = (int)file.fileSize();
				}
				
				break;
			}
		}
	}
	
	/**
	 * Add a new entry to the current zip archive.
	 * If entry with such name already exists,
	 * it is deleted and then new entry is added.
	 * If source stream is null, then an empty entry is written.
	 * The file data is entirely copied from specified input stream,
	 * compressed with DEFTATED method and specified compression level,
	 * and zip entry with specified name is written into the file,
	 * overwriting central directory (if present).
	 * Therefore zip file is no longer valid until finish() is called
	 * and correct central directory is restored at it's end.
	 */
	protected ArchiveEntry createActualEntry(RandomAccessInputStream source, String name, long time, ProgressCallback callback) throws IOException
	{
		ZipEntry entry = new ZipEntry(name);

		entry.setLastModified(time);
		entry.setMethod(DEFLATED);
		entry.flags = 8;
		entry.offset = cder.centralOffset;

		FileConnection fc = null;

		if(virtualizeToRAM)
		{
			try
			{
				fc = new ByteArrayFileConnection(file.getPath(), -1, -1);
			}
			catch(Throwable t)
			{
			}
		}

		if(fc == null)
		{
			fc = (FileConnection)Connector.open("file:///" + Connector.createTempFile(file.getPath(), false));
			fc.create();
		}

		int repstart = cder.centralOffset;

		OutputStream os = fc.openOutputStream(); // cder.centralOffset);

		/* Write the local file header */
		int delta = entry.writeLocalHeader(os);

		cder.centralOffset += delta;

		CRC32 crc = new CRC32();
		DeflaterOutputStream dos = new DeflaterOutputStream(os, new Deflater(complevel, true));
		int size = 0;

		if(source != null)
		{
			byte[] buf = new byte[AuxClass.ARCBUFSIZE];
			int len;

			if(callback != null)
			{
				while(source.available() > 0)
				{
					len = source.read(buf);

					dos.write(buf, 0, len);
					crc.update(buf, 0, len);

					size += len;
					callback.progress(len);
				}
			}
			else
			{
				while(source.available() > 0)
				{
					len = source.read(buf);

					dos.write(buf, 0, len);
					crc.update(buf, 0, len);

					size += len;
				}
			}

			source.close();
		}

		dos.finish();

		int csize = dos.getTotalOut();

		cder.centralOffset += csize;

		entry.setSize(size);
		entry.setCompressedSize(csize);
		entry.setCRC((int)crc.getValue());

		/* Now write the data descriptor entry if needed. */
		if((entry.flags & 8) != 0)
		{
			AuxClass.writeLeInt(os, ZipConstants.EXTSIG);

			AuxClass.writeLeInt(os, entry.getCRC());
			AuxClass.writeLeInt(os, entry.getCompressedSize());
			AuxClass.writeLeInt(os, entry.getSize());

			cder.centralOffset += ZipConstants.EXTHDR;
		}

		dos.close();

		rais = new ReplaceableInputStream(rais);
		((ReplaceableInputStream)rais).setReplace(new FileInputStream(fc), repstart, repstart);

		return entry;
	}
	
	/**
	 * Delete the specified entry from the current zip archive.
	 * It also totally truncates central directory,
	 * so finish() must be called to restore this zip file valid.
	 */
	protected void deleteActualEntry(ArchiveEntry entry) throws IOException
	{
		ZipEntry zipentry = (ZipEntry)entry;

		rais = new ReplaceableInputStream(rais);

		int delta = ((ReplaceableInputStream)rais).setReplace(new byte[0], zipentry.offset, zipentry.offset + zipentry.getLocalSize());

		cder.centralOffset += delta;
		updateOffset(zipentry.offset, delta);
	}
	
	/**
	 * Rename specified entry.
	 */
	protected void renameActualEntry(ArchiveEntry entry, String newName) throws IOException
	{
		ZipEntry zipentry = (ZipEntry)entry;

		byte[] b = StringEncoder.encodeString(newName, StringEncoder.ENC_ARCHIVE);
		byte[] r = new byte[4 + b.length];

		rais.setPosition(zipentry.offset + ZipConstants.LOCEXT);
		rais.read(r, 2, 2);

		int len = b.length;

		r[0] = (byte)(len & 0xFF);
		r[1] = (byte)(len >>> 8);

		System.arraycopy(b, 0, r, 4, b.length);

		rais = new ReplaceableInputStream(rais);

		int delta = ((ReplaceableInputStream)rais).setReplace(r, zipentry.offset + ZipConstants.LOCNAM, zipentry.offset + ZipConstants.LOCNAM + 4 + StringEncoder.getEncodedLength(zipentry.getName(), StringEncoder.ENC_ARCHIVE));
		
		cder.centralOffset += delta;
		updateOffset(zipentry.offset, delta);
		
		entry.setName(newName);
	}
	
	/**
	 * Write central directory at the end of file.
	 */
	public void finish() throws IOException
	{
		if(finished)
		{
			return;
		}
		
		checkReadOnly();

		ProgressCallback callback = ProgressBar.getProgressCallback();
		ProgressBar.show();

		callback.setText(Locale.getString(super.getClass().getName(), Locale.WRITING_FILE, file.getName()));
		callback.setPercentMode(true);
		
		Enumeration enm = entries();
		ZipEntry entry;
		
		cder.centralSize = 0;
		cder.numEntries = 0;
		
		if(rais instanceof ReplaceableInputStream)
		{
			file = AuxClass.updateFileData(file, (ReplaceableInputStream)rais, callback);
		}
		else
		{
			rais.close();
		}

		file = AuxClass.getWriteableFileConnection(file);
		file.truncate(cder.centralOffset);
		OutputStream os = file.openOutputStream(cder.centralOffset);

		callback.setProgress(0);
		callback.setMax(size());
		callback.setPercentMode(false);
		
		while(enm.hasMoreElements())
		{
			entry = (ZipEntry)enm.nextElement();
			
			cder.centralSize += entry.writeCentralHeader(os);
			cder.numEntries++;

			callback.progress(1);
		}
		
		cder.write(os);
		
		os.flush();
		os.close();
		
		finished = true;

		rais = new FileInputStream(file);

		callback.setPercentMode(true);
		ProgressBar.hide();
	}

	/**
	 * 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(ArchiveEntry entry) throws IOException
	{
		if(entry == null)
		{
			return null;
		}

		checkClosed();

		ZipEntry zipentry = (ZipEntry)entry;
		ZipEntry readEntry = new ZipEntry();
		
		rais.setPosition(zipentry.offset);
		
		if(!readEntry.readLocalHeader(rais))
		{
			throw new ZipException("Wrong Local Header signature");
		}

		PartialInputStream inp = new PartialInputStream(rais, rais.getPosition(), zipentry.getCompressedSize());

		switch(zipentry.getMethod())
		{
			case STORED:
				return inp;
			
			case DEFLATED:
				inp.addDummyByte();
				final Inflater inf = new Inflater(true);
				final int sz = zipentry.getSize();
				
				return new InflaterInputStream(inp, inf)
				{
					public int available() throws IOException
					{
						if(sz == -1)
						{
							return super.available();
						}
						
						if(super.available() != 0)
						{
							return sz - inf.getTotalOut();
						}
						
						return 0;
					}
				};
			
			default:
				throw new ZipException("Unknown compression method " + zipentry.getMethod());
		}
	}
	
//	private static void out(String s)
//	{
//		System.out.println("[ZipFile] " + s);
//	}
}
