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