1 /*
   2  * Copyright (c) 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.nio.file.attribute.BasicFileAttributes;
  29 import java.io.*;
  30 import java.nio.ByteBuffer;
  31 import java.nio.channels.*;
  32 import java.nio.file.*;
  33 import java.nio.file.Files;
  34 import java.security.MessageDigest;
  35 import java.util.*;
  36 import java.util.jar.*;
  37 import java.util.zip.*;
  38 import org.openjdk.jigsaw.ModuleFileParser.Event;
  39 import static org.openjdk.jigsaw.FileConstants.ModuleFile.*;
  40 import static org.openjdk.jigsaw.ModuleFile.*;
  41 
  42 
  43 /**
  44  * <p> A writer of module files </p>
  45  */
  46 
  47 public class ModuleFileWriter {
  48 
  49     private final File outfile;
  50     private final HashType hashtype;
  51     private final boolean fastestCompression;
  52     private long usize;
  53     private int headerLength;
  54 
  55     public ModuleFileWriter(File outfile) {
  56         this(outfile, false);
  57     }
  58 
  59     public ModuleFileWriter(File outfile, boolean useFastestCompression) {
  60         this.outfile = outfile;
  61         this.hashtype = HashType.SHA256;
  62         this.fastestCompression = useFastestCompression;
  63     }
  64 
  65     /**
  66      * Generates an unsigned module file.
  67      */
  68     public void writeModule(File mdir, File nativelibs, File nativecmds,
  69                             File config, ModuleArchitecture modArch)


  70         throws IOException
  71     {
  72         if (!mdir.isDirectory()) {
  73             throw new IOException("Not a directory: " + mdir);
  74         }
  75 
  76         try (RandomAccessFile file = new RandomAccessFile(outfile, "rw")) {
  77             // Truncate the file if it already exists
  78             file.setLength(0);
  79 
  80             // set appropriate fields in the header before determining the length
  81             ModuleFileHeader dummyHeader = ModuleFileHeader.newBuilder()
  82                                            .setArchitecture(modArch)
  83                                            .setHashType(hashtype)
  84                                            .setHash(new byte[hashtype.length()])
  85                                            .build();
  86 
  87             // Reset module file to right after module header
  88             file.seek(headerLength = dummyHeader.getLength());
  89 



  90             // Write out the Module-Info Section
  91             File miclass = new File(mdir, "module-info.class");
  92             if (!miclass.exists())
  93                 throw new IOException(miclass + " does not exist");
  94 
  95             writeSection(file,
  96                          SectionType.MODULE_INFO,
  97                          mdir,
  98                          Collections.singletonList(miclass.toPath()),
  99                          Compressor.NONE);
 100 
 101             // Write out the optional file sections
 102             writeOptionalSections(file, mdir, nativelibs, nativecmds, config);
 103 
 104             // Write out the module file header
 105             writeModuleFileHeader(file, headerLength, modArch);
 106         }
 107     }
 108 
 109     /*
 110      * Write a section to the given module file.
 111      *
 112      * @params file RandomAccessFile for the resulting jmod file
 113      * @params type section type
 114      * @params sourcedir the directory containing the files to be written
 115      * @params files list of files to be written
 116      * @params compressor compression type
 117      */
 118     private void writeSection(RandomAccessFile file,
 119                               SectionType type,
 120                               File sourcedir,
 121                               List<Path> files,
 122                               Compressor compressor)
 123         throws IOException
 124     {
 125         // Start of section header
 126         final long start = file.getFilePointer();
 127 
 128         // Start of section content
 129         final long cstart = start + SectionHeader.LENGTH;
 130         // Seek to start of section content
 131         file.seek(cstart);
 132 
 133         writeSectionContent(file, type, sourcedir, files, compressor);
 134 
 135         // End of section
 136         final long end = file.getFilePointer();
 137         final int csize = (int) (end - cstart);
 138 
 139         // A section type that has no files has a section count of 0.
 140         int count = type.hasFiles() ? files.size() : 0;
 141         if (count > Short.MAX_VALUE) {
 142             throw new IOException("Too many files: " + count);
 143         }
 144         writeSectionHeader(file, type, compressor, start, csize, (short) count);
 145     }
 146 
 147     private void writeSectionContent(RandomAccessFile file,
 148                                      SectionType type,
 149                                      File sourcedir,
 150                                      List<Path> files,
 151                                      Compressor compressor)
 152         throws IOException
 153     {
 154         if (type.hasFiles()) {
 155             for (Path p : files) {
 156                 writeSubSection(file, sourcedir, p, compressor);
 157             }
 158         } else if (type == SectionType.CLASSES) {
 159             writeClassesContent(file, sourcedir, files, compressor);
 160         } else if (files.size() == 1) {
 161             writeFileContent(file, files.get(0), compressor);
 162         } else {
 163             throw new IllegalArgumentException("Section type: " + type
 164                     + " can only have one single file but given " + files);
 165         }
 166     }
 167 
 168     private void writeSectionHeader(RandomAccessFile file,
 169                                     SectionType type,
 170                                     Compressor compressor,
 171                                     long start, int csize,
 172                                     short subsections)
 173         throws IOException
 174     {
 175         // Compute hash of content
 176         MessageDigest md = getHashInstance(hashtype);
 177         FileChannel channel = file.getChannel();
 178         ByteBuffer content = ByteBuffer.allocate(csize);
 179 
 180         final long cstart = start + SectionHeader.LENGTH;
 181         int n = channel.read(content, cstart);
 182         if (n != csize) {
 183             throw new IOException("too few bytes read");
 184         }
 185         content.position(0);
 186         md.update(content);
 187         final byte[] hash = md.digest();
 188 
 189         // Write section header at section header start,
 190         // and seek to end of section.
 191         SectionHeader header =
 192                 new SectionHeader(type, compressor, csize, subsections, hash);
 193         file.seek(start);
 194         header.write(file);
 195         file.seek(cstart + csize);
 196     }
 197 
 198     private void writeClassesContent(DataOutput out,
 199                                      File sourcedir,
 200                                      List<Path> files,
 201                                      Compressor compressor)
 202         throws IOException
 203     {
 204         CompressedClassOutputStream cos =
 205             CompressedClassOutputStream.newInstance(sourcedir.toPath(),
 206                                                     files,
 207                                                     compressor,
 208                                                     fastestCompression);
 209         usize += cos.getUSize();
 210         cos.writeTo(out);
 211     }
 212 
 213     private void writeFileContent(DataOutput out,
 214                                   Path p,
 215                                   Compressor compressor)
 216         throws IOException
 217     {
 218         CompressedOutputStream cos = CompressedOutputStream.newInstance(p, compressor);
 219         usize += cos.getUSize();
 220         cos.writeTo(out);
 221     }
 222 
 223     /*
 224      * Write a subsection to the given module file.
 225      *
 226      * @params file RandomAccessFile for the resulting jmod file
 227      * @params sourcedir the directory containing the file to be written
 228      * @params p Path of a file to be written
 229      * @params compressor compression type
 230      */
 231     private void writeSubSection(RandomAccessFile file,
 232                                  File sourcedir,
 233                                  Path p,
 234                                  Compressor compressor)
 235         throws IOException
 236     {
 237         CompressedOutputStream cos = CompressedOutputStream.newInstance(p, compressor);
 238         usize += cos.getUSize();
 239 
 240         String storedpath = relativePath(sourcedir.toPath(), p);
 241         SubSectionFileHeader header = new SubSectionFileHeader((int)cos.getCSize(), storedpath);
 242         header.write(file);
 243         cos.writeTo(file);
 244     }
 245 
 246 
 247     /*
 248      * Processes each of the optional file sections.
 249      */
 250     private void writeOptionalSections(RandomAccessFile file,
 251                                        File mdir,
 252                                        File nativelibs,
 253                                        File nativecmds,
 254                                        File config)
 255         throws IOException
 256     {
 257         List<Path> classes = new ArrayList<>();
 258         List<Path> resources = new ArrayList<>();
 259         listClassesResources(mdir.toPath(), classes, resources);
 260 
 261         if (!classes.isEmpty()) {
 262             writeSection(file,
 263                          SectionType.CLASSES,
 264                          mdir,
 265                          classes,
 266                          Compressor.PACK200_GZIP);
 267         }
 268         if (!resources.isEmpty()) {
 269             writeSection(file,
 270                          SectionType.RESOURCES,
 271                          mdir,
 272                          resources,
 273                          Compressor.GZIP);
 274         }
 275 
 276         if (nativelibs != null && directoryIsNotEmpty(nativelibs)) {
 277             writeSection(file,
 278                          SectionType.NATIVE_LIBS,
 279                          nativelibs,
 280                          listFiles(nativelibs.toPath()),
 281                          Compressor.GZIP);
 282         }
 283         if (nativecmds != null && directoryIsNotEmpty(nativecmds)) {
 284             writeSection(file,
 285                          SectionType.NATIVE_CMDS,
 286                          nativecmds,
 287                          listFiles(nativecmds.toPath()),
 288                          Compressor.GZIP);
 289         }
 290         if (config != null && directoryIsNotEmpty(config)) {
 291             writeSection(file,
 292                          SectionType.CONFIG,
 293                          config,
 294                          listFiles(config.toPath()),
 295                          Compressor.GZIP);
 296         }
 297     }
 298 
 299     /*
 300      * Writes out the module file header.
 301      */
 302     private void writeModuleFileHeader(RandomAccessFile file,
 303                                        long headerLength,
 304                                        ModuleArchitecture modArch)
 305         throws IOException
 306     {
 307         long csize = file.length() - headerLength;
 308 


 309         // Header Step 1
 310         // Write out the module file header (using a dummy file hash value)
 311         ModuleFileHeader.Builder hBuilder = ModuleFileHeader.newBuilder()
 312                                             .setCSize(csize)
 313                                             .setUSize(usize)
 314                                             .setArchitecture(modArch)
 315                                             .setHashType(hashtype)
 316                                             .setHash(new byte[hashtype.length()]);
 317         ModuleFileHeader fileHeader = hBuilder.build();
 318         assert (fileHeader.getLength() == headerLength);
 319 
 320         file.seek(0);
 321         fileHeader.write(file);
 322 
 323         // Calculate the correct module file hash, using a non-validating parser
 324         file.seek(0);
 325         byte[] fileHash = null;
 326         ModuleFileParser parser =
 327                 ModuleFile.newParser(Channels.newInputStream(file.getChannel()));
 328         while (parser.hasNext()) {
 329             if (parser.next().equals(Event.END_FILE))
 330                 fileHash =  parser.getHash();
 331         }
 332 
 333         // Header Step 2
 334         // Write out the module file header (using correct file hash value)
 335         hBuilder.setHash(fileHash);
 336         file.seek(0);
 337         fileHeader = hBuilder.build();
 338         assert (fileHeader.getLength() == headerLength);
 339         fileHeader.write(file);
 340     }
 341 
 342     private void listClassesResources(Path dir,
 343                                       final List<Path> classes,
 344                                       final List<Path> resources)
 345         throws IOException
 346     {

 347         Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
 348             @Override
 349             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 350                     throws IOException {
 351                 if (!file.endsWith("module-info.class")) {
 352                     if (file.toFile().getName().endsWith(".class")) {
 353                         classes.add(file);
 354                     } else {
 355                         resources.add(file);
 356                     }
 357                 }
 358                 return FileVisitResult.CONTINUE;
 359             }
 360         });
 361     }
 362 
 363     // CompressedOutputStream and CompressedClassOutputStream are
 364     // subclass of ByteArrayOutputStream to avoid the array copy
 365     // from the compressed bytes and write buf directly to DataOutput
 366     static class CompressedOutputStream extends ByteArrayOutputStream {
 367         protected long size = 0;   // uncompressed size
 368         protected CompressedOutputStream() {
 369         }
 370 
 371         private CompressedOutputStream(Path p) throws IOException {
 372             // no compression
 373             size += Files.copy(p, this);
 374         }
 375 
 376         long getUSize() {
 377             return size;
 378         }
 379 
 380         long getCSize() {
 381             return count;
 382         }
 383 
 384         void writeTo(DataOutput out) throws IOException {
 385             out.write(buf, 0, count);
 386         }
 387 
 388         static CompressedOutputStream newInstance(Path p, Compressor type)
 389                 throws IOException
 390         {
 391             switch (type) {
 392                 case NONE:
 393                     return new CompressedOutputStream(p);
 394                 case GZIP:
 395                     return new GZIPCompressedOutputStream(p);
 396                 case PACK200_GZIP:
 397                 default:
 398                     throw new IllegalArgumentException("Unsupported type: " + type);
 399             }
 400         }
 401 
 402         static class GZIPCompressedOutputStream extends CompressedOutputStream {
 403             GZIPCompressedOutputStream(Path p) throws IOException {
 404                 super();
 405                 size += p.toFile().length();
 406                 try (GZIPOutputStream gos = new GZIPOutputStream(this)) {
 407                     Files.copy(p, gos);
 408                     gos.finish();
 409                 }
 410             }
 411         }
 412     }
 413 
 414     static abstract class CompressedClassOutputStream extends ByteArrayOutputStream {
 415         protected long size = 0;
 416         long getUSize() {
 417             return size;
 418         }
 419 
 420         long getCSize() {
 421             return count;
 422         }
 423 
 424         void writeTo(DataOutput out) throws IOException {
 425             out.write(buf, 0, count);
 426         }
 427 
 428         static CompressedClassOutputStream newInstance(Path sourcepath,
 429                                                        List<Path> classes,
 430                                                        Compressor type,
 431                                                        boolean fastestCompression)
 432             throws IOException
 433         {
 434             switch (type) {
 435                 case PACK200_GZIP:
 436                     Pack200GZipOutputStream pos =
 437                             new Pack200GZipOutputStream(fastestCompression);
 438                     pos.compress(sourcepath, classes);
 439                     return pos;
 440                 default:
 441                     throw new IllegalArgumentException("Unsupported type: " + type);
 442             }
 443         }
 444 
 445         static class Pack200GZipOutputStream extends CompressedClassOutputStream {
 446             final Pack200.Packer packer = Pack200.newPacker();
 447             Pack200GZipOutputStream(boolean fastestCompression) {
 448                 Map<String, String> p = packer.properties();
 449                 p.put(Pack200.Packer.SEGMENT_LIMIT, "-1");
 450                 if (fastestCompression) {
 451                     p.put(Pack200.Packer.EFFORT, "1");
 452                 }
 453                 p.put(Pack200.Packer.KEEP_FILE_ORDER, Pack200.Packer.FALSE);
 454                 p.put(Pack200.Packer.MODIFICATION_TIME, Pack200.Packer.LATEST);
 455                 p.put(Pack200.Packer.DEFLATE_HINT, Pack200.Packer.FALSE);
 456             }
 457 
 458             void compress(Path sourcepath, List<Path> classes)
 459                 throws IOException
 460             {
 461                 ByteArrayOutputStream os = new ByteArrayOutputStream();
 462                 try (JarOutputStream jos = new JarOutputStream(os)) {
 463                     jos.setLevel(0);
 464                     for (Path file : classes) {
 465                         // write to the JarInputStream for later pack200 compression
 466                         String name = file.toFile().getName().toLowerCase();
 467                         String p = sourcepath.relativize(file).toString();
 468                         if (!p.equals("module-info.class")) {
 469                             JarEntry entry = new JarEntry(p);
 470                             jos.putNextEntry(entry);
 471                             Files.copy(file, jos);
 472                             jos.closeEntry();
 473                         }
 474                         size += file.toFile().length();
 475                     }
 476                 }
 477 
 478                 byte[] data = os.toByteArray();
 479                 try (JarInputStream jin =
 480                         new JarInputStream(new ByteArrayInputStream(data));
 481                      GZIPOutputStream gout = new GZIPOutputStream(this)) {
 482                     packer.pack(jin, gout);
 483                 }
 484             }
 485         }
 486     }
 487 
 488     private String relativePath(Path sourcepath, Path path)
 489         throws IOException
 490     {
 491         Path relativepath = sourcepath.relativize(path);
 492 
 493         // The '/' character separates nested directories in path names.
 494         String pathseparator = relativepath.getFileSystem().getSeparator();
 495         String stored = relativepath.toString().replace(pathseparator, "/");
 496 
 497         // The path names of native-code files
 498         // must not include more than one element.
 499         // ## Temporarily turn off this check until the location of
 500         // ## the native libraries in jdk modules are changed
 501         // ensureShortNativePath(realpath, stored);
 502         return stored;
 503     }
 504 
 505     private List<Path> listFiles(Path path)
 506         throws IOException
 507     {
 508         final List<Path> list = new ArrayList<>();
 509         Files.walkFileTree(path, new SimpleFileVisitor<Path>() {

 510             @Override
 511             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 512                     throws IOException {
 513                 list.add(file);
 514                 return FileVisitResult.CONTINUE;
 515             }
 516         });
 517         return list;
 518     }
 519 
 520     /*




































 521      * Check if a given directory is not empty.
 522      */
 523     private static boolean directoryIsNotEmpty(File dir)
 524         throws IOException
 525     {
 526         try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir.toPath())) {
 527             return ds.iterator().hasNext();
 528         }
 529     }

 530 }
--- EOF ---