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