blob: 73d14c3e4c6c56f0b5c84b640100ad3eeea64d41 [file] [log] [blame]
Stuart McCulloch5ec302d2008-12-04 07:58:07 +00001/* Copyright 2006 aQute SARL
2 * Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
3package aQute.lib.osgi;
4
5import java.io.*;
6import java.util.*;
7import java.util.jar.*;
8import java.util.regex.*;
9
10import aQute.libg.qtokens.*;
11
12public class Verifier extends Analyzer {
13
14 Jar dot;
15 Manifest manifest;
16 Map<String, Map<String, String>> referred = newHashMap();
17 Map<String, Map<String, String>> contained = newHashMap();
18 Map<String, Set<String>> uses = newHashMap();
19 Map<String, Map<String, String>> mimports;
20 Map<String, Map<String, String>> mdynimports;
21 Map<String, Map<String, String>> mexports;
22 List<Jar> bundleClasspath;
23 Map<String, Map<String, String>> ignore = newHashMap(); // Packages
24 // to
25 // ignore
26
27 Map<String, Clazz> classSpace;
28 boolean r3;
29 boolean usesRequire;
30 boolean fragment;
31 Attributes main;
32
33 final static Pattern EENAME = Pattern
34 .compile("CDC-1\\.0/Foundation-1\\.0"
35 + "|CDC-1\\.1/Foundation-1\\.1"
36 + "|OSGi/Minimum-1\\.1"
37 + "|JRE-1\\.1"
38 + "|J2SE-1\\.2"
39 + "|J2SE-1\\.3"
40 + "|J2SE-1\\.4"
41 + "|J2SE-1\\.5"
42 + "|PersonalJava-1\\.1"
43 + "|PersonalJava-1\\.2"
44 + "|CDC-1\\.0/PersonalBasis-1\\.0"
45 + "|CDC-1\\.0/PersonalJava-1\\.0");
46
47 final static Pattern BUNDLEMANIFESTVERSION = Pattern
48 .compile("2");
49 public final static String SYMBOLICNAME_STRING = "[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*";
50 public final static Pattern SYMBOLICNAME = Pattern
51 .compile(SYMBOLICNAME_STRING);
52
53 public final static String VERSION_STRING = "[0-9]+(\\.[0-9]+(\\.[0-9]+(\\.[0-9A-Za-z_-]+)?)?)?";
54 public final static Pattern VERSION = Pattern
55 .compile(VERSION_STRING);
56 final static Pattern FILTEROP = Pattern
57 .compile("=|<=|>=|~=");
58 final static Pattern VERSIONRANGE = Pattern
59 .compile("((\\(|\\[)"
60 + VERSION_STRING
61 + ","
62 + VERSION_STRING
63 + "(\\]|\\)))|"
64 + VERSION_STRING);
65 final static Pattern FILE = Pattern
66 .compile("/?[^/\"\n\r\u0000]+(/[^/\"\n\r\u0000]+)*");
67 final static Pattern WILDCARDPACKAGE = Pattern
68 .compile("((\\p{Alnum}|_)+(\\.(\\p{Alnum}|_)+)*(\\.\\*)?)|\\*");
69 final static Pattern ISO639 = Pattern
70 .compile("[A-Z][A-Z]");
71 public static Pattern HEADER_PATTERN = Pattern
72 .compile("[A-Za-z0-9][-a-zA-Z0-9_]+");
73 public static Pattern TOKEN = Pattern
74 .compile("[-a-zA-Z0-9_]+");
75
76 Properties properties;
77
78 public Verifier(Jar jar) throws Exception {
79 this(jar, null);
80 }
81
82 public Verifier(Jar jar, Properties properties) throws Exception {
83 this.dot = jar;
84 this.properties = properties;
85 this.manifest = jar.getManifest();
86 if (manifest == null) {
87 manifest = new Manifest();
88 error("This file contains no manifest and is therefore not a bundle");
89 }
90 main = this.manifest.getMainAttributes();
91 verifyHeaders(main);
92 r3 = getHeader(Analyzer.BUNDLE_MANIFESTVERSION) == null;
93 usesRequire = getHeader(Analyzer.REQUIRE_BUNDLE) != null;
94 fragment = getHeader(Analyzer.FRAGMENT_HOST) != null;
95
96 bundleClasspath = getBundleClassPath();
97 mimports = parseHeader(manifest.getMainAttributes().getValue(
98 Analyzer.IMPORT_PACKAGE));
99 mdynimports = parseHeader(manifest.getMainAttributes().getValue(
100 Analyzer.DYNAMICIMPORT_PACKAGE));
101 mexports = parseHeader(manifest.getMainAttributes().getValue(
102 Analyzer.EXPORT_PACKAGE));
103
104 ignore = parseHeader(manifest.getMainAttributes().getValue(
105 Analyzer.IGNORE_PACKAGE));
106 }
107
108 public Verifier() {
109 // TODO Auto-generated constructor stub
110 }
111
112 private void verifyHeaders(Attributes main) {
113 for (Object element : main.keySet()) {
114 Attributes.Name header = (Attributes.Name) element;
115 String h = header.toString();
116 if (!HEADER_PATTERN.matcher(h).matches())
117 error("Invalid Manifest header: " + h + ", pattern="
118 + HEADER_PATTERN);
119 }
120 }
121
122 private List<Jar> getBundleClassPath() {
123 List<Jar> list = newList();
124 String bcp = getHeader(Analyzer.BUNDLE_CLASSPATH);
125 if (bcp == null) {
126 list.add(dot);
127 } else {
128 Map<String,Map<String,String>> entries = parseHeader(bcp);
129 for (String jarOrDir : entries.keySet()) {
130 if (jarOrDir.equals(".")) {
131 list.add(dot);
132 } else {
133 if (jarOrDir.equals("/"))
134 jarOrDir = "";
135 if (jarOrDir.endsWith("/")) {
136 error("Bundle-Classpath directory must not end with a slash: "
137 + jarOrDir);
138 jarOrDir = jarOrDir.substring(0, jarOrDir.length() - 1);
139 }
140
141 Resource resource = dot.getResource(jarOrDir);
142 if (resource != null) {
143 try {
144 Jar sub = new Jar(jarOrDir);
145 addClose(sub);
146 EmbeddedResource.build(sub, resource);
147 if (!jarOrDir.endsWith(".jar"))
148 warning("Valid JAR file on Bundle-Classpath does not have .jar extension: "
149 + jarOrDir);
150 list.add(sub);
151 } catch (Exception e) {
152 error("Invalid embedded JAR file on Bundle-Classpath: "
153 + jarOrDir + ", " + e);
154 }
155 } else if (dot.getDirectories().containsKey(jarOrDir)) {
156 if (r3)
157 error("R3 bundles do not support directories on the Bundle-ClassPath: "
158 + jarOrDir);
159
160 try {
161 Jar sub = new Jar(jarOrDir);
Stuart McCullochf2418922008-12-04 08:57:54 +0000162 addClose(sub);
Stuart McCulloch5ec302d2008-12-04 07:58:07 +0000163 // TODO verify if directory exists and see how to
164 // get it in a JAR ...
165 list.add(sub);
166 } catch (Exception e) {
167 error("Invalid embedded directory file on Bundle-Classpath: "
168 + jarOrDir + ", " + e);
169 }
170 } else {
171 error("Cannot find a file or directory for Bundle-Classpath entry: "
172 + jarOrDir);
173 }
174 }
175 }
176 }
177 return list;
178 }
179
180 /*
181 * Bundle-NativeCode ::= nativecode ( ',' nativecode )* ( ’,’ optional) ?
182 * nativecode ::= path ( ';' path )* // See 1.4.2 ( ';' parameter )+
183 * optional ::= ’*’
184 */
185 public void verifyNative() {
186 String nc = getHeader("Bundle-NativeCode");
187 doNative(nc);
188 }
189
190 public void doNative(String nc) {
191 if (nc != null) {
192 QuotedTokenizer qt = new QuotedTokenizer(nc, ",;=", false);
193 char del;
194 do {
195 do {
196 String name = qt.nextToken();
197 if (name == null) {
198 error("Can not parse name from bundle native code header: "
199 + nc);
200 return;
201 }
202 del = qt.getSeparator();
203 if (del == ';') {
204 if (dot != null && !dot.exists(name)) {
205 error("Native library not found in JAR: " + name);
206 }
207 } else {
208 String value = null;
209 if (del == '=')
210 value = qt.nextToken();
211
212 String key = name.toLowerCase();
213 if (key.equals("osname")) {
214 // ...
215 } else if (key.equals("osversion")) {
216 // verify version range
217 verify(value, VERSIONRANGE);
218 } else if (key.equals("language")) {
219 verify(value, ISO639);
220 } else if (key.equals("processor")) {
221 // verify(value, PROCESSORS);
222 } else if (key.equals("selection-filter")) {
223 // verify syntax filter
224 verifyFilter(value);
225 } else if (name.equals("*") && value == null) {
226 // Wildcard must be at end.
227 if (qt.nextToken() != null)
228 error("Bundle-Native code header may only END in wildcard: nc");
229 } else {
230 warning("Unknown attribute in native code: " + name
231 + "=" + value);
232 }
233 del = qt.getSeparator();
234 }
235 } while (del == ';');
236 } while (del == ',');
237 }
238 }
239
240 public void verifyFilter(String value) {
241 try {
242 verifyFilter(value, 0);
243 } catch (Exception e) {
244 error("Not a valid filter: " + value + e.getMessage());
245 }
246 }
247
248 private void verifyActivator() {
249 String bactivator = getHeader("Bundle-Activator");
250 if (bactivator != null) {
251 Clazz cl = loadClass(bactivator);
252 if (cl == null) {
253 int n = bactivator.lastIndexOf('.');
254 if (n > 0) {
255 String pack = bactivator.substring(0, n);
256 if (mimports.containsKey(pack))
257 return;
258 error("Bundle-Activator not found on the bundle class path nor in imports: "
259 + bactivator);
260 } else
261 error("Activator uses default package and is not local (default package can not be imported): "
262 + bactivator);
263 }
264 }
265 }
266
267 private Clazz loadClass(String className) {
268 String path = className.replace('.', '/') + ".class";
269 return (Clazz) classSpace.get(path);
270 }
271
272 private void verifyComponent() {
273 String serviceComponent = getHeader("Service-Component");
274 if (serviceComponent != null) {
275 Map<String,Map<String,String>> map = parseHeader(serviceComponent);
276 for (String component : map.keySet()) {
277 if (!dot.exists(component)) {
278 error("Service-Component entry can not be located in JAR: "
279 + component);
280 } else {
281 // validate component ...
282 }
283 }
284 }
285 }
286
287 public void info() {
288 System.out.println("Refers : " + referred);
289 System.out.println("Contains : " + contained);
290 System.out.println("Manifest Imports : " + mimports);
291 System.out.println("Manifest Exports : " + mexports);
292 }
293
294 /**
295 * Invalid exports are exports mentioned in the manifest but not found on
296 * the classpath. This can be calculated with: exports - contains.
297 *
298 * Unfortunately, we also must take duplicate names into account. These
299 * duplicates are of course no erroneous.
300 */
301 private void verifyInvalidExports() {
302 Set<String> invalidExport = newSet(mexports.keySet());
303 invalidExport.removeAll(contained.keySet());
304
305 // We might have duplicate names that are marked for it. These
306 // should not be counted. Should we test them against the contained
307 // set? Hmm. If someone wants to hang himself by using duplicates than
308 // I guess he can go ahead ... This is not a recommended practice
309 for (Iterator<String> i = invalidExport.iterator(); i.hasNext();) {
310 String pack = i.next();
311 if (isDuplicate(pack))
312 i.remove();
313 }
314
315 if (!invalidExport.isEmpty())
316 error("Exporting packages that are not on the Bundle-Classpath"
317 + bundleClasspath + ": " + invalidExport);
318 }
319
320 /**
321 * Invalid imports are imports that we never refer to. They can be
322 * calculated by removing the refered packages from the imported packages.
323 * This leaves packages that the manifest imported but that we never use.
324 */
325 private void verifyInvalidImports() {
326 Set<String> invalidImport = newSet(mimports.keySet());
327 invalidImport.removeAll(referred.keySet());
328 // TODO Added this line but not sure why it worked before ...
329 invalidImport.removeAll(contained.keySet());
330 String bactivator = getHeader(Analyzer.BUNDLE_ACTIVATOR);
331 if (bactivator != null) {
332 int n = bactivator.lastIndexOf('.');
333 if (n > 0) {
334 invalidImport.remove(bactivator.substring(0, n));
335 }
336 }
337 if (isPedantic() && !invalidImport.isEmpty())
338 warning("Importing packages that are never refered to by any class on the Bundle-Classpath"
339 + bundleClasspath + ": " + invalidImport);
340 }
341
342 /**
343 * Check for unresolved imports. These are referals that are not imported by
344 * the manifest and that are not part of our bundle classpath. The are
345 * calculated by removing all the imported packages and contained from the
346 * refered packages.
347 */
348 private void verifyUnresolvedReferences() {
349 Set<String> unresolvedReferences = new TreeSet<String>(referred
350 .keySet());
351 unresolvedReferences.removeAll(mimports.keySet());
352 unresolvedReferences.removeAll(contained.keySet());
353
354 // Remove any java.** packages.
355 for (Iterator<String> p = unresolvedReferences.iterator(); p.hasNext();) {
356 String pack = p.next();
357 if (pack.startsWith("java.") || ignore.containsKey(pack))
358 p.remove();
359 else {
360 // Remove any dynamic imports
361 if (isDynamicImport(pack))
362 p.remove();
363 }
364 }
365
366 if (!unresolvedReferences.isEmpty()) {
367 // Now we want to know the
368 // classes that are the culprits
369 Set<String> culprits = new HashSet<String>();
370 for (Clazz clazz : classSpace.values()) {
371 if (hasOverlap(unresolvedReferences, clazz.imports.keySet()))
372 culprits.add(clazz.getPath());
373 }
374
375 error("Unresolved references to " + unresolvedReferences
376 + " by class(es) on the Bundle-Classpath" + bundleClasspath
377 + ": " + culprits);
378 }
379 }
380
381 /**
382 * @param p
383 * @param pack
384 */
385 private boolean isDynamicImport(String pack) {
386 for (String pattern : mdynimports.keySet()) {
387 // Wildcard?
388 if (pattern.equals("*"))
389 return true; // All packages can be dynamically imported
390
391 if (pattern.endsWith(".*")) {
392 pattern = pattern.substring(0, pattern.length() - 2);
393 if (pack.startsWith(pattern)
394 && (pack.length() == pattern.length() || pack
395 .charAt(pattern.length()) == '.'))
396 return true;
397 } else {
398 if (pack.equals(pattern))
399 return true;
400 }
401 }
402 return false;
403 }
404
405 private boolean hasOverlap(Set<?> a, Set<?> b) {
406 for (Iterator<?> i = a.iterator(); i.hasNext();) {
407 if (b.contains(i.next()))
408 return true;
409 }
410 return false;
411 }
412
413 public void verify() throws IOException {
414 if (classSpace == null)
415 classSpace = analyzeBundleClasspath(dot,
416 parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH)),
417 contained, referred, uses);
418 verifyManifestFirst();
419 verifyActivator();
420 verifyComponent();
421 verifyNative();
422 verifyInvalidExports();
423 verifyInvalidImports();
424 verifyUnresolvedReferences();
425 verifySymbolicName();
426 verifyListHeader("Bundle-RequiredExecutionEnvironment", EENAME, false);
427 verifyHeader("Bundle-ManifestVersion", BUNDLEMANIFESTVERSION, false);
428 verifyHeader("Bundle-Version", VERSION, true);
429 verifyListHeader("Bundle-Classpath", FILE, false);
430 verifyDynamicImportPackage();
431 if (usesRequire) {
432 if (!getErrors().isEmpty()) {
433 getWarnings()
434 .add(
435 0,
436 "Bundle uses Require Bundle, this can generate false errors because then not enough information is available without the required bundles");
437 }
438 }
439 }
440
441 /**
442 * <pre>
443 * DynamicImport-Package ::= dynamic-description
444 * ( ',' dynamic-description )*
445 *
446 * dynamic-description::= wildcard-names ( ';' parameter )*
447 * wildcard-names ::= wildcard-name ( ';' wildcard-name )*
448 * wildcard-name ::= package-name
449 * | ( package-name '.*' ) // See 1.4.2
450 * | '*'
451 * </pre>
452 */
453 private void verifyDynamicImportPackage() {
454 verifyListHeader("DynamicImport-Package", WILDCARDPACKAGE, true);
455 String dynamicImportPackage = getHeader("DynamicImport-Package");
456 if (dynamicImportPackage == null)
457 return;
458
459 Map<String, Map<String,String>> map = parseHeader(dynamicImportPackage);
460 for (String name : map.keySet()) {
461 name = name.trim();
462 if (!verify(name, WILDCARDPACKAGE))
463 error("DynamicImport-Package header contains an invalid package name: "
464 + name);
465
466 Map<String,String> sub = map.get(name);
467 if (r3 && sub.size() != 0) {
468 error("DynamicPackage-Import has attributes on import: "
469 + name
470 + ". This is however, an <=R3 bundle and attributes on this header were introduced in R4. ");
471 }
472 }
473 }
474
475 private void verifyManifestFirst() {
476 if (!dot.manifestFirst) {
477 error("Invalid JAR stream: Manifest should come first to be compatible with JarInputStream, it was not");
478 }
479 }
480
481 private void verifySymbolicName() {
482 Map<String,Map<String,String>> bsn = parseHeader(getHeader(Analyzer.BUNDLE_SYMBOLICNAME));
483 if (!bsn.isEmpty()) {
484 if (bsn.size() > 1)
485 error("More than one BSN specified " + bsn);
486
487 String name = (String) bsn.keySet().iterator().next();
488 if (!SYMBOLICNAME.matcher(name).matches()) {
489 error("Symbolic Name has invalid format: " + name);
490 }
491 }
492 }
493
494 /**
495 * <pre>
496 * filter ::= ’(’ filter-comp ’)’
497 * filter-comp ::= and | or | not | operation
498 * and ::= ’&amp;’ filter-list
499 * or ::= ’|’ filter-list
500 * not ::= ’!’ filter
501 * filter-list ::= filter | filter filter-list
502 * operation ::= simple | present | substring
503 * simple ::= attr filter-type value
504 * filter-type ::= equal | approx | greater | less
505 * equal ::= ’=’
506 * approx ::= ’&tilde;=’
507 * greater ::= ’&gt;=’
508 * less ::= ’&lt;=’
509 * present ::= attr ’=*’
510 * substring ::= attr ’=’ initial any final
511 * inital ::= () | value
512 * any ::= ’*’ star-value
513 * star-value ::= () | value ’*’ star-value
514 * final ::= () | value
515 * value ::= &lt;see text&gt;
516 * </pre>
517 *
518 * @param expr
519 * @param index
520 * @return
521 */
522
523 int verifyFilter(String expr, int index) {
524 try {
525 while (Character.isWhitespace(expr.charAt(index)))
526 index++;
527
528 if (expr.charAt(index) != '(')
529 throw new IllegalArgumentException(
530 "Filter mismatch: expected ( at position " + index
531 + " : " + expr);
532
533 index++;
534 while (Character.isWhitespace(expr.charAt(index)))
535 index++;
536
537 switch (expr.charAt(index)) {
538 case '!':
539 case '&':
540 case '|':
541 return verifyFilterSubExpression(expr, index) + 1;
542
543 default:
544 return verifyFilterOperation(expr, index) + 1;
545 }
546 } catch (IndexOutOfBoundsException e) {
547 throw new IllegalArgumentException(
548 "Filter mismatch: early EOF from " + index);
549 }
550 }
551
552 private int verifyFilterOperation(String expr, int index) {
553 StringBuffer sb = new StringBuffer();
554 while ("=><~()".indexOf(expr.charAt(index)) < 0) {
555 sb.append(expr.charAt(index++));
556 }
557 String attr = sb.toString().trim();
558 if (attr.length() == 0)
559 throw new IllegalArgumentException(
560 "Filter mismatch: attr at index " + index + " is 0");
561 sb = new StringBuffer();
562 while ("=><~".indexOf(expr.charAt(index)) >= 0) {
563 sb.append(expr.charAt(index++));
564 }
565 String operator = sb.toString();
566 if (!verify(operator, FILTEROP))
567 throw new IllegalArgumentException(
568 "Filter error, illegal operator " + operator + " at index "
569 + index);
570
571 sb = new StringBuffer();
572 while (")".indexOf(expr.charAt(index)) < 0) {
573 switch (expr.charAt(index)) {
574 case '\\':
575 if (expr.charAt(index + 1) == '*'
576 || expr.charAt(index + 1) == ')')
577 index++;
578 else
579 throw new IllegalArgumentException(
580 "Filter error, illegal use of backslash at index "
581 + index
582 + ". Backslash may only be used before * or (");
583 }
584 sb.append(expr.charAt(index++));
585 }
586 return index;
587 }
588
589 private int verifyFilterSubExpression(String expr, int index) {
590 do {
591 index = verifyFilter(expr, index + 1);
592 while (Character.isWhitespace(expr.charAt(index)))
593 index++;
594 if (expr.charAt(index) != ')')
595 throw new IllegalArgumentException(
596 "Filter mismatch: expected ) at position " + index
597 + " : " + expr);
598 index++;
599 } while (expr.charAt(index) == '(');
600 return index;
601 }
602
603 private String getHeader(String string) {
604 return main.getValue(string);
605 }
606
607 @SuppressWarnings("unchecked")
608 private boolean verifyHeader(String name, Pattern regex, boolean error) {
609 String value = manifest.getMainAttributes().getValue(name);
610 if (value == null)
611 return false;
612
613 QuotedTokenizer st = new QuotedTokenizer(value.trim(), ",");
614 for (Iterator<String> i = st.getTokenSet().iterator(); i.hasNext();) {
615 if (!verify((String) i.next(), regex)) {
616 String msg = "Invalid value for " + name + ", " + value
617 + " does not match " + regex.pattern();
618 if (error)
619 error(msg);
620 else
621 warning(msg);
622 }
623 }
624 return true;
625 }
626
627 private boolean verify(String value, Pattern regex) {
628 return regex.matcher(value).matches();
629 }
630
631 private boolean verifyListHeader(String name, Pattern regex, boolean error) {
632 String value = manifest.getMainAttributes().getValue(name);
633 if (value == null)
634 return false;
635
636 Map<String,Map<String,String>> map = parseHeader(value);
637 for (String header : map.keySet()) {
638 if (!regex.matcher(header).matches()) {
639 String msg = "Invalid value for " + name + ", " + value
640 + " does not match " + regex.pattern();
641 if (error)
642 error(msg);
643 else
644 warning(msg);
645 }
646 }
647 return true;
648 }
649
650 public String getProperty(String key, String deflt) {
651 if (properties == null)
652 return deflt;
653 return properties.getProperty(key, deflt);
654 }
655
656 public void setClassSpace(Map<String,Clazz> classspace,
657 Map<String, Map<String, String>> contained,
658 Map<String, Map<String, String>> referred,
659 Map<String, Set<String>> uses) {
660 this.classSpace = classspace;
661 this.contained = contained;
662 this.referred = referred;
663 this.uses = uses;
664 }
665
666 public static boolean isVersion(String version) {
667 return VERSION.matcher(version).matches();
668 }
669
670}