diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4efb8672..c4d70c3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,7 +65,7 @@ jobs: fail-fast: true matrix: os: [ ubuntu-latest, macOS-latest, windows-latest ] - java: [ 8, 9, 10, 11, 12, 13, 14, 15, 16 ] + java: [ 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 ] steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 diff --git a/brut.apktool/apktool-cli/build.gradle b/brut.apktool/apktool-cli/build.gradle index 083c7b52..d115f36d 100644 --- a/brut.apktool/apktool-cli/build.gradle +++ b/brut.apktool/apktool-cli/build.gradle @@ -23,11 +23,12 @@ dependencies { } buildscript { - repositories { - mavenCentral() - } - dependencies { + repositories { + mavenCentral() + gradlePluginPortal() + } + classpath(depends.proguard_gradle) { exclude group: 'com.android.tools.build' } diff --git a/brut.apktool/apktool-lib/build.gradle b/brut.apktool/apktool-lib/build.gradle index 36c37493..9dfea7d4 100644 --- a/brut.apktool/apktool-lib/build.gradle +++ b/brut.apktool/apktool-lib/build.gradle @@ -13,33 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import org.apache.tools.ant.filters.ReplaceTokens +import org.apache.tools.ant.filters.* apply plugin: 'java-library' processResources { - from('src/main/resources/properties') { - include '**/*.properties' - into 'properties' - filter(ReplaceTokens, tokens: [version: project.apktool_version, gitrev: project.hash]) - duplicatesStrategy = DuplicatesStrategy.INCLUDE - } - from('src/main/resources/') { - include '**/*.jar' - duplicatesStrategy = DuplicatesStrategy.INCLUDE - } + from('src/main/resources/properties') { + include '**/*.properties' + into 'properties' + filter(ReplaceTokens, tokens: [version: project.apktool_version, gitrev: project.hash] ) + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + from('src/main/resources/') { + include '**/*.jar' + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } - includeEmptyDirs = false + includeEmptyDirs = false } dependencies { - testImplementation depends.junit - - api project(':brut.j.dir'), - project(':brut.j.util'), - project(':brut.j.common') - def androidJar def root = System.getenv('ANDROID_SDK_ROOT') if (root == null) { @@ -52,14 +45,19 @@ dependencies { compileOnly androidJar - implementation depends.baksmali, - depends.smali, - depends.snakeyaml, - depends.xmlpull, - depends.guava, - depends.commons_lang, - depends.commons_io, - depends.commons_text + api project(':brut.j.dir') + api project(':brut.j.util') + api project(':brut.j.common') + implementation depends.baksmali + implementation depends.smali + implementation depends.snakeyaml + implementation depends.xmlpull + implementation depends.guava + implementation depends.commons_lang + implementation depends.commons_io + implementation depends.commons_text + + testImplementation depends.junit testImplementation depends.xmlunit } 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 8e433b77..b1d27a09 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 @@ -177,16 +177,19 @@ public class Androlib { for (String file : files) { if (isAPKFileNames(file) && unk.getCompressionLevel(file) == 0) { - String ext = ""; + String extOrFile = ""; if (unk.getSize(file) != 0) { - ext = FilenameUtils.getExtension(file); + extOrFile = FilenameUtils.getExtension(file); } - if (ext.isEmpty() || !NO_COMPRESS_PATTERN.matcher(ext).find()) { - ext = file; + if (extOrFile.isEmpty() || !NO_COMPRESS_PATTERN.matcher(extOrFile).find()) { + extOrFile = file; + if (mAndRes.mResFileMapping.containsKey(extOrFile)) { + extOrFile = mAndRes.mResFileMapping.get(extOrFile); + } } - if (!uncompressedFilesOrExts.contains(ext)) { - uncompressedFilesOrExts.add(ext); + if (!uncompressedFilesOrExts.contains(extOrFile)) { + uncompressedFilesOrExts.add(extOrFile); } } } @@ -839,7 +842,7 @@ public class Androlib { private final static String APK_DIRNAME = "build/apk"; private final static String UNK_DIRNAME = "unknown"; private final static String[] APK_RESOURCES_FILENAMES = new String[] { - "resources.arsc", "AndroidManifest.xml", "res" }; + "resources.arsc", "AndroidManifest.xml", "res", "r", "R" }; private final static String[] APK_RESOURCES_WITHOUT_RES_FILENAMES = new String[] { "resources.arsc", "AndroidManifest.xml" }; private final static String[] APP_RESOURCES_FILENAMES = new String[] { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java index 4392fcc5..1c3518ff 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java @@ -180,6 +180,10 @@ public class ApkDecoder { if (mode != DECODE_SOURCES_NONE && mode != DECODE_SOURCES_SMALI && mode != DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES) { throw new AndrolibException("Invalid decode sources mode: " + mode); } + if (mDecodeSources == DECODE_SOURCES_NONE && mode == DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES) { + LOGGER.info("--only-main-classes cannot be paired with -s/--no-src. Ignoring."); + return; + } mDecodeSources = mode; } 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 45933332..184506b7 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 @@ -223,22 +223,12 @@ final public class AndrolibResources { ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder(); attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next()); - Directory inApk, in = null, out; + Directory in, out; try { out = new FileDirectory(outDir); - - inApk = apkFile.getDirectory(); + in = apkFile.getDirectory(); out = out.createDir("res"); - if (inApk.containsDir("res")) { - in = inApk.getDir("res"); - } - if (in == null && inApk.containsDir("r")) { - in = inApk.getDir("r"); - } - if (in == null && inApk.containsDir("R")) { - in = inApk.getDir("R"); - } } catch (DirectoryException ex) { throw new AndrolibException(ex); } @@ -249,7 +239,7 @@ final public class AndrolibResources { LOGGER.fine("Decoding file-resources..."); for (ResResource res : pkg.listFiles()) { - fileDecoder.decode(res, in, out); + fileDecoder.decode(res, in, out, mResFileMapping); } LOGGER.fine("Decoding values */* XMLs..."); @@ -974,7 +964,12 @@ final public class AndrolibResources { } else if (OSDetection.isWindows()) { path = parentPath.getAbsolutePath() + String.format("%1$sAppData%1$sLocal%1$sapktool%1$sframework", File.separatorChar); } else { - path = parentPath.getAbsolutePath() + String.format("%1$s.local%1$sshare%1$sapktool%1$sframework", File.separatorChar); + String xdgDataFolder = System.getenv("XDG_DATA_HOME"); + if (xdgDataFolder != null) { + path = xdgDataFolder + String.format("%1$sapktool%1$sframework", File.separatorChar); + } else { + path = parentPath.getAbsolutePath() + String.format("%1$s.local%1$sshare%1$sapktool%1$sframework", File.separatorChar); + } } } @@ -1030,6 +1025,8 @@ final public class AndrolibResources { public BuildOptions buildOptions; + public Map mResFileMapping = new HashMap(); + // TODO: dirty static hack. I have to refactor decoding mechanisms. public static boolean sKeepBroken = false; diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java index 88cded09..32b4065f 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java @@ -36,6 +36,8 @@ public class ResResSpec { this.mId = id; String cleanName; + name = (("(name removed)".equals(name)) ? null : name); + ResResSpec resResSpec = type.getResSpecUnsafe(name); if (resResSpec != null) { cleanName = String.format("APKTOOL_DUPLICATE_%s_%s", type, id.toString()); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java index 50e7f615..64e51f0c 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java @@ -71,7 +71,7 @@ public class ResEnumAttr extends ResAttr { break; } } - if (ref != null) { + if (ref != null && !ref.referentIsNull()) { value2 = ref.getReferent().getName(); mItemsCache.put(value, value2); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java index 896ae9c9..cfc03555 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java @@ -177,7 +177,19 @@ public class ARSCDecoder { nextChunk(); } - private void readOverlaySpec() throws IOException { + private void readOverlaySpec() throws AndrolibException, IOException { + checkChunkType(Header.XML_TYPE_OVERLAY); + String name = mIn.readNullEndedString(128, true); + String actor = mIn.readNullEndedString(128, true); + LOGGER.fine(String.format("Overlay name: \"%s\", actor: \"%s\")", name, actor)); + + while(nextChunk().type == Header.XML_TYPE_OVERLAY_POLICY) { + readOverlayPolicySpec(); + } + } + + private void readOverlayPolicySpec() throws AndrolibException, IOException { + checkChunkType(Header.XML_TYPE_OVERLAY_POLICY); /* policyFlags */mIn.skipInt(); int count = mIn.readInt(); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java index c6e94f89..374aa887 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java @@ -28,6 +28,7 @@ import brut.directory.DirectoryException; import brut.util.OSDetection; import java.io.*; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -38,10 +39,11 @@ public class ResFileDecoder { this.mDecoders = decoders; } - public void decode(ResResource res, Directory inDir, Directory outDir) + public void decode(ResResource res, Directory inDir, Directory outDir, Map resFileMapping) throws AndrolibException { ResFileValue fileValue = (ResFileValue) res.getValue(); + String inFilePath = fileValue.toString(); String inFileName = fileValue.getStrippedPath(); String outResName = res.getFilePath(); String typeName = res.getResSpec().getType().getName(); @@ -56,13 +58,18 @@ public class ResFileDecoder { outFileName = outResName + ext; } + String outFilePath = "res/" + outFileName; + if (!inFilePath.equals(outFilePath)) { + resFileMapping.put(inFilePath, outFilePath); + } + try { if (typeName.equals("raw")) { - decode(inDir, inFileName, outDir, outFileName, "raw"); + decode(inDir, inFilePath, outDir, outFileName, "raw"); return; } if (typeName.equals("font") && !".xml".equals(ext)) { - decode(inDir, inFileName, outDir, outFileName, "raw"); + decode(inDir, inFilePath, outDir, outFileName, "raw"); return; } if (typeName.equals("drawable") || typeName.equals("mipmap")) { @@ -77,26 +84,24 @@ public class ResFileDecoder { // check for raw 9patch images for (String extension : RAW_9PATCH_IMAGE_EXTENSIONS) { if (inFileName.toLowerCase().endsWith("." + extension)) { - copyRaw(inDir, outDir, inFileName, outFileName); + copyRaw(inDir, outDir, inFilePath, outFileName); return; } } // check for xml 9 patches which are just xml files if (inFileName.toLowerCase().endsWith(".xml")) { - decode(inDir, inFileName, outDir, outFileName, "xml"); + decode(inDir, inFilePath, outDir, outFileName, "xml"); return; } try { - decode(inDir, inFileName, outDir, outFileName, "9patch"); + decode(inDir, inFilePath, outDir, outFileName, "9patch"); return; } catch (CantFind9PatchChunkException ex) { - LOGGER.log( - Level.WARNING, - String.format( - "Cant find 9patch chunk in file: \"%s\". Renaming it to *.png.", - inFileName), ex); + LOGGER.log(Level.WARNING, String.format( + "Cant find 9patch chunk in file: \"%s\". Renaming it to *.png.", inFileName + ), ex); outDir.removeFile(outFileName); outFileName = outResName + ext; } @@ -105,27 +110,27 @@ public class ResFileDecoder { // check for raw image for (String extension : RAW_IMAGE_EXTENSIONS) { if (inFileName.toLowerCase().endsWith("." + extension)) { - copyRaw(inDir, outDir, inFileName, outFileName); + copyRaw(inDir, outDir, inFilePath, outFileName); return; } } if (!".xml".equals(ext)) { - decode(inDir, inFileName, outDir, outFileName, "raw"); + decode(inDir, inFilePath, outDir, outFileName, "raw"); return; } } - decode(inDir, inFileName, outDir, outFileName, "xml"); + decode(inDir, inFilePath, outDir, outFileName, "xml"); } catch (RawXmlEncounteredException ex) { // If we got an error to decode XML, lets assume the file is in raw format. // This is a large assumption, that might increase runtime, but will save us for situations where // XSD files are AXML`d on aapt1, but left in plaintext in aapt2. - decode(inDir, inFileName, outDir, outFileName, "raw"); + decode(inDir, inFilePath, outDir, outFileName, "raw"); } catch (AndrolibException ex) { LOGGER.log(Level.SEVERE, String.format( - "Could not decode file, replacing by FALSE value: %s", - inFileName), ex); + "Could not decode file, replacing by FALSE value: %s", + inFileName), ex); res.replace(new ResBoolValue(false, 0, null)); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java index 540ca0ba..057d1d2e 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java @@ -50,7 +50,7 @@ public class StyledString { public static class Span implements Comparable { private static final MapSplitter ATTRIBUTES_SPLITTER = - Splitter.on(';').withKeyValueSeparator(Splitter.on('=').limit(2)); + Splitter.on(';').omitEmptyStrings().withKeyValueSeparator(Splitter.on('=').limit(2)); private final String tag; private final int firstChar; diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java index db78c4c5..ec60e4d4 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java @@ -56,4 +56,18 @@ public class AndResGuardTest extends BaseTest { File aPng = new File(sTestOrigDir,"res/mipmap-hdpi-v4/a.png"); assertTrue(aPng.isFile()); } + + @Test + public void checkifAndResDecodeRemapsRFolderInRawMode() throws BrutException, IOException { + String apk = "issue1170.apk"; + ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".raw.out"); + + apkDecoder.setOutDir(new File(sTmpDir + File.separator + apk + ".raw.out")); + apkDecoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_NONE); + apkDecoder.decode(); + + File aPng = new File(sTestOrigDir,"r/a/a.png"); + assertTrue(aPng.isFile()); + } } diff --git a/brut.j.common/build.gradle b/brut.j.common/build.gradle index 98929c3f..41a98796 100644 --- a/brut.j.common/build.gradle +++ b/brut.j.common/build.gradle @@ -13,8 +13,3 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -dependencies { - testImplementation depends.junit -} - diff --git a/brut.j.dir/build.gradle b/brut.j.dir/build.gradle index 0512ffe9..0e5e1cfe 100644 --- a/brut.j.dir/build.gradle +++ b/brut.j.dir/build.gradle @@ -15,8 +15,7 @@ */ dependencies { - implementation project(':brut.j.common'), - project(':brut.j.util'), - depends.commons_io - testImplementation depends.junit + implementation project(':brut.j.common') + implementation project(':brut.j.util') + implementation depends.commons_io } diff --git a/brut.j.dir/src/main/java/brut/directory/DirUtil.java b/brut.j.dir/src/main/java/brut/directory/DirUtil.java index b22735a9..53701f59 100644 --- a/brut.j.dir/src/main/java/brut/directory/DirUtil.java +++ b/brut.j.dir/src/main/java/brut/directory/DirUtil.java @@ -23,6 +23,7 @@ import brut.common.TraversalUnknownFileException; import brut.util.BrutIO; import brut.util.OS; import java.io.*; +import java.nio.file.Files; import java.util.logging.Logger; public class DirUtil { @@ -84,14 +85,13 @@ public class DirUtil { if (in.containsDir(fileName)) { OS.rmdir(new File(out, fileName)); in.getDir(fileName).copyToDir(new File(out, fileName)); + } else if (!in.containsDir(fileName) && !in.containsFile(fileName)) { + // Skip copies of directories/files not found. } else { - if (fileName.equals("res") && !in.containsFile(fileName)) { - return; - } String cleanedFilename = BrutIO.sanitizeUnknownFile(out, fileName); File outFile = new File(out, cleanedFilename); outFile.getParentFile().mkdirs(); - BrutIO.copyAndClose(in.getFileInput(fileName), new FileOutputStream(outFile)); + BrutIO.copyAndClose(in.getFileInput(fileName), Files.newOutputStream(outFile.toPath())); } } catch (RootUnknownFileException | InvalidUnknownFileException | TraversalUnknownFileException exception) { LOGGER.warning(String.format("Skipping file %s (%s)", fileName, exception.getMessage())); diff --git a/brut.j.util/build.gradle b/brut.j.util/build.gradle index 5e8c7cd5..92d7c923 100644 --- a/brut.j.util/build.gradle +++ b/brut.j.util/build.gradle @@ -15,7 +15,6 @@ */ dependencies { - implementation project(':brut.j.common'), - depends.commons_io - testImplementation depends.junit + implementation project(':brut.j.common') + implementation depends.commons_io } diff --git a/build.gradle b/build.gradle index 1092fde1..4e8a27e2 100644 --- a/build.gradle +++ b/build.gradle @@ -18,23 +18,22 @@ import java.nio.charset.StandardCharsets buildscript { ext { depends = [ - baksmali : 'org.smali:baksmali:2.5.2', + baksmali : 'com.github.iBotPeaches.smali:baksmali:403e90375e', commons_cli : 'commons-cli:commons-cli:1.5.0', commons_io : 'commons-io:commons-io:2.11.0', commons_lang : 'org.apache.commons:commons-lang3:3.12.0', - commons_text : 'org.apache.commons:commons-text:1.9', + commons_text : 'org.apache.commons:commons-text:1.10.0', guava : 'com.google.guava:guava:31.0.1-jre', junit : 'junit:junit:4.13.2', - proguard_gradle: 'com.guardsquare:proguard-gradle:7.1.1', - snakeyaml : 'org.yaml:snakeyaml:1.29:android', - smali : 'org.smali:smali:2.5.2', + proguard_gradle: 'com.guardsquare:proguard-gradle:7.3.0', + snakeyaml : 'org.yaml:snakeyaml:1.32:android', + smali : 'com.github.iBotPeaches.smali:smali:403e90375e', xmlpull : 'xpp3:xpp3:1.1.4c', xmlunit : 'xmlunit:xmlunit:1.6', ] } repositories { - mavenCentral() gradlePluginPortal() } dependencies { @@ -49,7 +48,7 @@ def ghPassword = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN" apply from: 'gradle/functions.gradle' version = '2.7.0' -def suffix = 'SNAPSHOT' +def suffix = '' defaultTasks 'build', 'shadowJar', 'proguard' @@ -97,6 +96,19 @@ allprojects { "licenseTest" ] + repositories { + mavenCentral() + + // Obtain baksmali/smali from source builds - https://github.com/iBotPeaches/smali + // Remove when official smali releases come out again. + maven { + url 'https://jitpack.io' + content { + includeGroup('com.github.iBotPeaches.smali') + } + } + } + publishing { repositories { maven { @@ -165,10 +177,6 @@ task snapshot { subprojects { apply plugin: 'java' - repositories { - mavenCentral() - } - test { testLogging { exceptionFormat = 'full'