Merge pull request #911 from iBotPeaches/issue_636

Wires up rewriter of @string references in provider attrs
This commit is contained in:
Connor Tumbleson 2015-04-19 10:31:09 -05:00
commit d76d7d8f11
5 changed files with 359 additions and 116 deletions

View File

@ -22,6 +22,7 @@ import brut.androlib.res.data.ResPackage;
import brut.androlib.res.data.ResTable;
import brut.androlib.res.data.ResUnknownFiles;
import brut.androlib.res.util.ExtFile;
import brut.androlib.res.xml.ResXmlPatcher;
import brut.androlib.src.SmaliBuilder;
import brut.androlib.src.SmaliDecoder;
import brut.common.BrutException;
@ -280,7 +281,9 @@ public class Androlib {
new File(appDir, APK_DIRNAME).mkdirs();
buildSources(appDir);
buildNonDefaultSources(appDir);
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(new File(appDir, "AndroidManifest.xml"));
buildResources(appDir, (Map<String, Object>) meta.get("usesFramework"));
buildLib(appDir);
buildCopyOriginalFiles(appDir);
buildApk(appDir, outFile);
@ -481,7 +484,7 @@ public class Androlib {
File apkDir = new File(appDir, APK_DIRNAME);
if (apkOptions.debugMode) {
mAndRes.remove_application_debug(new File(apkDir, "AndroidManifest.xml").getAbsolutePath());
ResXmlPatcher.removeApplicationDebugTag(new File(apkDir,"AndroidManifest.xml"));
}
if (apkOptions.forceBuildAll || isModified(newFiles(APK_MANIFEST_FILENAMES, appDir),

View File

@ -25,6 +25,7 @@ import brut.androlib.res.decoder.ARSCDecoder.ARSCData;
import brut.androlib.res.decoder.ARSCDecoder.FlagsOffset;
import brut.androlib.res.util.*;
import brut.androlib.res.xml.ResValuesXmlSerializable;
import brut.androlib.res.xml.ResXmlPatcher;
import brut.common.BrutException;
import brut.directory.*;
import brut.util.*;
@ -35,19 +36,8 @@ import java.util.zip.*;
import java.io.File;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.apache.commons.io.IOUtils;
import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlSerializer;
/**
@ -167,115 +157,30 @@ final public class AndrolibResources {
}
}
public void remove_application_debug(String filePath)
throws AndrolibException {
// change application:debug to true
try {
Document doc = loadDocument(filePath);
Node application = doc.getElementById("application");
// load attr
NamedNodeMap attr = application.getAttributes();
Node debugAttr = attr.getNamedItem("debug");
// remove application:debug
if (debugAttr != null) {
attr.removeNamedItem("debug");
}
saveDocument(filePath, doc);
} catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
}
}
public void adjust_package_manifest(ResTable resTable, String filePath)
public void adjustPackageManifest(ResTable resTable, String filePath)
throws AndrolibException {
// compare resources.arsc package name to the one present in AndroidManifest
ResPackage resPackage = resTable.getCurrentResPackage();
mPackageOriginal = resPackage.getName();
String packageOriginal = resPackage.getName();
mPackageRenamed = resTable.getPackageRenamed();
resTable.setPackageId(resPackage.getId());
resTable.setPackageOriginal(mPackageOriginal);
resTable.setPackageOriginal(packageOriginal);
// 1) Check if mPackageOriginal === mPackageRenamed
// 2) Check if mPackageOriginal is ignored via IGNORED_PACKAGES
// 1) Check if packageOriginal === mPackageRenamed
// 2) Check if packageOriginal is ignored via IGNORED_PACKAGES
// 2a) If its ignored, make sure the mPackageRenamed isn't explicitly allowed
if (mPackageOriginal.equalsIgnoreCase(mPackageRenamed) ||
(Arrays.asList(IGNORED_PACKAGES).contains(mPackageOriginal) &&
if (packageOriginal.equalsIgnoreCase(mPackageRenamed) ||
(Arrays.asList(IGNORED_PACKAGES).contains(packageOriginal) &&
! Arrays.asList(ALLOWED_PACKAGES).contains(mPackageRenamed))) {
LOGGER.info("Regular manifest package...");
} else {
try {
LOGGER.info("Renamed manifest package found! Replacing " + mPackageRenamed + " with " + mPackageOriginal);
Document doc = loadDocument(filePath);
// Get the manifest line
Node manifest = doc.getFirstChild();
// update package attribute
NamedNodeMap attr = manifest.getAttributes();
Node nodeAttr = attr.getNamedItem("package");
nodeAttr.setNodeValue(mPackageOriginal);
saveDocument(filePath, doc);
} catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
}
LOGGER.info("Renamed manifest package found! Replacing " + mPackageRenamed + " with " + packageOriginal);
ResXmlPatcher.renameManifestPackage(new File(filePath), packageOriginal);
}
}
public void remove_manifest_versions(String filePath)
throws AndrolibException {
File f = new File(filePath);
if (f.exists()) {
try {
Document doc = loadDocument(filePath);
Node manifest = doc.getFirstChild();
// load attr
NamedNodeMap attr = manifest.getAttributes();
Node vCode = attr.getNamedItem("android:versionCode");
Node vName = attr.getNamedItem("android:versionName");
// remove versionCode
if (vCode != null) {
attr.removeNamedItem("android:versionCode");
}
if (vName != null) {
attr.removeNamedItem("android:versionName");
}
saveDocument(filePath, doc);
} catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
}
}
}
private Document loadDocument(String filename)
throws IOException, SAXException, ParserConfigurationException {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
return docBuilder.parse(filename);
}
private void saveDocument(String filename, Document doc)
throws IOException, SAXException, ParserConfigurationException, TransformerException {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.STANDALONE,"yes");
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(new File(filename));
transformer.transform(source, result);
}
public void decodeManifestWithResources(ResTable resTable, ExtFile apkFile, File outDir)
throws AndrolibException {
@ -300,8 +205,11 @@ final public class AndrolibResources {
// also remove the android::versionCode / versionName from manifest for rebuild
// this is a required change to prevent aapt warning about conflicting versions
// it will be passed as a parameter to aapt like "--min-sdk-version" via apktool.yml
adjust_package_manifest(resTable, outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml");
remove_manifest_versions(outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml");
adjustPackageManifest(resTable, outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml");
ResXmlPatcher.removeManifestVersions(new File(
outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml"));
mPackageId = String.valueOf(resTable.getPackageId());
}
} catch (DirectoryException ex) {
@ -796,13 +704,15 @@ final public class AndrolibResources {
* @throws AndrolibException
*/
public File getAaptBinaryFile() throws AndrolibException {
File aaptBinary;
try {
if (OSDetection.isMacOSX()) {
mAaptBinary = Jar.getResourceAsFile("/prebuilt/aapt/macosx/aapt");
aaptBinary = Jar.getResourceAsFile("/prebuilt/aapt/macosx/aapt");
} else if (OSDetection.isUnix()) {
mAaptBinary = Jar.getResourceAsFile("/prebuilt/aapt/linux/aapt");
aaptBinary = Jar.getResourceAsFile("/prebuilt/aapt/linux/aapt");
} else if (OSDetection.isWindows()) {
mAaptBinary = Jar.getResourceAsFile("/prebuilt/aapt/windows/aapt.exe");
aaptBinary = Jar.getResourceAsFile("/prebuilt/aapt/windows/aapt.exe");
} else {
LOGGER.warning("Unknown Operating System: " + OSDetection.returnOS());
return null;
@ -810,8 +720,8 @@ final public class AndrolibResources {
} catch (BrutException ex) {
throw new AndrolibException(ex);
}
if (mAaptBinary.setExecutable(true)) {
return mAaptBinary;
if (aaptBinary.setExecutable(true)) {
return aaptBinary;
}
System.err.println("Can't set aapt binary as executable");
@ -841,13 +751,10 @@ final public class AndrolibResources {
private String mVersionCode = null;
private String mVersionName = null;
private String mPackageRenamed = null;
private String mPackageOriginal = null;
private String mPackageId = null;
private boolean mSharedLibrary = false;
private File mAaptBinary = null;
private final static String[] IGNORED_PACKAGES = new String[] {
"android", "com.htc", "miui", "com.lge", "com.lge.internal", "yi" };

View File

@ -0,0 +1,242 @@
/**
* Copyright 2014 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright 2015 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package brut.androlib.res.xml;
import brut.androlib.AndrolibException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;
import java.io.File;
import java.io.IOException;
/**
* @author Connor Tumbleson <connor.tumbleson@gmail.com>
*/
public final class ResXmlPatcher {
/**
* Removes "debug" tag from file
*
* @param file AndroidManifest file
* @throws AndrolibException
*/
public static void removeApplicationDebugTag(File file) throws AndrolibException {
if (file.exists()) {
try {
Document doc = loadDocument(file);
Node application = doc.getElementById("application");
// load attr
NamedNodeMap attr = application.getAttributes();
Node debugAttr = attr.getNamedItem("debug");
// remove application:debug
if (debugAttr != null) {
attr.removeNamedItem("debug");
}
saveDocument(file, doc);
} catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
}
}
}
/**
* Any @string reference in a <provider> value in AndroidManifest.xml will break on
* build, thus preventing the application from installing. This is from a bug/error
* in AOSP where public resources cannot be part of an authorities attribute within
* a <provider> tag.
*
* This finds any reference and replaces it with the literal value found in the
* res/values/strings.xml file.
*
* @param file File for AndroidManifest.xml
* @throws AndrolibException
*/
public static void fixingPublicAttrsInProviderAttributes(File file) throws AndrolibException {
if (file.exists()) {
try {
Document doc = loadDocument(file);
XPath xPath = XPathFactory.newInstance().newXPath();
XPathExpression expression = xPath.compile("/manifest/application/provider");
Object result = expression.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
NamedNodeMap attrs = node.getAttributes();
if (attrs != null) {
Node provider = attrs.getNamedItem("android:authorities");
if (provider != null) {
String reference = provider.getNodeValue();
String replacement = pullValueFromStrings(file.getParentFile(), reference);
if (replacement != null) {
provider.setNodeValue(replacement);
saveDocument(file, doc);
}
}
}
}
} catch (SAXException | ParserConfigurationException | IOException |
XPathExpressionException | TransformerException ignored) {
}
}
}
/**
* Finds key in strings.xml file and returns text value
*
* @param directory Root directory of apk
* @param key String reference (ie @string/foo)
* @return String|null
* @throws AndrolibException
*/
public static String pullValueFromStrings(File directory, String key) throws AndrolibException {
if (! key.contains("@")) {
return null;
}
File file = new File(directory, "/res/values/strings.xml");
key = key.replace("@string/", "");
if (file.exists()) {
try {
Document doc = loadDocument(file);
XPath xPath = XPathFactory.newInstance().newXPath();
XPathExpression expression = xPath.compile("/resources/string[@name=" + '"' + key + "\"]/text()");
Object result = expression.evaluate(doc, XPathConstants.STRING);
if (result != null) {
return (String) result;
}
} catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) {
}
}
return null;
}
/**
* Removes attributes like "versionCode" and "versionName" from file.
*
* @param file File representing AndroidManifest.xml
* @throws AndrolibException
*/
public static void removeManifestVersions(File file) throws AndrolibException {
if (file.exists()) {
try {
Document doc = loadDocument(file);
Node manifest = doc.getFirstChild();
NamedNodeMap attr = manifest.getAttributes();
Node vCode = attr.getNamedItem("android:versionCode");
Node vName = attr.getNamedItem("android:versionName");
if (vCode != null) {
attr.removeNamedItem("android:versionCode");
}
if (vName != null) {
attr.removeNamedItem("android:versionName");
}
saveDocument(file, doc);
} catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
}
}
}
/**
* Replaces package value with passed packageOriginal string
*
* @param file File for AndroidManifest.xml
* @param packageOriginal Package name to replace
* @throws AndrolibException
*/
public static void renameManifestPackage(File file, String packageOriginal) throws AndrolibException {
try {
Document doc = loadDocument(file);
// Get the manifest line
Node manifest = doc.getFirstChild();
// update package attribute
NamedNodeMap attr = manifest.getAttributes();
Node nodeAttr = attr.getNamedItem("package");
nodeAttr.setNodeValue(packageOriginal);
saveDocument(file, doc);
} catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
}
}
/**
*
* @param file File to load into Document
* @return Document
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
*/
private static Document loadDocument(File file)
throws IOException, SAXException, ParserConfigurationException {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
return docBuilder.parse(file);
}
/**
*
* @param file File to save Document to (ie AndroidManifest.xml)
* @param doc Document being saved
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
* @throws TransformerException
*/
private static void saveDocument(File file, Document doc)
throws IOException, SAXException, ParserConfigurationException, TransformerException {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.STANDALONE,"yes");
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(file);
transformer.transform(source, result);
}
}

View File

@ -0,0 +1,91 @@
/**
* Copyright 2014 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright 2014 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package brut.androlib;
import brut.androlib.res.util.ExtFile;
import brut.common.BrutException;
import brut.directory.DirectoryException;
import brut.util.OS;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ProviderAttributeTest {
@BeforeClass
public static void beforeClass() throws BrutException {
sTmpDir = new ExtFile(OS.createTempDirectory());
TestUtils.copyResourceDir(ProviderAttributeTest.class, "brut/apktool/issue636/", sTmpDir);
}
@AfterClass
public static void afterClass() throws BrutException {
OS.rmdir(sTmpDir);
}
@Test
public void isProviderStringReplacementWorking() throws BrutException, IOException {
String apk = "issue636.apk";
// decode issue636.apk
ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk));
apkDecoder.setOutDir(new File(sTmpDir + File.separator + apk + ".out"));
apkDecoder.decode();
// build issue636
ExtFile testApk = new ExtFile(sTmpDir, apk + ".out");
new Androlib().build(testApk, null);
String newApk = apk + ".out" + File.separator + "dist" + File.separator + apk;
assertTrue(fileExists(newApk));
// decode issues636 again
apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + newApk));
apkDecoder.setOutDir(new File(sTmpDir + File.separator + apk + ".out.two"));
apkDecoder.decode();
String expected = replaceNewlines("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n" +
"<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"com.ibotpeaches.issue636\" platformBuildVersionCode=\"22\" platformBuildVersionName=\"5.1-1756733\">\n" +
" <application android:allowBackup=\"true\" android:debuggable=\"true\" android:icon=\"@mipmap/ic_launcher\" android:label=\"@string/app_name\" android:theme=\"@style/AppTheme\">\n" +
" <provider android:authorities=\"com.ibotpeaches.issue636.Provider\" android:exported=\"false\" android:grantUriPermissions=\"true\" android:label=\"@string/app_name\" android:multiprocess=\"false\" android:name=\"com.ibotpeaches.issue636.Provider\"/>\n" +
" <provider android:authorities=\"com.ibotpeaches.issue636.ProviderTwo\" android:exported=\"false\" android:grantUriPermissions=\"true\" android:label=\"@string/app_name\" android:multiprocess=\"false\" android:name=\"com.ibotpeaches.issue636.ProviderTwo\"/>\n" +
" </application>\n" +
"</manifest>");
byte[] encoded = Files.readAllBytes(Paths.get(sTmpDir + File.separator + apk + ".out.two" + File.separator + "AndroidManifest.xml"));
String obtained = replaceNewlines(new String(encoded));
assertEquals(expected, obtained);
}
private boolean fileExists(String filepath) {
return Files.exists(Paths.get(sTmpDir.getAbsolutePath() + File.separator + filepath));
}
private String replaceNewlines(String value) {
return value.replace("\n", "").replace("\r", "");
}
private static ExtFile sTmpDir;
}