diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java index 8b4a9b4b..88f8f64a 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java @@ -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) 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), diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java index 0bfa0efe..478632ea 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java @@ -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" }; diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java new file mode 100644 index 00000000..b2d4efbd --- /dev/null +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java @@ -0,0 +1,242 @@ +/** + * Copyright 2014 Ryszard Wiśniewski + * Copyright 2015 Connor Tumbleson + * + * 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 + */ +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 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 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); + } +} diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/ProviderAttributeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/ProviderAttributeTest.java new file mode 100644 index 00000000..ad1c1624 --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/ProviderAttributeTest.java @@ -0,0 +1,91 @@ +/** + * Copyright 2014 Ryszard Wiśniewski + * Copyright 2014 Connor Tumbleson + * + * 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("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""); + + + 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; +} diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/issue636/issue636.apk b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/issue636/issue636.apk new file mode 100644 index 00000000..1a1f76ef Binary files /dev/null and b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/issue636/issue636.apk differ