1 /* 2 * Copyright (c) 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 26 package sun.tools.jar; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.lang.module.InvalidModuleDescriptorException; 32 import java.lang.module.ModuleDescriptor; 33 import java.lang.module.ModuleDescriptor.Exports; 34 import java.lang.module.InvalidModuleDescriptorException; 35 import java.lang.module.ModuleDescriptor.Opens; 36 import java.lang.module.ModuleDescriptor.Provides; 37 import java.lang.module.ModuleDescriptor.Requires; 38 import java.util.Collections; 39 import java.util.Comparator; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 import java.util.function.Consumer; 46 import java.util.jar.JarEntry; 47 import java.util.jar.JarFile; 48 import java.util.zip.ZipEntry; 49 50 import static java.util.jar.JarFile.MANIFEST_NAME; 51 import static sun.tools.jar.Main.VERSIONS_DIR; 52 import static sun.tools.jar.Main.VERSIONS_DIR_LENGTH; 53 import static sun.tools.jar.Main.MODULE_INFO; 54 import static sun.tools.jar.Main.getMsg; 55 import static sun.tools.jar.Main.formatMsg; 56 import static sun.tools.jar.Main.formatMsg2; 57 import static sun.tools.jar.Main.toBinaryName; 58 import static sun.tools.jar.Main.isModuleInfoEntry; 59 60 final class Validator { 61 private final static boolean DEBUG = Boolean.getBoolean("jar.debug"); 62 private final Map<String,FingerPrint> fps = new HashMap<>(); 63 private final Main main; 64 private final JarFile jf; 65 private int oldVersion = -1; 66 private String currentTopLevelName; 67 private boolean isValid = true; 68 private Set<String> concealedPkgs = Collections.emptySet(); 69 private ModuleDescriptor md; 70 private String mdName; 71 72 private Validator(Main main, JarFile jf) { 73 this.main = main; 74 this.jf = jf; 75 checkModuleDescriptor(MODULE_INFO); 76 } 77 78 static boolean validate(Main main, JarFile jf) throws IOException { 79 return new Validator(main, jf).validate(); 80 } 81 82 private boolean validate() { 83 try { 84 jf.stream() 85 .filter(e -> !e.isDirectory() && 86 !e.getName().equals(MANIFEST_NAME)) 87 .sorted(ENTRY_COMPARATOR) 88 .forEachOrdered(e -> validate(e)); 89 return isValid; 90 } catch (InvalidJarException e) { 91 error(formatMsg("error.validator.bad.entry.name", e.getMessage())); 92 } 93 return false; 94 } 95 96 private static class InvalidJarException extends RuntimeException { 97 private static final long serialVersionUID = -3642329147299217726L; 98 InvalidJarException(String msg) { 99 super(msg); 100 } 101 } 102 103 // sort base entries before versioned entries, and sort entry classes with 104 // nested classes so that the top level class appears before the associated 105 // nested class 106 static Comparator<String> ENTRYNAME_COMPARATOR = (s1, s2) -> { 107 108 if (s1.equals(s2)) return 0; 109 boolean b1 = s1.startsWith(VERSIONS_DIR); 110 boolean b2 = s2.startsWith(VERSIONS_DIR); 111 if (b1 && !b2) return 1; 112 if (!b1 && b2) return -1; 113 int n = 0; // starting char for String compare 114 if (b1 && b2) { 115 // normally strings would be sorted so "10" goes before "9", but 116 // version number strings need to be sorted numerically 117 n = VERSIONS_DIR.length(); // skip the common prefix 118 int i1 = s1.indexOf('/', n); 119 int i2 = s2.indexOf('/', n); 120 if (i1 == -1) throw new InvalidJarException(s1); 121 if (i2 == -1) throw new InvalidJarException(s2); 122 // shorter version numbers go first 123 if (i1 != i2) return i1 - i2; 124 // otherwise, handle equal length numbers below 125 } 126 int l1 = s1.length(); 127 int l2 = s2.length(); 128 int lim = Math.min(l1, l2); 129 for (int k = n; k < lim; k++) { 130 char c1 = s1.charAt(k); 131 char c2 = s2.charAt(k); 132 if (c1 != c2) { 133 // change natural ordering so '.' comes before '$' 134 // i.e. top level classes come before nested classes 135 if (c1 == '$' && c2 == '.') return 1; 136 if (c1 == '.' && c2 == '$') return -1; 137 return c1 - c2; 138 } 139 } 140 return l1 - l2; 141 }; 142 143 static Comparator<ZipEntry> ENTRY_COMPARATOR = 144 Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR); 145 146 /* 147 * Validator has state and assumes entries provided to accept are ordered 148 * from base entries first and then through the versioned entries in 149 * ascending version order. Also, to find isolated nested classes, 150 * classes must be ordered so that the top level class is before the associated 151 * nested class(es). 152 */ 153 public void validate(JarEntry je) { 154 String entryName = je.getName(); 155 156 // directories are always accepted 157 if (entryName.endsWith("/")) { 158 debug("%s is a directory", entryName); 159 return; 160 } 161 162 // validate the versioned module-info 163 if (isModuleInfoEntry(entryName)) { 164 if (!entryName.equals(mdName)) 165 checkModuleDescriptor(entryName); 166 return; 167 } 168 169 // figure out the version and basename from the JarEntry 170 int version; 171 String basename; 172 String versionStr = null;; 173 if (entryName.startsWith(VERSIONS_DIR)) { 174 int n = entryName.indexOf("/", VERSIONS_DIR_LENGTH); 175 if (n == -1) { 176 error(formatMsg("error.validator.version.notnumber", entryName)); 177 isValid = false; 178 return; 179 } 180 versionStr = entryName.substring(VERSIONS_DIR_LENGTH, n); 181 try { 182 version = Integer.parseInt(versionStr); 183 } catch (NumberFormatException x) { 184 error(formatMsg("error.validator.version.notnumber", entryName)); 185 isValid = false; 186 return; 187 } 188 if (n == entryName.length()) { 189 error(formatMsg("error.validator.entryname.tooshort", entryName)); 190 isValid = false; 191 return; 192 } 193 basename = entryName.substring(n + 1); 194 } else { 195 version = 0; 196 basename = entryName; 197 } 198 debug("\n===================\nversion %d %s", version, entryName); 199 200 if (oldVersion != version) { 201 oldVersion = version; 202 currentTopLevelName = null; 203 if (md == null && versionStr != null) { 204 // don't have a base module-info.class yet, try to see if 205 // a versioned one exists 206 checkModuleDescriptor(VERSIONS_DIR + versionStr + "/" + MODULE_INFO); 207 } 208 } 209 210 // analyze the entry, keeping key attributes 211 FingerPrint fp; 212 try (InputStream is = jf.getInputStream(je)) { 213 fp = new FingerPrint(basename, is.readAllBytes()); 214 } catch (IOException x) { 215 error(x.getMessage()); 216 isValid = false; 217 return; 218 } 219 String internalName = fp.name(); 220 221 // process a base entry paying attention to nested classes 222 if (version == 0) { 223 debug("base entry found"); 224 if (fp.isNestedClass()) { 225 debug("nested class found"); 226 if (fp.topLevelName().equals(currentTopLevelName)) { 227 fps.put(internalName, fp); 228 return; 229 } 230 error(formatMsg("error.validator.isolated.nested.class", entryName)); 231 isValid = false; 232 return; 233 } 234 // top level class or resource entry 235 if (fp.isClass()) { 236 currentTopLevelName = fp.topLevelName(); 237 if (!checkInternalName(entryName, basename, internalName)) { 238 isValid = false; 239 return; 240 } 241 } 242 fps.put(internalName, fp); 243 return; 244 } 245 246 // process a versioned entry, look for previous entry with same name 247 FingerPrint matchFp = fps.get(internalName); 248 debug("looking for match"); 249 if (matchFp == null) { 250 debug("no match found"); 251 if (fp.isClass()) { 252 if (fp.isNestedClass()) { 253 if (!checkNestedClass(version, entryName, internalName, fp)) { 254 isValid = false; 255 } 256 return; 257 } 258 if (fp.isPublicClass()) { 259 if (!isConcealed(internalName)) { 260 error(Main.formatMsg("error.validator.new.public.class", entryName)); 261 isValid = false; 262 return; 263 } 264 warn(formatMsg("warn.validator.concealed.public.class", entryName)); 265 debug("%s is a public class entry in a concealed package", entryName); 266 } 267 debug("%s is a non-public class entry", entryName); 268 fps.put(internalName, fp); 269 currentTopLevelName = fp.topLevelName(); 270 return; 271 } 272 debug("%s is a resource entry"); 273 fps.put(internalName, fp); 274 return; 275 } 276 debug("match found"); 277 278 // are the two classes/resources identical? 279 if (fp.isIdentical(matchFp)) { 280 warn(formatMsg("warn.validator.identical.entry", entryName)); 281 return; // it's okay, just takes up room 282 } 283 debug("sha1 not equal -- different bytes"); 284 285 // ok, not identical, check for compatible class version and api 286 if (fp.isClass()) { 287 if (fp.isNestedClass()) { 288 if (!checkNestedClass(version, entryName, internalName, fp)) { 289 isValid = false; 290 } 291 return; 292 } 293 debug("%s is a class entry", entryName); 294 if (!fp.isCompatibleVersion(matchFp)) { 295 error(formatMsg("error.validator.incompatible.class.version", entryName)); 296 isValid = false; 297 return; 298 } 299 if (!fp.isSameAPI(matchFp)) { 300 error(formatMsg("error.validator.different.api", entryName)); 301 isValid = false; 302 return; 303 } 304 if (!checkInternalName(entryName, basename, internalName)) { 305 isValid = false; 306 return; 307 } 308 debug("fingerprints same -- same api"); 309 fps.put(internalName, fp); 310 currentTopLevelName = fp.topLevelName(); 311 return; 312 } 313 debug("%s is a resource", entryName); 314 315 warn(formatMsg("warn.validator.resources.with.same.name", entryName)); 316 fps.put(internalName, fp); 317 return; 318 } 319 320 /** 321 * Checks whether or not the given versioned module descriptor's attributes 322 * are valid when compared against the root/base module descriptor. 323 * 324 * A versioned module descriptor must be identical to the root/base module 325 * descriptor, with two exceptions: 326 * - A versioned descriptor can have different non-public `requires` 327 * clauses of platform ( `java.*` and `jdk.*` ) modules, and 328 * - A versioned descriptor can have different `uses` clauses, even of 329 * service types defined outside of the platform modules. 330 */ 331 private void checkModuleDescriptor(String miName) { 332 ZipEntry je = jf.getEntry(miName); 333 if (je != null) { 334 try (InputStream jis = jf.getInputStream(je)) { 335 ModuleDescriptor md = ModuleDescriptor.read(jis); 336 // Initialize the base md if it's not yet. A "base" md can be either the 337 // root module-info.class or the first versioned module-info.class 338 ModuleDescriptor base = this.md; 339 340 if (base == null) { 341 concealedPkgs = new HashSet<>(md.packages()); 342 md.exports().stream().map(Exports::source).forEach(concealedPkgs::remove); 343 md.opens().stream().map(Opens::source).forEach(concealedPkgs::remove); 344 // must have the implementation class of the services it 'provides'. 345 if (md.provides().stream().map(Provides::providers) 346 .flatMap(List::stream) 347 .filter(p -> jf.getEntry(toBinaryName(p)) == null) 348 .peek(p -> error(formatMsg("error.missing.provider", p))) 349 .count() != 0) { 350 isValid = false; 351 return; 352 } 353 this.md = md; 354 this.mdName = miName; 355 return; 356 } 357 358 if (!base.name().equals(md.name())) { 359 error(getMsg("error.validator.info.name.notequal")); 360 isValid = false; 361 } 362 if (!base.requires().equals(md.requires())) { 363 Set<Requires> baseRequires = base.requires(); 364 for (Requires r : md.requires()) { 365 if (baseRequires.contains(r)) 366 continue; 367 if (r.modifiers().contains(Requires.Modifier.TRANSITIVE)) { 368 error(getMsg("error.validator.info.requires.transitive")); 369 isValid = false; 370 } else if (!isPlatformModule(r.name())) { 371 error(getMsg("error.validator.info.requires.added")); 372 isValid = false; 373 } 374 } 375 for (Requires r : baseRequires) { 376 Set<Requires> mdRequires = md.requires(); 377 if (mdRequires.contains(r)) 378 continue; 379 if (!isPlatformModule(r.name())) { 380 error(getMsg("error.validator.info.requires.dropped")); 381 isValid = false; 382 } 383 } 384 } 385 if (!base.exports().equals(md.exports())) { 386 error(getMsg("error.validator.info.exports.notequal")); 387 isValid = false; 388 } 389 if (!base.opens().equals(md.opens())) { 390 error(getMsg("error.validator.info.opens.notequal")); 391 isValid = false; 392 } 393 if (!base.provides().equals(md.provides())) { 394 error(getMsg("error.validator.info.provides.notequal")); 395 isValid = false; 396 } 397 if (!base.mainClass().equals(md.mainClass())) { 398 error(formatMsg("error.validator.info.manclass.notequal", je.getName())); 399 isValid = false; 400 } 401 if (!base.version().equals(md.version())) { 402 error(formatMsg("error.validator.info.version.notequal", je.getName())); 403 isValid = false; 404 } 405 } catch (Exception x) { 406 error(x.getMessage() + " : " + miName); 407 this.isValid = false; 408 } 409 } 410 } 411 412 private static boolean isPlatformModule(String name) { 413 return name.startsWith("java.") || name.startsWith("jdk."); 414 } 415 416 private boolean checkInternalName(String entryName, String basename, String internalName) { 417 String className = className(basename); 418 if (internalName.equals(className)) { 419 return true; 420 } 421 error(formatMsg2("error.validator.names.mismatch", 422 entryName, internalName.replace("/", "."))); 423 return false; 424 } 425 426 private boolean checkNestedClass(int version, String entryName, String internalName, FingerPrint fp) { 427 debug("%s is a nested class entry in top level class %s", entryName, fp.topLevelName()); 428 if (fp.topLevelName().equals(currentTopLevelName)) { 429 debug("%s (top level class) was accepted", fp.topLevelName()); 430 fps.put(internalName, fp); 431 return true; 432 } 433 debug("top level class was not accepted"); 434 error(formatMsg("error.validator.isolated.nested.class", entryName)); 435 return false; 436 } 437 438 private String className(String entryName) { 439 return entryName.endsWith(".class") ? entryName.substring(0, entryName.length() - 6) : null; 440 } 441 442 private boolean isConcealed(String internalName) { 443 if (concealedPkgs.isEmpty()) { 444 return false; 445 } 446 int idx = internalName.lastIndexOf('/'); 447 String pkgName = idx != -1 ? internalName.substring(0, idx).replace('/', '.') : ""; 448 return concealedPkgs.contains(pkgName); 449 } 450 451 private void debug(String fmt, Object... args) { 452 if (DEBUG) System.err.format(fmt, args); 453 } 454 455 private void error(String msg) { 456 main.error(msg); 457 } 458 459 private void warn(String msg) { 460 main.warn(msg); 461 } 462 463 }