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