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