WALA/com.ibm.wala.dalvik/src/com/ibm/wala/dalvik/util/AndroidManifestXMLReader.java

692 lines
27 KiB
Java

/*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html.
*
* This file is a derivative of code released under the terms listed below.
*
*/
/*
* Copyright (c) 2013,
* Tobias Blaschke <code@tobiasblaschke.de>
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The names of the contributors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.ibm.wala.dalvik.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import com.ibm.wala.dalvik.ipa.callgraph.propagation.cfa.Intent;
/**
* Read in an extracted AndroidManifest.xml.
*
* The file has to be in the extracted (human readable) XML-Format. You can extract it using the program
* `apktool`.
*
* Tags and Attributes not known by the Parser are skipped over.
*
* To add a Tag to the parsed ones:
* You have to extend the enum Tags: All Tags on the path to the Tag in question have to be in the enum.
* Eventually you will have to adapt the ParserItem of the Parent-Tags and add it to their allowed Sub-Tags.
*
* To add an Attribute to the handled ones:
* You'll have to extend the Enum Attrs if the Attribute-Name is not yet present there. Then add the
* Attribute to the relevant Attributes of it's containing Tag.
*
* You will be able to access it using attributesHistory.get(Attr).peek()
*
* TODO:
* @todo Handle Info in the DATA-Tag correctly!
* @since 2013-10-13
* @author Tobias Blaschke <code@tobiasblaschke.de>
*/
public class AndroidManifestXMLReader {
/**
* This logs low-level parsing.
*
* Mainly pushes and pops from stacks and seen tags.
* If you only want Information about objects created use the logger in AndroidSettingFactory as this
* parser generates all objects using it.
*/
private static final Logger logger = LoggerFactory.getLogger(AndroidSettingFactory.class);
public AndroidManifestXMLReader(File xmlFile) throws IOException {
if (xmlFile == null) {
throw new IllegalArgumentException("xmlFile may not be null");
}
try {
readXML(new FileInputStream(xmlFile));
} catch (Exception e) {
e.printStackTrace();
throw new IllegalStateException("Exception was thrown");
}
}
public AndroidManifestXMLReader(InputStream xmlFile) {
if (xmlFile == null) {
throw new IllegalArgumentException("xmlFile may not be null");
}
try {
readXML(xmlFile);
} catch (Exception e) {
e.printStackTrace();
throw new IllegalStateException("Exception was thrown");
}
}
private void readXML(InputStream xml) throws SAXException, IOException, ParserConfigurationException {
assert (xml != null) : "xmlFile may not be null";
final SAXHandler handler = new SAXHandler();
final SAXParserFactory factory = SAXParserFactory.newInstance();
factory.newSAXParser().parse(new InputSource(xml), handler);
}
// Needed to delay initialization
private interface ISubTags {
public Set<Tag> getSubTags();
}
private interface HistoryKey {} ;
/**
* Only includes relevant tags.
*/
@SuppressWarnings("unchecked")
private enum Tag implements HistoryKey {
/**
* This tag is nat an actual part of the document.
*/
ROOT("ROOT",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.MANIFEST); }},
null,
NoOpItem.class),
MANIFEST("manifest",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.APPLICATION); }},
EnumSet.of(Attr.PACKAGE),
ManifestItem.class),
APPLICATION("application",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.ACTIVITY, Tag.SERVICE, Tag.RECEIVER, Tag.PROVIDER, Tag.ALIAS); }}, // Allowed children..
Collections.EMPTY_SET, // Interesting Attributes
NoOpItem.class), // Handler
ACTIVITY("activity",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.INTENT); }},
EnumSet.of(Attr.NAME, Attr.ENABLED, Attr.PROCESS),
ComponentItem.class),
ALIAS("activity-alias",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.INTENT); }},
EnumSet.of(Attr.ENABLED, Attr.TARGET, Attr.NAME),
null),
SERVICE("service",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.INTENT); }},
EnumSet.of(Attr.ENABLED, Attr.NAME, Attr.PROCESS),
ComponentItem.class),
RECEIVER("receiver",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.INTENT); }},
EnumSet.of(Attr.ENABLED, Attr.NAME, Attr.PROCESS),
ComponentItem.class),
PROVIDER("provider",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.INTENT); }},
EnumSet.of(Attr.ENABLED, Attr.ORDER, Attr.NAME, Attr.PROCESS),
ComponentItem.class),
INTENT("intent-filter",
new ISubTags() { public Set<Tag> getSubTags() {
return EnumSet.of(Tag.ACTION, Tag.DATA); }},
Collections.EMPTY_SET,
IntentItem.class),
ACTION("action",
new ISubTags() { public Set<Tag> getSubTags() {
return Collections.EMPTY_SET; }},
EnumSet.of(Attr.NAME),
FinalItem.class), //(new ITagDweller() {
//public Tag getTag() { return Tag.ACTION; }})),
DATA("data",
new ISubTags() { public Set<Tag> getSubTags() {
return Collections.EMPTY_SET; }},
EnumSet.of(Attr.SCHEME, Attr.HOST, Attr.PATH, Attr.MIME),
FinalItem.class), //(new ITagDweller() {
//public Tag getTag() { return Tag.DATA; }})),
/**
* Internal pseudo-tag used for tags in the documents, that have no parser representation.
*/
UNIMPORTANT("UNIMPORTANT",
null,
Collections.EMPTY_SET,
null);
private final String tagName;
private final Set<Attr> relevantAttributes;
private final ISubTags allowedSubTagsHolder;
private final ParserItem item;
private Set<Tag> allowedSubTags; // Delay init
private static final Map<String, Tag> reverseMap = new HashMap<String, Tag>();// HashMapFactory.make(9);
Tag (String tagName, ISubTags allowedSubTags, Set<Attr> relevant, Class<? extends ParserItem> item) {
this.tagName = tagName;
this.relevantAttributes = relevant;
this.allowedSubTagsHolder = allowedSubTags;
if (item != null) {
try {
this.item = item.newInstance();
this.item.setSelf(this);
} catch (java.lang.InstantiationException e) {
e.getCause().printStackTrace();
throw new IllegalStateException("InstantiationException was thrown");
} catch (java.lang.IllegalAccessException e) {
e.printStackTrace();
if (e.getCause() != null) {
e.getCause().printStackTrace();
}
throw new IllegalStateException("IllegalAccessException was thrown");
}
} else {
this.item = null;
}
}
static {
for (Tag tag: Tag.values()) {
reverseMap.put(tag.tagName, tag);
}
}
/**
* The class that takes action on this tag.
*/
public ParserItem getHandler() {
if (this.item == null) {
System.err.println("Requested non existing handler for: " + this.toString());
}
return this.item;
}
/**
* The Tags that may appear as a child of this Tag.
*/
public Set<Tag> getAllowedSubTags() {
if (this.allowedSubTagsHolder == null) {
return null;
} else if (this.allowedSubTags == null) {
this.allowedSubTags = allowedSubTagsHolder.getSubTags();
}
return Collections.unmodifiableSet(this.allowedSubTags);
}
/**
* The Attributes read in when parsing the Tag.
*
* The read attributes get thrown on a Stack, thus they don't need to be evaluated
* in the Tag itself but may also be handled by a parent.
*
* The handling Item has to pop them after it has evaluated them else it gets a big
* mess.
*/
public Set<Attr> getRelevantAttributes() {
return Collections.unmodifiableSet(this.relevantAttributes);
}
/**
* The given Attr is in {@link #getRelevantAttributes()}.
*/
public boolean isRelevant(Attr attr) {
return relevantAttributes.contains(attr);
}
/**
* All Tags in this Enum but UNIMPORTANT are relevant.
*/
public boolean isRelevant() {
return (this != Tag.UNIMPORTANT);
}
/**
* Match the Tag-Name in the XML-File against the one associated to the Enums Tag.
*
* If no Tag in this Enum matches Tag.UNIMPORTANT is returned and the parser will ignore the
* tree under this tag.
*
* Matching is case insensitive of course.
*/
public static Tag fromString(String tag) {
tag = tag.toLowerCase();
if (reverseMap.containsKey(tag)) {
return reverseMap.get(tag);
} else {
return Tag.UNIMPORTANT;
}
}
/**
* The Tag appears in the XML File using this name.
*/
public String getName() {
return this.tagName;
}
}
/**
* Attributes that may appear in a Tags.Tag.
*
* In order to evaluate a new attribute it has to be added to the Tags it may appear in and
* at least one ParserItem has to be adapted.
*
* Values read for the single Attrs will get pushed to the attributesHistory (in Items).
*/
private enum Attr implements HistoryKey {
PACKAGE("package"),
NAME("name"),
SCHEME("scheme"),
HOST("host"),
PATH("path"),
ENABLED("enabled"),
TARGET("targetActivity"),
PROCESS("process"),
ORDER("initOrder"),
MIME("mimeType");
private final String attrName;
Attr(String attrName) {
this.attrName = attrName;
}
public boolean isRelevantIn(Tag tag) {
return tag.isRelevant(this);
}
public String getName() {
return this.attrName;
}
}
/**
* Contains the "path" from Tag.ROOT that currently gets evaluated.
*
* On an opening Tag the Tag gets pushed. It get's popped again once it's evaluated.
* That is on the closing Tag or on the closing Tag of a parent: A Item does not remove its
* own Tag from the Stack.
*/
private static final Stack<Tag> parserStack = new Stack<Tag>();
/**
* Contains either Attributes of a child or the evaluation-result of a child-Tag.
*
* The Item that consumes an Attribute has to pop it.
*/
private static final Map<HistoryKey, Stack<Object>> attributesHistory = new HashMap<HistoryKey, Stack<Object>>(); // No EnumMap possible :(
static {
for (Attr attr : Attr.values()) {
attributesHistory.put(attr, new Stack<Object>());
}
for (Tag tag : Tag.values()) {
attributesHistory.put(tag, new Stack<Object>());
}
}
/**
* Handling of a Tag.
*
* Does (if not overridden) all the needed Push- and Pop-Operations. Items may however choose to leave some
* stuff on the Stack. In this case this data has to be popped by the handling parent, or the Tag may only
* occur once or bad things will happen.
*
* _CAUTION_: This will be instantiated by an Enum, so if you write local Fields you will get surprising
* results! You should mark all of them as final to be sure.
*/
private static abstract class ParserItem {
protected Tag self;
/**
* Set the Tag this ParserItem-Instance is an Handler for.
*
* This may only be set once!
*/
public void setSelf(Tag self) {
if (this.self != null) {
throw new IllegalStateException("Self can only be set once!");
}
this.self = self;
}
public ParserItem() {
}
/**
* Remember attributes to the tag.
*
* The read attributes will be pushed to the attributesHistory.
*
* Leave Parser-Stack alone! This is called by SAXHandler only!
*/
public void enter(Attributes saxAttrs) {
for (Attr relevant : self.getRelevantAttributes()) {
String attr = saxAttrs.getValue(relevant.getName());
if (attr == null) {
attr = saxAttrs.getValue("android:" + relevant.getName());
}
attributesHistory.get(relevant).push(attr);
logger.debug("Pushing '{}' for {} in {}", attr, relevant, self);
// if there is no such value in saxAttrs it returns null
}
}
/**
* Remove all Attributes generated by self and self itself.
*
* This is called by the consuming ParserItem.
*/
public void popAttributes() {
for (Attr relevant : self.getRelevantAttributes()) {
try {
logger.debug("Popping {} of value {} in {}", relevant, attributesHistory.get(relevant).peek(), self);
attributesHistory.get(relevant).pop();
} catch (java.util.EmptyStackException e) {
System.err.println(self + " failed to pop " + relevant);
throw e;
}
}
if (attributesHistory.containsKey(self) && attributesHistory.get(self) != null &&
(! attributesHistory.get(self).isEmpty())) {
try {
attributesHistory.get(self).pop();
} catch (java.util.EmptyStackException e) {
System.err.println("The Stack for " + self + " was Empty when trying to pop");
throw e;
}
}
}
/**
* Consume sub-items on the stack.
*
* Do this by popping them, but leave self on the stack!
* For each Item popped call its popAttributes()!
*/
public void leave() {
while (parserStack.peek() != self) {
final Set<Tag> allowedSubTags = self.getAllowedSubTags();
Tag subTag = parserStack.pop();
if (allowedSubTags.contains(subTag)) {
if (subTag.getHandler() == null) {
throw new IllegalArgumentException("The SubTag " + subTag.toString() + " has no handler!");
}
subTag.getHandler().popAttributes(); // hmmm....
logger.debug("New Stack: {}", parserStack);
//parserStack.pop();
} else {
throw new IllegalStateException(subTag + " is not allowed as sub-tag of " + self + " in Context:\n\t" + parserStack);
}
}
}
}
/**
* An ParserItem that contains no sub-tags.
*
* You can use it directly if you don't intend to do any computation on this Tag but remember its
* Attributes.
*/
private static class FinalItem extends ParserItem {
public FinalItem() {
super();
}
@Override
public void leave() {
final Set<Tag> subs = self.getAllowedSubTags();
if (!((subs == null) || subs.isEmpty())) {
throw new IllegalArgumentException("FinalItem can not be applied to " + self + " as it contains sub-tags: " +
self.getAllowedSubTags());
}
if (parserStack.peek() != self) {
throw new IllegalStateException("Topstack is not " + self + " which is disallowed for a FinalItem!\n" +
"This is most certainly caused by an implementation mistake on a ParserItem. Stack is:\n\t" + parserStack);
}
}
}
/**
* Only extracts Attributes.
*
* It's like FinalItem but may contain sub-tags.
*/
private static class NoOpItem extends ParserItem {
public NoOpItem() {
super();
}
}
/**
* The root-element of an AndroidManifest contains the package.
*/
private static class ManifestItem extends ParserItem {
public ManifestItem() {
super();
}
@Override
public void enter(Attributes saxAttrs) {
super.enter(saxAttrs);
AndroidEntryPointManager.MANAGER.setPackage((String) attributesHistory.get(Attr.PACKAGE).peek());
}
}
/**
* Read the specification of an Intent from AndroidManifest.
*
* @todo Handle the URI
*/
private static class IntentItem extends ParserItem {
public IntentItem() {
super();
}
@Override
public void leave() {
Set<Tag> allowedTags = EnumSet.copyOf(self.getAllowedSubTags());
Set<String> urls = new HashSet<String>();
Set<String> names = new HashSet<String>();
while (parserStack.peek() != self) {
Tag current = parserStack.pop();
if (allowedTags.contains(current)) {
if (current == Tag.ACTION) {
Object oName = attributesHistory.get(Attr.NAME).peek();
if (oName == null) {
throw new IllegalStateException("The currently parsed Action did not leave the required 'name' Attribute" +
" on the Stack! Attributes-Stack for name is: " + attributesHistory.get(Attr.NAME));
} else if (oName instanceof String) {
names.add((String) oName);
} else {
throw new IllegalStateException("Unexpected Attribute type for name: " + oName.getClass().toString());
}
} else if (current == Tag.DATA) {
Object oUrl = attributesHistory.get(Attr.SCHEME).peek();
if (oUrl == null) {
// TODO
} else if (oUrl instanceof String) {
urls.add((String) oUrl);
} else {
throw new IllegalStateException("Unexpected Attribute type for name: " + oUrl.getClass().toString());
}
} else {
throw new IllegalStateException("Error in parser implementation");
}
current.getHandler().popAttributes();
} else {
throw new IllegalStateException("In INTENT: Tag " + current + " not allowed in Context " + parserStack + "\n\t"+
"Allowed Tags: " + allowedTags);
}
}
// Pushing intent...
final String pack;
if ((attributesHistory.get(Attr.PACKAGE) != null ) && (!(attributesHistory.get(Attr.PACKAGE).isEmpty()))) {
pack = (String) attributesHistory.get(Attr.PACKAGE).peek();
} else {
logger.warn("Empty Package {}", attributesHistory.get(Attr.PACKAGE).peek());
pack = null;
}
if (!names.isEmpty()) {
for (String name : names) {
if (urls.isEmpty()) urls.add(null);
for (String url : urls) {
logger.info("New Intent ({}, {})", name, url);
final Intent intent = AndroidSettingFactory.intent(name, url);
attributesHistory.get(self).push(intent);
}
}
} else {
throw new IllegalStateException("Error in parser implementation! The required attribute 'name' which should have been " +
"defined in ACTION could not be retrieved. This should have been thrown before as it is a required attribute for " +
"ACTION");
}
}
}
private static class ComponentItem extends ParserItem {
public ComponentItem() {
super();
}
@Override
public void leave() {
final Set<Tag> allowedTags = self.getAllowedSubTags();
final Set<Intent> overrideTargets = new HashSet<Intent>();
while (parserStack.peek() != self) {
Tag current = parserStack.pop();
if (allowedTags.contains(current)) {
if (current == Tag.INTENT) {
Object oIntent = attributesHistory.get(Tag.INTENT).peek();
if (oIntent == null) {
throw new IllegalStateException("The currently parsed Intent did not push a Valid intent to the " +
"Stack! Attributes-Stack for name is: " + attributesHistory.get(Attr.NAME));
} else if (oIntent instanceof Intent) {
overrideTargets.add( (Intent) oIntent );
} else {
throw new IllegalStateException("Unexpected Attribute type for Intent: " + oIntent.getClass().toString());
}
} else {
throw new IllegalStateException("Error in parser implementation");
}
current.getHandler().popAttributes();
} else {
throw new IllegalStateException("In " + self + ": Tag " + current + " not allowed in Context " + parserStack + "\n\t"+
"Allowed Tags: " + allowedTags);
}
}
// Generating Intent for this...
final String pack;
if ((attributesHistory.get(Attr.PACKAGE) != null ) && (!(attributesHistory.get(Attr.PACKAGE).isEmpty()))) {
pack = (String) attributesHistory.get(Attr.PACKAGE).peek();
} else {
logger.warn("Empty Package {}", attributesHistory.get(Attr.PACKAGE).peek());
pack = null;
}
final String name = (String) attributesHistory.get(Attr.NAME).peek(); // TODO: Verify type!
final Intent intent = AndroidSettingFactory.intent(pack, name, null);
logger.info("\tRegister: {}", intent);
AndroidEntryPointManager.MANAGER.registerIntent(intent);
for (Intent ovr: overrideTargets) {
logger.info("\tOverride: {} --> {}", ovr, intent);
AndroidEntryPointManager.MANAGER.setOverride(ovr, intent);
}
}
}
private class SAXHandler extends DefaultHandler {
private int unimportantDepth = 0;
public SAXHandler() {
super();
parserStack.push(Tag.ROOT);
}
@Override
public void startElement(String uri, String name, String qName, Attributes attrs) {
Tag tag = Tag.fromString(qName);
if ((tag == Tag.UNIMPORTANT) || (unimportantDepth > 0)) {
unimportantDepth++;
} else {
logger.debug("Handling {} made from {}", tag, qName);
final ParserItem handler = tag.getHandler();
if (handler != null) {
handler.enter(attrs);
}
parserStack.push(tag);
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if (unimportantDepth > 0) {
unimportantDepth--;
} else {
final Tag tag = Tag.fromString(qName);
final ParserItem handler = tag.getHandler();
if (handler != null) {
handler.leave();
}
}
}
}
}