blob: a990f5139d7143923047587596d6f67fcdccc67d [file] [log] [blame]
/* 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.*;
import java.util.*;
import java.util.jar.*;
import java.util.regex.*;
import aQute.libg.qtokens.*;
public class Verifier extends Analyzer {
Jar dot;
Manifest manifest;
Map<String, Map<String, String>> referred = newHashMap();
Map<String, Map<String, String>> contained = newHashMap();
Map<String, Set<String>> uses = newHashMap();
Map<String, Map<String, String>> mimports;
Map<String, Map<String, String>> mdynimports;
Map<String, Map<String, String>> mexports;
List<Jar> bundleClasspath;
Map<String, Map<String, String>> ignore = newHashMap(); // Packages
// to
// ignore
Map<String, Clazz> classSpace;
boolean r3;
boolean usesRequire;
boolean fragment;
Attributes main;
final static Pattern EENAME = Pattern
.compile("CDC-1\\.0/Foundation-1\\.0"
+ "|CDC-1\\.1/Foundation-1\\.1"
+ "|OSGi/Minimum-1\\.[1-9]"
+ "|JRE-1\\.1"
+ "|J2SE-1\\.2"
+ "|J2SE-1\\.3"
+ "|J2SE-1\\.4"
+ "|J2SE-1\\.5"
+ "|JavaSE-1\\.6"
+ "|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");
public final static String SYMBOLICNAME_STRING = "[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*";
public final static Pattern SYMBOLICNAME = Pattern
.compile(SYMBOLICNAME_STRING);
public final static String VERSION_STRING = "[0-9]+(\\.[0-9]+(\\.[0-9]+(\\.[0-9A-Za-z_-]+)?)?)?";
public final static Pattern VERSION = Pattern
.compile(VERSION_STRING);
final static Pattern FILTEROP = Pattern
.compile("=|<=|>=|~=");
final static Pattern VERSIONRANGE = Pattern
.compile("((\\(|\\[)"
+ VERSION_STRING
+ ","
+ VERSION_STRING
+ "(\\]|\\)))|"
+ VERSION_STRING);
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 static Pattern HEADER_PATTERN = Pattern
.compile("[A-Za-z0-9][-a-zA-Z0-9_]+");
public static Pattern TOKEN = Pattern
.compile("[-a-zA-Z0-9_]+");
Properties properties;
public Verifier(Jar jar) throws Exception {
this(jar, null);
}
public Verifier(Jar jar, Properties properties) throws Exception {
this.dot = jar;
this.properties = properties;
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();
verifyHeaders(main);
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));
}
public Verifier() {
// TODO Auto-generated constructor stub
}
private void verifyHeaders(Attributes main) {
for (Object element : main.keySet()) {
Attributes.Name header = (Attributes.Name) element;
String h = header.toString();
if (!HEADER_PATTERN.matcher(h).matches())
error("Invalid Manifest header: " + h + ", pattern="
+ HEADER_PATTERN);
}
}
private List<Jar> getBundleClassPath() {
List<Jar> list = newList();
String bcp = getHeader(Analyzer.BUNDLE_CLASSPATH);
if (bcp == null) {
list.add(dot);
} else {
Map<String,Map<String,String>> entries = parseHeader(bcp);
for (String jarOrDir : entries.keySet()) {
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);
addClose(sub);
EmbeddedResource.build(sub, resource);
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);
try {
Jar sub = new Jar(jarOrDir);
addClose(sub);
for ( Map.Entry<String, Resource> entry : dot.getResources().entrySet()) {
if ( entry.getKey().startsWith(jarOrDir))
sub.putResource( entry.getKey().substring(jarOrDir.length()+1), entry.getValue());
}
list.add(sub);
} catch (Exception e) {
error("Invalid embedded directory file on Bundle-Classpath: "
+ jarOrDir + ", " + e);
}
} 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();
if (name == null) {
error("Can not parse name from bundle native code header: "
+ nc);
return;
}
del = qt.getSeparator();
if (del == ';') {
if (dot != null && !dot.exists(name)) {
error("Native library not found in JAR: " + name);
}
} else {
String value = null;
if (del == '=')
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("language")) {
verify(value, ISO639);
} else if (key.equals("processor")) {
// verify(value, PROCESSORS);
} else if (key.equals("selection-filter")) {
// verify syntax filter
verifyFilter(value);
} else if (name.equals("*") && value == null) {
// Wildcard must be at end.
if (qt.nextToken() != null)
error("Bundle-Native code header may only END in wildcard: nc");
} else {
warning("Unknown attribute in native code: " + name
+ "=" + value);
}
del = qt.getSeparator();
}
} while (del == ';');
} while (del == ',');
}
}
public void verifyFilter(String value) {
try {
verifyFilter(value, 0);
} catch (Exception e) {
error("Not a valid filter: " + value + e.getMessage());
}
}
private void verifyActivator() {
String bactivator = getHeader("Bundle-Activator");
if (bactivator != null) {
Clazz cl = loadClass(bactivator);
if (cl == null) {
int n = bactivator.lastIndexOf('.');
if (n > 0) {
String pack = bactivator.substring(0, n);
if (mimports.containsKey(pack))
return;
error("Bundle-Activator not found on the bundle class path nor in imports: "
+ bactivator);
} else
error("Activator uses default package and is not local (default package can not be imported): "
+ 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<String,Map<String,String>> map = parseHeader(serviceComponent);
for (String component : map.keySet()) {
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.
*
* Unfortunately, we also must take duplicate names into account. These
* duplicates are of course no erroneous.
*/
private void verifyInvalidExports() {
Set<String> invalidExport = newSet(mexports.keySet());
invalidExport.removeAll(contained.keySet());
// We might have duplicate names that are marked for it. These
// should not be counted. Should we test them against the contained
// set? Hmm. If someone wants to hang himself by using duplicates than
// I guess he can go ahead ... This is not a recommended practice
for (Iterator<String> i = invalidExport.iterator(); i.hasNext();) {
String pack = i.next();
if (isDuplicate(pack))
i.remove();
}
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<String> invalidImport = newSet(mimports.keySet());
invalidImport.removeAll(referred.keySet());
// TODO Added this line but not sure why it worked before ...
invalidImport.removeAll(contained.keySet());
String bactivator = getHeader(Analyzer.BUNDLE_ACTIVATOR);
if (bactivator != null) {
int n = bactivator.lastIndexOf('.');
if (n > 0) {
invalidImport.remove(bactivator.substring(0, n));
}
}
if (isPedantic() && !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<String> unresolvedReferences = new TreeSet<String>(referred
.keySet());
unresolvedReferences.removeAll(mimports.keySet());
unresolvedReferences.removeAll(contained.keySet());
// Remove any java.** packages.
for (Iterator<String> p = unresolvedReferences.iterator(); p.hasNext();) {
String pack = 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<String> culprits = new HashSet<String>();
for (Clazz clazz : classSpace.values()) {
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 (String pattern : mdynimports.keySet()) {
// 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 {
if (classSpace == null)
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();
verifyBundleClasspath();
if (usesRequire) {
if (!getErrors().isEmpty()) {
getWarnings()
.add(
0,
"Bundle uses Require Bundle, this can generate false errors because then not enough information is available without the required bundles");
}
}
}
public void verifyBundleClasspath() {
Map<String,Map<String,String>> bcp = parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH));
if ( bcp.isEmpty() || bcp.containsKey("."))
return;
for ( String path : dot.getResources().keySet()) {
if ( path.endsWith(".class")) {
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?");
return;
}
}
}
/**
* <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<String, Map<String,String>> map = parseHeader(dynamicImportPackage);
for (String name : map.keySet()) {
name = name.trim();
if (!verify(name, WILDCARDPACKAGE))
error("DynamicImport-Package header contains an invalid package name: "
+ name);
Map<String,String> sub = 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) {
error("Invalid JAR stream: Manifest should come first to be compatible with JarInputStream, it was not");
}
}
private void verifySymbolicName() {
Map<String,Map<String,String>> bsn = parseHeader(getHeader(Analyzer.BUNDLE_SYMBOLICNAME));
if (!bsn.isEmpty()) {
if (bsn.size() > 1)
error("More than one BSN specified " + bsn);
String name = (String) bsn.keySet().iterator().next();
if (!SYMBOLICNAME.matcher(name).matches()) {
error("Symbolic Name has invalid format: " + name);
}
}
}
/**
* <pre>
* filter ::= ’(’ filter-comp ’)’
* filter-comp ::= and | or | not | operation
* and ::= ’&amp;’ 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 ::= ’&tilde;=’
* greater ::= ’&gt;=’
* less ::= ’&lt;=’
* present ::= attr ’=*’
* substring ::= attr ’=’ initial any final
* inital ::= () | value
* any ::= ’*’ star-value
* star-value ::= () | value ’*’ star-value
* final ::= () | value
* value ::= &lt;see text&gt;
* </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);
index++;
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);
}
@SuppressWarnings("unchecked")
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<String> i = st.getTokenSet().iterator(); i.hasNext();) {
if (!verify((String) i.next(), regex)) {
String msg = "Invalid value for " + name + ", " + value
+ " does not match " + regex.pattern();
if (error)
error(msg);
else
warning(msg);
}
}
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;
Map<String,Map<String,String>> map = parseHeader(value);
for (String header : map.keySet()) {
if (!regex.matcher(header).matches()) {
String msg = "Invalid value for " + name + ", " + value
+ " does not match " + regex.pattern();
if (error)
error(msg);
else
warning(msg);
}
}
return true;
}
public String getProperty(String key, String deflt) {
if (properties == null)
return deflt;
return properties.getProperty(key, deflt);
}
public void setClassSpace(Map<String,Clazz> classspace,
Map<String, Map<String, String>> contained,
Map<String, Map<String, String>> referred,
Map<String, Set<String>> uses) {
this.classSpace = classspace;
this.contained = contained;
this.referred = referred;
this.uses = uses;
}
public static boolean isVersion(String version) {
return VERSION.matcher(version).matches();
}
}