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