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