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 }