From 628286c022e3a872d6ab6bfb3431579f98743c25 Mon Sep 17 00:00:00 2001 From: Greg Leach Date: Tue, 24 Mar 2015 20:14:05 -0700 Subject: [PATCH] Java NIO doesn't allow the preservation of the compression method (STORED vs DEFLATED), so unfortunately we need to fall back to ZipEntry-based output for unknown files. --- .../src/main/java/brut/androlib/Androlib.java | 112 +++++++++++++----- .../brut/androlib/BuildAndDecodeTest.java | 5 + .../brut/apktool/testapp/apktool.yml | 1 + .../brut/apktool/testapp/unknown/stored.file | 1 + 4 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/unknown/stored.file 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 e742c8a0..8da9bb1e 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 @@ -33,9 +33,11 @@ import java.nio.file.*; import java.nio.file.Path; import java.util.*; import java.util.logging.Logger; +import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.nio.file.Files; import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; @@ -544,41 +546,97 @@ public class Androlib { public void buildUnknownFiles(File appDir, File outFile, Map meta) throws AndrolibException { - Path globalPath = Paths.get(appDir.getPath() + File.separatorChar + UNK_DIRNAME); - if (meta.containsKey("unknownFiles")) { LOGGER.info("Copying unknown files/dir..."); Map files = (Map)meta.get("unknownFiles"); + File tempFile = new File(outFile.getParent(), outFile.getName() + ".apktool_temp"); + boolean renamed = outFile.renameTo(tempFile); + if(!renamed) { + throw new AndrolibException("Unable to rename temporary file"); + } - try { - // set our filesystem options - Map zip_properties = new HashMap<>(); - zip_properties.put("create", "false"); - zip_properties.put("encoding", "UTF-8"); - - // create filesystem - Path path = Paths.get(outFile.getAbsolutePath()); - - try( - FileSystem fs = FileSystems.newFileSystem(path, null) - ) { - // loop through files inside - for (Map.Entry entry : files.entrySet()) { - - File file = new File(globalPath.toFile(), entry.getKey()); - Path dest = fs.getPath(entry.getKey()); - Path destParent = dest.getParent(); - if (destParent != null && !Files.exists(destParent)) { - Files.createDirectories(destParent); - } - Path newFile = Paths.get(file.getAbsolutePath()); - Files.copy(newFile, dest, StandardCopyOption.REPLACE_EXISTING); - } - } + try ( + ZipFile inputFile = new ZipFile(tempFile); + ZipOutputStream actualOutput = new ZipOutputStream(new FileOutputStream(outFile)); + ) { + byte[] buffer = new byte[4096 * 1024]; + copyExistingFiles(inputFile, actualOutput, buffer); + copyUnknownFiles(appDir, actualOutput, files, buffer); } catch (IOException ex) { throw new AndrolibException(ex); } + + // Remove our temporary file. + tempFile.delete(); + } + } + + private void copyExistingFiles(ZipFile inputFile, ZipOutputStream outputFile, byte[] buffer) throws IOException { + // First, copy the contents from the existing outFile: + Enumeration entries = inputFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = new ZipEntry(entries.nextElement()); + // We can't reuse the compressed size because it depends on compression sizes. + entry.setCompressedSize(-1); + outputFile.putNextEntry(entry); + + // No need to create directory entries in the final apk + if (!entry.isDirectory()) { + copy(inputFile.getInputStream(entry), outputFile, buffer); + } + + outputFile.closeEntry(); + } + } + + private void copyUnknownFiles(File appDir, ZipOutputStream outputFile, Map files, byte[] buffer) throws IOException { + File unknownFileDir = new File(appDir, UNK_DIRNAME); + + // loop through unknown files + for (Map.Entry unknownFileInfo : files.entrySet()) { + File inputFile = new File(unknownFileDir, unknownFileInfo.getKey()); + if(inputFile.isDirectory()) { + continue; + } + + ZipEntry newEntry = new ZipEntry(unknownFileInfo.getKey()); + int method = Integer.valueOf(unknownFileInfo.getValue()); + LOGGER.fine("Copying unknown file " + unknownFileInfo.getKey() + " with method " + unknownFileInfo.getValue()); + if(method == ZipEntry.STORED) { + newEntry.setMethod(ZipEntry.STORED); + newEntry.setSize(inputFile.length()); + newEntry.setCompressedSize(-1); + BufferedInputStream unknownFile = new BufferedInputStream(new FileInputStream(inputFile)); + CRC32 crc = calculateCrc(unknownFile, buffer); + newEntry.setCrc(crc.getValue()); + + LOGGER.fine("\tsize: " + newEntry.getSize()); + } + else { + newEntry.setMethod(ZipEntry.DEFLATED); + } + outputFile.putNextEntry(newEntry); + + BufferedInputStream unknownFile = new BufferedInputStream(new FileInputStream(inputFile)); + copy(unknownFile, outputFile, buffer); + outputFile.closeEntry(); + } + } + + private CRC32 calculateCrc(InputStream input, byte[] buffer) throws IOException { + CRC32 crc = new CRC32(); + int bytesRead = 0; + while((bytesRead = input.read(buffer)) != -1) { + crc.update(buffer, 0, bytesRead); + } + return crc; + } + + private static void copy(InputStream input, OutputStream output, byte[] buffer) throws IOException { + int bytesRead; + while((bytesRead = input.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java index 11f7c10a..cf6f5b4d 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java @@ -308,6 +308,11 @@ public class BuildAndDecodeTest { Map control_files = (Map)control.get("unknownFiles"); Map test_files = (Map)test.get("unknownFiles"); assertTrue(control_files.size() == test_files.size()); + + // Make sure that the compression methods are still the same + for(Map.Entry controlEntry : control_files.entrySet()) { + assertTrue(controlEntry.getValue().equals(test_files.get(controlEntry.getKey()))); + } } private boolean compareBinaryFolder(String path, boolean res) diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/apktool.yml index 51536bc9..2098ee8d 100644 --- a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/apktool.yml @@ -12,5 +12,6 @@ versionInfo: compressionType: false unknownFiles: hidden.file: '8' + stored.file: '0' unk_folder/unknown_file: '8' lib_bug603/bug603: '8' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/unknown/stored.file b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/unknown/stored.file new file mode 100644 index 00000000..b3c9c4a0 --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/unknown/stored.file @@ -0,0 +1 @@ +This file is not compressed. \ No newline at end of file