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 ---