/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine 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. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.util.zip;
import totalcross.io.*;
import totalcross.sys.*;
import totalcross.util.*;
/**
A tcz (TotalCross Zip) file has the following format:
<ul>
<li> version (2 bytes)
<li> attributes (2 bytes)
<li> base offset (header size + 4)
<li> header
<li> compressed data chunks
</ul>
The header is:
<ul>
<li> length (4 bytes)
<li> offsets array (length+1) - offset[i+1]-offset[i] gives the compressed size
<li> uncompressed sizes array (length)
<li> names array (length)
</ul>
The header is compressed to save space.
The first record is the class that implements totalcross.MainClass or extends totalcross.ui.MainWindow.
*/
public class TCZ
{
/** Set this to be the main class name. It is stored in record #0. */
public static String mainClassName; // remove dependency on DeploySettings
/** An entry of the TCZ file. */
public static class Entry
{
/** The compressed byte block. */
public byte[] bytes;
/** The name of the entry. */
public String name;
/** The size of the block when it is uncompressed. */
public int uncompressedSize;
/** Anything you want to hold here. */
public Object extra; // JavaClass for the converter
private String name2write; // guich@tc115_23
public Entry(byte []bytes, String name, int uncompressedSize) throws Exception
{
this(bytes, name, uncompressedSize, null);
}
public Entry(byte []bytes, String name, int uncompressedSize, Object extra) throws Exception
{
this.uncompressedSize = uncompressedSize;
this.bytes = bytes;
this.name = name.replace('\\','/');
this.extra = extra;
if (name.endsWith(".class"))
name = name.substring(0,name.length()-6);
name2write = name;
if (name2write.indexOf('.') < 0) // no dots on string? note that an image will have a dot, so its correctly kept with / on the name
name2write = name2write.replace('/','.'); // assuming not ending with .class
}
/** Returns a String representing this Entry. Used in the Vector.qsort method */
public String toString()
{
if (mainClassName != null && name.equals(mainClassName))
return '\1'+name2write; // make sure this name will go first when sorting
return name2write;
}
}
public static final short TCZ_VERSION = 200; // must sync with tcz.h
/** Defines that the tcz file has a MainClass at the first record. If false, it is a library-only module */
public static final short ATTR_HAS_MAINCLASS = 1;
/** Defines that the tcz file has a MainWindow at the first record. */
public static final short ATTR_HAS_MAINWINDOW = 2;
/** Defines that the tcz file is a library-only module. */
public static final short ATTR_LIBRARY = 4;
/** Defines that the application uses the new font set. */
//public static final short ATTR_NEW_FONT_SET = 8;
/** Defines that the application has resizable window. */
public static final short ATTR_RESIZABLE_WINDOW = 16;
/** Defines that the application uses the default font. */
public static final short ATTR_WINDOWFONT_DEFAULT = 32;
/** Defines that the application uses the given window size. */
public static final short ATTR_WINDOWSIZE_320X480 = 64;
/** Defines that the application uses the given window size. */
public static final short ATTR_WINDOWSIZE_480X640 = 128;
/** Defines that the application uses the given window size. */
public static final short ATTR_WINDOWSIZE_600X800 = 256;
/** The names of the files. */
public String[] names;
/** The attributes.
* @see #ATTR_HAS_MAINCLASS
* @see #ATTR_HAS_MAINWINDOW
* @see #ATTR_LIBRARY
* @see #ATTR_RESIZABLE_WINDOW
*/
public int attr;
/** Version of the tcz file.
* @see #TCZ_VERSION
*/
public int version;
/** Offsets to the compressed data chunks */
public int[] offsets;
/** Sizes of the data chunks when they are uncompressed. */
public int[] uncompressedSizes;
/** The number of chunks. */
public int numberOfChunks;
/** Stores the total size when a TCZ file is created. */
public int size;
/** Bag that can be used to store anything that the user wants. */
public Object bag;
private int idx;
private Stream in;
/** Create a TCZ file with the given vector of Entry(s).
* @throws IOException
* @throws ZipException */
public TCZ(Vector vout, String outName, short attr) throws IOException, ZipException
{
if (!outName.toLowerCase().endsWith(".tcz"))
outName = outName.concat(".tcz");
// creates an empty file for output
String path = Convert.getFilePath(outName);
if (path != null) try {new totalcross.io.File(path).createDir();} catch (totalcross.io.IOException e) {}
File fout = new File(outName, File.CREATE_EMPTY);
vout.qsort(); // sort the files
// now we process the files.
int n = vout.size();
// first pass, we setup the names and offset arrays
offsets = new int[n+1]; // first offset is 0
names = new String[n];
uncompressedSizes = new int[n];
int ofs = 0;
for (int i =0; i < n; i++)
{
Entry of = (Entry)vout.items[i];
names[i] = of.name2write;
ofs += of.bytes.length;
offsets[i+1] = ofs;
uncompressedSizes[i] = of.uncompressedSize;
}
// prepare the header
ByteArrayStream header = new ByteArrayStream(4096);
DataStreamLE dsh = new DataStreamLE(header);
dsh.writeInt(n);
for (int i = 0; i <= n; i++)
dsh.writeInt(offsets[i]); // the offsets are stored first to ensure alignment.
for (int i = 0; i < n; i++)
dsh.writeInt(uncompressedSizes[i]); // idem
for (int i = 0; i < n; i++) // strings are stored as UTF8
{
String s = names[i];
int l = s.length();
if (l > 255) throw new ZipException("Error: Name too long: "+s+". ("+l+" chars). Maximum allowed: 255 chars");
dsh.writeSmallString(s);
}
// write the first part: the version, the attribute and the compressed header
DataStreamLE dsf = new DataStreamLE(fout);
size += dsf.writeShort(TCZ_VERSION);
size += dsf.writeShort(attr);
ByteArrayStream bc = new ByteArrayStream(2048);
header.mark();
ZLib.deflate(header, bc, 9); // the smaller the compression level, the slower is the vm's startup
size += dsf.writeInt(bc.getPos()+8); // base offset = compressed header size + 4
size += dsf.writeBytes(bc.getBuffer(), 0, bc.getPos());
// now write the compressed chunks
for (int i = 0; i < n; i++)
size += dsf.writeBytes(((Entry)vout.items[i]).bytes);
fout.close();
}
/** Reads a TCZ file and fill the public members available in this class: version, attr,
* baseOffset, offsets, uncompressedSizes, names, chunks, numberOfChunks.
* To preserve memory, you must request each chunk using readNextChunk <b>right after</b>
* you call this constructor.
* @throws IOException
*/
public TCZ(Stream fin) throws IOException
{
this.in = fin;
int baseOffset;
DataStreamLE ds = new DataStreamLE(fin);
this.version = ds.readShort();
this.attr = ds.readShort();
baseOffset = ds.readInt();
ByteArrayStream bas = new ByteArrayStream(baseOffset-8);
DataStreamLE dsbas = new DataStreamLE(bas);
ZLib.inflate(ds, bas, baseOffset-8);
bas.mark();
int n = numberOfChunks = dsbas.readInt();
offsets = new int[n+1];
uncompressedSizes = new int[n];
for (int i = 0; i <= n; i++)
offsets[i] = baseOffset + dsbas.readInt();
for (int i = 0; i < n; i++)
uncompressedSizes[i] = dsbas.readInt();
names = new String[n];
for (int i = 0; i < n; i++)
names[i] = dsbas.readSmallString();
}
/** Returns the size of the next available chunk */
public int getNextChunkSize()
{
return uncompressedSizes[idx];
}
/** Fills the given stream with the next available chunk.
* The size of the chunk can be retrieved with getNextChunkSize.
* If out is a ByteArrayStream, don't forget to call reset before starting to read from it!
* @throws IOException
*/
public void readNextChunk(Stream out) throws IOException
{
int s = offsets[idx+1]-offsets[idx];
idx++;
ZLib.inflate(in, out, s);
}
/** Finds the position of the given name in this tcz. */
public int findNamePosition(String name)
{
String[] names = this.names;
if (names[0].equals(name)) // is it the first file (extends MainClass)?
return 0;
else
{
int inf = 1, sup = names.length - 1, half, res;
while (inf <= sup)
{
half = (inf + sup) >> 1;
res = name.compareTo(names[half]);
if (res == 0)
return half;
if (res < 0)
sup = half - 1;
else
inf = half + 1;
}
return -1;
}
}
}