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