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