    001/*
    002 *  Licensed to the Apache Software Foundation (ASF) under one or more
    003 *  contributor license agreements.  See the NOTICE file distributed with
    004 *  this work for additional information regarding copyright ownership.
    005 *  The ASF licenses this file to You under the Apache License, Version 2.0
    006 *  (the "License"); you may not use this file except in compliance with
    007 *  the License.  You may obtain a copy of the License at
    008 *
    009 *     http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 *  Unless required by applicable law or agreed to in writing, software
    012 *  distributed under the License is distributed on an "AS IS" BASIS,
    013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 *  See the License for the specific language governing permissions and
    015 *  limitations under the License.
    016 */
    017package org.apache.commons.compress.harmony.unpack200;
    018
    019import java.io.BufferedInputStream;
    020import java.io.ByteArrayInputStream;
    021import java.io.ByteArrayOutputStream;
    022import java.io.DataOutputStream;
    023import java.io.IOException;
    024import java.io.InputStream;
    025import java.io.OutputStream;
    026import java.io.OutputStreamWriter;
    027import java.io.PrintWriter;
    028import java.nio.charset.Charset;
    029import java.util.ArrayList;
    030import java.util.HashSet;
    031import java.util.List;
    032import java.util.Set;
    033import java.util.TimeZone;
    034import java.util.jar.JarEntry;
    035import java.util.jar.JarOutputStream;
    036import java.util.zip.CRC32;
    037import java.util.zip.GZIPInputStream;
    038import java.util.zip.ZipEntry;
    039
    040import org.apache.commons.compress.harmony.pack200.Codec;
    041import org.apache.commons.compress.harmony.pack200.Pack200Exception;
    042import org.apache.commons.compress.harmony.unpack200.bytecode.Attribute;
    043import org.apache.commons.compress.harmony.unpack200.bytecode.CPClass;
    044import org.apache.commons.compress.harmony.unpack200.bytecode.CPField;
    045import org.apache.commons.compress.harmony.unpack200.bytecode.CPMethod;
    046import org.apache.commons.compress.harmony.unpack200.bytecode.CPUTF8;
    047import org.apache.commons.compress.harmony.unpack200.bytecode.ClassConstantPool;
    048import org.apache.commons.compress.harmony.unpack200.bytecode.ClassFile;
    049import org.apache.commons.compress.harmony.unpack200.bytecode.ClassFileEntry;
    050import org.apache.commons.compress.harmony.unpack200.bytecode.InnerClassesAttribute;
    051import org.apache.commons.compress.harmony.unpack200.bytecode.SourceFileAttribute;
    052import org.apache.commons.io.input.BoundedInputStream;
    053
    054/**
    055 * A Pack200 archive consists of one or more segments. Each segment is stand-alone, in the sense that every segment has the magic number header; thus, every
    056 * segment is also a valid archive. However, it is possible to combine (non-GZipped) archives into a single large archive by concatenation alone. Thus, all the
    057 * hard work in unpacking an archive falls to understanding a segment.
    058 *
    059 * The first component of a segment is the header; this contains (amongst other things) the expected counts of constant pool entries, which in turn defines how
    060 * many values need to be read from the stream. Because values are variable width (see {@link Codec}), it is not possible to calculate the start of the next
    061 * segment, although one of the header values does hint at the size of the segment if non-zero, which can be used for buffering purposes.
    062 *
    063 * Note that this does not perform any buffering of the input stream; each value will be read on a byte-by-byte basis. It does not perform GZip decompression
    064 * automatically; both of these are expected to be done by the caller if the stream has the magic header for GZip streams ({@link GZIPInputStream#GZIP_MAGIC}).
    065 * In any case, if GZip decompression is being performed the input stream will be buffered at a higher level, and thus this can read on a byte-oriented basis.
    066 */
    067public class Segment {
    068
    069    public static final int LOG_LEVEL_VERBOSE = 2;
    070
    071    public static final int LOG_LEVEL_STANDARD = 1;
    072
    073    public static final int LOG_LEVEL_QUIET = 0;
    074
    075    private SegmentHeader header;
    076
    077    private CpBands cpBands;
    078
    079    private AttrDefinitionBands attrDefinitionBands;
    080
    081    private IcBands icBands;
    082
    083    private ClassBands classBands;
    084
    085    private BcBands bcBands;
    086
    087    private FileBands fileBands;
    088
    089    private boolean overrideDeflateHint;
    090
    091    private boolean deflateHint;
    092
    093    private boolean doPreRead;
    094
    095    private int logLevel;
    096
    097    private PrintWriter logStream;
    098
    099    private byte[][] classFilesContents;
    100
    101    private boolean[] fileDeflate;
    102
    103    private boolean[] fileIsClass;
    104
    105    private InputStream internalBuffer;
    106
    107    private ClassFile buildClassFile(final int classNum) {
    108        final ClassFile classFile = new ClassFile();
    109        final int[] major = classBands.getClassVersionMajor();
    110        final int[] minor = classBands.getClassVersionMinor();
    111        if (major != null) {
    112            classFile.major = major[classNum];
    113            classFile.minor = minor[classNum];
    114        } else {
    115            classFile.major = header.getDefaultClassMajorVersion();
    116            classFile.minor = header.getDefaultClassMinorVersion();
    117        }
    118        // build constant pool
    119        final ClassConstantPool cp = classFile.pool;
    120        final int fullNameIndexInCpClass = classBands.getClassThisInts()[classNum];
    121        final String fullName = cpBands.getCpClass()[fullNameIndexInCpClass];
    122        // SourceFile attribute
    123        int i = fullName.lastIndexOf("/") + 1; // if lastIndexOf==-1, then
    124        // -1+1=0, so str.substring(0)
    125        // == str
    126
    127        // Get the source file attribute
    128        final List<Attribute> classAttributes = classBands.getClassAttributes()[classNum];
    129        SourceFileAttribute sourceFileAttribute = null;
    130        for (final Attribute classAttribute : classAttributes) {
    131            if (classAttribute.isSourceFileAttribute()) {
    132                sourceFileAttribute = (SourceFileAttribute) classAttribute;
    133            }
    134        }
    135
    136        if (sourceFileAttribute == null) {
    137            // If we don't have a source file attribute yet, we need
    138            // to infer it from the class.
    139            final AttributeLayout SOURCE_FILE = attrDefinitionBands.getAttributeDefinitionMap().getAttributeLayout(AttributeLayout.ATTRIBUTE_SOURCE_FILE,
    140                    AttributeLayout.CONTEXT_CLASS);
    141            if (SOURCE_FILE.matches(classBands.getRawClassFlags()[classNum])) {
    142                int firstDollar = -1;
    143                for (int index = 0; index < fullName.length(); index++) {
    144                    if (fullName.charAt(index) <= '$') {
    145                        firstDollar = index;
    146                    }
    147                }
    148                String fileName;
    149
    150                if (firstDollar > -1 && i <= firstDollar) {
    151                    fileName = fullName.substring(i, firstDollar) + ".java";
    152                } else {
    153                    fileName = fullName.substring(i) + ".java";
    154                }
    155                sourceFileAttribute = new SourceFileAttribute(cpBands.cpUTF8Value(fileName, false));
    156                classFile.attributes = new Attribute[] { (Attribute) cp.add(sourceFileAttribute) };
    157            } else {
    158                classFile.attributes = new Attribute[] {};
    159            }
    160        } else {
    161            classFile.attributes = new Attribute[] { (Attribute) cp.add(sourceFileAttribute) };
    162        }
    163
    164        // If we see any class attributes, add them to the class's attributes
    165        // that will
    166        // be written out. Keep SourceFileAttributes out since we just
    167        // did them above.
    168        final List<Attribute> classAttributesWithoutSourceFileAttribute = new ArrayList<>(classAttributes.size());
    169        for (int index = 0; index < classAttributes.size(); index++) {
    170            final Attribute attrib = classAttributes.get(index);
    171            if (!attrib.isSourceFileAttribute()) {
    172                classAttributesWithoutSourceFileAttribute.add(attrib);
    173            }
    174        }
    175        final Attribute[] originalAttributes = classFile.attributes;
    176        classFile.attributes = new Attribute[originalAttributes.length + classAttributesWithoutSourceFileAttribute.size()];
    177        System.arraycopy(originalAttributes, 0, classFile.attributes, 0, originalAttributes.length);
    178        for (int index = 0; index < classAttributesWithoutSourceFileAttribute.size(); index++) {
    179            final Attribute attrib = classAttributesWithoutSourceFileAttribute.get(index);
    180            cp.add(attrib);
    181            classFile.attributes[originalAttributes.length + index] = attrib;
    182        }
    183
    184        // this/superclass
    185        final ClassFileEntry cfThis = cp.add(cpBands.cpClassValue(fullNameIndexInCpClass));
    186        final ClassFileEntry cfSuper = cp.add(cpBands.cpClassValue(classBands.getClassSuperInts()[classNum]));
    187        // add interfaces
    188        final ClassFileEntry[] cfInterfaces = new ClassFileEntry[classBands.getClassInterfacesInts()[classNum].length];
    189        for (i = 0; i < cfInterfaces.length; i++) {
    190            cfInterfaces[i] = cp.add(cpBands.cpClassValue(classBands.getClassInterfacesInts()[classNum][i]));
    191        }
    192        // add fields
    193        final ClassFileEntry[] cfFields = new ClassFileEntry[classBands.getClassFieldCount()[classNum]];
    194        // fieldDescr and fieldFlags used to create this
    195        for (i = 0; i < cfFields.length; i++) {
    196            final int descriptorIndex = classBands.getFieldDescrInts()[classNum][i];
    197            final int nameIndex = cpBands.getCpDescriptorNameInts()[descriptorIndex];
    198            final int typeIndex = cpBands.getCpDescriptorTypeInts()[descriptorIndex];
    199            final CPUTF8 name = cpBands.cpUTF8Value(nameIndex);
    200            final CPUTF8 descriptor = cpBands.cpSignatureValue(typeIndex);
    201            cfFields[i] = cp.add(new CPField(name, descriptor, classBands.getFieldFlags()[classNum][i], classBands.getFieldAttributes()[classNum][i]));
    202        }
    203        // add methods
    204        final ClassFileEntry[] cfMethods = new ClassFileEntry[classBands.getClassMethodCount()[classNum]];
    205        // methodDescr and methodFlags used to create this
    206        for (i = 0; i < cfMethods.length; i++) {
    207            final int descriptorIndex = classBands.getMethodDescrInts()[classNum][i];
    208            final int nameIndex = cpBands.getCpDescriptorNameInts()[descriptorIndex];
    209            final int typeIndex = cpBands.getCpDescriptorTypeInts()[descriptorIndex];
    210            final CPUTF8 name = cpBands.cpUTF8Value(nameIndex);
    211            final CPUTF8 descriptor = cpBands.cpSignatureValue(typeIndex);
    212            cfMethods[i] = cp.add(new CPMethod(name, descriptor, classBands.getMethodFlags()[classNum][i], classBands.getMethodAttributes()[classNum][i]));
    213        }
    214        cp.addNestedEntries();
    215
    216        // add inner class attribute (if required)
    217        boolean addInnerClassesAttr = false;
    218        final IcTuple[] icLocal = getClassBands().getIcLocal()[classNum];
    219        final boolean icLocalSent = icLocal != null;
    220        final InnerClassesAttribute innerClassesAttribute = new InnerClassesAttribute("InnerClasses");
    221        final IcTuple[] icRelevant = getIcBands().getRelevantIcTuples(fullName, cp);
    222        final List<IcTuple> ic_stored = computeIcStored(icLocal, icRelevant);
    223        for (final IcTuple icStored : ic_stored) {
    224            final int innerClassIndex = icStored.thisClassIndex();
    225            final int outerClassIndex = icStored.outerClassIndex();
    226            final int simpleClassNameIndex = icStored.simpleClassNameIndex();
    227
    228            final String innerClassString = icStored.thisClassString();
    229            final String outerClassString = icStored.outerClassString();
    230            final String simpleClassName = icStored.simpleClassName();
    231
    232            CPUTF8 innerName = null;
    233            CPClass outerClass = null;
    234
    235            final CPClass innerClass = innerClassIndex != -1 ? cpBands.cpClassValue(innerClassIndex) : cpBands.cpClassValue(innerClassString);
    236            if (!icStored.isAnonymous()) {
    237                innerName = simpleClassNameIndex != -1 ? cpBands.cpUTF8Value(simpleClassNameIndex) : cpBands.cpUTF8Value(simpleClassName);
    238            }
    239
    240            if (icStored.isMember()) {
    241                outerClass = outerClassIndex != -1 ? cpBands.cpClassValue(outerClassIndex) : cpBands.cpClassValue(outerClassString);
    242            }
    243            final int flags = icStored.F;
    244            innerClassesAttribute.addInnerClassesEntry(innerClass, outerClass, innerName, flags);
    245            addInnerClassesAttr = true;
    246        }
    247        // If ic_local is sent, and it's empty, don't add
    248        // the inner classes attribute.
    249        if (icLocalSent && icLocal.length == 0) {
    250            addInnerClassesAttr = false;
    251        }
    252
    253        // If ic_local is not sent and ic_relevant is empty,
    254        // don't add the inner class attribute.
    255        if (!icLocalSent && icRelevant.length == 0) {
    256            addInnerClassesAttr = false;
    257        }
    258
    259        if (addInnerClassesAttr) {
    260            // Need to add the InnerClasses attribute to the
    261            // existing classFile attributes.
    262            final Attribute[] originalAttrs = classFile.attributes;
    263            final Attribute[] newAttrs = new Attribute[originalAttrs.length + 1];
    264            System.arraycopy(originalAttrs, 0, newAttrs, 0, originalAttrs.length);
    265            newAttrs[newAttrs.length - 1] = innerClassesAttribute;
    266            classFile.attributes = newAttrs;
    267            cp.addWithNestedEntries(innerClassesAttribute);
    268        }
    269        // sort CP according to cp_All
    270        cp.resolve(this);
    271        // NOTE the indexOf is only valid after the cp.resolve()
    272        // build up remainder of file
    273        classFile.accessFlags = (int) classBands.getClassFlags()[classNum];
    274        classFile.thisClass = cp.indexOf(cfThis);
    275        classFile.superClass = cp.indexOf(cfSuper);
    276        // TODO placate format of file for writing purposes
    277        classFile.interfaces = new int[cfInterfaces.length];
    278        for (i = 0; i < cfInterfaces.length; i++) {
    279            classFile.interfaces[i] = cp.indexOf(cfInterfaces[i]);
    280        }
    281        classFile.fields = cfFields;
    282        classFile.methods = cfMethods;
    283        return classFile;
    284    }
    285
    286    /**
    287     * Given an ic_local and an ic_relevant, use them to calculate what should be added as ic_stored.
    288     *
    289     * @param icLocal    IcTuple[] array of local transmitted tuples
    290     * @param icRelevant IcTuple[] array of relevant tuples
    291     * @return List of tuples to be stored. If ic_local is null or empty, the values returned may not be correct. The caller will have to determine if this is
    292     *         the case.
    293     */
    294    private List<IcTuple> computeIcStored(final IcTuple[] icLocal, final IcTuple[] icRelevant) {
    295        final List<IcTuple> result = new ArrayList<>(icRelevant.length);
    296        final List<IcTuple> duplicates = new ArrayList<>(icRelevant.length);
    297        final Set<IcTuple> isInResult = new HashSet<>(icRelevant.length);
    298
    299        // need to compute:
    300        // result = ic_local XOR ic_relevant
    301
    302        // add ic_local
    303        if (icLocal != null) {
    304            for (final IcTuple element : icLocal) {
    305                if (isInResult.add(element)) {
    306                    result.add(element);
    307                }
    308            }
    309        }
    310
    311        // add ic_relevant
    312        for (final IcTuple element : icRelevant) {
    313            if (isInResult.add(element)) {
    314                result.add(element);
    315            } else {
    316                duplicates.add(element);
    317            }
    318        }
    319
    320        // eliminate "duplicates"
    321        duplicates.forEach(result::remove);
    322
    323        return result;
    324    }
    325
    326    protected AttrDefinitionBands getAttrDefinitionBands() {
    327        return attrDefinitionBands;
    328    }
    329
    330    protected ClassBands getClassBands() {
    331        return classBands;
    332    }
    333
    334    public SegmentConstantPool getConstantPool() {
    335        return cpBands.getConstantPool();
    336    }
    337
    338    protected CpBands getCpBands() {
    339        return cpBands;
    340    }
    341
    342    protected IcBands getIcBands() {
    343        return icBands;
    344    }
    345
    346    public SegmentHeader getSegmentHeader() {
    347        return header;
    348    }
    349
    350    public void log(final int logLevel, final String message) {
    351        if (this.logLevel >= logLevel) {
    352            logStream.println(message);
    353        }
    354    }
    355
    356    /**
    357     * Override the archive's deflate hint with the given boolean
    358     *
    359     * @param deflateHint the deflate hint to use
    360     */
    361    public void overrideDeflateHint(final boolean deflateHint) {
    362        this.overrideDeflateHint = true;
    363        this.deflateHint = deflateHint;
    364    }
    365
    366    /**
    367     * This performs the actual work of parsing against a non-static instance of Segment. This method is intended to run concurrently for multiple segments.
    368     *
    369     * @throws IOException      if a problem occurs during reading from the underlying stream
    370     * @throws Pack200Exception if a problem occurs with an unexpected value or unsupported codec
    371     */
    372    private void parseSegment() throws IOException, Pack200Exception {
    373
    374        header.unpack();
    375        cpBands.unpack();
    376        attrDefinitionBands.unpack();
    377        icBands.unpack();
    378        classBands.unpack();
    379        bcBands.unpack();
    380        fileBands.unpack();
    381
    382        int classNum = 0;
    383        final int numberOfFiles = header.getNumberOfFiles();
    384        final String[] fileName = fileBands.getFileName();
    385        final int[] fileOptions = fileBands.getFileOptions();
    386        final SegmentOptions options = header.getOptions();
    387
    388        classFilesContents = new byte[numberOfFiles][];
    389        fileDeflate = new boolean[numberOfFiles];
    390        fileIsClass = new boolean[numberOfFiles];
    391
    392        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    393        final DataOutputStream dos = new DataOutputStream(bos);
    394
    395        for (int i = 0; i < numberOfFiles; i++) {
    396            String name = fileName[i];
    397
    398            final boolean nameIsEmpty = name == null || name.isEmpty();
    399            final boolean isClass = (fileOptions[i] & 2) == 2 || nameIsEmpty;
    400            if (isClass && nameIsEmpty) {
    401                name = cpBands.getCpClass()[classBands.getClassThisInts()[classNum]] + ".class";
    402                fileName[i] = name;
    403            }
    404
    405            if (!overrideDeflateHint) {
    406                fileDeflate[i] = (fileOptions[i] & 1) == 1 || options.shouldDeflate();
    407            } else {
    408                fileDeflate[i] = deflateHint;
    409            }
    410
    411            fileIsClass[i] = isClass;
    412
    413            if (isClass) {
    414                final ClassFile classFile = buildClassFile(classNum);
    415                classFile.write(dos);
    416                dos.flush();
    417
    418                classFilesContents[classNum] = bos.toByteArray();
    419                bos.reset();
    420
    421                classNum++;
    422            }
    423        }
    424    }
    425
    426    /**
    427     * This performs reading the data from the stream into non-static instance of Segment. After the completion of this method stream can be freed.
    428     *
    429     * @param in the input stream to read from
    430     * @throws IOException      if a problem occurs during reading from the underlying stream
    431     * @throws Pack200Exception if a problem occurs with an unexpected value or unsupported codec
    432     */
    433    private void readSegment(final InputStream in) throws IOException, Pack200Exception {
    434        log(LOG_LEVEL_VERBOSE, "-------");
    435        cpBands = new CpBands(this);
    436        cpBands.read(in);
    437        attrDefinitionBands = new AttrDefinitionBands(this);
    438        attrDefinitionBands.read(in);
    439        icBands = new IcBands(this);
    440        icBands.read(in);
    441        classBands = new ClassBands(this);
    442        classBands.read(in);
    443        bcBands = new BcBands(this);
    444        bcBands.read(in);
    445        fileBands = new FileBands(this);
    446        fileBands.read(in);
    447
    448        fileBands.processFileBits();
    449    }
    450
    451    public void setLogLevel(final int logLevel) {
    452        this.logLevel = logLevel;
    453    }
    454
    455    public void setLogStream(final OutputStream logStream) {
    456        this.logStream = new PrintWriter(new OutputStreamWriter(logStream, Charset.defaultCharset()), false);
    457    }
    458
    459    public void setPreRead(final boolean value) {
    460        doPreRead = value;
    461    }
    462
    463    /**
    464     * Unpacks a packed stream (either .pack. or .pack.gz) into a corresponding JarOuputStream.
    465     *
    466     * @param inputStream  a packed input stream, preferably a {@link BoundedInputStream}.
    467     * @param out output stream.
    468     * @throws Pack200Exception if there is a problem unpacking
    469     * @throws IOException      if there is a problem with I/O during unpacking
    470     */
    471    public void unpack(final InputStream inputStream, final JarOutputStream out) throws IOException, Pack200Exception {
    472        unpackRead(inputStream);
    473        unpackProcess();
    474        unpackWrite(out);
    475    }
    476
    477    void unpackProcess() throws IOException, Pack200Exception {
    478        if (internalBuffer != null) {
    479            readSegment(internalBuffer);
    480        }
    481        parseSegment();
    482    }
    483
    484    /*
    485     * Package-private accessors for unpacking stages
    486     */
    487    void unpackRead(final InputStream inputStream) throws IOException, Pack200Exception {
    488        @SuppressWarnings("resource")
    489        final InputStream in = Pack200UnpackerAdapter.newBoundedInputStream(inputStream);
    490
    491        header = new SegmentHeader(this);
    492        header.read(in);
    493
    494        final int size = (int) header.getArchiveSize() - header.getArchiveSizeOffset();
    495
    496        if (doPreRead && header.getArchiveSize() != 0) {
    497            final byte[] data = new byte[size];
    498            in.read(data);
    499            internalBuffer = new BufferedInputStream(new ByteArrayInputStream(data));
    500        } else {
    501            readSegment(in);
    502        }
    503    }
    504
    505    void unpackWrite(final JarOutputStream out) throws IOException {
    506        writeJar(out);
    507        if (logStream != null) {
    508            logStream.close();
    509        }
    510    }
    511
    512    /**
    513     * Writes the segment to an output stream. The output stream should be pre-buffered for efficiency. Also takes the same input stream for reading, since the
    514     * file bits may not be loaded and thus just copied from one stream to another. Doesn't close the output stream when finished, in case there are more
    515     * entries (e.g. further segments) to be written.
    516     *
    517     * @param out the JarOutputStream to write data to
    518     * @throws IOException if an error occurs while reading or writing to the streams
    519     */
    520    public void writeJar(final JarOutputStream out) throws IOException {
    521        final String[] fileName = fileBands.getFileName();
    522        final int[] fileModtime = fileBands.getFileModtime();
    523        final long[] fileSize = fileBands.getFileSize();
    524        final byte[][] fileBits = fileBands.getFileBits();
    525
    526        // now write the files out
    527        int classNum = 0;
    528        final int numberOfFiles = header.getNumberOfFiles();
    529        final long archiveModtime = header.getArchiveModtime();
    530
    531        for (int i = 0; i < numberOfFiles; i++) {
    532            final String name = fileName[i];
    533            // For Pack200 archives, modtime is in seconds
    534            // from the epoch. JarEntries need it to be in
    535            // milliseconds from the epoch.
    536            // Even though we're adding two longs and multiplying
    537            // by 1000, we won't overflow because both longs are
    538            // always under 2^32.
    539            final long modtime = 1000 * (archiveModtime + fileModtime[i]);
    540            final boolean deflate = fileDeflate[i];
    541
    542            final JarEntry entry = new JarEntry(name);
    543            if (deflate) {
    544                entry.setMethod(ZipEntry.DEFLATED);
    545            } else {
    546                entry.setMethod(ZipEntry.STORED);
    547                final CRC32 crc = new CRC32();
    548                if (fileIsClass[i]) {
    549                    crc.update(classFilesContents[classNum]);
    550                    entry.setSize(classFilesContents[classNum].length);
    551                } else {
    552                    crc.update(fileBits[i]);
    553                    entry.setSize(fileSize[i]);
    554                }
    555                entry.setCrc(crc.getValue());
    556            }
    557            // On Windows at least, need to correct for timezone
    558            entry.setTime(modtime - TimeZone.getDefault().getRawOffset());
    559            out.putNextEntry(entry);
    560
    561            // write to output stream
    562            if (fileIsClass[i]) {
    563                entry.setSize(classFilesContents[classNum].length);
    564                out.write(classFilesContents[classNum]);
    565                classNum++;
    566            } else {
    567                entry.setSize(fileSize[i]);
    568                out.write(fileBits[i]);
    569            }
    570        }
    571    }
    572
    573}