1 /* 2 * Copyright (c) 2015, 2017, 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 package jdk.internal.module; 26 27 import java.io.Closeable; 28 import java.io.File; 29 import java.io.IOError; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.UncheckedIOException; 33 import java.lang.module.ModuleDescriptor; 34 import java.lang.module.ModuleDescriptor.Builder; 35 import java.lang.module.ModuleReader; 36 import java.lang.module.ModuleReference; 37 import java.net.MalformedURLException; 38 import java.net.URI; 39 import java.net.URL; 40 import java.nio.ByteBuffer; 41 import java.nio.file.Files; 42 import java.nio.file.Path; 43 import java.nio.file.Paths; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Optional; 51 import java.util.Set; 52 import java.util.jar.JarEntry; 53 import java.util.jar.JarFile; 54 import java.util.stream.Collectors; 55 import java.util.stream.Stream; 56 57 import jdk.internal.loader.Resource; 58 import jdk.internal.loader.ResourceHelper; 59 import jdk.internal.misc.JavaLangModuleAccess; 60 import jdk.internal.misc.SharedSecrets; 61 import sun.net.www.ParseUtil; 62 63 64 /** 65 * Provides support for patching modules, mostly the boot layer. 66 */ 67 68 public final class ModulePatcher { 69 70 private static final JavaLangModuleAccess JLMA 71 = SharedSecrets.getJavaLangModuleAccess(); 72 73 // module name -> sequence of patches (directories or JAR files) 74 private final Map<String, List<Path>> map; 75 76 /** 77 * Initialize the module patcher with the given map. The map key is 78 * the module name, the value is a list of path strings. 79 */ 80 public ModulePatcher(Map<String, List<String>> input) { 81 if (input.isEmpty()) { 82 this.map = Collections.emptyMap(); 83 } else { 84 Map<String, List<Path>> map = new HashMap<>(); 85 for (Map.Entry<String, List<String>> e : input.entrySet()) { 86 String mn = e.getKey(); 87 List<Path> paths = e.getValue().stream() 88 .map(Paths::get) 89 .collect(Collectors.toList()); 90 map.put(mn, paths); 91 } 92 this.map = map; 93 } 94 } 95 96 /** 97 * Returns a module reference that interposes on the given module if 98 * needed. If there are no patches for the given module then the module 99 * reference is simply returned. Otherwise the patches for the module 100 * are scanned (to find any new packages) and a new module reference is 101 * returned. 102 * 103 * @throws UncheckedIOException if an I/O error is detected 104 */ 105 public ModuleReference patchIfNeeded(ModuleReference mref) { 106 // if there are no patches for the module then nothing to do 107 ModuleDescriptor descriptor = mref.descriptor(); 108 String mn = descriptor.name(); 109 List<Path> paths = map.get(mn); 110 if (paths == null) 111 return mref; 112 113 // Scan the JAR file or directory tree to get the set of packages. 114 // For automatic modules then packages that do not contain class files 115 // must be ignored. 116 Set<String> packages = new HashSet<>(); 117 boolean isAutomatic = descriptor.isAutomatic(); 118 try { 119 for (Path file : paths) { 120 if (Files.isRegularFile(file)) { 121 122 // JAR file - do not open as a multi-release JAR as this 123 // is not supported by the boot class loader 124 try (JarFile jf = new JarFile(file.toFile())) { 125 jf.stream() 126 .filter(e -> !e.isDirectory() 127 && (!isAutomatic || e.getName().endsWith(".class"))) 128 .map(e -> toPackageName(file, e)) 129 .filter(Checks::isPackageName) 130 .forEach(packages::add); 131 } 132 133 } else if (Files.isDirectory(file)) { 134 135 // exploded directory without following sym links 136 Path top = file; 137 Files.find(top, Integer.MAX_VALUE, 138 ((path, attrs) -> attrs.isRegularFile())) 139 .filter(path -> !isAutomatic 140 || path.toString().endsWith(".class")) 141 .map(path -> toPackageName(top, path)) 142 .filter(Checks::isPackageName) 143 .forEach(packages::add); 144 145 } 146 } 147 148 } catch (IOException ioe) { 149 throw new UncheckedIOException(ioe); 150 } 151 152 // if there are new packages then we need a new ModuleDescriptor 153 packages.removeAll(descriptor.packages()); 154 if (!packages.isEmpty()) { 155 Builder builder = JLMA.newModuleBuilder(descriptor.name(), 156 /*strict*/ false, 157 descriptor.modifiers()); 158 if (!descriptor.isAutomatic()) { 159 descriptor.requires().forEach(builder::requires); 160 descriptor.exports().forEach(builder::exports); 161 descriptor.opens().forEach(builder::opens); 162 descriptor.uses().forEach(builder::uses); 163 } 164 descriptor.provides().forEach(builder::provides); 165 166 descriptor.version().ifPresent(builder::version); 167 descriptor.mainClass().ifPresent(builder::mainClass); 168 descriptor.osName().ifPresent(builder::osName); 169 descriptor.osArch().ifPresent(builder::osArch); 170 descriptor.osVersion().ifPresent(builder::osVersion); 171 172 // original + new packages 173 builder.packages(descriptor.packages()); 174 builder.packages(packages); 175 176 descriptor = builder.build(); 177 } 178 179 // return a module reference to the patched module 180 URI location = mref.location().orElse(null); 181 182 ModuleHashes recordedHashes = null; 183 ModuleResolution mres = null; 184 if (mref instanceof ModuleReferenceImpl) { 185 ModuleReferenceImpl impl = (ModuleReferenceImpl)mref; 186 recordedHashes = impl.recordedHashes(); 187 mres = impl.moduleResolution(); 188 } 189 190 return new ModuleReferenceImpl(descriptor, 191 location, 192 () -> new PatchedModuleReader(paths, mref), 193 this, 194 recordedHashes, 195 null, 196 mres); 197 198 } 199 200 /** 201 * Returns true is this module patcher has no patches. 202 */ 203 public boolean isEmpty() { 204 return map.isEmpty(); 205 } 206 207 /* 208 * Returns the names of the patched modules. 209 */ 210 Set<String> patchedModules() { 211 return map.keySet(); 212 } 213 214 /** 215 * A ModuleReader that reads resources from a patched module. 216 * 217 * This class is public so as to expose the findResource method to the 218 * built-in class loaders and avoid locating the resource twice during 219 * class loading (once to locate the resource, the second to gets the 220 * URL for the CodeSource). 221 */ 222 public static class PatchedModuleReader implements ModuleReader { 223 private final List<ResourceFinder> finders; 224 private final ModuleReference mref; 225 private final URL delegateCodeSourceURL; 226 private volatile ModuleReader delegate; 227 228 /** 229 * Creates the ModuleReader to reads resources a patched module. 230 */ 231 PatchedModuleReader(List<Path> patches, ModuleReference mref) { 232 List<ResourceFinder> finders = new ArrayList<>(); 233 boolean initialized = false; 234 try { 235 for (Path file : patches) { 236 if (Files.isRegularFile(file)) { 237 finders.add(new JarResourceFinder(file)); 238 } else { 239 finders.add(new ExplodedResourceFinder(file)); 240 } 241 } 242 initialized = true; 243 } catch (IOException ioe) { 244 throw new UncheckedIOException(ioe); 245 } finally { 246 // close all ResourceFinder in the event of an error 247 if (!initialized) closeAll(finders); 248 } 249 250 this.finders = finders; 251 this.mref = mref; 252 this.delegateCodeSourceURL = codeSourceURL(mref); 253 } 254 255 /** 256 * Closes all resource finders. 257 */ 258 private static void closeAll(List<ResourceFinder> finders) { 259 for (ResourceFinder finder : finders) { 260 try { finder.close(); } catch (IOException ioe) { } 261 } 262 } 263 264 /** 265 * Returns the code source URL for the given module. 266 */ 267 private static URL codeSourceURL(ModuleReference mref) { 268 try { 269 Optional<URI> ouri = mref.location(); 270 if (ouri.isPresent()) 271 return ouri.get().toURL(); 272 } catch (MalformedURLException e) { } 273 return null; 274 } 275 276 /** 277 * Returns the ModuleReader to delegate to when the resource is not 278 * found in a patch location. 279 */ 280 private ModuleReader delegate() throws IOException { 281 ModuleReader r = delegate; 282 if (r == null) { 283 synchronized (this) { 284 r = delegate; 285 if (r == null) { 286 delegate = r = mref.open(); 287 } 288 } 289 } 290 return r; 291 } 292 293 /** 294 * Finds a resources in the patch locations. Returns null if not found. 295 */ 296 private Resource findResourceInPatch(String name) throws IOException { 297 for (ResourceFinder finder : finders) { 298 Resource r = finder.find(name); 299 if (r != null) 300 return r; 301 } 302 return null; 303 } 304 305 /** 306 * Finds a resource of the given name in the patched module. 307 */ 308 public Resource findResource(String name) throws IOException { 309 310 // patch locations 311 Resource r = findResourceInPatch(name); 312 if (r != null) 313 return r; 314 315 // original module 316 ByteBuffer bb = delegate().read(name).orElse(null); 317 if (bb == null) 318 return null; 319 320 return new Resource() { 321 private <T> T shouldNotGetHere(Class<T> type) { 322 throw new InternalError("should not get here"); 323 } 324 @Override 325 public String getName() { 326 return shouldNotGetHere(String.class); 327 } 328 @Override 329 public URL getURL() { 330 return shouldNotGetHere(URL.class); 331 } 332 @Override 333 public URL getCodeSourceURL() { 334 return delegateCodeSourceURL; 335 } 336 @Override 337 public ByteBuffer getByteBuffer() throws IOException { 338 return bb; 339 } 340 @Override 341 public InputStream getInputStream() throws IOException { 342 return shouldNotGetHere(InputStream.class); 343 } 344 @Override 345 public int getContentLength() throws IOException { 346 return shouldNotGetHere(int.class); 347 } 348 }; 349 } 350 351 @Override 352 public Optional<URI> find(String name) throws IOException { 353 Resource r = findResourceInPatch(name); 354 if (r != null) { 355 URI uri = URI.create(r.getURL().toString()); 356 return Optional.of(uri); 357 } else { 358 return delegate().find(name); 359 } 360 } 361 362 @Override 363 public Optional<InputStream> open(String name) throws IOException { 364 Resource r = findResourceInPatch(name); 365 if (r != null) { 366 return Optional.of(r.getInputStream()); 367 } else { 368 return delegate().open(name); 369 } 370 } 371 372 @Override 373 public Optional<ByteBuffer> read(String name) throws IOException { 374 Resource r = findResourceInPatch(name); 375 if (r != null) { 376 ByteBuffer bb = r.getByteBuffer(); 377 assert !bb.isDirect(); 378 return Optional.of(bb); 379 } else { 380 return delegate().read(name); 381 } 382 } 383 384 @Override 385 public void release(ByteBuffer bb) { 386 if (bb.isDirect()) { 387 try { 388 delegate().release(bb); 389 } catch (IOException ioe) { 390 throw new InternalError(ioe); 391 } 392 } 393 } 394 395 @Override 396 public Stream<String> list() throws IOException { 397 Stream<String> s = delegate().list(); 398 for (ResourceFinder finder : finders) { 399 s = Stream.concat(s, finder.list()); 400 } 401 return s.distinct(); 402 } 403 404 @Override 405 public void close() throws IOException { 406 closeAll(finders); 407 delegate().close(); 408 } 409 } 410 411 412 /** 413 * A resource finder that find resources in a patch location. 414 */ 415 private static interface ResourceFinder extends Closeable { 416 Resource find(String name) throws IOException; 417 Stream<String> list() throws IOException; 418 } 419 420 421 /** 422 * A ResourceFinder that finds resources in a JAR file. 423 */ 424 private static class JarResourceFinder implements ResourceFinder { 425 private final JarFile jf; 426 private final URL csURL; 427 428 JarResourceFinder(Path path) throws IOException { 429 this.jf = new JarFile(path.toFile()); 430 this.csURL = path.toUri().toURL(); 431 } 432 433 @Override 434 public void close() throws IOException { 435 jf.close(); 436 } 437 438 @Override 439 public Resource find(String name) throws IOException { 440 JarEntry entry = jf.getJarEntry(name); 441 if (entry == null) 442 return null; 443 444 return new Resource() { 445 @Override 446 public String getName() { 447 return name; 448 } 449 @Override 450 public URL getURL() { 451 String encodedPath = ParseUtil.encodePath(name, false); 452 try { 453 return new URL("jar:" + csURL + "!/" + encodedPath); 454 } catch (MalformedURLException e) { 455 return null; 456 } 457 } 458 @Override 459 public URL getCodeSourceURL() { 460 return csURL; 461 } 462 @Override 463 public ByteBuffer getByteBuffer() throws IOException { 464 byte[] bytes = getInputStream().readAllBytes(); 465 return ByteBuffer.wrap(bytes); 466 } 467 @Override 468 public InputStream getInputStream() throws IOException { 469 return jf.getInputStream(entry); 470 } 471 @Override 472 public int getContentLength() throws IOException { 473 long size = entry.getSize(); 474 return (size > Integer.MAX_VALUE) ? -1 : (int) size; 475 } 476 }; 477 } 478 479 @Override 480 public Stream<String> list() throws IOException { 481 return jf.stream() 482 .filter(e -> !e.isDirectory()) 483 .map(JarEntry::getName); 484 } 485 } 486 487 488 /** 489 * A ResourceFinder that finds resources on the file system. 490 */ 491 private static class ExplodedResourceFinder implements ResourceFinder { 492 private final Path dir; 493 494 ExplodedResourceFinder(Path dir) { 495 this.dir = dir; 496 } 497 498 @Override 499 public void close() { } 500 501 @Override 502 public Resource find(String name) throws IOException { 503 Path path = ResourceHelper.toFilePath(name); 504 if (path != null) { 505 Path file = dir.resolve(path); 506 if (Files.isRegularFile(file)) { 507 return newResource(name, dir, file); 508 } 509 } 510 return null; 511 } 512 513 private Resource newResource(String name, Path top, Path file) { 514 return new Resource() { 515 @Override 516 public String getName() { 517 return name; 518 } 519 @Override 520 public URL getURL() { 521 try { 522 return file.toUri().toURL(); 523 } catch (IOException | IOError e) { 524 return null; 525 } 526 } 527 @Override 528 public URL getCodeSourceURL() { 529 try { 530 return top.toUri().toURL(); 531 } catch (IOException | IOError e) { 532 return null; 533 } 534 } 535 @Override 536 public ByteBuffer getByteBuffer() throws IOException { 537 return ByteBuffer.wrap(Files.readAllBytes(file)); 538 } 539 @Override 540 public InputStream getInputStream() throws IOException { 541 return Files.newInputStream(file); 542 } 543 @Override 544 public int getContentLength() throws IOException { 545 long size = Files.size(file); 546 return (size > Integer.MAX_VALUE) ? -1 : (int)size; 547 } 548 }; 549 } 550 551 @Override 552 public Stream<String> list() throws IOException { 553 return Files.find(dir, Integer.MAX_VALUE, 554 (path, attrs) -> attrs.isRegularFile()) 555 .map(f -> dir.relativize(f) 556 .toString() 557 .replace(File.separatorChar, '/')); 558 } 559 } 560 561 562 /** 563 * Derives a package name from a file path to a .class file. 564 */ 565 private static String toPackageName(Path top, Path file) { 566 Path entry = top.relativize(file); 567 Path parent = entry.getParent(); 568 if (parent == null) { 569 return warnIfModuleInfo(top, entry.toString()); 570 } else { 571 return parent.toString().replace(File.separatorChar, '.'); 572 } 573 } 574 575 /** 576 * Derives a package name from the name of an entry in a JAR file. 577 */ 578 private static String toPackageName(Path file, JarEntry entry) { 579 String name = entry.getName(); 580 int index = name.lastIndexOf("/"); 581 if (index == -1) { 582 return warnIfModuleInfo(file, name); 583 } else { 584 return name.substring(0, index).replace('/', '.'); 585 } 586 } 587 588 private static String warnIfModuleInfo(Path file, String e) { 589 if (e.equals("module-info.class")) 590 System.err.println("WARNING: " + e + " ignored in patch: " + file); 591 return ""; 592 } 593 }