1 /* 2 * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package org.openjdk.jigsaw; 27 28 import java.io.*; 29 import java.security.DigestInputStream; 30 import java.security.MessageDigest; 31 import java.security.NoSuchAlgorithmException; 32 import java.util.Map.Entry; 33 import java.util.*; 34 import java.util.jar.JarOutputStream; 35 import java.util.jar.Pack200; 36 import java.util.zip.GZIPInputStream; 37 import java.util.zip.ZipEntry; 38 import org.openjdk.jigsaw.FileConstants.ModuleFile.Compressor; 39 import org.openjdk.jigsaw.FileConstants.ModuleFile.HashType; 40 import org.openjdk.jigsaw.FileConstants.ModuleFile.SectionType; 41 import static org.openjdk.jigsaw.FileConstants.ModuleFile.SectionType.*; 42 import org.openjdk.jigsaw.ModuleFile.ModuleFileHeader; 43 import org.openjdk.jigsaw.ModuleFile.SectionHeader; 44 import org.openjdk.jigsaw.ModuleFile.SubSectionFileHeader; 45 import static org.openjdk.jigsaw.ModuleFileParser.Event.*; 46 47 public class ModuleFileParserImpl 48 implements ModuleFileParser 49 { 50 private static class CountingInputStream extends FilterInputStream { 51 private int count; 52 public CountingInputStream(InputStream stream, int count) { 53 super(stream); 54 this.count = count; 55 } 56 57 public int available() throws IOException { 58 return count; 59 } 60 61 public boolean markSupported() { 62 return false; 63 } 64 65 public int read() throws IOException { 66 if (count == 0) 67 return -1; 68 int read = super.read(); 69 if (-1 != read) 70 count--; 71 return read; 72 } 73 74 public int read(byte[] b, int off, int len) throws IOException { 75 if (count == 0) 76 return -1; 77 len = Math.min(len, count); 78 int read = super.read(b, off, len); 79 if (-1 != read) 80 count-=read; 81 return read; 82 } 83 84 public void reset() throws IOException { 85 throw new IOException("Can't reset this stream"); 86 } 87 88 public long skip(long n) throws IOException { 89 // ## never skip, always read for digest, skip could just call read 90 throw new IOException("skip should never be called"); 91 } 92 93 public void close() throws IOException { 94 // Do nothing, CountingInputStream is used to wrap (sub)section 95 // content. We never want to close the underlying stream. 96 } 97 } 98 99 private final DataInputStream stream; // dataInput wrapped raw stream 100 private final HashType hashtype = HashType.SHA256; 101 private final ModuleFileHeader fileHeader; 102 private final MessageDigest fileDigest; 103 private final MessageDigest sectionDigest; 104 private DataInputStream digestStream; // fileDigest, wrapper input stream 105 106 // parser state 107 private Event curEvent; 108 private SectionHeader curSectionHeader; 109 private SubSectionFileHeader curSubSectionHeader; 110 private InputStream curSectionIn; 111 private InputStream curSubSectionIn; 112 private int subSectionCount; 113 private byte[] hash; 114 private ModuleFileParserException parserException; 115 116 /*package*/ ModuleFileParserImpl(InputStream in) { 117 // Ensure that mark/reset is supported 118 if (in.markSupported()) 119 stream = new DataInputStream(in); 120 else 121 stream = new DataInputStream(new BufferedInputStream(in)); 122 123 try { 124 fileDigest = getHashInstance(hashtype); 125 sectionDigest = getHashInstance(hashtype); 126 DigestInputStream dis = new DigestInputStream(stream, fileDigest); 127 fileHeader = ModuleFileHeader.read(dis); 128 // calculate module header hash 129 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 130 fileHeader.write(new DataOutputStream(baos)); 131 sectionDigest.update(baos.toByteArray()); 132 digestStream = new DataInputStream(dis); 133 hash = sectionDigest.digest(); 134 curEvent = START_FILE; 135 } catch (IOException | ModuleFileParserException x) { 136 throw parserException(x); 137 } 138 } 139 140 @Override 141 public ModuleFileHeader fileHeader() { 142 return fileHeader; 143 } 144 145 @Override 146 public Event event() { 147 return curEvent; 148 } 149 150 @Override 151 public boolean hasNext() { 152 if (parserException != null) 153 return false; 154 155 return curEvent != END_FILE; 156 } 157 158 @Override 159 public Event next() { 160 if (!hasNext()) { 161 if (parserException != null) 162 throw new NoSuchElementException("END_FILE reached"); 163 else 164 throw parserException("Error processing input. The input stream is not complete."); 165 } 166 167 // Reset general state 168 hash = null; 169 170 try { 171 switch (curEvent) { 172 case START_FILE: 173 // can only transition to START_SECTION, module-info 174 curSectionHeader = SectionHeader.read(digestStream); 175 SectionType type = curSectionHeader.getType(); 176 if (type != MODULE_INFO) 177 throw parserException(type + ": expected MODULE_INFO"); 178 sectionDigest.reset(); 179 curSectionIn = new DigestInputStream(new CountingInputStream(digestStream, 180 curSectionHeader.getCSize()), sectionDigest); 181 return curEvent = START_SECTION; 182 case START_SECTION : 183 // can only transition to START_SUBSECTION or END_SECTION 184 if (subSectionCount != 0) 185 return curEvent = startSubSection(); 186 // END_SECTION 187 skipAnyUnread(curSectionIn); 188 hash = sectionDigest.digest(); 189 return curEvent = END_SECTION; 190 case START_SUBSECTION : 191 // can only transition to END_SUBSECTION 192 skipAnyUnread(curSubSectionIn); 193 return curEvent = END_SUBSECTION; 194 case END_SUBSECTION : 195 // can only transition to START_SUBSECTION or END_SECTION 196 if (subSectionCount != 0) 197 return curEvent = startSubSection(); 198 checkAllRead(curSectionIn, 199 "subsections do not consume all section data"); 200 hash = sectionDigest.digest(); 201 return curEvent = END_SECTION; 202 case END_SECTION : 203 // must transition to START_SECTION or END_FILE 204 SectionHeader nextHeader = peekNextSection(false); 205 if (nextHeader == null) { 206 hash = fileDigest.digest(); 207 return curEvent = END_FILE; 208 } 209 // START_SECTION 210 return curEvent = startSection(nextHeader); 211 case END_FILE : 212 throw parserException( 213 "should not reach here, next with current event END_FILE"); 214 default : 215 throw parserException("Unknown event: " + curEvent); 216 } 217 } catch (IOException | ModuleFileParserException x) { 218 throw parserException(x); 219 } 220 } 221 222 private Event startSection(SectionHeader nextHeader) throws IOException { 223 sectionDigest.reset(); 224 DataInputStream in = digestStream; 225 if (nextHeader.getType() == SIGNATURE ) 226 // special handling for SIGNATURE section, skip file digest 227 in = stream; 228 229 curSectionHeader = SectionHeader.read(in); 230 if (curSectionHeader.getType() == MODULE_INFO) 231 throw parserException("Unexpected MODULE_INFO"); 232 curSectionIn = new DigestInputStream(new CountingInputStream(in, 233 curSectionHeader.getCSize()), sectionDigest); 234 235 if (curSectionHeader.getType().hasFiles()) 236 subSectionCount = curSectionHeader.getSubsections(); 237 else 238 subSectionCount = 0; 239 curSubSectionIn = null; 240 return START_SECTION; 241 } 242 243 private Event startSubSection() throws IOException { 244 curSubSectionHeader = SubSectionFileHeader.read(new DataInputStream(curSectionIn)); 245 curSubSectionIn = new CountingInputStream(curSectionIn, curSubSectionHeader.getCSize()); 246 subSectionCount--; 247 return START_SUBSECTION; 248 } 249 250 private ModuleFileParserException parserException(String message) { 251 return parserException = new ModuleFileParserException(message); 252 } 253 254 private ModuleFileParserException parserException(Exception x) { 255 if (x instanceof ModuleFileParserException) 256 return parserException = (ModuleFileParserException) x; 257 258 return parserException = new ModuleFileParserException(x); 259 } 260 261 private static void skipAnyUnread(InputStream is) 262 throws IOException 263 { 264 byte[] ba = new byte[8192]; 265 while (is.read(ba) != -1); 266 } 267 268 private void checkAllRead(InputStream is, String message) 269 throws IOException 270 { 271 if (is.read() != -1) 272 throw parserException(message); 273 } 274 275 // required to be able to handle special case the signature 276 private SectionHeader peekNextSection(boolean throwOnEOF) 277 throws IOException 278 { 279 // Mark the position & read from stream (does not effect digest) 280 stream.mark(SectionHeader.LENGTH); 281 282 if (stream.read() == -1) 283 return null; 284 285 stream.reset(); 286 SectionHeader header = SectionHeader.read(stream); 287 stream.reset(); 288 if (header != null) 289 return header; 290 291 throw parserException("Error parsing section header"); 292 } 293 294 @Override 295 public boolean skipToNextStartSection() { 296 if (curEvent == END_FILE) return false; 297 298 while (hasNext()) { 299 Event e = next(); 300 if (e == START_SECTION) 301 return true; 302 if (e == END_FILE) 303 return false; 304 } 305 return false; 306 } 307 308 @Override 309 public boolean skipToNextStartSubSection() { 310 if (!(curEvent == START_SECTION || curEvent == START_SUBSECTION || 311 curEvent == END_SUBSECTION)) 312 return false; 313 314 if (!getSectionHeader().getType().hasFiles()) 315 throw parserException(getSectionHeader().getType() + 316 " section does not contain subsections"); 317 318 while(hasNext()) { 319 Event e = next(); 320 if (e == START_SUBSECTION) return true; 321 if (e == END_SECTION) return false; 322 } 323 return false; 324 } 325 326 @Override 327 public SectionHeader getSectionHeader() { 328 if (curEvent == START_FILE || curEvent == END_FILE) 329 throw parserException("No section header for: " + curEvent); 330 return curSectionHeader; 331 } 332 333 @Override 334 public SubSectionFileHeader getSubSectionFileHeader() { 335 if (!(curEvent == START_SUBSECTION || curEvent == END_SUBSECTION)) 336 throw parserException("No subsection header for " + curEvent); 337 return curSubSectionHeader; 338 } 339 340 @Override 341 public byte[] getHash() { 342 if (!(curEvent == START_FILE || curEvent == END_SECTION || 343 curEvent == END_FILE)) 344 throw parserException("Hash not calculatable at " + curEvent); 345 346 return hash; 347 } 348 349 @Override 350 public InputStream getContentStream() { 351 if (!(curEvent == START_SECTION || curEvent == START_SUBSECTION)) 352 throw parserException("current event " + curEvent + 353 ", expected one of START_SECTION or START_SUBSECTION"); 354 355 InputStream is = curSubSectionIn != null ? curSubSectionIn : curSectionIn; 356 357 SectionType type = curSectionHeader.getType(); 358 Compressor compressor = curSectionHeader.getCompressor(); 359 360 if (type == CLASSES) { 361 throw parserException("should not be called for CLASSES"); 362 } else { 363 try { 364 Decompressor decompressor = Decompressor.newInstance(is, compressor); 365 return decompressor.extractStream(); 366 } catch (IOException | ModuleFileParserException x) { 367 throw parserException(x); 368 } 369 } 370 } 371 372 @Override 373 public InputStream getRawStream() { 374 if (!(curEvent == START_SECTION || curEvent == START_SUBSECTION)) 375 throw parserException("current event " + curEvent + 376 ", expected one of START_SECTION or START_SUBSECTION"); 377 378 return curSubSectionIn != null ? curSubSectionIn : curSectionIn; 379 } 380 381 @Override 382 public Iterator<Entry<String,InputStream>> getClasses() { 383 if (curEvent != START_SECTION) 384 throw parserException("current event " + curEvent + 385 ", expected START_SECTION"); 386 387 SectionType type = curSectionHeader.getType(); 388 Compressor compressor = curSectionHeader.getCompressor(); 389 390 if (type != CLASSES) 391 throw parserException(type + ": not classes section"); 392 if (curSectionIn == null) 393 throw parserException("not at a valid classes section"); 394 395 try { 396 ClassesDecompressor decompressor = 397 ClassesDecompressor.newInstance(curSectionIn, compressor, /*deflate*/false); 398 ClassesJarOutputStream cjos = new ClassesJarOutputStream(); 399 decompressor.extractTo(cjos); 400 401 return cjos.classes().iterator(); 402 } catch (IOException | ModuleFileParserException x) { 403 throw parserException(x); 404 } 405 } 406 407 private static class ClassesEntry 408 implements Entry<String,InputStream>, java.io.Serializable 409 { 410 //private static final long serialVersionUID = -8499721149061103585L; 411 412 private final String key; 413 private InputStream value; 414 415 ClassesEntry(String key, InputStream value) { 416 this.key = key; 417 this.value = value; 418 } 419 420 @Override 421 public String getKey() { 422 return key; 423 } 424 425 @Override 426 public InputStream getValue() { 427 return value; 428 } 429 430 @Override 431 public InputStream setValue(InputStream value) { 432 InputStream oldValue = this.value; 433 this.value = value; 434 return oldValue; 435 } 436 437 @Override 438 public boolean equals(Object o) { 439 if (!(o instanceof ClassesEntry)) 440 return false; 441 ClassesEntry e = (ClassesEntry)o; 442 if (key == null ? e.key != null : !key.equals(e.key)) 443 return false; 444 if (value == null ? e.value != null : !value.equals(e.value)) 445 return false; 446 return true; 447 } 448 449 @Override 450 public int hashCode() { 451 return (key == null ? 0 : key.hashCode()) ^ 452 (value == null ? 0 : value.hashCode()); 453 } 454 455 @Override 456 public String toString() { 457 return key + ":" + value; 458 } 459 } 460 461 private static class ClassesJarOutputStream extends JarOutputStream { 462 private Set<Entry<String,InputStream>> classes; 463 private ByteArrayOutputStream classBytes; 464 private String path; 465 466 ClassesJarOutputStream() 467 throws IOException 468 { 469 super(nullOutputStream); 470 classes = new HashSet<>(); 471 classBytes = new ByteArrayOutputStream(); 472 } 473 474 @Override 475 public void putNextEntry(ZipEntry ze) throws IOException { 476 classBytes.reset(); 477 path = ze.getName(); 478 } 479 480 @Override 481 public void closeEntry() throws IOException { 482 classes.add(new ClassesEntry(path, 483 new ByteArrayInputStream(classBytes.toByteArray()))); 484 } 485 486 @Override 487 public void write(int b) throws IOException { 488 classBytes.write(b); 489 } 490 491 @Override 492 public void write(byte[] ba) throws IOException { 493 classBytes.write(ba); 494 } 495 496 @Override 497 public void write(byte[] b, int off, int len) throws IOException { 498 classBytes.write(b, off, len); 499 } 500 501 Set<Entry<String,InputStream>> classes() { 502 return classes; 503 } 504 } 505 506 private static OutputStream nullOutputStream = new NullOutputStream(); 507 508 static class NullOutputStream extends OutputStream { 509 @Override 510 public void write(int b) throws IOException {} 511 @Override 512 public void write(byte[] b) throws IOException {} 513 @Override 514 public void write(byte[] b, int off, int len) throws IOException {} 515 } 516 517 static class Decompressor { 518 protected InputStream in; 519 protected Decompressor() { } 520 protected Decompressor(InputStream in) { 521 // no decompression 522 this.in = in; 523 } 524 525 InputStream extractStream() { 526 return in; 527 } 528 529 static Decompressor newInstance(InputStream in, 530 Compressor compressor) 531 throws IOException 532 { 533 switch (compressor) { 534 case NONE: 535 return new Decompressor(in); 536 case GZIP: 537 return new GZIPDecompressor(in); 538 default: 539 throw new ModuleFileParserException( 540 "Unsupported compressor type: " + compressor); 541 } 542 } 543 } 544 545 static class GZIPDecompressor extends Decompressor { 546 GZIPDecompressor(InputStream in) throws IOException { 547 this.in = new GZIPInputStream(in) { 548 public void close() throws IOException {} 549 }; 550 } 551 } 552 553 static abstract class ClassesDecompressor { 554 protected InputStream in; 555 556 abstract void extractTo(JarOutputStream out) throws IOException; 557 558 static ClassesDecompressor newInstance(InputStream in, 559 Compressor compressor, 560 boolean deflate) 561 throws IOException 562 { 563 switch (compressor) { 564 case PACK200_GZIP: 565 return new Pack200GZIPDecompressor(in, deflate); 566 default: 567 throw new ModuleFileParserException( 568 "Unsupported compressor type: " + compressor); 569 } 570 } 571 } 572 573 static class Pack200GZIPDecompressor extends ClassesDecompressor { 574 private Pack200.Unpacker unpacker; 575 576 Pack200GZIPDecompressor(InputStream in, boolean deflate) 577 throws IOException 578 { 579 this.in = new GZIPInputStream(in) { 580 public void close() throws IOException {} 581 }; 582 unpacker = Pack200.newUnpacker(); 583 if (deflate) { 584 Map<String,String> p = unpacker.properties(); 585 p.put(Pack200.Unpacker.DEFLATE_HINT, Pack200.Unpacker.TRUE); 586 } 587 } 588 589 void extractTo(JarOutputStream out) throws IOException { 590 unpacker.unpack(in, out); 591 } 592 } 593 594 static MessageDigest getHashInstance(HashType hashtype) { 595 try { 596 switch(hashtype) { 597 case SHA256: 598 return MessageDigest.getInstance("SHA-256"); 599 default: 600 throw new ModuleFileParserException("Unknown hash type: " + hashtype); 601 } 602 } catch (NoSuchAlgorithmException x) { 603 throw new ModuleFileParserException(hashtype + " not found", x); 604 } 605 } 606 }