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