/* Copyright 2006 aQute SARL
* Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
package aQute.lib.osgi;
import java.io.IOException;
import java.util.*;
import java.util.jar.*;
import java.util.regex.Pattern;
import aQute.qtokens.QuotedTokenizer;
public class Verifier extends Processor {
Jar dot;
Manifest manifest;
Map referred = new HashMap();
Map contained = new HashMap();
Map uses = new HashMap();
Map mimports;
Map mdynimports;
Map mexports;
Map ignore = new HashMap(); // Packages
// to
// ignore
List bundleClassPath;
Map classSpace;
boolean r3;
boolean usesRequire;
boolean fragment;
Attributes main;
final static Pattern EENAME = Pattern
.compile("CDC-1\\.0/Foundation-1\\.0"
+ "|OSGi/Minimum-1\\.1"
+ "|JRE-1\\.1"
+ "|J2SE-1\\.2"
+ "|J2SE-1\\.3"
+ "|J2SE-1\\.4"
+ "|J2SE-1\\.5"
+ "|PersonalJava-1\\.1"
+ "|PersonalJava-1\\.2"
+ "|CDC-1\\.0/PersonalBasis-1\\.0"
+ "|CDC-1\\.0/PersonalJava-1\\.0");
final static Pattern BUNDLEMANIFESTVERSION = Pattern.compile("2");
final static Pattern SYMBOLICNAME = Pattern
.compile("[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*");
final static String version = "[0-9]+(\\.[0-9]+(\\.[0-9]+(\\.[0-9A-Za-z_-]+)?)?)?";
final static Pattern VERSION = Pattern.compile(version);
final static Pattern FILTEROP = Pattern
.compile("=|<=|>=|~=");
final static Pattern VERSIONRANGE = Pattern
.compile("((\\(|\\[)"
+ version
+ ","
+ version
+ "(\\]|\\)))|"
+ version);
final static Pattern FILE = Pattern
.compile("/?[^/\"\n\r\u0000]+(/[^/\"\n\r\u0000]+)*");
final static Pattern WILDCARDPACKAGE = Pattern
.compile("((\\p{Alnum}|_)+(\\.(\\p{Alnum}|_)+)*(\\.\\*)?)|\\*");
final static Pattern ISO639 = Pattern
.compile("[A-Z][A-Z]");
public Verifier(Jar jar) throws Exception {
this.dot = jar;
this.manifest = jar.getManifest();
if (manifest == null) {
manifest = new Manifest();
error("This file contains no manifest and is therefore not a bundle");
}
main = this.manifest.getMainAttributes();
r3 = getHeader(Analyzer.BUNDLE_MANIFESTVERSION) == null;
usesRequire = getHeader(Analyzer.REQUIRE_BUNDLE) != null;
fragment = getHeader(Analyzer.FRAGMENT_HOST) != null;
bundleClassPath = getBundleClassPath();
mimports = parseHeader(manifest.getMainAttributes().getValue(
Analyzer.IMPORT_PACKAGE));
mdynimports = parseHeader(manifest.getMainAttributes().getValue(
Analyzer.DYNAMICIMPORT_PACKAGE));
mexports = parseHeader(manifest.getMainAttributes().getValue(
Analyzer.EXPORT_PACKAGE));
ignore = parseHeader(manifest.getMainAttributes().getValue(
Analyzer.IGNORE_PACKAGE));
}
private List getBundleClassPath() {
List list = new ArrayList();
String bcp = getHeader("Bundle-Classpath");
if (bcp == null) {
list.add(dot);
}
else {
Map entries = parseHeader(bcp);
for (Iterator i = entries.keySet().iterator(); i.hasNext();) {
String jarOrDir = (String) i.next();
if (jarOrDir.equals(".")) {
list.add(dot);
}
else {
if (jarOrDir.equals("/"))
jarOrDir = "";
if (jarOrDir.endsWith("/")) {
error("Bundle-Classpath directory must not end with a slash: "
+ jarOrDir);
jarOrDir = jarOrDir.substring(0, jarOrDir.length() - 1);
}
Resource resource = dot.getResource(jarOrDir);
if (resource != null) {
try {
Jar sub = new Jar(jarOrDir);
EmbeddedResource.build(sub, resource
.openInputStream());
if (!jarOrDir.endsWith(".jar"))
warning("Valid JAR file on Bundle-Classpath does not have .jar extension: "
+ jarOrDir);
list.add(sub);
}
catch (Exception e) {
error("Invalid embedded JAR file on Bundle-Classpath: "
+ jarOrDir + ", " + e);
}
}
else if (dot.getDirectories().containsKey(jarOrDir)) {
if (r3)
error("R3 bundles do not support directories on the Bundle-ClassPath: "
+ jarOrDir);
list.add(jarOrDir);
}
else {
error("Cannot find a file or directory for Bundle-Classpath entry: "
+ jarOrDir);
}
}
}
}
return list;
}
/*
* Bundle-NativeCode ::= nativecode ( ',' nativecode )* ( ’,’ optional) ?
* nativecode ::= path ( ';' path )* // See 1.4.2 ( ';' parameter )+
* optional ::= ’*’
*/
public void verifyNative() {
String nc = getHeader("Bundle-NativeCode");
doNative(nc);
}
public void doNative(String nc) {
if (nc != null) {
QuotedTokenizer qt = new QuotedTokenizer(nc, ",;=", false);
char del;
do {
do {
String name = qt.nextToken();
del = qt.getSeparator();
if (del == ';') {
if (!dot.exists(name)) {
error("Native library not found in JAR: " + name);
}
}
else {
String value = qt.nextToken();
String key = name.toLowerCase();
if (key.equals("osname")) {
// ...
}
else if (key.equals("osversion")) {
// verify version range
verify(value, VERSIONRANGE);
}
else if (key.equals("lanuage")) {
verify(value, ISO639);
}
else if (key.equals("processor")) {
// verify(value, PROCESSORS);
}
else if (key.equals("selection-filter")) {
// verify syntax filter
verifyFilter(value, 0);
}
else {
warning("Unknown attribute in native code: " + name
+ "=" + value);
}
del = qt.getSeparator();
}
} while (del == ';');
} while (del == ',');
}
}
private void verifyActivator() {
String bactivator = getHeader("Bundle-Activator");
if (bactivator != null) {
Clazz cl = loadClass(bactivator);
if (cl == null) {
error("Bundle-Activator not found on the bundle class path or imports: "
+ bactivator);
}
}
}
private Clazz loadClass(String className) {
String path = className.replace('.', '/') + ".class";
return (Clazz) classSpace.get(path);
}
private void verifyComponent() {
String serviceComponent = getHeader("Service-Component");
if (serviceComponent != null) {
Map map = parseHeader(serviceComponent);
for (Iterator i = map.keySet().iterator(); i.hasNext();) {
String component = (String) i.next();
if (!dot.exists(component)) {
error("Service-Component entry can not be located in JAR: "
+ component);
}
else {
// validate component ...
}
}
}
}
public void info() {
System.out.println("Refers : " + referred);
System.out.println("Contains : " + contained);
System.out.println("Manifest Imports : " + mimports);
System.out.println("Manifest Exports : " + mexports);
}
/**
* Invalid exports are exports mentioned in the manifest but not found on
* the classpath. This can be calculated with: exports - contains.
*/
private void verifyInvalidExports() {
Set invalidExport = new HashSet(mexports.keySet());
invalidExport.removeAll(contained.keySet());
if (!invalidExport.isEmpty())
error("Exporting packages that are not on the Bundle-Classpath"
+ bundleClassPath + ": " + invalidExport);
}
/**
* Invalid imports are imports that we never refer to. They can be
* calculated by removing the refered packages from the imported packages.
* This leaves packages that the manifest imported but that we never use.
*/
private void verifyInvalidImports() {
Set invalidImport = new TreeSet(mimports.keySet());
invalidImport.removeAll(referred.keySet());
if (!invalidImport.isEmpty())
warning("Importing packages that are never refered to by any class on the Bundle-Classpath"
+ bundleClassPath + ": " + invalidImport);
}
/**
* Check for unresolved imports. These are referals that are not imported by
* the manifest and that are not part of our bundle classpath. The are
* calculated by removing all the imported packages and contained from the
* refered packages.
*/
private void verifyUnresolvedReferences() {
Set unresolvedReferences = new TreeSet(referred.keySet());
unresolvedReferences.removeAll(mimports.keySet());
unresolvedReferences.removeAll(contained.keySet());
// Remove any java.** packages.
for (Iterator p = unresolvedReferences.iterator(); p.hasNext();) {
String pack = (String) p.next();
if (pack.startsWith("java.") || ignore.containsKey(pack))
p.remove();
else {
// Remove any dynamic imports
if ( isDynamicImport(pack) )
p.remove();
}
}
if (!unresolvedReferences.isEmpty()) {
// Now we want to know the
// classes that are the culprits
Set culprits = new HashSet();
for (Iterator i = classSpace.values().iterator(); i.hasNext();) {
Clazz clazz = (Clazz) i.next();
if (hasOverlap(unresolvedReferences, clazz.imports.keySet()))
culprits.add(clazz.getPath());
}
error("Unresolved references to " + unresolvedReferences
+ " by class(es) on the Bundle-Classpath" + bundleClassPath
+ ": " + culprits);
}
}
/**
* @param p
* @param pack
*/
private boolean isDynamicImport(String pack) {
for (Iterator dimp = mdynimports.keySet().iterator(); dimp
.hasNext();) {
String pattern = (String) dimp.next();
// Wildcard?
if (pattern.equals("*"))
return true; // All packages can be dynamically imported
if (pattern.endsWith(".*")) {
pattern = pattern.substring(0, pattern.length() - 2);
if (pack.startsWith(pattern)
&& (pack.length() == pattern.length() || pack
.charAt(pattern.length()) == '.'))
return true;
}
else {
if (pack.equals(pattern))
return true;
}
}
return false;
}
private boolean hasOverlap(Set a, Set b) {
for (Iterator i = a.iterator(); i.hasNext();) {
if (b.contains(i.next()))
return true;
}
return false;
}
public void verify() throws IOException {
classSpace = analyzeBundleClasspath(
dot,
parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH)),
contained,
referred,
uses);
verifyManifestFirst();
verifyActivator();
verifyComponent();
verifyNative();
verifyInvalidExports();
verifyInvalidImports();
verifyUnresolvedReferences();
verifySymbolicName();
verifyListHeader("Bundle-RequiredExecutionEnvironment", EENAME, false);
verifyHeader("Bundle-ManifestVersion", BUNDLEMANIFESTVERSION, false);
verifyHeader("Bundle-Version", VERSION, true);
verifyListHeader("Bundle-Classpath", FILE, false);
verifyDynamicImportPackage();
if (usesRequire) {
if (!errors.isEmpty()) {
warnings
.add(
0,
"Bundle uses Require Bundle, this can generate false errors because then not enough information is available without the required bundles");
}
}
}
/**
* <pre>
* DynamicImport-Package ::= dynamic-description
* ( ',' dynamic-description )*
*
* dynamic-description::= wildcard-names ( ';' parameter )*
* wildcard-names ::= wildcard-name ( ';' wildcard-name )*
* wildcard-name ::= package-name
* | ( package-name '.*' ) // See 1.4.2
* | '*'
* </pre>
*/
private void verifyDynamicImportPackage() {
verifyListHeader("DynamicImport-Package", WILDCARDPACKAGE, true);
String dynamicImportPackage = getHeader("DynamicImport-Package");
if (dynamicImportPackage == null)
return;
Map map = parseHeader(dynamicImportPackage);
for (Iterator i = map.keySet().iterator(); i.hasNext();) {
String name = (String) i.next();
name = name.trim();
if (!verify(name, WILDCARDPACKAGE))
error("DynamicImport-Package header contains an invalid package name: "
+ name);
Map sub = (Map) map.get(name);
if (r3 && sub.size() != 0) {
error("DynamicPackage-Import has attributes on import: "
+ name
+ ". This is however, an <=R3 bundle and attributes on this header were introduced in R4. ");
}
}
}
private void verifyManifestFirst() {
if (!dot.manifestFirst) {
errors
.add("Invalid JAR stream: Manifest should come first to be compatible with JarInputStream, it was not");
}
}
private void verifySymbolicName() {
Map bsn = parseHeader(getHeader("Bundle-SymbolicName"));
if (!bsn.isEmpty()) {
if (bsn.size() > 1)
errors.add("More than one BSN specified " + bsn);
String name = (String) bsn.keySet().iterator().next();
if (!SYMBOLICNAME.matcher(name).matches()) {
errors.add("Symbolic Name has invalid format: " + name);
}
}
}
/**
* <pre>
* filter ::= ’(’ filter-comp ’)’
* filter-comp ::= and | or | not | operation
* and ::= ’&’ filter-list
* or ::= ’|’ filter-list
* not ::= ’!’ filter
* filter-list ::= filter | filter filter-list
* operation ::= simple | present | substring
* simple ::= attr filter-type value
* filter-type ::= equal | approx | greater | less
* equal ::= ’=’
* approx ::= ’˜=’
* greater ::= ’>=’
* less ::= ’<=’
* present ::= attr ’=*’
* substring ::= attr ’=’ initial any final
* inital ::= () | value
* any ::= ’*’ star-value
* star-value ::= () | value ’*’ star-value
* final ::= () | value
* value ::= <see text>
* </pre>
*
* @param expr
* @param index
* @return
*/
int verifyFilter(String expr, int index) {
try {
while (Character.isWhitespace(expr.charAt(index)))
index++;
if (expr.charAt(index) != '(')
throw new IllegalArgumentException(
"Filter mismatch: expected ( at position " + index
+ " : " + expr);
while (Character.isWhitespace(expr.charAt(index)))
index++;
switch (expr.charAt(index)) {
case '!' :
case '&' :
case '|' :
return verifyFilterSubExpression(expr, index) + 1;
default :
return verifyFilterOperation(expr, index) + 1;
}
}
catch (IndexOutOfBoundsException e) {
throw new IllegalArgumentException(
"Filter mismatch: early EOF from " + index);
}
}
private int verifyFilterOperation(String expr, int index) {
StringBuffer sb = new StringBuffer();
while ("=><~()".indexOf(expr.charAt(index)) < 0) {
sb.append(expr.charAt(index++));
}
String attr = sb.toString().trim();
if (attr.length() == 0)
throw new IllegalArgumentException(
"Filter mismatch: attr at index " + index + " is 0");
sb = new StringBuffer();
while ("=><~".indexOf(expr.charAt(index)) >= 0) {
sb.append(expr.charAt(index++));
}
String operator = sb.toString();
if (!verify(operator, FILTEROP))
throw new IllegalArgumentException(
"Filter error, illegal operator " + operator + " at index "
+ index);
sb = new StringBuffer();
while (")".indexOf(expr.charAt(index)) < 0) {
switch (expr.charAt(index)) {
case '\\' :
if (expr.charAt(index + 1) == '*'
|| expr.charAt(index + 1) == ')')
index++;
else
throw new IllegalArgumentException(
"Filter error, illegal use of backslash at index "
+ index
+ ". Backslash may only be used before * or (");
}
sb.append(expr.charAt(index++));
}
return index;
}
private int verifyFilterSubExpression(String expr, int index) {
do {
index = verifyFilter(expr, index + 1);
while (Character.isWhitespace(expr.charAt(index)))
index++;
if (expr.charAt(index) != ')')
throw new IllegalArgumentException(
"Filter mismatch: expected ) at position " + index
+ " : " + expr);
index++;
} while (expr.charAt(index) == '(');
return index;
}
private String getHeader(String string) {
return main.getValue(string);
}
private boolean verifyHeader(String name, Pattern regex, boolean error) {
String value = manifest.getMainAttributes().getValue(name);
if (value == null)
return false;
QuotedTokenizer st = new QuotedTokenizer(value.trim(), ",");
for (Iterator i = st.getTokenSet().iterator(); i.hasNext();) {
if (!verify((String) i.next(), regex)) {
(error ? errors : warnings).add("Invalid value for " + name
+ ", " + value + " does not match " + regex.pattern());
}
}
return true;
}
private boolean verify(String value, Pattern regex) {
return regex.matcher(value).matches();
}
private boolean verifyListHeader(String name, Pattern regex, boolean error) {
String value = manifest.getMainAttributes().getValue(name);
if (value == null)
return false;
QuotedTokenizer st = new QuotedTokenizer(value.trim(), ",");
for (Iterator i = st.getTokenSet().iterator(); i.hasNext();) {
if (!regex.matcher((String) i.next()).matches()) {
(error ? errors : warnings).add("Invalid value for " + name
+ ", " + value + " does not match " + regex.pattern());
}
}
return true;
}
}