1 /*
   2  * Copyright (c) 2010, 2011, 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.nio.charset.Charset;
  30 import java.security.*;
  31 import java.util.*;
  32 import java.util.jar.*;
  33 import java.util.zip.*;
  34 
  35 import static org.openjdk.jigsaw.FileConstants.ModuleFile.*;
  36 import static org.openjdk.jigsaw.FileConstants.ModuleFile.SectionType.*;
  37 
  38 public final class ModuleFile {
  39     /**
  40      * Return the subdir of a section in an extracted module file.
  41      */
  42     public static String getSubdirOfSection(SectionType type) {
  43         switch (type) {
  44         case MODULE_INFO:
  45         case SIGNATURE:
  46             return ".";
  47         case CLASSES:
  48         case RESOURCES:
  49             return "classes";
  50         case NATIVE_LIBS:
  51             return "lib";
  52         case NATIVE_CMDS:
  53             return "bin";
  54         case CONFIG:
  55             return "etc";
  56         default:
  57             throw new AssertionError(type);
  58         }
  59     }
  60 
  61     public final static class Reader implements Closeable {
  62 
  63         private DataInputStream stream;
  64         private File destination;
  65         private boolean deflate;
  66         private HashType hashtype;
  67         private File natlibs;
  68         private File natcmds;
  69         private File configs;
  70 
  71         private static class CountingInputStream extends FilterInputStream {
  72             private int count;
  73             public CountingInputStream(InputStream stream, int count) {
  74                 super(stream);
  75                 this.count = count;
  76             }
  77 
  78             public int available() throws IOException {
  79                 return count;
  80             }
  81 
  82             public boolean markSupported() {
  83                 return false;
  84             }
  85 
  86             public int read() throws IOException {
  87                 if (count == 0)
  88                     return -1;
  89                 int read = super.read();
  90                 if (-1 != read)
  91                     count--;
  92                 return read;
  93             }
  94 
  95             public int read(byte[] b, int off, int len) throws IOException {
  96                 if (count == 0)
  97                     return -1;
  98                 len = Math.min(len, count);
  99                 int read = super.read(b, off, len);
 100                 if (-1 != read)
 101                     count-=read;
 102                 return read;
 103             }
 104 
 105             public void reset() throws IOException {
 106                 throw new IOException("Can't reset this stream");
 107             }
 108 
 109             public long skip(long n) throws IOException {
 110                 if (count == 0)
 111                     return -1;
 112                 n = Math.min(n, count);
 113                 long skipped = super.skip(n);
 114                 if (n > 0)
 115                     count-=skipped;
 116                 return skipped;
 117             }
 118 
 119             public void close() throws IOException {
 120                 // Do nothing, CountingInputStream is used to wrap (sub)section
 121                 // content. We never want to close the underlying stream.
 122             }
 123         }
 124 
 125         public Reader(DataInputStream stream) {
 126             hashtype = HashType.SHA256;
 127             // Ensure that mark/reset is supported
 128             if (stream.markSupported()) {
 129                 this.stream = stream;
 130             } else {
 131                 this.stream =
 132                     new DataInputStream(new BufferedInputStream(stream));
 133             }
 134         }
 135 
 136         private ModuleFileHeader fileHeader = null;
 137         private MessageDigest fileDigest = null;
 138         private MessageDigest sectionDigest = null;
 139         private DataInputStream fileIn = null;
 140         private byte[] moduleInfoBytes = null;
 141         private Integer moduleSignatureType = null;
 142         private byte[] moduleSignatureBytes = null;
 143         private final int MAX_SECTION_HEADER_LENGTH = 128;
 144         private List<byte[]> calculatedHashes = new ArrayList<>();
 145         private boolean extract = true;
 146         private List<String> contents;  // list of the module-file contents
 147 
 148         /*
 149          * Reads the MODULE_INFO section and the Signature section, if present,
 150          * but does not write any files.
 151          */
 152         public byte[] readStart() throws IOException {
 153 
 154             try {
 155                 fileDigest = getHashInstance(hashtype);
 156                 sectionDigest = getHashInstance(hashtype);
 157                 DigestInputStream dis =
 158                     new DigestInputStream(stream, fileDigest);
 159                 fileHeader = ModuleFileHeader.read(dis);
 160                 // calculate module header hash
 161                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
 162                 fileHeader.write(new DataOutputStream(baos));
 163                 sectionDigest.update(baos.toByteArray());
 164                 calculatedHashes.add(sectionDigest.digest());
 165 
 166                 fileIn = new DataInputStream(dis);
 167                 if (readSection(fileIn) != MODULE_INFO)
 168                     throw new IOException("First module-file section"
 169                                           + " is not MODULE_INFO");
 170                 assert moduleInfoBytes != null;
 171 
 172                 // Read the Signature Section, if present
 173                 readSignatureSection(fileIn, dis);
 174 
 175                 return moduleInfoBytes.clone();
 176             } catch (IOException x) {
 177                 close();
 178                 throw x;
 179             }
 180         }
 181 
 182         public void readRest() throws IOException {
 183             extract = false;
 184             readRest(null, false, null, null, null);
 185         }
 186 
 187         public void readRest(File dst, boolean deflate) throws IOException {
 188             readRest(dst, deflate, null, null, null);
 189         }
 190 
 191         public void readRest(File dst, boolean deflate, File natlibs,
 192                              File natcmds, File configs)
 193                 throws IOException
 194         {
 195             this.deflate = deflate;
 196             this.destination = dst != null ? dst.getCanonicalFile() : null;
 197             this.natlibs = natlibs != null ? natlibs : new File(destination, "lib");
 198             this.natcmds = natcmds != null ? natcmds : new File(destination, "bin");
 199             this.configs = configs != null ? configs : new File(destination, "etc");
 200             contents = new ArrayList<>();
 201 
 202             try {
 203                 if (extract)
 204                     Files.store(moduleInfoBytes, computeRealPath("info"));
 205                 contents.add("module-info.class");
 206                 // Module-Info and Signature, if present, have been consumed
 207 
 208                 // Read rest of file until all sections have been read
 209                 stream.mark(1);
 210                 while (stream.read() != -1) {
 211                     stream.reset();
 212                     readSection(fileIn);
 213                     stream.mark(1);
 214                 }
 215 
 216                 close();
 217                 byte[] fileHeaderHash = fileHeader.getHashNoClone();
 218                 checkHashMatch(fileHeaderHash, fileDigest.digest());
 219                 calculatedHashes.add(fileHeaderHash);
 220             } finally {
 221                 close();
 222             }
 223         }
 224 
 225         public byte[] getHash() throws IOException {
 226             if (null == fileHeader)
 227                 readStart();
 228             return fileHeader.getHash();
 229         }
 230 
 231         public List<byte[]> getCalculatedHashes() {
 232             return calculatedHashes;
 233         }
 234 
 235         public boolean hasSignature() throws IOException {
 236             if (null == fileHeader)
 237                 readStart();
 238             return moduleSignatureBytes != null;
 239         }
 240 
 241         public Integer getSignatureType() throws IOException {
 242             if (null == fileHeader)
 243                 readStart();
 244             return moduleSignatureType;
 245         }
 246 
 247         public byte[] getSignature() throws IOException {
 248             if (null == fileHeader)
 249                 readStart();
 250             return moduleSignatureBytes != null
 251                 ? moduleSignatureBytes.clone()
 252                 : null;
 253         }
 254 
 255         byte[] getSignatureNoClone() {
 256             return moduleSignatureBytes;
 257         }
 258 
 259         public List<String> getContents() throws IOException {
 260             if (contents == null)
 261                 readModule();
 262             Collections.sort(contents);
 263             return contents;
 264         }
 265 
 266         public void close() throws IOException {
 267             try {
 268                 try {
 269                     if (contentStream != null) {
 270                         contentStream.close();
 271                         contentStream = null;
 272                     }
 273                 } finally {
 274                     if (fileIn != null) {
 275                         fileIn.close();
 276                         fileIn = null;
 277                     }
 278                 }
 279             } finally {
 280                 if (filesWriter != null) {
 281                     filesWriter.close();
 282                     filesWriter = null;
 283                 }
 284             }
 285         }
 286 
 287         public void readModule() throws IOException {
 288             extract = false;
 289             readStart();
 290             readRest();
 291         }
 292 
 293         public void readModule(File dst) throws IOException {
 294             readStart();
 295             readRest(dst, false);
 296         }
 297 
 298         private void readSignatureSection(DataInputStream stream,
 299                                           DigestInputStream dis)
 300             throws IOException
 301         {
 302             // Turn off digest computation before reading Signature Section
 303             dis.on(false);
 304 
 305             // Mark the starting position
 306             stream.mark(MAX_SECTION_HEADER_LENGTH);
 307             if (stream.read() != -1) {
 308                 stream.reset();
 309                 SectionHeader header = SectionHeader.read(stream);
 310                 if (header != null && header.getType() == SIGNATURE)
 311                     readSectionContent(header, stream);
 312                 else
 313                     stream.reset();  // No signature, reset back to start
 314             }
 315 
 316             // Turn on digest computation again
 317             dis.on(true);
 318         }
 319 
 320         private SectionType readSection(DataInputStream stream)
 321             throws IOException
 322         {
 323             SectionHeader header = SectionHeader.read(stream);
 324             readSectionContent(header, stream);
 325             return header.getType();
 326         }
 327 
 328         private void readSectionContent(SectionHeader header,
 329                                         DataInputStream stream)
 330             throws IOException
 331         {
 332             SectionType type = header.getType();
 333             Compressor compressor = header.getCompressor();
 334             int csize = header.getCSize();
 335 
 336             CountingInputStream cs = new CountingInputStream(stream, csize);
 337             sectionDigest.reset();
 338             DigestInputStream dis = new DigestInputStream(cs, sectionDigest);
 339             DataInputStream in = new DataInputStream(dis);
 340 
 341             if (type.hasFiles()) {
 342                 short subsections = header.getSubsections();
 343                 for (int subsection = 0; subsection < subsections; subsection++)
 344                     readSubSection(in, compressor, type);
 345             } else if (type == CLASSES) {
 346                 readClassesContent(in, compressor);
 347             } else {
 348                 readSectionBytes(in, compressor, type);
 349             }
 350 
 351             // ## appropriate exception ??
 352             if (cs.read() != -1)
 353                 throw new IllegalArgumentException("All section content not read");
 354 
 355             byte[] headerHash = header.getHashNoClone();
 356             checkHashMatch(headerHash, sectionDigest.digest());
 357             if (header.getType() != SIGNATURE) {
 358                 calculatedHashes.add(headerHash);
 359             }
 360         }
 361 
 362         // module-info OR module signature
 363         public void readSectionBytes(DataInputStream in, Compressor compressor,
 364                                      SectionType type)
 365             throws IOException
 366         {
 367             Decompressor decompressor = Decompressor.newInstance(in, compressor);
 368             ByteArrayOutputStream baos = new ByteArrayOutputStream();
 369             decompressor.extractTo(baos);
 370 
 371             if (type == MODULE_INFO)
 372                 moduleInfoBytes = baos.toByteArray();
 373             else if (type == SIGNATURE) {
 374                 SignatureSection sh = SignatureSection.read(new DataInputStream(
 375                         new ByteArrayInputStream(baos.toByteArray())));
 376                 moduleSignatureType = sh.getSignatureType();
 377                 moduleSignatureBytes = sh.getSignature();
 378             } else
 379                 throw new IllegalArgumentException(
 380                         "Unsupported raw bytes section type: " + type);
 381         }
 382 
 383         public void readClassesContent(DataInputStream in, Compressor compressor)
 384             throws IOException
 385         {
 386             ClassesDecompressor decompressor =
 387                     ClassesDecompressor.newInstance(in, compressor, deflate);
 388             decompressor.extractTo(contentStream());
 389         }
 390 
 391         // subsections/files (resources, libs, cmds, configs)
 392         public void readSubSection(DataInputStream in, Compressor compressor,
 393                                    SectionType type)
 394             throws IOException
 395         {
 396             assert type == RESOURCES || type == NATIVE_LIBS ||
 397                    type == NATIVE_CMDS || type == CONFIG;
 398 
 399             SubSectionFileHeader header = SubSectionFileHeader.read(in);
 400             CountingInputStream cs = new CountingInputStream(in, header.getCSize());
 401             Decompressor decompressor = Decompressor.newInstance(cs, compressor);
 402             String path = header.getPath();
 403             try (OutputStream sink = openOutputStream(type, path)) {
 404                 decompressor.extractTo(sink);
 405             }
 406 
 407             String prefix = type == RESOURCES ? "" :
 408                     getSubdirOfSection(type) + File.separator;
 409             contents.add(prefix + path);
 410             // post processing for executable and files outside the module dir
 411             if (extract)
 412                 postExtract(type, currentPath);
 413         }
 414 
 415         static class Decompressor {
 416             protected InputStream source;
 417             protected Decompressor() { }
 418             protected Decompressor(InputStream source) {
 419                 // no decompression
 420                 this.source = source;
 421             }
 422 
 423             void extractTo(OutputStream sink) throws IOException {
 424                 copyStream(source, sink);
 425             }
 426 
 427             static Decompressor newInstance(InputStream source,
 428                                             Compressor compressor)
 429                     throws IOException
 430             {
 431                 switch (compressor) {
 432                     case NONE:
 433                         return new Decompressor(source);
 434                     case GZIP:
 435                         return new GZIPDecompressor(source);
 436                     default:
 437                         throw new IllegalArgumentException(
 438                                 "Unsupported compressor type: " + compressor);
 439                 }
 440             }
 441         }
 442 
 443         static class GZIPDecompressor extends Decompressor {
 444             GZIPDecompressor(InputStream source) throws IOException {
 445                 this.source = new GZIPInputStream(source) {
 446                     public void close() throws IOException {}
 447                 };
 448             }
 449         }
 450 
 451         static abstract class ClassesDecompressor {
 452             protected InputStream source;
 453 
 454             abstract void extractTo(JarOutputStream sink) throws IOException;
 455 
 456             static ClassesDecompressor newInstance(InputStream source,
 457                                                    Compressor compressor,
 458                                                    boolean deflate)
 459                     throws IOException
 460             {
 461                 switch (compressor) {
 462                     case PACK200_GZIP:
 463                         return new Pack200GZIPDecompressor(source, deflate);
 464                     default:
 465                         throw new IllegalArgumentException(
 466                                 "Unsupported compressor type: " + compressor);
 467                 }
 468             }
 469         }
 470 
 471         static class Pack200GZIPDecompressor extends ClassesDecompressor {
 472             private Pack200.Unpacker unpacker;
 473 
 474             Pack200GZIPDecompressor(InputStream source, boolean deflate)
 475                 throws IOException
 476             {
 477                 this.source = new GZIPInputStream(source) {
 478                     public void close() throws IOException {}
 479                 };
 480                 unpacker = Pack200.newUnpacker();
 481                 if (deflate) {
 482                     Map<String,String> p = unpacker.properties();
 483                     p.put(Pack200.Unpacker.DEFLATE_HINT, Pack200.Unpacker.TRUE);
 484                 }
 485             }
 486 
 487             void extractTo(JarOutputStream sink) throws IOException {
 488                 unpacker.unpack(source, sink);
 489             }
 490         }
 491 
 492         private TrackingJarOutputStream contentStream = null;
 493 
 494         // Used to retrieved archive contents
 495         private static class TrackingJarOutputStream extends JarOutputStream {
 496             private final List<String> contents;
 497             TrackingJarOutputStream(OutputStream out, List<String> contents)
 498                 throws IOException
 499             {
 500                 super(out);
 501                 this.contents = contents;
 502             }
 503 
 504             public void putNextEntry(ZipEntry ze) throws IOException {
 505                 super.putNextEntry(ze);
 506                 contents.add(ze.getName());
 507             }
 508         }
 509 
 510         private JarOutputStream contentStream() throws IOException {
 511             if (contentStream != null)
 512                 return contentStream;
 513 
 514             OutputStream sink;
 515             if (extract)
 516                 sink = new BufferedOutputStream(
 517                            new FileOutputStream(computeRealPath("classes")));
 518             else
 519                 sink = new NullOutputStream();
 520 
 521             return contentStream = new TrackingJarOutputStream(sink, contents);
 522         }
 523 
 524         private File currentPath = null;
 525 
 526         private OutputStream openOutputStream(SectionType type, String path)
 527             throws IOException
 528         {
 529             if (!extract)
 530                 return new NullOutputStream();
 531 
 532             currentPath = null;
 533             assert type != CLASSES;
 534             if (type == RESOURCES)
 535                 return Files.newOutputStream(contentStream(), deflate, path);
 536             currentPath = computeRealPath(type, path);
 537             File parent = currentPath.getParentFile();
 538             if (!parent.exists())
 539                 Files.mkdirs(parent, currentPath.getName());
 540             return new BufferedOutputStream(new FileOutputStream(currentPath));
 541         }
 542 
 543         private static class NullOutputStream extends OutputStream {
 544             @Override
 545             public void write(int b) throws IOException {}
 546             @Override
 547             public void write(byte[] b) throws IOException {}
 548             @Override
 549             public void write(byte[] b, int off, int len) throws IOException {}
 550         }
 551 
 552         private static void checkHashMatch(byte[] expected, byte[] computed)
 553             throws IOException
 554         {
 555             if (!MessageDigest.isEqual(expected, computed))
 556                 throw new IOException("Expected hash "
 557                                       + hashHexString(expected)
 558                                       + " instead of "
 559                                       + hashHexString(computed));
 560         }
 561 
 562         // Track files installed outside the module library. For later removal.
 563         // files are relative to the modules directory.
 564         private PrintWriter filesWriter;
 565 
 566         private void trackFiles(File file)
 567             throws IOException
 568         {
 569             if (file == null || file.toPath().startsWith(destination.toPath()))
 570                 return;
 571 
 572             // Lazy construction, not all modules will need this.
 573             if (filesWriter == null)
 574                 filesWriter = new PrintWriter(computeRealPath("files"), "UTF-8");
 575 
 576             filesWriter.println(Files.convertSeparator(relativize(destination, file)));
 577             filesWriter.flush();
 578         }
 579 
 580         List<IOException> remove() {
 581             return ModuleFile.Reader.remove(destination);
 582         }
 583 
 584         // Removes a module, given its module install directory
 585         static List<IOException> remove(File moduleDir) {
 586             List<IOException> excs = new ArrayList<>();
 587             // Firstly remove any files installed outside of the module dir
 588             File files = new File(moduleDir, "files");
 589             if (files.exists()) {
 590                 try {
 591                     List<String> filenames =
 592                         java.nio.file.Files.readAllLines(files.toPath(),
 593                                                          Charset.forName("UTF-8"));
 594                     for (String fn : filenames) {
 595                         try {
 596                             Files.delete(new File(moduleDir,
 597                                                   Files.platformSeparator(fn)));
 598                         } catch (IOException x) {
 599                             excs.add(x);
 600                         }
 601                     }
 602                 } catch (IOException x) {
 603                     excs.add(x);
 604                 }
 605             }
 606 
 607             excs.addAll(Files.deleteTreeUnchecked(moduleDir.toPath()));
 608             return excs;
 609         }
 610 
 611         // Returns the absolute path of the given section type.
 612         private File getDirOfSection(SectionType type) {
 613             if (type == NATIVE_LIBS)
 614                 return natlibs;
 615             else if (type == NATIVE_CMDS)
 616                 return natcmds;
 617             else if (type == CONFIG)
 618                 return configs;
 619 
 620             // resolve sub dir section paths against the modules directory
 621             return new File(destination, ModuleFile.getSubdirOfSection(type));
 622         }
 623 
 624         private File computeRealPath(String path) throws IOException {
 625             return resolveAndNormalize(destination, path);
 626         }
 627 
 628         private File computeRealPath(SectionType type, String storedpath)
 629             throws IOException
 630         {
 631             File sectionPath = getDirOfSection(type);
 632             File realpath = new File(sectionPath,
 633                  Files.ensureNonAbsolute(Files.platformSeparator(storedpath)));
 634 
 635             validatePath(sectionPath, realpath);
 636 
 637             // Create the parent directories if necessary
 638             File parent = realpath.getParentFile();
 639             if (!parent.exists())
 640                 Files.mkdirs(parent, realpath.getName());
 641 
 642             return realpath;
 643         }
 644 
 645         private static void markNativeCodeExecutable(SectionType type,
 646                                                      File file)
 647         {
 648             if (type == NATIVE_CMDS || (type == NATIVE_LIBS &&
 649                     System.getProperty("os.name").startsWith("Windows")))
 650                 file.setExecutable(true);
 651         }
 652 
 653         private void postExtract(SectionType type, File path)
 654             throws IOException
 655         {
 656             markNativeCodeExecutable(type, path);
 657             trackFiles(path);
 658         }
 659     }
 660 
 661     private static void checkCompressor(SectionType type,
 662                                         Compressor compressor) {
 663 
 664         if ((MODULE_INFO == type && Compressor.NONE != compressor) ||
 665             (CLASSES == type && Compressor.PACK200_GZIP != compressor))
 666             throw new IllegalArgumentException(type
 667                                                + " may not use compressor "
 668                                                + compressor);
 669     }
 670 
 671     private static void checkSubsectionCount(SectionType type,
 672                                              short subsections) {
 673         if (!type.hasFiles() && subsections != 0)
 674             throw new IllegalArgumentException(type
 675                                                + " subsection count not 0: "
 676                                                + subsections);
 677         else if (type.hasFiles() && subsections == 0)
 678             throw new IllegalArgumentException(type + " subsection count is 0");
 679     }
 680 
 681     private static void copyStream(InputStream source, DataOutput sink)
 682         throws IOException
 683     {
 684         byte[] buf = new byte[8192];
 685         int b_read = 0;
 686         while((b_read = source.read(buf)) > 0)
 687             sink.write(buf, 0, b_read);
 688     }
 689 
 690     private static void copyStream(InputStream source, OutputStream sink)
 691         throws IOException
 692     {
 693         copyStream(source, (DataOutput) new DataOutputStream(sink));
 694     }
 695 
 696     private static void ensureNonNegativity(long size, String parameter) {
 697         if (size < 0)
 698             throw new IllegalArgumentException(parameter + "<0: " + size);
 699     }
 700 
 701     private static void ensureNonNull(Object reference, String parameter) {
 702         if (null == reference)
 703             throw new IllegalArgumentException(parameter + " == null");
 704     }
 705 
 706     private static void ensureMatch(int found, int expected, String field)
 707         throws IOException
 708     {
 709         if (found != expected)
 710             throw new IOException(field + " expected : "
 711                 + Integer.toHexString(expected) + " found: "
 712                 + Integer.toHexString(found));
 713     }
 714 
 715     private static void ensureShortNativePath(File path, String name)
 716         throws IOException
 717     {
 718             // TODO: check for native code file in a stricter way
 719             if (path.canExecute()
 720                 && name.indexOf('/') != -1)
 721                 throw new IOException("Native code path too long: " + path);
 722     }
 723 
 724     private static void ensureValidFileSize(long size, File path)
 725         throws IOException
 726     {
 727         if (size < 0 || size > Integer.MAX_VALUE)
 728             throw new IOException("File " + path + " too large: " + size);
 729     }
 730 
 731     static MessageDigest getHashInstance(HashType hashtype)
 732         throws IOException
 733     {
 734         try {
 735             switch(hashtype) {
 736             case SHA256:
 737                 return MessageDigest.getInstance("SHA-256");
 738             default:
 739                 throw new IOException("Unknown hash type: " + hashtype);
 740             }
 741         }
 742         catch (NoSuchAlgorithmException ex) {
 743             throw (IOException) (new IOException(hashtype + " not found"))
 744                 .initCause(ex);
 745         }
 746     }
 747 
 748     private static short getMUTF8Length(String name) {
 749         short size = 2;
 750 
 751         for (int i = name.length()-1; i >= 0; i--) {
 752             char ch = name.charAt(i);
 753 
 754             if ('\u0001' <= ch && ch <= '\u007F')
 755                 size += 1;
 756             else if ('\u0000' == ch
 757                      || '\u0080' <= ch && ch <= '\u07FF')
 758                 size += 2;
 759             else
 760                 size += 3;
 761         }
 762 
 763         return size;
 764     }
 765 
 766     private static String hashHexString(byte[] hash) {
 767         StringBuilder hex = new StringBuilder("0x");
 768         for (int i = 0; i < hash.length; i++) {
 769             int val = (hash[i] & 0xFF);
 770             if (val <= 16)
 771                 hex.append("0");
 772             hex.append(Integer.toHexString(val));
 773         }
 774         return hex.toString();
 775     }
 776 
 777     private static File resolveAndNormalize(File directory, String path)
 778         throws IOException
 779     {
 780         File realpath = new File(directory, path);
 781         if (directory != null &&
 782             ! realpath.toPath().startsWith(directory.toPath()))
 783             throw new IOException("Bogus relative path: " + path);
 784 
 785         return realpath;
 786     }
 787 
 788 
 789     private static String relativize(File directory, File path) throws IOException {
 790         return (directory.toPath().relativize(path.toPath().toRealPath())).toString();
 791     }
 792 
 793     private static void validatePath(File parent, File child)
 794         throws IOException
 795     {
 796         if (!child.toPath().startsWith(parent.toPath()) )
 797             throw new IOException("Bogus relative path: " + child);
 798         if (child.exists()) {
 799             // conflict, for now just fail
 800             throw new IOException("File " + child + " already exists");
 801         }
 802     }
 803 
 804     private static short readHashLength(DataInputStream in) throws IOException {
 805         final short hashLength = in.readShort();
 806         ensureNonNegativity(hashLength, "hashLength");
 807 
 808         return hashLength;
 809     }
 810 
 811     private static byte[] readHashBytes(DataInputStream in, short hashLength)
 812         throws IOException
 813     {
 814         final byte[] hash = new byte[hashLength];
 815         in.readFully(hash);
 816 
 817         return hash;
 818     }
 819 
 820     private static byte[] readHash(DataInputStream in) throws IOException {
 821         return readHashBytes(in, readHashLength(in));
 822     }
 823 
 824     private static byte[] readFileHash(DigestInputStream dis)
 825         throws IOException
 826     {
 827         DataInputStream in = new DataInputStream(dis);
 828 
 829         final short hashLength = readHashLength(in);
 830 
 831         // Turn digest computation off before reading the file hash
 832         dis.on(false);
 833         byte[] hash = readHashBytes(in, hashLength);
 834         // Turn digest computation on again afterwards.
 835         dis.on(true);
 836 
 837         return hash;
 838     }
 839 
 840     public final static class ModuleFileHeader {
 841         public static final int LENGTH_WITHOUT_HASH = 30;
 842         public static final int LENGTH =
 843             LENGTH_WITHOUT_HASH + HashType.SHA256.length();
 844 
 845         // Fields are specified as unsigned. Treat signed values as bugs.
 846         private final int magic;                // MAGIC
 847         private final FileConstants.Type type;  // Type.MODULE_FILE
 848         private final short major;              // ModuleFile.MAJOR_VERSION
 849         private final short minor;              // ModuleFile.MINOR_VERSION
 850         private final long csize;               // Size of rest of file, compressed
 851         private final long usize;               // Space required for uncompressed contents
 852                                                 //   (upper private final ound; need not be exact)
 853         private final HashType hashType;        // One of ModuleFile.HashType
 854                                                 //   (applies final o all hashes in this file)
 855         private final byte[] hash;              // Hash of entire file (except this hash
 856                                                 // and the Signature section, if present)
 857 
 858         public byte[] getHash() {
 859             return hash.clone();
 860         }
 861 
 862         private byte[] getHashNoClone() {
 863             return hash;
 864         }
 865 
 866         public ModuleFileHeader(long csize, long usize,
 867                                 HashType hashType, byte[] hash) {
 868             ensureNonNegativity(csize, "csize");
 869             ensureNonNegativity(usize, "usize");
 870 
 871             magic = FileConstants.MAGIC;
 872             type = FileConstants.Type.MODULE_FILE;
 873             major = MAJOR_VERSION;
 874             minor = MINOR_VERSION;
 875 
 876             this.csize = csize;
 877             this.usize = usize;
 878             this.hashType = hashType;
 879             this.hash = hash.clone();
 880         }
 881 
 882         public void write(final DataOutput out) throws IOException {
 883             out.writeInt(magic);
 884             out.writeShort(type.value());
 885             out.writeShort(major);
 886             out.writeShort(minor);
 887             out.writeLong(csize);
 888             out.writeLong(usize);
 889             out.writeShort(hashType.value());
 890             writeHash(out, hash);
 891         }
 892 
 893         private static HashType lookupHashType(short value) {
 894             for (HashType i : HashType.class.getEnumConstants()) {
 895                 if (i.value() == value) return i;
 896             }
 897 
 898             throw new IllegalArgumentException("No HashType exists with value "
 899                     + value);
 900         }
 901 
 902         public static ModuleFileHeader read(final DigestInputStream dis)
 903                 throws IOException
 904         {
 905             DataInputStream in = new DataInputStream(dis);
 906 
 907             final int magic = in.readInt();
 908             ensureMatch(magic, FileConstants.MAGIC,
 909                         "FileConstants.MAGIC");
 910 
 911             final short type = in.readShort();
 912             ensureMatch(type, FileConstants.Type.MODULE_FILE.value(),
 913                        "Type.MODULE_FILE");
 914 
 915             final short major = in.readShort();
 916             ensureMatch(major, MAJOR_VERSION,
 917                         "ModuleFile.MAJOR_VERSION");
 918 
 919             final short minor = in.readShort();
 920             ensureMatch(minor, MINOR_VERSION,
 921                         "ModuleFile.MINOR_VERSION");
 922 
 923             final long csize = in.readLong();
 924             final long usize = in.readLong();
 925             final short hashTypeValue = in.readShort();
 926             HashType hashType = lookupHashType(hashTypeValue);
 927             final byte[] hash = readFileHash(dis);
 928 
 929             return new ModuleFileHeader(csize, usize, hashType, hash);
 930         }
 931 
 932         public String toString() {
 933             return "MODULE{csize=" + csize +
 934                    ", hash=" + hashHexString(hash) + "}";
 935         }
 936     }
 937 
 938     public final static class SectionHeader {
 939         public static final int LENGTH_WITHOUT_HASH = 12;
 940         public static final int LENGTH =
 941             LENGTH_WITHOUT_HASH + HashType.SHA256.length();
 942 
 943         // Fields are specified as unsigned. Treat signed values as bugs.
 944         private final SectionType type;
 945         private final Compressor compressor;
 946         private final int csize;               // Size of section content, compressed
 947         private final short subsections;       // Number of following subsections
 948         private final byte[] hash;             // Hash of section content
 949 
 950         public SectionHeader(SectionType type,
 951                              Compressor compressor,
 952                              int csize, short subsections, byte[] hash) {
 953             ensureNonNull(type, "type");
 954             ensureNonNull(compressor, "compressor");
 955             ensureNonNegativity(csize, "csize");
 956             ensureNonNegativity(subsections, "subsections");
 957             ensureNonNull(hash, "hash");
 958             checkSubsectionCount(type, subsections);
 959             checkCompressor(type, compressor);
 960 
 961             this.type = type;
 962             this.compressor = compressor;
 963             this.csize = csize;
 964             this.subsections = subsections;
 965             this.hash = hash.clone();
 966         }
 967 
 968         public void write(DataOutput out) throws IOException {
 969             out.writeShort(type.value());
 970             out.writeShort(compressor.value());
 971             out.writeInt(csize);
 972             out.writeShort(subsections);
 973             writeHash(out, hash);
 974         }
 975 
 976         private static SectionType lookupSectionType(short value) {
 977             for (SectionType i : SectionType.class.getEnumConstants()) {
 978                 if (i.value() == value) return i;
 979             }
 980 
 981             throw new
 982                 IllegalArgumentException("No SectionType exists with value "
 983                                          + value);
 984         }
 985 
 986         private static Compressor lookupCompressor(short value) {
 987             for (Compressor i : Compressor.class.getEnumConstants()) {
 988                 if (i.value() == value) return i;
 989             }
 990 
 991             throw new
 992                 IllegalArgumentException("No Compressor exists with value "
 993                                          + value);
 994         }
 995 
 996         public static SectionHeader read(DataInputStream in) throws IOException {
 997             short tvalue = in.readShort();
 998             final SectionType type = lookupSectionType(tvalue);
 999             short cvalue = in.readShort();
1000             final Compressor compressor = lookupCompressor(cvalue);
1001             final int csize = in.readInt();
1002             final short sections = in.readShort();
1003             final byte[] hash = readHash(in);
1004 
1005             return new SectionHeader(type, compressor, csize,
1006                     sections, hash);
1007         }
1008 
1009         public SectionType getType() {
1010             return type;
1011         }
1012 
1013         public Compressor getCompressor() {
1014             return compressor;
1015         }
1016 
1017         public int getCSize() {
1018             return csize;
1019         }
1020 
1021         public short getSubsections() {
1022             return subsections;
1023         }
1024 
1025         public byte[] getHash() {
1026             return hash.clone();
1027         }
1028 
1029         private byte[] getHashNoClone() {
1030             return hash;
1031         }
1032 
1033         public String toString() {
1034             return "SectionHeader{type= " + type
1035                     + ", compressor=" + compressor
1036                     + ", csize=" + csize
1037                     + ", subsections=" + subsections
1038                     + ", hash=" + hashHexString(hash) + "}";
1039         }
1040     }
1041 
1042     public final static class SubSectionFileHeader {
1043         private final int csize;              // Size of file, compressed
1044         private final String path;            // Path name, in Java-modified UTF-8
1045 
1046         public int getCSize() {
1047             return csize;
1048         }
1049 
1050         public String getPath() {
1051             return path;
1052         }
1053 
1054         public SubSectionFileHeader(int csize, String path) {
1055             ensureNonNegativity(csize, "csize");
1056             ensureNonNull(path, "path");
1057 
1058             this.csize = csize;
1059             this.path = path;
1060         }
1061 
1062         public void write(DataOutput out) throws IOException {
1063             out.writeShort(SubSectionType.FILE.value());
1064             out.writeInt(csize);
1065             out.writeUTF(path);
1066         }
1067 
1068         public static SubSectionFileHeader read(DataInputStream in)
1069                 throws IOException
1070         {
1071             final short type = in.readShort();
1072             ensureMatch(type, SubSectionType.FILE.value(),
1073                         "ModuleFile.SubSectionType.FILE");
1074             final int csize = in.readInt();
1075             final String path = in.readUTF();
1076 
1077             return new SubSectionFileHeader(csize, path);
1078         }
1079     }
1080 
1081     public final static class SignatureSection {
1082         private final int signatureType;   // One of FileConstants.ModuleFile.HashType
1083         private final int signatureLength; // Length of signature
1084         private final byte[] signature;    // Signature bytes
1085 
1086         public int getSignatureType() {
1087             return signatureType;
1088         }
1089 
1090         public int getSignatureLength() {
1091             return signatureLength;
1092         }
1093 
1094         public byte[] getSignature() {
1095             return signature;
1096         }
1097 
1098         public SignatureSection(int signatureType, int signatureLength,
1099                                 byte[] signature) {
1100             ensureNonNegativity(signatureLength, "signatureLength");
1101 
1102             this.signatureType = signatureType;
1103             this.signatureLength = signatureLength;
1104             this.signature = signature.clone();
1105         }
1106 
1107         public void write(DataOutput out) throws IOException {
1108             out.writeShort(signatureType);
1109             out.writeInt(signatureLength);
1110             out.write(signature);
1111         }
1112 
1113         public static SignatureSection read(DataInputStream in)
1114             throws IOException
1115         {
1116             final short signatureType = in.readShort();
1117             ensureMatch(signatureType, SignatureType.PKCS7.value(), "SignatureType.PKCS7");
1118             final int signatureLength = in.readInt();
1119             ensureNonNegativity(signatureLength, "signatureLength");
1120             final byte[] signature = new byte[signatureLength];
1121             in.readFully(signature);
1122             return new SignatureSection(signatureType, signatureLength,
1123                                         signature);
1124         }
1125     }
1126 
1127     private static void writeHash(DataOutput out, byte[] hash)
1128             throws IOException
1129     {
1130         out.writeShort(hash.length);
1131         out.write(hash);
1132     }
1133 }