blob: ca253c19254c4fd848228c96307c6bf04f02b802 [file] [log] [blame]
Guillaume Nodet1c9211a2014-05-20 08:56:13 +00001/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19package aQute.bnd.osgi;
20
21import java.io.*;
22import java.util.*;
23import java.util.Map.Entry;
24import java.util.jar.*;
25import java.util.regex.*;
26
27import aQute.bnd.header.*;
28import aQute.bnd.osgi.Descriptors.PackageRef;
29import aQute.bnd.osgi.Descriptors.TypeRef;
30import aQute.bnd.version.*;
31import aQute.lib.base64.*;
32import aQute.lib.filter.*;
33import aQute.lib.io.*;
34import aQute.libg.cryptography.*;
35import aQute.libg.qtokens.*;
36
37public class Verifier extends Processor {
38
39 private final Jar dot;
40 private final Manifest manifest;
41 private final Domain main;
42
43 private boolean r3;
44 private boolean usesRequire;
45
46 final static Pattern EENAME = Pattern.compile("CDC-1\\.0/Foundation-1\\.0" + "|CDC-1\\.1/Foundation-1\\.1"
47 + "|OSGi/Minimum-1\\.[1-9]" + "|JRE-1\\.1" + "|J2SE-1\\.2" + "|J2SE-1\\.3"
48 + "|J2SE-1\\.4" + "|J2SE-1\\.5" + "|JavaSE-1\\.6" + "|JavaSE-1\\.7"
49 + "|PersonalJava-1\\.1" + "|PersonalJava-1\\.2"
50 + "|CDC-1\\.0/PersonalBasis-1\\.0" + "|CDC-1\\.0/PersonalJava-1\\.0");
51
52 final static int V1_1 = 45;
53 final static int V1_2 = 46;
54 final static int V1_3 = 47;
55 final static int V1_4 = 48;
56 final static int V1_5 = 49;
57 final static int V1_6 = 50;
58 final static int V1_7 = 51;
59 final static int V1_8 = 52;
60
61 static class EE {
62 String name;
63 int target;
64
65 EE(String name, @SuppressWarnings("unused")
66 int source, int target) {
67 this.name = name;
68 this.target = target;
69 }
70
71 @Override
72 public String toString() {
73 return name + "(" + target + ")";
74 }
75 }
76
77 final static EE[] ees = {
78 new EE("CDC-1.0/Foundation-1.0", V1_3, V1_1),
79 new EE("CDC-1.1/Foundation-1.1", V1_3, V1_2),
80 new EE("OSGi/Minimum-1.0", V1_3, V1_1),
81 new EE("OSGi/Minimum-1.1", V1_3, V1_2),
82 new EE("JRE-1.1", V1_1, V1_1), //
83 new EE("J2SE-1.2", V1_2, V1_1), //
84 new EE("J2SE-1.3", V1_3, V1_1), //
85 new EE("J2SE-1.4", V1_3, V1_2), //
86 new EE("J2SE-1.5", V1_5, V1_5), //
87 new EE("JavaSE-1.6", V1_6, V1_6), //
88 new EE("PersonalJava-1.1", V1_1, V1_1), //
89 new EE("JavaSE-1.7", V1_7, V1_7), //
90 new EE("PersonalJava-1.1", V1_1, V1_1), //
91 new EE("PersonalJava-1.2", V1_1, V1_1), new EE("CDC-1.0/PersonalBasis-1.0", V1_3, V1_1),
92 new EE("CDC-1.0/PersonalJava-1.0", V1_3, V1_1), new EE("CDC-1.1/PersonalBasis-1.1", V1_3, V1_2),
93 new EE("CDC-1.1/PersonalJava-1.1", V1_3, V1_2)
94 };
95
96 final static Pattern CARDINALITY_PATTERN = Pattern.compile("single|multiple");
97 final static Pattern RESOLUTION_PATTERN = Pattern.compile("optional|mandatory");
98 final static Pattern BUNDLEMANIFESTVERSION = Pattern.compile("2");
99 public final static String SYMBOLICNAME_STRING = "[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*";
100 public final static Pattern SYMBOLICNAME = Pattern.compile(SYMBOLICNAME_STRING);
101
102 public final static String VERSION_STRING = "[0-9]{1,9}(\\.[0-9]{1,9}(\\.[0-9]{1,9}(\\.[0-9A-Za-z_-]+)?)?)?";
103 public final static Pattern VERSION = Pattern.compile(VERSION_STRING);
104 final static Pattern FILTEROP = Pattern.compile("=|<=|>=|~=");
105 public final static Pattern VERSIONRANGE = Pattern.compile("((\\(|\\[)"
106
107 + VERSION_STRING + "," + VERSION_STRING + "(\\]|\\)))|"
108 + VERSION_STRING);
109 final static Pattern FILE = Pattern
110 .compile("/?[^/\"\n\r\u0000]+(/[^/\"\n\r\u0000]+)*");
111 final static Pattern WILDCARDPACKAGE = Pattern
112 .compile("((\\p{Alnum}|_)+(\\.(\\p{Alnum}|_)+)*(\\.\\*)?)|\\*");
113 public final static Pattern ISO639 = Pattern.compile("[A-Z][A-Z]");
114 public final static Pattern HEADER_PATTERN = Pattern.compile("[A-Za-z0-9][-a-zA-Z0-9_]+");
115 public final static Pattern TOKEN = Pattern.compile("[-a-zA-Z0-9_]+");
116
117 public final static Pattern NUMBERPATTERN = Pattern.compile("\\d+");
118 public final static Pattern PACKAGEPATTERN = Pattern
119 .compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*");
120 public final static Pattern MULTIPACKAGEPATTERN = Pattern
121 .compile("(" + PACKAGEPATTERN + ")(," + PACKAGEPATTERN + ")*");
122 public final static Pattern PATHPATTERN = Pattern.compile(".*");
123 public final static Pattern FQNPATTERN = Pattern.compile(".*");
124 public final static Pattern URLPATTERN = Pattern.compile(".*");
125 public final static Pattern ANYPATTERN = Pattern.compile(".*");
126 public final static Pattern FILTERPATTERN = Pattern.compile(".*");
127 public final static Pattern TRUEORFALSEPATTERN = Pattern.compile("true|false|TRUE|FALSE");
128 public static final Pattern WILDCARDNAMEPATTERN = Pattern.compile(".*");
129 public static final Pattern BUNDLE_ACTIVATIONPOLICYPATTERN = Pattern.compile("lazy");
130
131 public final static String VERSION_S = "[0-9]{1,9}(:?\\.[0-9]{1,9}(:?\\.[0-9]{1,9}(:?\\.[0-9A-Za-z_-]+)?)?)?";
132 public final static Pattern VERSION_P = Pattern.compile(VERSION_S);
133 public final static String VERSION_RANGE_S = "(?:(:?\\(|\\[)" + VERSION_S + "," + VERSION_S
134 + "(\\]|\\)))|" + VERSION_S;
135 public final static Pattern VERSIONRANGE_P = VERSIONRANGE;
136 public static String EXTENDED_S = "[-a-zA-Z0-9_.]+";
137 public static Pattern EXTENDED_P = Pattern.compile(EXTENDED_S);
138 public static String QUOTEDSTRING = "\"[^\"]*\"";
139 public static Pattern QUOTEDSTRING_P = Pattern.compile(QUOTEDSTRING);
140 public static String ARGUMENT_S = "(:?" + EXTENDED_S + ")|(?:" + QUOTEDSTRING + ")";
141 public static Pattern ARGUMENT_P = Pattern.compile(ARGUMENT_S);
142
143 public final static String EES[] = {
144 "CDC-1.0/Foundation-1.0", "CDC-1.1/Foundation-1.1", "OSGi/Minimum-1.0", "OSGi/Minimum-1.1",
145 "OSGi/Minimum-1.2", "JRE-1.1", "J2SE-1.2", "J2SE-1.3", "J2SE-1.4", "J2SE-1.5", "JavaSE-1.6", "JavaSE-1.7",
146 "PersonalJava-1.1", "PersonalJava-1.2", "CDC-1.0/PersonalBasis-1.0", "CDC-1.0/PersonalJava-1.0"
147 };
148
149 public final static String OSNAMES[] = {
150 "AIX", // IBM
151 "DigitalUnix", // Compaq
152 "Embos", // Segger Embedded Software Solutions
153 "Epoc32", // SymbianOS Symbian OS
154 "FreeBSD", // Free BSD
155 "HPUX", // hp-ux Hewlett Packard
156 "IRIX", // Silicon Graphics
157 "Linux", // Open source
158 "MacOS", // Apple
159 "NetBSD", // Open source
160 "Netware", // Novell
161 "OpenBSD", // Open source
162 "OS2", // OS/2 IBM
163 "QNX", // procnto QNX
164 "Solaris", // Sun (almost an alias of SunOS)
165 "SunOS", // Sun Microsystems
166 "VxWorks", // WindRiver Systems
167 "Windows95", "Win32", "Windows98", "WindowsNT", "WindowsCE", "Windows2000", // Win2000
168 "Windows2003", // Win2003
169 "WindowsXP", "WindowsVista",
170 };
171
172 public final static String PROCESSORNAMES[] = { //
173 //
174 "68k", // Motorola 68000
175 "ARM_LE", // Intel Strong ARM. Deprecated because it does not
176 // specify the endianness. See the following two rows.
177 "arm_le", // Intel Strong ARM Little Endian mode
178 "arm_be", // Intel String ARM Big Endian mode
179 "Alpha", //
180 "ia64n",// Hewlett Packard 32 bit
181 "ia64w",// Hewlett Packard 64 bit mode
182 "Ignite", // psc1k PTSC
183 "Mips", // SGI
184 "PArisc", // Hewlett Packard
185 "PowerPC", // power ppc Motorola/IBM Power PC
186 "Sh4", // Hitachi
187 "Sparc", // SUN
188 "Sparcv9", // SUN
189 "S390", // IBM Mainframe 31 bit
190 "S390x", // IBM Mainframe 64-bit
191 "V850E", // NEC V850E
192 "x86", // pentium i386
193 "i486", // i586 i686 Intel& AMD 32 bit
194 "x86-64",
195 };
196
197 final Analyzer analyzer;
198 private Instructions dynamicImports;
199
200 public Verifier(Jar jar) throws Exception {
201 this.analyzer = new Analyzer(this);
202 this.analyzer.use(this);
203 addClose(analyzer);
204 this.analyzer.setJar(jar);
205 this.manifest = this.analyzer.calcManifest();
206 this.main = Domain.domain(manifest);
207 this.dot = jar;
208 getInfo(analyzer);
209 }
210
211 public Verifier(Analyzer analyzer) throws Exception {
212 super(analyzer);
213 this.analyzer = analyzer;
214 this.dot = analyzer.getJar();
215 this.manifest = dot.getManifest();
216 this.main = Domain.domain(manifest);
217 }
218
219 private void verifyHeaders() {
220 for (String h : main) {
221 if (!HEADER_PATTERN.matcher(h).matches())
222 error("Invalid Manifest header: " + h + ", pattern=" + HEADER_PATTERN);
223 }
224 }
225
226 /*
227 * Bundle-NativeCode ::= nativecode ( ',' nativecode )* ( ’,’ optional) ?
228 * nativecode ::= path ( ';' path )* // See 1.4.2 ( ';' parameter )+
229 * optional ::= ’*’
230 */
231 public void verifyNative() {
232 String nc = get("Bundle-NativeCode");
233 doNative(nc);
234 }
235
236 public void doNative(String nc) {
237 if (nc != null) {
238 QuotedTokenizer qt = new QuotedTokenizer(nc, ",;=", false);
239 char del;
240 do {
241 do {
242 String name = qt.nextToken();
243 if (name == null) {
244 error("Can not parse name from bundle native code header: " + nc);
245 return;
246 }
247 del = qt.getSeparator();
248 if (del == ';') {
249 if (dot != null && !dot.exists(name)) {
250 error("Native library not found in JAR: " + name);
251 }
252 } else {
253 String value = null;
254 if (del == '=')
255 value = qt.nextToken();
256
257 String key = name.toLowerCase();
258 if (key.equals("osname")) {
259 // ...
260 } else if (key.equals("osversion")) {
261 // verify version range
262 verify(value, VERSIONRANGE);
263 } else if (key.equals("language")) {
264 verify(value, ISO639);
265 } else if (key.equals("processor")) {
266 // verify(value, PROCESSORS);
267 } else if (key.equals("selection-filter")) {
268 // verify syntax filter
269 verifyFilter(value);
270 } else if (name.equals("*") && value == null) {
271 // Wildcard must be at end.
272 if (qt.nextToken() != null)
273 error("Bundle-Native code header may only END in wildcard: nc");
274 } else {
275 warning("Unknown attribute in native code: " + name + "=" + value);
276 }
277 del = qt.getSeparator();
278 }
279 } while (del == ';');
280 } while (del == ',');
281 }
282 }
283
284 public boolean verifyFilter(String value) {
285 String s = validateFilter(value);
286 if (s == null)
287 return true;
288
289 error(s);
290 return false;
291 }
292
293 public static String validateFilter(String value) {
294 try {
295 verifyFilter(value, 0);
296 return null;
297 }
298 catch (Exception e) {
299 return "Not a valid filter: " + value + e.getMessage();
300 }
301 }
302
303 private void verifyActivator() throws Exception {
304 String bactivator = main.get("Bundle-Activator");
305 if (bactivator != null) {
306 TypeRef ref = analyzer.getTypeRefFromFQN(bactivator);
307 if (analyzer.getClassspace().containsKey(ref))
308 return;
309
310 PackageRef packageRef = ref.getPackageRef();
311 if (packageRef.isDefaultPackage())
312 error("The Bundle Activator is not in the bundle and it is in the default package ");
313 else if (!analyzer.isImported(packageRef)) {
314 error("Bundle-Activator not found on the bundle class path nor in imports: " + bactivator);
315 }
316 }
317 }
318
319 private void verifyComponent() {
320 String serviceComponent = main.get("Service-Component");
321 if (serviceComponent != null) {
322 Parameters map = parseHeader(serviceComponent);
323 for (String component : map.keySet()) {
324 if (component.indexOf("*") < 0 && !dot.exists(component)) {
325 error("Service-Component entry can not be located in JAR: " + component);
326 } else {
327 // validate component ...
328 }
329 }
330 }
331 }
332
333 /**
334 * Check for unresolved imports. These are referrals that are not imported
335 * by the manifest and that are not part of our bundle class path. The are
336 * calculated by removing all the imported packages and contained from the
337 * referred packages.
338 */
339 private void verifyUnresolvedReferences() {
340 Set<PackageRef> unresolvedReferences = new TreeSet<PackageRef>(analyzer.getReferred().keySet());
341 unresolvedReferences.removeAll(analyzer.getImports().keySet());
342 unresolvedReferences.removeAll(analyzer.getContained().keySet());
343
344 // Remove any java.** packages.
345 for (Iterator<PackageRef> p = unresolvedReferences.iterator(); p.hasNext();) {
346 PackageRef pack = p.next();
347 if (pack.isJava())
348 p.remove();
349 else {
350 // Remove any dynamic imports
351 if (isDynamicImport(pack))
352 p.remove();
353 }
354 }
355
356 if (!unresolvedReferences.isEmpty()) {
357 // Now we want to know the
358 // classes that are the culprits
359 Set<String> culprits = new HashSet<String>();
360 for (Clazz clazz : analyzer.getClassspace().values()) {
361 if (hasOverlap(unresolvedReferences, clazz.getReferred()))
362 culprits.add(clazz.getAbsolutePath());
363 }
364
365 error("Unresolved references to %s by class(es) %s on the Bundle-Classpath: %s", unresolvedReferences,
366 culprits, analyzer.getBundleClasspath().keySet());
367 }
368 }
369
370 /**
371 * @param p
372 * @param pack
373 */
374 private boolean isDynamicImport(PackageRef pack) {
375 if (dynamicImports == null)
376 dynamicImports = new Instructions(main.getDynamicImportPackage());
377
378 return dynamicImports.matches(pack.getFQN());
379 }
380
381 private boolean hasOverlap(Set< ? > a, Set< ? > b) {
382 for (Iterator< ? > i = a.iterator(); i.hasNext();) {
383 if (b.contains(i.next()))
384 return true;
385 }
386 return false;
387 }
388
389 public void verify() throws Exception {
390 verifyHeaders();
391 verifyDirectives("Export-Package", "uses:|mandatory:|include:|exclude:|" + IMPORT_DIRECTIVE, PACKAGEPATTERN,
392 "package");
393 verifyDirectives("Import-Package", "resolution:", PACKAGEPATTERN, "package");
394 verifyDirectives("Require-Bundle", "visibility:|resolution:", SYMBOLICNAME, "bsn");
395 verifyDirectives("Fragment-Host", "extension:", SYMBOLICNAME, "bsn");
396 verifyDirectives("Provide-Capability", "effective:|uses:", null, null);
397 verifyDirectives("Require-Capability", "effective:|resolution:|filter:", null, null);
398 verifyDirectives("Bundle-SymbolicName", "singleton:|fragment-attachment:|mandatory:", SYMBOLICNAME, "bsn");
399
400 verifyManifestFirst();
401 verifyActivator();
402 verifyActivationPolicy();
403 verifyComponent();
404 verifyNative();
405 verifyImports();
406 verifyExports();
407 verifyUnresolvedReferences();
408 verifySymbolicName();
409 verifyListHeader("Bundle-RequiredExecutionEnvironment", EENAME, false);
410 verifyHeader("Bundle-ManifestVersion", BUNDLEMANIFESTVERSION, false);
411 verifyHeader("Bundle-Version", VERSION, true);
412 verifyListHeader("Bundle-Classpath", FILE, false);
413 verifyDynamicImportPackage();
414 verifyBundleClasspath();
415 verifyUses();
416 if (usesRequire) {
417 if (!getErrors().isEmpty()) {
418 getWarnings()
419 .add(0,
420 "Bundle uses Require Bundle, this can generate false errors because then not enough information is available without the required bundles");
421 }
422 }
423
424 verifyRequirements();
425 verifyCapabilities();
426 }
427
428 /**
429 * Verify that the imports properly use version ranges.
430 */
431 private void verifyImports() {
432 if (isStrict()) {
433 Parameters map = parseHeader(manifest.getMainAttributes().getValue(Constants.IMPORT_PACKAGE));
434 Set<String> noimports = new HashSet<String>();
435 Set<String> toobroadimports = new HashSet<String>();
436
437 for (Entry<String,Attrs> e : map.entrySet()) {
438 String version = e.getValue().get(Constants.VERSION_ATTRIBUTE);
439 if (version == null) {
440 if (!e.getKey().startsWith("javax.")) {
441 noimports.add(e.getKey());
442 }
443 } else {
444 if (!VERSIONRANGE.matcher(version).matches()) {
445 Location location = error("Import Package %s has an invalid version range syntax %s",
446 e.getKey(), version).location();
447 location.header = Constants.IMPORT_PACKAGE;
448 location.context = e.getKey();
449 } else {
450 try {
451 VersionRange range = new VersionRange(version);
452 if (!range.isRange()) {
453 toobroadimports.add(e.getKey());
454 }
455 if (range.includeHigh() == false && range.includeLow() == false
456 && range.getLow().equals(range.getHigh())) {
457 Location location = error(
458 "Import Package %s has an empty version range syntax %s, likely want to use [%s,%s]",
459 e.getKey(), version, range.getLow(), range.getHigh()).location();
460 location.header = Constants.IMPORT_PACKAGE;
461 location.context = e.getKey();
462 }
463 // TODO check for exclude low, include high?
464 }
465 catch (Exception ee) {
466 Location location = error("Import Package %s has an invalid version range syntax %s:%s",
467 e.getKey(), version, ee.getMessage()).location();
468 location.header = Constants.IMPORT_PACKAGE;
469 location.context = e.getKey();
470 }
471 }
472 }
473 }
474
475 if (!noimports.isEmpty()) {
476 Location location = error("Import Package clauses without version range (excluding javax.*): %s",
477 noimports).location();
478 location.header = Constants.IMPORT_PACKAGE;
479 }
480 if (!toobroadimports.isEmpty()) {
481 Location location = error(
482 "Import Package clauses which use a version instead of a version range. This imports EVERY later package and not as many expect until the next major number: %s",
483 toobroadimports).location();
484 location.header = Constants.IMPORT_PACKAGE;
485 }
486 }
487 }
488
489 /**
490 * Verify that the exports only use versions.
491 */
492 private void verifyExports() {
493 if (isStrict()) {
494 Parameters map = parseHeader(manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE));
495 Set<String> noexports = new HashSet<String>();
496
497 for (Entry<String,Attrs> e : map.entrySet()) {
498
499 String version = e.getValue().get(Constants.VERSION_ATTRIBUTE);
500 if (version == null) {
501 noexports.add(e.getKey());
502 } else {
503 if (!VERSION.matcher(version).matches()) {
504 Location location;
505 if (VERSIONRANGE.matcher(version).matches()) {
506 location = error(
507 "Export Package %s version is a range: %s; Exports do not allow for ranges.",
508 e.getKey(), version).location();
509 } else {
510 location = error("Export Package %s version has invalid syntax: %s", e.getKey(), version)
511 .location();
512 }
513 location.header = Constants.EXPORT_PACKAGE;
514 location.context = e.getKey();
515 }
516 }
517
518 if (e.getValue().containsKey(Constants.SPECIFICATION_VERSION)) {
519 Location location = error(
520 "Export Package %s uses deprecated specification-version instead of version", e.getKey())
521 .location();
522 location.header = Constants.EXPORT_PACKAGE;
523 location.context = e.getKey();
524 }
525
526 String mandatory = e.getValue().get(Constants.MANDATORY_DIRECTIVE);
527 if (mandatory != null) {
528 Set<String> missing = new HashSet<String>(split(mandatory));
529 missing.removeAll(e.getValue().keySet());
530 if (!missing.isEmpty()) {
531 Location location = error("Export Package %s misses mandatory attribute: %s", e.getKey(),
532 missing).location();
533 location.header = Constants.EXPORT_PACKAGE;
534 location.context = e.getKey();
535 }
536 }
537 }
538
539 if (!noexports.isEmpty()) {
540 Location location = error("Export Package clauses without version range: %s", noexports).location();
541 location.header = Constants.EXPORT_PACKAGE;
542 }
543 }
544 }
545
546 private void verifyRequirements() {
547 Parameters map = parseHeader(manifest.getMainAttributes().getValue(Constants.REQUIRE_CAPABILITY));
548 for (String key : map.keySet()) {
549 Attrs attrs = map.get(key);
550 verify(attrs, "filter:", FILTERPATTERN, false, "Requirement %s filter not correct", key);
551
552 String filter = attrs.get("filter:");
553 if (filter != null) {
554 String verify = new Filter(filter).verify();
555 if (verify != null)
556 error("Invalid filter syntax in requirement %s=%s. Reason %s", key, attrs, verify);
557 }
558 verify(attrs, "cardinality:", CARDINALITY_PATTERN, false, "Requirement %s cardinality not correct", key);
559 verify(attrs, "resolution:", RESOLUTION_PATTERN, false, "Requirement %s resolution not correct", key);
560
561 if (key.equals("osgi.extender")) {
562 // No requirements on extender
563 } else if (key.equals("osgi.serviceloader")) {
564 verify(attrs, "register:", PACKAGEPATTERN, false,
565 "Service Loader extender register: directive not a fully qualified Java name");
566 } else if (key.equals("osgi.contract")) {
567
568 } else if (key.equals("osgi.service")) {
569
570 } else if (key.equals("osgi.ee")) {
571
572 } else if (key.startsWith("osgi.wiring.") || key.startsWith("osgi.identity")) {
573 error("osgi.wiring.* namespaces must not be specified with generic requirements/capabilities");
574 }
575
576 verifyAttrs(attrs);
577
578 if (attrs.containsKey("mandatory:"))
579 error("mandatory: directive is intended for Capabilities, not Requirement %s", key);
580
581 if (attrs.containsKey("uses:"))
582 error("uses: directive is intended for Capabilities, not Requirement %s", key);
583 }
584 }
585
586 /**
587 * @param attrs
588 */
589 void verifyAttrs(Attrs attrs) {
590 for (String a : attrs.keySet()) {
591 String v = attrs.get(a);
592
593 if (!a.endsWith(":")) {
594 Attrs.Type t = attrs.getType(a);
595 if ("version".equals(a)) {
596 if (t != Attrs.Type.VERSION)
597 error("Version attributes should always be of type version, it is %s", t);
598 } else
599 verifyType(t, v);
600 }
601 }
602 }
603
604 private void verifyCapabilities() {
605 Parameters map = parseHeader(manifest.getMainAttributes().getValue(Constants.PROVIDE_CAPABILITY));
606 for (String key : map.keySet()) {
607 Attrs attrs = map.get(key);
608 verify(attrs, "cardinality:", CARDINALITY_PATTERN, false, "Requirement %s cardinality not correct", key);
609 verify(attrs, "resolution:", RESOLUTION_PATTERN, false, "Requirement %s resolution not correct", key);
610
611 if (key.equals("osgi.extender")) {
612 verify(attrs, "osgi.extender", SYMBOLICNAME, true,
613 "Extender %s must always have the osgi.extender attribute set", key);
614 verify(attrs, "version", VERSION, true, "Extender %s must always have a version", key);
615 } else if (key.equals("osgi.serviceloader")) {
616 verify(attrs, "register:", PACKAGEPATTERN, false,
617 "Service Loader extender register: directive not a fully qualified Java name");
618 } else if (key.equals("osgi.contract")) {
619 verify(attrs, "osgi.contract", SYMBOLICNAME, true,
620 "Contracts %s must always have the osgi.contract attribute set", key);
621
622 } else if (key.equals("osgi.service")) {
623 verify(attrs, "objectClass", MULTIPACKAGEPATTERN, true,
624 "osgi.service %s must have the objectClass attribute set", key);
625
626 } else if (key.equals("osgi.ee")) {
627 // TODO
628 } else if (key.startsWith("osgi.wiring.") || key.startsWith("osgi.identity")) {
629 error("osgi.wiring.* namespaces must not be specified with generic requirements/capabilities");
630 }
631
632 verifyAttrs(attrs);
633
634 if (attrs.containsKey("filter:"))
635 error("filter: directive is intended for Requirements, not Capability %s", key);
636 if (attrs.containsKey("cardinality:"))
637 error("cardinality: directive is intended for Requirements, not Capability %s", key);
638 if (attrs.containsKey("resolution:"))
639 error("resolution: directive is intended for Requirements, not Capability %s", key);
640 }
641 }
642
643 private void verify(Attrs attrs, String ad, Pattern pattern, boolean mandatory, String msg, String... args) {
644 String v = attrs.get(ad);
645 if (v == null) {
646 if (mandatory)
647 error("Missing required attribute/directive %s", ad);
648 } else {
649 Matcher m = pattern.matcher(v);
650 if (!m.matches())
651 error(msg, (Object[]) args);
652 }
653 }
654
655 private void verifyType(@SuppressWarnings("unused")
656 Attrs.Type type, @SuppressWarnings("unused")
657 String string) {
658
659 }
660
661 /**
662 * Verify if the header does not contain any other directives
663 *
664 * @param header
665 * @param directives
666 */
667 private void verifyDirectives(String header, String directives, Pattern namePattern, String type) {
668 Pattern pattern = Pattern.compile(directives);
669 Parameters map = parseHeader(manifest.getMainAttributes().getValue(header));
670 for (Entry<String,Attrs> entry : map.entrySet()) {
671 String pname = removeDuplicateMarker(entry.getKey());
672
673 if (namePattern != null) {
674 if (!namePattern.matcher(pname).matches())
675 if (isPedantic())
676 error("Invalid %s name: '%s'", type, pname);
677 else
678 warning("Invalid %s name: '%s'", type, pname);
679 }
680
681 for (String key : entry.getValue().keySet()) {
682 if (key.endsWith(":")) {
683 if (!key.startsWith("x-")) {
684 Matcher m = pattern.matcher(key);
685 if (m.matches())
686 continue;
687
688 warning("Unknown directive %s in %s, allowed directives are %s, and 'x-*'.", key, header,
689 directives.replace('|', ','));
690 }
691 }
692 }
693 }
694 }
695
696 /**
697 * Verify the use clauses
698 */
699 private void verifyUses() {
700 // Set<String> uses = Create.set();
701 // for ( Map<String,String> attrs : analyzer.getExports().values()) {
702 // if ( attrs.containsKey(Constants.USES_DIRECTIVE)) {
703 // String s = attrs.get(Constants.USES_DIRECTIVE);
704 // uses.addAll( split(s));
705 // }
706 // }
707 // uses.removeAll(analyzer.getExports().keySet());
708 // uses.removeAll(analyzer.getImports().keySet());
709 // if ( !uses.isEmpty())
710 // warning("Export-Package uses: directive contains packages that are not imported nor exported: %s",
711 // uses);
712 }
713
714 public boolean verifyActivationPolicy() {
715 String policy = main.get(Constants.BUNDLE_ACTIVATIONPOLICY);
716 if (policy == null)
717 return true;
718
719 return verifyActivationPolicy(policy);
720 }
721
722 public boolean verifyActivationPolicy(String policy) {
723 Parameters map = parseHeader(policy);
724 if (map.size() == 0)
725 warning("Bundle-ActivationPolicy is set but has no argument %s", policy);
726 else if (map.size() > 1)
727 warning("Bundle-ActivationPolicy has too many arguments %s", policy);
728 else {
729 Map<String,String> s = map.get("lazy");
730 if (s == null)
731 warning("Bundle-ActivationPolicy set but is not set to lazy: %s", policy);
732 else
733 return true;
734 }
735
736 return false;
737 }
738
739 public void verifyBundleClasspath() {
740 Parameters bcp = main.getBundleClassPath();
741 if (bcp.isEmpty() || bcp.containsKey("."))
742 return;
743
744 for (String path : bcp.keySet()) {
745 if (path.endsWith("/"))
746 error("A Bundle-ClassPath entry must not end with '/': %s", path);
747
748 if (dot.getDirectories().containsKey(path))
749 // We assume that any classes are in a directory
750 // and therefore do not care when the bundle is included
751 return;
752 }
753
754 for (String path : dot.getResources().keySet()) {
755 if (path.endsWith(".class")) {
756 warning("The Bundle-Classpath does not contain the actual bundle JAR (as specified with '.' in the Bundle-Classpath) but the JAR does contain classes. Is this intentional?");
757 return;
758 }
759 }
760 }
761
762 /**
763 * <pre>
764 * DynamicImport-Package ::= dynamic-description
765 * ( ',' dynamic-description )*
766 *
767 * dynamic-description::= wildcard-names ( ';' parameter )*
768 * wildcard-names ::= wildcard-name ( ';' wildcard-name )*
769 * wildcard-name ::= package-name
770 * | ( package-name '.*' ) // See 1.4.2
771 * | '*'
772 * </pre>
773 */
774 private void verifyDynamicImportPackage() {
775 verifyListHeader("DynamicImport-Package", WILDCARDPACKAGE, true);
776 String dynamicImportPackage = get("DynamicImport-Package");
777 if (dynamicImportPackage == null)
778 return;
779
780 Parameters map = main.getDynamicImportPackage();
781 for (String name : map.keySet()) {
782 name = name.trim();
783 if (!verify(name, WILDCARDPACKAGE))
784 error("DynamicImport-Package header contains an invalid package name: " + name);
785
786 Map<String,String> sub = map.get(name);
787 if (r3 && sub.size() != 0) {
788 error("DynamicPackage-Import has attributes on import: " + name
789 + ". This is however, an <=R3 bundle and attributes on this header were introduced in R4. ");
790 }
791 }
792 }
793
794 private void verifyManifestFirst() {
795 if (!dot.isManifestFirst()) {
796 error("Invalid JAR stream: Manifest should come first to be compatible with JarInputStream, it was not");
797 }
798 }
799
800 private void verifySymbolicName() {
801 Parameters bsn = parseHeader(main.get(Analyzer.BUNDLE_SYMBOLICNAME));
802 if (!bsn.isEmpty()) {
803 if (bsn.size() > 1)
804 error("More than one BSN specified " + bsn);
805
806 String name = bsn.keySet().iterator().next();
807 if (!isBsn(name)) {
808 error("Symbolic Name has invalid format: " + name);
809 }
810 }
811 }
812
813 /**
814 * @param name
815 * @return
816 */
817 public static boolean isBsn(String name) {
818 return SYMBOLICNAME.matcher(name).matches();
819 }
820
821 /**
822 * <pre>
823 * filter ::= ’(’ filter-comp ’)’
824 * filter-comp ::= and | or | not | operation
825 * and ::= ’&amp;’ filter-list
826 * or ::= ’|’ filter-list
827 * not ::= ’!’ filter
828 * filter-list ::= filter | filter filter-list
829 * operation ::= simple | present | substring
830 * simple ::= attr filter-type value
831 * filter-type ::= equal | approx | greater | less
832 * equal ::= ’=’
833 * approx ::= ’&tilde;=’
834 * greater ::= ’&gt;=’
835 * less ::= ’&lt;=’
836 * present ::= attr ’=*’
837 * substring ::= attr ’=’ initial any final
838 * inital ::= () | value
839 * any ::= ’*’ star-value
840 * star-value ::= () | value ’*’ star-value
841 * final ::= () | value
842 * value ::= &lt;see text&gt;
843 * </pre>
844 *
845 * @param expr
846 * @param index
847 * @return
848 */
849
850 public static int verifyFilter(String expr, int index) {
851 try {
852 while (Character.isWhitespace(expr.charAt(index)))
853 index++;
854
855 if (expr.charAt(index) != '(')
856 throw new IllegalArgumentException("Filter mismatch: expected ( at position " + index + " : " + expr);
857
858 index++; // skip (
859
860 while (Character.isWhitespace(expr.charAt(index)))
861 index++;
862
863 switch (expr.charAt(index)) {
864 case '!' :
865 index++; // skip !
866 while (Character.isWhitespace(expr.charAt(index)))
867 index++;
868
869 if (expr.charAt(index) != '(')
870 throw new IllegalArgumentException("Filter mismatch: ! (not) must have one sub expression "
871 + index + " : " + expr);
872 while (Character.isWhitespace(expr.charAt(index)))
873 index++;
874
875 index = verifyFilter(expr, index);
876 while (Character.isWhitespace(expr.charAt(index)))
877 index++;
878 if (expr.charAt(index) != ')')
879 throw new IllegalArgumentException("Filter mismatch: expected ) at position " + index + " : "
880 + expr);
881 return index + 1;
882
883 case '&' :
884 case '|' :
885 index++; // skip operator
886 while (Character.isWhitespace(expr.charAt(index)))
887 index++;
888 while (expr.charAt(index) == '(') {
889 index = verifyFilter(expr, index);
890 while (Character.isWhitespace(expr.charAt(index)))
891 index++;
892 }
893
894 if (expr.charAt(index) != ')')
895 throw new IllegalArgumentException("Filter mismatch: expected ) at position " + index + " : "
896 + expr);
897 return index + 1; // skip )
898
899 default :
900 index = verifyFilterOperation(expr, index);
901 if (expr.charAt(index) != ')')
902 throw new IllegalArgumentException("Filter mismatch: expected ) at position " + index + " : "
903 + expr);
904 return index + 1;
905 }
906 }
907 catch (IndexOutOfBoundsException e) {
908 throw new IllegalArgumentException("Filter mismatch: early EOF from " + index);
909 }
910 }
911
912 static private int verifyFilterOperation(String expr, int index) {
913 StringBuilder sb = new StringBuilder();
914 while ("=><~()".indexOf(expr.charAt(index)) < 0) {
915 sb.append(expr.charAt(index++));
916 }
917 String attr = sb.toString().trim();
918 if (attr.length() == 0)
919 throw new IllegalArgumentException("Filter mismatch: attr at index " + index + " is 0");
920 sb = new StringBuilder();
921 while ("=><~".indexOf(expr.charAt(index)) >= 0) {
922 sb.append(expr.charAt(index++));
923 }
924 String operator = sb.toString();
925 if (!verify(operator, FILTEROP))
926 throw new IllegalArgumentException("Filter error, illegal operator " + operator + " at index " + index);
927
928 sb = new StringBuilder();
929 while (")".indexOf(expr.charAt(index)) < 0) {
930 switch (expr.charAt(index)) {
931 case '\\' :
932 if ("\\)(*".indexOf(expr.charAt(index + 1)) >= 0)
933 index++;
934 else
935 throw new IllegalArgumentException("Filter error, illegal use of backslash at index " + index
936 + ". Backslash may only be used before * or () or \\");
937 }
938 sb.append(expr.charAt(index++));
939 }
940 return index;
941 }
942
943 private boolean verifyHeader(String name, Pattern regex, boolean error) {
944 String value = manifest.getMainAttributes().getValue(name);
945 if (value == null)
946 return false;
947
948 QuotedTokenizer st = new QuotedTokenizer(value.trim(), ",");
949 for (Iterator<String> i = st.getTokenSet().iterator(); i.hasNext();) {
950 if (!verify(i.next(), regex)) {
951 String msg = "Invalid value for " + name + ", " + value + " does not match " + regex.pattern();
952 if (error)
953 error(msg);
954 else
955 warning(msg);
956 }
957 }
958 return true;
959 }
960
961 static private boolean verify(String value, Pattern regex) {
962 return regex.matcher(value).matches();
963 }
964
965 private boolean verifyListHeader(String name, Pattern regex, boolean error) {
966 String value = manifest.getMainAttributes().getValue(name);
967 if (value == null)
968 return false;
969
970 Parameters map = parseHeader(value);
971 for (String header : map.keySet()) {
972 if (!regex.matcher(header).matches()) {
973 String msg = "Invalid value for " + name + ", " + value + " does not match " + regex.pattern();
974 if (error)
975 error(msg);
976 else
977 warning(msg);
978 }
979 }
980 return true;
981 }
982
983 @Override
984 public String getProperty(String key, String deflt) {
985 if (properties == null)
986 return deflt;
987 return properties.getProperty(key, deflt);
988 }
989
990 public static boolean isVersion(String version) {
991 return VERSION.matcher(version).matches();
992 }
993
994 public static boolean isIdentifier(String value) {
995 if (value.length() < 1)
996 return false;
997
998 if (!Character.isJavaIdentifierStart(value.charAt(0)))
999 return false;
1000
1001 for (int i = 1; i < value.length(); i++) {
1002 if (!Character.isJavaIdentifierPart(value.charAt(i)))
1003 return false;
1004 }
1005 return true;
1006 }
1007
1008 public static boolean isMember(String value, String[] matches) {
1009 for (String match : matches) {
1010 if (match.equals(value))
1011 return true;
1012 }
1013 return false;
1014 }
1015
1016 public static boolean isFQN(String name) {
1017 if (name.length() == 0)
1018 return false;
1019 if (!Character.isJavaIdentifierStart(name.charAt(0)))
1020 return false;
1021
1022 for (int i = 1; i < name.length(); i++) {
1023 char c = name.charAt(i);
1024 if (Character.isJavaIdentifierPart(c) || c == '$' || c == '.')
1025 continue;
1026
1027 return false;
1028 }
1029
1030 return true;
1031 }
1032
1033 /**
1034 * Verify checksums
1035 */
1036 /**
1037 * Verify the checksums from the manifest against the real thing.
1038 *
1039 * @param all
1040 * if each resources must be digested
1041 * @return true if ok
1042 * @throws Exception
1043 */
1044
1045 public void verifyChecksums(boolean all) throws Exception {
1046 Manifest m = dot.getManifest();
1047 if (m == null || m.getEntries().isEmpty()) {
1048 if (all)
1049 error("Verify checksums with all but no digests");
1050 return;
1051 }
1052
1053 List<String> missingDigest = new ArrayList<String>();
1054
1055 for (String path : dot.getResources().keySet()) {
1056 if (path.equals("META-INF/MANIFEST.MF"))
1057 continue;
1058
1059 Attributes a = m.getAttributes(path);
1060 String digest = a.getValue("SHA1-Digest");
1061 if (digest == null) {
1062 if (!path.matches(""))
1063 missingDigest.add(path);
1064 } else {
1065 byte[] d = Base64.decodeBase64(digest);
1066 SHA1 expected = new SHA1(d);
1067 Digester<SHA1> digester = SHA1.getDigester();
1068 InputStream in = dot.getResource(path).openInputStream();
1069 IO.copy(in, digester);
1070 digester.digest();
1071 if (!expected.equals(digester.digest())) {
1072 error("Checksum mismatch %s, expected %s, got %s", path, expected, digester.digest());
1073 }
1074 }
1075 }
1076 if (missingDigest.size() > 0) {
1077 error("Entries in the manifest are missing digests: %s", missingDigest);
1078 }
1079 }
1080
1081 /**
1082 * Verify the EXTENDED_S syntax
1083 *
1084 * @param key
1085 * @return
1086 */
1087 public static boolean isExtended(String key) {
1088 if (key == null)
1089 return false;
1090
1091 return EXTENDED_P.matcher(key).matches();
1092 }
1093
1094 /**
1095 * Verify the ARGUMENT_S syntax
1096 *
1097 * @param key
1098 * @return
1099 */
1100 public static boolean isArgument(String arg) {
1101 return arg != null && ARGUMENT_P.matcher(arg).matches();
1102 }
1103
1104 /**
1105 * Verify the QUOTEDSTRING syntax
1106 *
1107 * @param key
1108 * @return
1109 */
1110 public static boolean isQuotedString(String s) {
1111 return s != null && QUOTEDSTRING_P.matcher(s).matches();
1112 }
1113
1114 public static boolean isVersionRange(String range) {
1115 return range != null && VERSIONRANGE_P.matcher(range).matches();
1116 }
1117}