/* * 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(); } } } } }