blob: 896443268e814cbd3cb2c1983d66b6a63366025c [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);
162 EmbeddedResource.build(sub, resource);
163
164 // TODO verify if directory exists and see how to
165 // get it in a JAR ...
166 list.add(sub);
167 } catch (Exception e) {
168 error("Invalid embedded directory file on Bundle-Classpath: "
169 + jarOrDir + ", " + e);
170 }
171 } else {
172 error("Cannot find a file or directory for Bundle-Classpath entry: "
173 + jarOrDir);
174 }
175 }
176 }
177 }
178 return list;
179 }
180
181 /*
182 * Bundle-NativeCode ::= nativecode ( ',' nativecode )* ( ’,’ optional) ?
183 * nativecode ::= path ( ';' path )* // See 1.4.2 ( ';' parameter )+
184 * optional ::= ’*’
185 */
186 public void verifyNative() {
187 String nc = getHeader("Bundle-NativeCode");
188 doNative(nc);
189 }
190
191 public void doNative(String nc) {
192 if (nc != null) {
193 QuotedTokenizer qt = new QuotedTokenizer(nc, ",;=", false);
194 char del;
195 do {
196 do {
197 String name = qt.nextToken();
198 if (name == null) {
199 error("Can not parse name from bundle native code header: "
200 + nc);
201 return;
202 }
203 del = qt.getSeparator();
204 if (del == ';') {
205 if (dot != null && !dot.exists(name)) {
206 error("Native library not found in JAR: " + name);
207 }
208 } else {
209 String value = null;
210 if (del == '=')
211 value = qt.nextToken();
212
213 String key = name.toLowerCase();
214 if (key.equals("osname")) {
215 // ...
216 } else if (key.equals("osversion")) {
217 // verify version range
218 verify(value, VERSIONRANGE);
219 } else if (key.equals("language")) {
220 verify(value, ISO639);
221 } else if (key.equals("processor")) {
222 // verify(value, PROCESSORS);
223 } else if (key.equals("selection-filter")) {
224 // verify syntax filter
225 verifyFilter(value);
226 } else if (name.equals("*") && value == null) {
227 // Wildcard must be at end.
228 if (qt.nextToken() != null)
229 error("Bundle-Native code header may only END in wildcard: nc");
230 } else {
231 warning("Unknown attribute in native code: " + name
232 + "=" + value);
233 }
234 del = qt.getSeparator();
235 }
236 } while (del == ';');
237 } while (del == ',');
238 }
239 }
240
241 public void verifyFilter(String value) {
242 try {
243 verifyFilter(value, 0);
244 } catch (Exception e) {
245 error("Not a valid filter: " + value + e.getMessage());
246 }
247 }
248
249 private void verifyActivator() {
250 String bactivator = getHeader("Bundle-Activator");
251 if (bactivator != null) {
252 Clazz cl = loadClass(bactivator);
253 if (cl == null) {
254 int n = bactivator.lastIndexOf('.');
255 if (n > 0) {
256 String pack = bactivator.substring(0, n);
257 if (mimports.containsKey(pack))
258 return;
259 error("Bundle-Activator not found on the bundle class path nor in imports: "
260 + bactivator);
261 } else
262 error("Activator uses default package and is not local (default package can not be imported): "
263 + bactivator);
264 }
265 }
266 }
267
268 private Clazz loadClass(String className) {
269 String path = className.replace('.', '/') + ".class";
270 return (Clazz) classSpace.get(path);
271 }
272
273 private void verifyComponent() {
274 String serviceComponent = getHeader("Service-Component");
275 if (serviceComponent != null) {
276 Map<String,Map<String,String>> map = parseHeader(serviceComponent);
277 for (String component : map.keySet()) {
278 if (!dot.exists(component)) {
279 error("Service-Component entry can not be located in JAR: "
280 + component);
281 } else {
282 // validate component ...
283 }
284 }
285 }
286 }
287
288 public void info() {
289 System.out.println("Refers : " + referred);
290 System.out.println("Contains : " + contained);
291 System.out.println("Manifest Imports : " + mimports);
292 System.out.println("Manifest Exports : " + mexports);
293 }
294
295 /**
296 * Invalid exports are exports mentioned in the manifest but not found on
297 * the classpath. This can be calculated with: exports - contains.
298 *
299 * Unfortunately, we also must take duplicate names into account. These
300 * duplicates are of course no erroneous.
301 */
302 private void verifyInvalidExports() {
303 Set<String> invalidExport = newSet(mexports.keySet());
304 invalidExport.removeAll(contained.keySet());
305
306 // We might have duplicate names that are marked for it. These
307 // should not be counted. Should we test them against the contained
308 // set? Hmm. If someone wants to hang himself by using duplicates than
309 // I guess he can go ahead ... This is not a recommended practice
310 for (Iterator<String> i = invalidExport.iterator(); i.hasNext();) {
311 String pack = i.next();
312 if (isDuplicate(pack))
313 i.remove();
314 }
315
316 if (!invalidExport.isEmpty())
317 error("Exporting packages that are not on the Bundle-Classpath"
318 + bundleClasspath + ": " + invalidExport);
319 }
320
321 /**
322 * Invalid imports are imports that we never refer to. They can be
323 * calculated by removing the refered packages from the imported packages.
324 * This leaves packages that the manifest imported but that we never use.
325 */
326 private void verifyInvalidImports() {
327 Set<String> invalidImport = newSet(mimports.keySet());
328 invalidImport.removeAll(referred.keySet());
329 // TODO Added this line but not sure why it worked before ...
330 invalidImport.removeAll(contained.keySet());
331 String bactivator = getHeader(Analyzer.BUNDLE_ACTIVATOR);
332 if (bactivator != null) {
333 int n = bactivator.lastIndexOf('.');
334 if (n > 0) {
335 invalidImport.remove(bactivator.substring(0, n));
336 }
337 }
338 if (isPedantic() && !invalidImport.isEmpty())
339 warning("Importing packages that are never refered to by any class on the Bundle-Classpath"
340 + bundleClasspath + ": " + invalidImport);
341 }
342
343 /**
344 * Check for unresolved imports. These are referals that are not imported by
345 * the manifest and that are not part of our bundle classpath. The are
346 * calculated by removing all the imported packages and contained from the
347 * refered packages.
348 */
349 private void verifyUnresolvedReferences() {
350 Set<String> unresolvedReferences = new TreeSet<String>(referred
351 .keySet());
352 unresolvedReferences.removeAll(mimports.keySet());
353 unresolvedReferences.removeAll(contained.keySet());
354
355 // Remove any java.** packages.
356 for (Iterator<String> p = unresolvedReferences.iterator(); p.hasNext();) {
357 String pack = p.next();
358 if (pack.startsWith("java.") || ignore.containsKey(pack))
359 p.remove();
360 else {
361 // Remove any dynamic imports
362 if (isDynamicImport(pack))
363 p.remove();
364 }
365 }
366
367 if (!unresolvedReferences.isEmpty()) {
368 // Now we want to know the
369 // classes that are the culprits
370 Set<String> culprits = new HashSet<String>();
371 for (Clazz clazz : classSpace.values()) {
372 if (hasOverlap(unresolvedReferences, clazz.imports.keySet()))
373 culprits.add(clazz.getPath());
374 }
375
376 error("Unresolved references to " + unresolvedReferences
377 + " by class(es) on the Bundle-Classpath" + bundleClasspath
378 + ": " + culprits);
379 }
380 }
381
382 /**
383 * @param p
384 * @param pack
385 */
386 private boolean isDynamicImport(String pack) {
387 for (String pattern : mdynimports.keySet()) {
388 // Wildcard?
389 if (pattern.equals("*"))
390 return true; // All packages can be dynamically imported
391
392 if (pattern.endsWith(".*")) {
393 pattern = pattern.substring(0, pattern.length() - 2);
394 if (pack.startsWith(pattern)
395 && (pack.length() == pattern.length() || pack
396 .charAt(pattern.length()) == '.'))
397 return true;
398 } else {
399 if (pack.equals(pattern))
400 return true;
401 }
402 }
403 return false;
404 }
405
406 private boolean hasOverlap(Set<?> a, Set<?> b) {
407 for (Iterator<?> i = a.iterator(); i.hasNext();) {
408 if (b.contains(i.next()))
409 return true;
410 }
411 return false;
412 }
413
414 public void verify() throws IOException {
415 if (classSpace == null)
416 classSpace = analyzeBundleClasspath(dot,
417 parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH)),
418 contained, referred, uses);
419 verifyManifestFirst();
420 verifyActivator();
421 verifyComponent();
422 verifyNative();
423 verifyInvalidExports();
424 verifyInvalidImports();
425 verifyUnresolvedReferences();
426 verifySymbolicName();
427 verifyListHeader("Bundle-RequiredExecutionEnvironment", EENAME, false);
428 verifyHeader("Bundle-ManifestVersion", BUNDLEMANIFESTVERSION, false);
429 verifyHeader("Bundle-Version", VERSION, true);
430 verifyListHeader("Bundle-Classpath", FILE, false);
431 verifyDynamicImportPackage();
432 if (usesRequire) {
433 if (!getErrors().isEmpty()) {
434 getWarnings()
435 .add(
436 0,
437 "Bundle uses Require Bundle, this can generate false errors because then not enough information is available without the required bundles");
438 }
439 }
440 }
441
442 /**
443 * <pre>
444 * DynamicImport-Package ::= dynamic-description
445 * ( ',' dynamic-description )*
446 *
447 * dynamic-description::= wildcard-names ( ';' parameter )*
448 * wildcard-names ::= wildcard-name ( ';' wildcard-name )*
449 * wildcard-name ::= package-name
450 * | ( package-name '.*' ) // See 1.4.2
451 * | '*'
452 * </pre>
453 */
454 private void verifyDynamicImportPackage() {
455 verifyListHeader("DynamicImport-Package", WILDCARDPACKAGE, true);
456 String dynamicImportPackage = getHeader("DynamicImport-Package");
457 if (dynamicImportPackage == null)
458 return;
459
460 Map<String, Map<String,String>> map = parseHeader(dynamicImportPackage);
461 for (String name : map.keySet()) {
462 name = name.trim();
463 if (!verify(name, WILDCARDPACKAGE))
464 error("DynamicImport-Package header contains an invalid package name: "
465 + name);
466
467 Map<String,String> sub = map.get(name);
468 if (r3 && sub.size() != 0) {
469 error("DynamicPackage-Import has attributes on import: "
470 + name
471 + ". This is however, an <=R3 bundle and attributes on this header were introduced in R4. ");
472 }
473 }
474 }
475
476 private void verifyManifestFirst() {
477 if (!dot.manifestFirst) {
478 error("Invalid JAR stream: Manifest should come first to be compatible with JarInputStream, it was not");
479 }
480 }
481
482 private void verifySymbolicName() {
483 Map<String,Map<String,String>> bsn = parseHeader(getHeader(Analyzer.BUNDLE_SYMBOLICNAME));
484 if (!bsn.isEmpty()) {
485 if (bsn.size() > 1)
486 error("More than one BSN specified " + bsn);
487
488 String name = (String) bsn.keySet().iterator().next();
489 if (!SYMBOLICNAME.matcher(name).matches()) {
490 error("Symbolic Name has invalid format: " + name);
491 }
492 }
493 }
494
495 /**
496 * <pre>
497 * filter ::= ’(’ filter-comp ’)’
498 * filter-comp ::= and | or | not | operation
499 * and ::= ’&amp;’ filter-list
500 * or ::= ’|’ filter-list
501 * not ::= ’!’ filter
502 * filter-list ::= filter | filter filter-list
503 * operation ::= simple | present | substring
504 * simple ::= attr filter-type value
505 * filter-type ::= equal | approx | greater | less
506 * equal ::= ’=’
507 * approx ::= ’&tilde;=’
508 * greater ::= ’&gt;=’
509 * less ::= ’&lt;=’
510 * present ::= attr ’=*’
511 * substring ::= attr ’=’ initial any final
512 * inital ::= () | value
513 * any ::= ’*’ star-value
514 * star-value ::= () | value ’*’ star-value
515 * final ::= () | value
516 * value ::= &lt;see text&gt;
517 * </pre>
518 *
519 * @param expr
520 * @param index
521 * @return
522 */
523
524 int verifyFilter(String expr, int index) {
525 try {
526 while (Character.isWhitespace(expr.charAt(index)))
527 index++;
528
529 if (expr.charAt(index) != '(')
530 throw new IllegalArgumentException(
531 "Filter mismatch: expected ( at position " + index
532 + " : " + expr);
533
534 index++;
535 while (Character.isWhitespace(expr.charAt(index)))
536 index++;
537
538 switch (expr.charAt(index)) {
539 case '!':
540 case '&':
541 case '|':
542 return verifyFilterSubExpression(expr, index) + 1;
543
544 default:
545 return verifyFilterOperation(expr, index) + 1;
546 }
547 } catch (IndexOutOfBoundsException e) {
548 throw new IllegalArgumentException(
549 "Filter mismatch: early EOF from " + index);
550 }
551 }
552
553 private int verifyFilterOperation(String expr, int index) {
554 StringBuffer sb = new StringBuffer();
555 while ("=><~()".indexOf(expr.charAt(index)) < 0) {
556 sb.append(expr.charAt(index++));
557 }
558 String attr = sb.toString().trim();
559 if (attr.length() == 0)
560 throw new IllegalArgumentException(
561 "Filter mismatch: attr at index " + index + " is 0");
562 sb = new StringBuffer();
563 while ("=><~".indexOf(expr.charAt(index)) >= 0) {
564 sb.append(expr.charAt(index++));
565 }
566 String operator = sb.toString();
567 if (!verify(operator, FILTEROP))
568 throw new IllegalArgumentException(
569 "Filter error, illegal operator " + operator + " at index "
570 + index);
571
572 sb = new StringBuffer();
573 while (")".indexOf(expr.charAt(index)) < 0) {
574 switch (expr.charAt(index)) {
575 case '\\':
576 if (expr.charAt(index + 1) == '*'
577 || expr.charAt(index + 1) == ')')
578 index++;
579 else
580 throw new IllegalArgumentException(
581 "Filter error, illegal use of backslash at index "
582 + index
583 + ". Backslash may only be used before * or (");
584 }
585 sb.append(expr.charAt(index++));
586 }
587 return index;
588 }
589
590 private int verifyFilterSubExpression(String expr, int index) {
591 do {
592 index = verifyFilter(expr, index + 1);
593 while (Character.isWhitespace(expr.charAt(index)))
594 index++;
595 if (expr.charAt(index) != ')')
596 throw new IllegalArgumentException(
597 "Filter mismatch: expected ) at position " + index
598 + " : " + expr);
599 index++;
600 } while (expr.charAt(index) == '(');
601 return index;
602 }
603
604 private String getHeader(String string) {
605 return main.getValue(string);
606 }
607
608 @SuppressWarnings("unchecked")
609 private boolean verifyHeader(String name, Pattern regex, boolean error) {
610 String value = manifest.getMainAttributes().getValue(name);
611 if (value == null)
612 return false;
613
614 QuotedTokenizer st = new QuotedTokenizer(value.trim(), ",");
615 for (Iterator<String> i = st.getTokenSet().iterator(); i.hasNext();) {
616 if (!verify((String) i.next(), regex)) {
617 String msg = "Invalid value for " + name + ", " + value
618 + " does not match " + regex.pattern();
619 if (error)
620 error(msg);
621 else
622 warning(msg);
623 }
624 }
625 return true;
626 }
627
628 private boolean verify(String value, Pattern regex) {
629 return regex.matcher(value).matches();
630 }
631
632 private boolean verifyListHeader(String name, Pattern regex, boolean error) {
633 String value = manifest.getMainAttributes().getValue(name);
634 if (value == null)
635 return false;
636
637 Map<String,Map<String,String>> map = parseHeader(value);
638 for (String header : map.keySet()) {
639 if (!regex.matcher(header).matches()) {
640 String msg = "Invalid value for " + name + ", " + value
641 + " does not match " + regex.pattern();
642 if (error)
643 error(msg);
644 else
645 warning(msg);
646 }
647 }
648 return true;
649 }
650
651 public String getProperty(String key, String deflt) {
652 if (properties == null)
653 return deflt;
654 return properties.getProperty(key, deflt);
655 }
656
657 public void setClassSpace(Map<String,Clazz> classspace,
658 Map<String, Map<String, String>> contained,
659 Map<String, Map<String, String>> referred,
660 Map<String, Set<String>> uses) {
661 this.classSpace = classspace;
662 this.contained = contained;
663 this.referred = referred;
664 this.uses = uses;
665 }
666
667 public static boolean isVersion(String version) {
668 return VERSION.matcher(version).matches();
669 }
670
671}