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