mirror of
https://github.com/iBotPeaches/Apktool.git
synced 2026-11-07 10:16:04 -10:00
refactor: zip archive revision and perf tweaks (#4253)
This commit is contained in:
1 parent
d3d77cd1da
commit
fd9d5d4dc7
26 files changed
+547
-630
No files matched your search
@@ -301,7 +301,7 @@ public class Main {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws AndrolibException {
|
||||
if (!SystemUtils.is64Bit()) {
|
||||
if (!SystemUtils.IS_64BIT) {
|
||||
System.err.println("Warning: Apktool no longer supports 32-bit platforms.");
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import brut.util.BackgroundWorker;
|
||||
import brut.util.BinaryDataInputStream;
|
||||
import brut.util.IOUtils;
|
||||
import brut.zip.ZipArchive;
|
||||
import brut.zip.ZipArchiveEntry;
|
||||
import brut.zip.ZipUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -116,7 +117,7 @@ public class ApkBuilder {
|
||||
while (it.hasNext()) {
|
||||
String fileName = it.next().getFileName().toString();
|
||||
if (fileName.endsWith(".dex")) {
|
||||
copySourcesRaw(fileName);
|
||||
copyRawFile(mApkDir.resolve(fileName));
|
||||
dexFiles.add(fileName);
|
||||
}
|
||||
}
|
||||
@@ -149,18 +150,6 @@ public class ApkBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private void copySourcesRaw(String fileName) throws IOException {
|
||||
Path dexFile = mApkDir.resolve(fileName);
|
||||
Path outDexFile = mOutDir.resolve(fileName);
|
||||
if (!isFileNewer(dexFile, outDexFile)) {
|
||||
Log.i(TAG, fileName + " has not changed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Copying raw " + fileName + "...");
|
||||
Files.copy(dexFile, outDexFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private void buildSourcesSmali(String dirName, String fileName) throws AndrolibException, IOException {
|
||||
if (mWorker != null) {
|
||||
mWorker.submit(() -> {
|
||||
@@ -204,13 +193,13 @@ public class ApkBuilder {
|
||||
|
||||
// Copy raw manifest if it's binary XML.
|
||||
if (isBinaryManifest) {
|
||||
copyManifestRaw(manifest);
|
||||
copyRawFile(manifest);
|
||||
}
|
||||
|
||||
// Copy raw resources if possible.
|
||||
Path arscFile = mApkDir.resolve("resources.arsc");
|
||||
if (Files.isRegularFile(arscFile)) {
|
||||
copyResourcesRaw(arscFile);
|
||||
copyRawFile(arscFile);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,28 +219,6 @@ public class ApkBuilder {
|
||||
buildResourcesFully(manifest, arscFile, resDir);
|
||||
}
|
||||
|
||||
private void copyManifestRaw(Path manifest) throws IOException {
|
||||
Path outManifest = mOutDir.resolve(mApkDir.relativize(manifest));
|
||||
if (!isFileNewer(manifest, outManifest)) {
|
||||
Log.i(TAG, "AndroidManifest.xml has not changed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Copying raw AndroidManifest.xml...");
|
||||
Files.copy(manifest, outManifest, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private void copyResourcesRaw(Path arscFile) throws IOException {
|
||||
Path outArscFile = mOutDir.resolve(mApkDir.relativize(arscFile));
|
||||
if (!isFileNewer(arscFile, outArscFile)) {
|
||||
Log.i(TAG, "resources.arsc has not changed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Copying raw resources.arsc...");
|
||||
Files.copy(arscFile, outArscFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private void buildManifestOnly(Path manifest) throws AndrolibException, IOException {
|
||||
Path outManifest = mOutDir.resolve(mApkDir.relativize(manifest));
|
||||
if (!isFileNewer(manifest, outManifest)) {
|
||||
@@ -279,7 +246,7 @@ public class ApkBuilder {
|
||||
mAaptInvoker.invoke(tmpFile, tmpManifest, null);
|
||||
|
||||
try (ZipArchive tmpZip = new ZipArchive(tmpFile)) {
|
||||
tmpZip.extract(mOutDir, mOutDir.relativize(outManifest).toString());
|
||||
tmpZip.getEntry("AndroidManifest.xml").extract(outManifest);
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(tmpFile);
|
||||
@@ -326,14 +293,40 @@ public class ApkBuilder {
|
||||
mAaptInvoker.invoke(tmpFile, tmpManifest, resDir);
|
||||
|
||||
try (ZipArchive tmpZip = new ZipArchive(tmpFile)) {
|
||||
tmpZip.extract(mOutDir, mOutDir.relativize(outManifest).toString(),
|
||||
mOutDir.relativize(outArscFile).toString(), mOutDir.relativize(outResDir).toString());
|
||||
tmpZip.getEntry("AndroidManifest.xml").extract(outManifest);
|
||||
|
||||
if (tmpZip.hasEntry("resources.arsc")) {
|
||||
tmpZip.getEntry("resources.arsc").extract(outArscFile);
|
||||
}
|
||||
|
||||
Iterator<ZipArchiveEntry> it = tmpZip.entries("res/").iterator();
|
||||
while (it.hasNext()) {
|
||||
ZipArchiveEntry entry = it.next();
|
||||
String name = entry.getName();
|
||||
try {
|
||||
entry.extract(outResDir.resolve(ZipUtils.normalizeForFs(name.substring(4))));
|
||||
} catch (IllegalArgumentException | IOException ex) {
|
||||
Log.w(TAG, "Could not extract ZIP entry: %s (%s)", name, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyRawFile(Path file) throws IOException {
|
||||
Path fileName = mApkDir.relativize(file);
|
||||
Path outFile = mOutDir.resolve(fileName);
|
||||
if (!isFileNewer(file, outFile)) {
|
||||
Log.i(TAG, fileName + " has not changed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Copying raw " + fileName + "...");
|
||||
Files.copy(file, outFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private void copyOriginalFiles() throws IOException {
|
||||
if (!mConfig.isCopyOriginal()) {
|
||||
return;
|
||||
@@ -348,7 +341,7 @@ public class ApkBuilder {
|
||||
IOUtils.copyDirectory(originalDir, mOutDir);
|
||||
}
|
||||
|
||||
private void buildApkFile(Path outFile) throws AndrolibException, IOException {
|
||||
private void buildApkFile(Path outFile) throws IOException {
|
||||
if (mConfig.isNoApk()) {
|
||||
return;
|
||||
}
|
||||
@@ -368,7 +361,7 @@ public class ApkBuilder {
|
||||
// Convert to set for fast lookup.
|
||||
Set<String> doNotCompress = new HashSet<>(mApkInfo.getDoNotCompress());
|
||||
shouldCompress = entryName -> !doNotCompress.contains(entryName)
|
||||
&& !doNotCompress.contains(IOUtils.getFileExtension(entryName));
|
||||
&& !doNotCompress.contains(IOUtils.getFileExtension(entryName, '/'));
|
||||
}
|
||||
|
||||
Log.i(TAG, "Building apk file...");
|
||||
@@ -417,10 +410,10 @@ public class ApkBuilder {
|
||||
private static void zipFile(Path baseDir, String fileName, ZipOutputStream out, Predicate<String> shouldCompress)
|
||||
throws IOException {
|
||||
Path file = baseDir.resolve(fileName);
|
||||
String entryName = ZipUtils.normalize(fileName);
|
||||
ZipEntry entry = new ZipEntry(entryName);
|
||||
String name = ZipUtils.normalizeForZip(fileName);
|
||||
ZipEntry entry = new ZipEntry(name);
|
||||
entry.setTime(ApkFile.TIME_MILLIS);
|
||||
if (shouldCompress.test(entryName)) {
|
||||
if (shouldCompress.test(name)) {
|
||||
entry.setMethod(ZipEntry.DEFLATED);
|
||||
} else {
|
||||
entry.setMethod(ZipEntry.STORED);
|
||||
|
||||
@@ -25,8 +25,9 @@ import brut.androlib.smali.SmaliDecoder;
|
||||
import brut.common.Log;
|
||||
import brut.util.BackgroundWorker;
|
||||
import brut.util.IOUtils;
|
||||
import brut.zip.ZipFileEntry;
|
||||
import com.google.common.collect.Sets;
|
||||
import brut.zip.ZipArchiveEntry;
|
||||
import brut.zip.ZipUtils;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -52,7 +53,7 @@ public class ApkDecoder {
|
||||
private static final Pattern STANDARD_FILES_PATTERN = Pattern.compile(
|
||||
"resources\\.arsc|(" + String.join("|", RAW_DIRS) + ")/.*|"
|
||||
+ CLASSES_FILES_PATTERN.pattern() + "|" + ORIGINAL_FILES_PATTERN.pattern());
|
||||
private static final Set<String> NO_COMPRESS_EXTS = Sets.newHashSet(
|
||||
private static final Set<String> NO_COMPRESS_EXTS = ImmutableSet.of(
|
||||
"dex", "arsc", "so", "jpg", "jpeg", "png", "gif", "wav", "mp2", "mp3", "ogg", "aac", "mpg", "mpeg", "mid",
|
||||
"midi", "smf", "jet", "rtttl", "imy", "xmf", "mp4", "m4a", "m4v", "3gp", "3gpp", "3g2", "3gpp2", "amr", "awb",
|
||||
"wma", "wmv", "webm", "webp", "mkv");
|
||||
@@ -138,89 +139,81 @@ public class ApkDecoder {
|
||||
boolean allSrc = mConfig.isDecodeSourcesFull();
|
||||
boolean noSrc = mConfig.isDecodeSourcesNone();
|
||||
|
||||
Iterator<ZipFileEntry> it = (allSrc ? mApkFile.walkFiles() : mApkFile.listFiles()).iterator();
|
||||
Iterator<ZipArchiveEntry> it = mApkFile.entries().iterator();
|
||||
while (it.hasNext()) {
|
||||
String fileName = it.next().getName();
|
||||
if (allSrc ? fileName.endsWith(".dex") : CLASSES_FILES_PATTERN.matcher(fileName).matches()) {
|
||||
String name = it.next().getName();
|
||||
if (allSrc ? name.endsWith(".dex") : CLASSES_FILES_PATTERN.matcher(name).matches()) {
|
||||
if (noSrc) {
|
||||
copySourcesRaw(outDir, fileName);
|
||||
copyRawFile(outDir, name);
|
||||
} else {
|
||||
decodeSourcesSmali(outDir, fileName);
|
||||
decodeSourcesSmali(outDir, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copySourcesRaw(Path outDir, String fileName) throws AndrolibException, IOException {
|
||||
Log.i(TAG, "Copying raw " + fileName + "...");
|
||||
mApkFile.extract(outDir, fileName);
|
||||
}
|
||||
|
||||
private void decodeSourcesSmali(Path outDir, String fileName) throws AndrolibException {
|
||||
private void decodeSourcesSmali(Path outDir, String name) throws AndrolibException {
|
||||
if (mWorker != null) {
|
||||
mWorker.submit(() -> {
|
||||
if (mFirstError.get() == null) {
|
||||
try {
|
||||
decodeSourcesSmaliJob(outDir, fileName);
|
||||
decodeSourcesSmaliJob(outDir, name);
|
||||
} catch (Exception ex) {
|
||||
mFirstError.compareAndSet(null, ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
decodeSourcesSmaliJob(outDir, fileName);
|
||||
decodeSourcesSmaliJob(outDir, name);
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeSourcesSmaliJob(Path outDir, String fileName) throws AndrolibException {
|
||||
Log.i(TAG, "Baksmaling " + fileName + "...");
|
||||
mSmaliDecoder.decode(fileName, outDir);
|
||||
private void decodeSourcesSmaliJob(Path outDir, String name) throws AndrolibException {
|
||||
Log.i(TAG, "Baksmaling " + name + "...");
|
||||
mSmaliDecoder.decode(name, outDir);
|
||||
}
|
||||
|
||||
private void decodeResources(Path outDir) throws AndrolibException, IOException {
|
||||
if (!mApkFile.containsFile("resources.arsc")) {
|
||||
String name = "resources.arsc";
|
||||
if (!mApkFile.hasEntry(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mConfig.isDecodeResourcesFull()) {
|
||||
mResDecoder.decodeResources(outDir);
|
||||
if (!mConfig.isDecodeResourcesFull()) {
|
||||
copyRawFile(outDir, name);
|
||||
} else {
|
||||
copyResourcesRaw(outDir);
|
||||
mResDecoder.decodeResources(outDir);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyResourcesRaw(Path outDir) throws AndrolibException, IOException {
|
||||
Log.i(TAG, "Copying raw resources.arsc...");
|
||||
mApkFile.extract(outDir, "resources.arsc");
|
||||
}
|
||||
|
||||
private void decodeManifest(Path outDir) throws AndrolibException, IOException {
|
||||
if (!mApkFile.containsFile("AndroidManifest.xml")) {
|
||||
String name = "AndroidManifest.xml";
|
||||
if (!mApkFile.hasEntry(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mConfig.isDecodeResourcesNone()) {
|
||||
mResDecoder.decodeManifest(outDir);
|
||||
if (mConfig.isDecodeResourcesNone()) {
|
||||
copyRawFile(outDir, name);
|
||||
} else {
|
||||
copyManifestRaw(outDir);
|
||||
mResDecoder.decodeManifest(outDir);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyManifestRaw(Path outDir) throws AndrolibException, IOException {
|
||||
Log.i(TAG, "Copying raw AndroidManifest.xml...");
|
||||
mApkFile.extract(outDir, "AndroidManifest.xml");
|
||||
private void copyRawFile(Path outDir, String name) throws AndrolibException, IOException {
|
||||
Log.i(TAG, "Copying raw " + name + "...");
|
||||
extractZipEntry(mApkFile.getEntry(name), outDir, name);
|
||||
}
|
||||
|
||||
private void copyOriginalFiles(Path outDir) throws AndrolibException, IOException {
|
||||
Path originalDir = outDir.resolve("original");
|
||||
|
||||
Log.i(TAG, "Copying original files...");
|
||||
Iterator<ZipFileEntry> it = mApkFile.walkFiles().iterator();
|
||||
Iterator<ZipArchiveEntry> it = mApkFile.entries().iterator();
|
||||
while (it.hasNext()) {
|
||||
ZipFileEntry file = it.next();
|
||||
String fileName = file.getName();
|
||||
if (ORIGINAL_FILES_PATTERN.matcher(fileName).matches()) {
|
||||
file.extract(originalDir.resolve(fileName));
|
||||
ZipArchiveEntry entry = it.next();
|
||||
String name = entry.getName();
|
||||
if (ORIGINAL_FILES_PATTERN.matcher(name).matches()) {
|
||||
extractZipEntry(entry, originalDir, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,18 +224,19 @@ public class ApkDecoder {
|
||||
boolean noAssets = mConfig.isDecodeAssetsNone();
|
||||
|
||||
for (String dirName : RAW_DIRS) {
|
||||
if (!mApkFile.containsDir(dirName) || (noAssets && dirName.equals("assets"))) {
|
||||
String prefix = dirName + "/";
|
||||
if (!mApkFile.hasEntries(prefix) || (noAssets && dirName.equals("assets"))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Copying " + dirName + "...");
|
||||
Iterator<ZipFileEntry> it = mApkFile.getDir(dirName).walkFiles().iterator();
|
||||
Iterator<ZipArchiveEntry> it = mApkFile.entries(prefix).iterator();
|
||||
while (it.hasNext()) {
|
||||
ZipFileEntry file = it.next();
|
||||
String fileName = file.getName();
|
||||
if (!ORIGINAL_FILES_PATTERN.matcher(fileName).matches()
|
||||
&& !dexFiles.contains(fileName) && !resFileMap.containsKey(fileName)) {
|
||||
file.extract(outDir.resolve(fileName));
|
||||
ZipArchiveEntry entry = it.next();
|
||||
String name = entry.getName();
|
||||
if (!ORIGINAL_FILES_PATTERN.matcher(name).matches() && !dexFiles.contains(name)
|
||||
&& !resFileMap.containsKey(name)) {
|
||||
extractZipEntry(entry, outDir, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,23 +248,31 @@ public class ApkDecoder {
|
||||
Path unknownDir = outDir.resolve("unknown");
|
||||
|
||||
Log.i(TAG, "Copying unknown files...");
|
||||
Iterator<ZipFileEntry> it = mApkFile.walkFiles().iterator();
|
||||
Iterator<ZipArchiveEntry> it = mApkFile.entries().iterator();
|
||||
while (it.hasNext()) {
|
||||
ZipFileEntry file = it.next();
|
||||
String fileName = file.getName();
|
||||
if (!STANDARD_FILES_PATTERN.matcher(fileName).matches() && !dexFiles.contains(fileName)
|
||||
&& !resFileMap.containsKey(fileName)) {
|
||||
file.extract(unknownDir.resolve(fileName));
|
||||
ZipArchiveEntry entry = it.next();
|
||||
String name = entry.getName();
|
||||
if (!STANDARD_FILES_PATTERN.matcher(name).matches() && !dexFiles.contains(name)
|
||||
&& !resFileMap.containsKey(name)) {
|
||||
extractZipEntry(entry, unknownDir, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractZipEntry(ZipArchiveEntry entry, Path outDir, String outName) throws IOException {
|
||||
try {
|
||||
entry.extract(outDir.resolve(ZipUtils.normalizeForFs(outName)));
|
||||
} catch (IllegalArgumentException | IOException ex) {
|
||||
Log.w(TAG, "Could not extract ZIP entry: %s (%s)", entry.getName(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeApkInfo(Path outDir) throws AndrolibException, IOException {
|
||||
ApkInfo apkInfo = mApkFile.getApkInfo();
|
||||
apkInfo.setVersion(mConfig.getVersion());
|
||||
|
||||
// If we did not decode the manifest, store the inferred dex opcode API level.
|
||||
if (mConfig.isDecodeResourcesNone() || !mApkFile.containsFile("AndroidManifest.xml")) {
|
||||
if (mConfig.isDecodeResourcesNone() || !mApkFile.hasEntry("AndroidManifest.xml")) {
|
||||
int apiLevel = mSmaliDecoder != null ? mSmaliDecoder.getInferredApiLevel() : 0;
|
||||
if (apiLevel > 0) {
|
||||
apkInfo.getSdkInfo().setMinSdkVersion(Integer.toString(apiLevel));
|
||||
@@ -282,23 +284,23 @@ public class ApkDecoder {
|
||||
Set<String> uncompressedExts = new HashSet<>();
|
||||
Set<String> uncompressedFiles = new HashSet<>();
|
||||
|
||||
Iterator<ZipFileEntry> it = mApkFile.walkFiles().filter(ZipFileEntry::isStored).iterator();
|
||||
Iterator<ZipArchiveEntry> it = mApkFile.entries().filter(ZipArchiveEntry::isStored).iterator();
|
||||
while (it.hasNext()) {
|
||||
ZipFileEntry file = it.next();
|
||||
String fileName = file.getName();
|
||||
fileName = resFileMap.getOrDefault(fileName, fileName);
|
||||
ZipArchiveEntry entry = it.next();
|
||||
String name = entry.getName();
|
||||
name = resFileMap.getOrDefault(name, name);
|
||||
String ext;
|
||||
if (file.getSize() > 0 && !(ext = IOUtils.getFileExtension(fileName)).isEmpty()
|
||||
if (entry.getSize() > 0 && !(ext = IOUtils.getFileExtension(name, '/')).isEmpty()
|
||||
&& NO_COMPRESS_EXTS.contains(ext)) {
|
||||
uncompressedExts.add(ext);
|
||||
} else {
|
||||
uncompressedFiles.add(fileName);
|
||||
uncompressedFiles.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude files with an already recorded extension.
|
||||
if (!uncompressedExts.isEmpty() && !uncompressedFiles.isEmpty()) {
|
||||
uncompressedFiles.removeIf(fileName -> uncompressedExts.contains(IOUtils.getFileExtension(fileName)));
|
||||
uncompressedFiles.removeIf(name -> uncompressedExts.contains(IOUtils.getFileExtension(name, '/')));
|
||||
}
|
||||
|
||||
// Update apk info.
|
||||
|
||||
@@ -19,8 +19,7 @@ package brut.androlib;
|
||||
import brut.androlib.exceptions.AndrolibException;
|
||||
import brut.androlib.meta.ApkInfo;
|
||||
import brut.zip.ZipArchive;
|
||||
import brut.zip.ZipDirEntry;
|
||||
import brut.zip.ZipFileEntry;
|
||||
import brut.zip.ZipArchiveEntry;
|
||||
import brut.zip.ZipNoSuchEntryException;
|
||||
|
||||
import java.io.Closeable;
|
||||
@@ -79,44 +78,34 @@ public class ApkFile implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<ZipDirEntry> listDirs() throws AndrolibException {
|
||||
public boolean hasEntries() throws AndrolibException {
|
||||
ensureOpen();
|
||||
return mArchive.listDirs();
|
||||
return mArchive.hasEntries();
|
||||
}
|
||||
|
||||
public Stream<ZipDirEntry> walkDirs() throws AndrolibException {
|
||||
public boolean hasEntries(String prefix) throws AndrolibException {
|
||||
ensureOpen();
|
||||
return mArchive.walkDirs();
|
||||
return mArchive.hasEntries(prefix);
|
||||
}
|
||||
|
||||
public boolean containsDir(String name) throws AndrolibException {
|
||||
public Stream<ZipArchiveEntry> entries() throws AndrolibException {
|
||||
ensureOpen();
|
||||
return mArchive.containsDir(name);
|
||||
return mArchive.entries();
|
||||
}
|
||||
|
||||
public ZipDirEntry getDir(String name) throws AndrolibException, ZipNoSuchEntryException {
|
||||
public Stream<ZipArchiveEntry> entries(String prefix) throws AndrolibException {
|
||||
ensureOpen();
|
||||
return mArchive.getDir(name);
|
||||
return mArchive.entries(prefix);
|
||||
}
|
||||
|
||||
public Stream<ZipFileEntry> listFiles() throws AndrolibException {
|
||||
public boolean hasEntry(String name) throws AndrolibException {
|
||||
ensureOpen();
|
||||
return mArchive.listFiles();
|
||||
return mArchive.hasEntry(name);
|
||||
}
|
||||
|
||||
public Stream<ZipFileEntry> walkFiles() throws AndrolibException {
|
||||
public ZipArchiveEntry getEntry(String name) throws AndrolibException, ZipNoSuchEntryException {
|
||||
ensureOpen();
|
||||
return mArchive.walkFiles();
|
||||
}
|
||||
|
||||
public boolean containsFile(String name) throws AndrolibException {
|
||||
ensureOpen();
|
||||
return mArchive.containsFile(name);
|
||||
}
|
||||
|
||||
public ZipFileEntry getFile(String name) throws AndrolibException, ZipNoSuchEntryException {
|
||||
ensureOpen();
|
||||
return mArchive.getFile(name);
|
||||
return mArchive.getEntry(name);
|
||||
}
|
||||
|
||||
public void extract(Path dest) throws AndrolibException, IOException {
|
||||
|
||||
@@ -37,23 +37,23 @@ public final class AaptManager {
|
||||
public static Path getBinaryFile() throws AndrolibException {
|
||||
String binName = getBinaryName();
|
||||
|
||||
if (!SystemUtils.is64Bit()) {
|
||||
if (!SystemUtils.IS_64BIT) {
|
||||
throw new AndrolibException(binName + " binaries are not available for 32-bit platforms.");
|
||||
}
|
||||
|
||||
StringBuilder binPath = new StringBuilder("/prebuilt/");
|
||||
if (SystemUtils.isUnix()) {
|
||||
if (SystemUtils.IS_OS_LINUX) {
|
||||
binPath.append("linux"); // ELF 64-bit LSB executable, x86-64
|
||||
} else if (SystemUtils.isMac()) {
|
||||
} else if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
binPath.append("macosx"); // fat binary x86_64 + arm64
|
||||
} else if (SystemUtils.isWindows()) {
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
binPath.append("windows"); // x86_64
|
||||
} else {
|
||||
throw new AndrolibException("Could not identify platform: " + SystemUtils.getOSName());
|
||||
throw new AndrolibException("Could not identify platform: " + SystemUtils.OS_NAME);
|
||||
}
|
||||
binPath.append('/');
|
||||
binPath.append(binName);
|
||||
if (SystemUtils.isWindows()) {
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
binPath.append(".exe");
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ import brut.util.JarUtils;
|
||||
import brut.util.Pair;
|
||||
import brut.util.SystemUtils;
|
||||
import brut.zip.ZipArchive;
|
||||
import brut.zip.ZipFileEntry;
|
||||
import brut.zip.ZipUtils;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
@@ -54,9 +52,9 @@ public class Framework {
|
||||
static {
|
||||
String home = System.getProperty("user.home");
|
||||
Path basePath;
|
||||
if (SystemUtils.isWindows()) {
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
basePath = Paths.get(home, "AppData", "Local");
|
||||
} else if (SystemUtils.isMac()) {
|
||||
} else if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
basePath = Paths.get(home, "Library");
|
||||
} else {
|
||||
// A valid XDG_DATA_HOME environment variable must be an absolute path.
|
||||
@@ -83,11 +81,7 @@ public class Framework {
|
||||
throw new InFileNotFoundException(apkFile);
|
||||
}
|
||||
try (ZipArchive zip = new ZipArchive(apkFile)) {
|
||||
ZipFileEntry file = zip.getFile("resources.arsc");
|
||||
byte[] data;
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
data = ByteStreams.toByteArray(in);
|
||||
}
|
||||
byte[] data = zip.getEntry("resources.arsc").getData();
|
||||
ResTable table = parseAndPublicizeResources(data);
|
||||
int pkgId = table.listPackageGroups().iterator().next().getId();
|
||||
Path outFile = getDirectory().resolve(pkgId + getFileSuffix());
|
||||
@@ -103,11 +97,8 @@ public class Framework {
|
||||
out.closeEntry();
|
||||
|
||||
// Copy AndroidManifest.xml, if available.
|
||||
if (zip.containsFile("AndroidManifest.xml")) {
|
||||
file = zip.getFile("AndroidManifest.xml");
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
data = ByteStreams.toByteArray(in);
|
||||
}
|
||||
if (zip.hasEntry("AndroidManifest.xml")) {
|
||||
data = zip.getEntry("AndroidManifest.xml").getData();
|
||||
entry = new ZipEntry("AndroidManifest.xml");
|
||||
entry.setTime(ApkFile.TIME_MILLIS);
|
||||
entry.setSize(data.length);
|
||||
|
||||
@@ -34,7 +34,7 @@ import brut.androlib.res.xml.ValuesXmlSerializable;
|
||||
import brut.common.Log;
|
||||
import brut.util.IOUtils;
|
||||
import brut.util.Pair;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
@@ -43,6 +43,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -74,7 +75,7 @@ public class ResDecoder {
|
||||
}
|
||||
|
||||
public void decodeResources(Path outDir) throws AndrolibException {
|
||||
if (!mApkFile.containsFile("resources.arsc")) {
|
||||
if (!mApkFile.hasEntry("resources.arsc")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ public class ResDecoder {
|
||||
ApkInfo apkInfo = mApkFile.getApkInfo();
|
||||
ResPackage pkg = mTable.getMainPackage();
|
||||
|
||||
Map<ResFileDecoder.Type, ResStreamDecoder> decoders = new HashMap<>();
|
||||
Map<ResFileDecoder.Type, ResStreamDecoder> decoders = new EnumMap<>(ResFileDecoder.Type.class);
|
||||
decoders.put(ResFileDecoder.Type.UNKNOWN, new ResRawStreamDecoder());
|
||||
decoders.put(ResFileDecoder.Type.PNG_9PATCH, new ResNinePatchStreamDecoder());
|
||||
|
||||
@@ -96,14 +97,14 @@ public class ResDecoder {
|
||||
ResFileDecoder fileDecoder = new ResFileDecoder(mApkFile, decoders);
|
||||
|
||||
Log.i(TAG, "Decoding value resources...");
|
||||
for (ResEntry entry : Lists.newArrayList(listEntries(pkg))) {
|
||||
for (ResEntry entry : ImmutableList.copyOf(listEntries(pkg))) {
|
||||
if (entry.getValue() instanceof ResBag) {
|
||||
((ResBag) entry.getValue()).resolveKeys();
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Decoding file resources...");
|
||||
for (ResEntry entry : Lists.newArrayList(listEntries(pkg))) {
|
||||
for (ResEntry entry : ImmutableList.copyOf(listEntries(pkg))) {
|
||||
if (entry.getValue() instanceof ResFileReference) {
|
||||
fileDecoder.decode(entry, outDir, mResFileMap);
|
||||
}
|
||||
@@ -188,7 +189,7 @@ public class ResDecoder {
|
||||
}
|
||||
|
||||
private void generatePublicXml(ResPackage pkg, Path outDir, ResXmlSerializer serial) throws AndrolibException {
|
||||
List<ResEntrySpec> specs = Lists.newArrayList(pkg.listEntrySpecs());
|
||||
List<ResEntrySpec> specs = new ArrayList<>(pkg.listEntrySpecs());
|
||||
specs.sort(Comparator.comparing(ResEntrySpec::getResId));
|
||||
|
||||
Path outFile = outDir.resolve("res/values/public.xml");
|
||||
@@ -333,7 +334,7 @@ public class ResDecoder {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ResOverlayable> overlayables = Lists.newArrayList(pkg.listOverlayables());
|
||||
List<ResOverlayable> overlayables = new ArrayList<>(pkg.listOverlayables());
|
||||
overlayables.sort(Comparator.comparing(ResOverlayable::getName));
|
||||
|
||||
Path outFile = outDir.resolve("res/values/overlayable.xml");
|
||||
@@ -354,7 +355,7 @@ public class ResDecoder {
|
||||
}
|
||||
|
||||
public void decodeManifest(Path outDir) throws AndrolibException {
|
||||
if (!mApkFile.containsFile("AndroidManifest.xml")) {
|
||||
if (!mApkFile.hasEntry("AndroidManifest.xml")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -369,7 +370,7 @@ public class ResDecoder {
|
||||
|
||||
Log.i(TAG, "Decoding AndroidManifest.xml with " + (pkg == null ? "only framework " : "") + "resources...");
|
||||
try (
|
||||
InputStream in = mApkFile.getFile("AndroidManifest.xml").getInputStream();
|
||||
InputStream in = mApkFile.getEntry("AndroidManifest.xml").getInputStream();
|
||||
OutputStream out = createOutputStream(outDir.resolve("AndroidManifest.xml"))
|
||||
) {
|
||||
decoder.decode(in, out);
|
||||
@@ -438,7 +439,7 @@ public class ResDecoder {
|
||||
}
|
||||
|
||||
// Record framework package IDs used by the resource table.
|
||||
List<Integer> framePackageIds = Lists.newArrayList(mTable.getFramePackageIds());
|
||||
List<Integer> framePackageIds = new ArrayList<>(mTable.getFramePackageIds());
|
||||
if (!framePackageIds.isEmpty()) {
|
||||
UsesFramework usesFramework = apkInfo.getUsesFramework();
|
||||
List<Integer> frameworkIds = usesFramework.getIds();
|
||||
@@ -450,7 +451,7 @@ public class ResDecoder {
|
||||
}
|
||||
|
||||
// Record library package names used by the resource table.
|
||||
List<Integer> libPackageIds = Lists.newArrayList(mTable.getLibPackageIds());
|
||||
List<Integer> libPackageIds = new ArrayList<>(mTable.getLibPackageIds());
|
||||
if (!libPackageIds.isEmpty()) {
|
||||
List<String> usesLibrary = apkInfo.getUsesLibrary();
|
||||
libPackageIds.sort(null);
|
||||
|
||||
@@ -20,6 +20,7 @@ import brut.androlib.res.decoder.ResChunkPullParser;
|
||||
import brut.common.Log;
|
||||
import brut.util.BinaryDataInputStream;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -28,7 +29,6 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ResStringPool {
|
||||
@@ -246,7 +246,7 @@ public class ResStringPool {
|
||||
}
|
||||
|
||||
if (mStringToIndex == null) {
|
||||
Map<String, Integer> stringToIndex = new HashMap<>(mStringOffsets.length, 1f);
|
||||
Map<String, Integer> stringToIndex = Maps.newHashMapWithExpectedSize(mStringOffsets.length);
|
||||
for (int i = 0; i < mStringOffsets.length; i++) {
|
||||
String value = getString(i);
|
||||
if (value != null) {
|
||||
|
||||
+55
-59
@@ -40,7 +40,6 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class BinaryResourceParser {
|
||||
private static final String TAG = BinaryResourceParser.class.getName();
|
||||
@@ -418,10 +417,10 @@ public class BinaryResourceParser {
|
||||
mHasSparseEntries = true;
|
||||
}
|
||||
|
||||
// #3778 - In some apps the res entries are unordered and might have to jump backwards.
|
||||
// We simply pre-sort them by offset.
|
||||
Map<Integer, List<Integer>> entryOffsets = new TreeMap<>();
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
// Pack each entry into a long as (offset << 32 | index).
|
||||
long[] entries = new long[entryCount];
|
||||
entryCount = 0;
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
int index, offset;
|
||||
|
||||
// #3372 - 16-bit offsets should be stored as real offsets (* 4u).
|
||||
@@ -439,42 +438,38 @@ public class BinaryResourceParser {
|
||||
}
|
||||
}
|
||||
|
||||
List<Integer> indexes = entryOffsets.get(offset);
|
||||
if (indexes == null) {
|
||||
indexes = new ArrayList<>();
|
||||
entryOffsets.put(offset, indexes);
|
||||
}
|
||||
indexes.add(index);
|
||||
}
|
||||
|
||||
// Exclude NO_ENTRY indexes.
|
||||
List<Integer> indexes = entryOffsets.get(NO_ENTRY);
|
||||
if (indexes != null) {
|
||||
if (type != null) {
|
||||
for (int index : indexes) {
|
||||
if (!mPackage.hasEntrySpec(id, index)) {
|
||||
mMissingEntrySpecs.add(ResId.of(mPackage.getId(), id, index));
|
||||
}
|
||||
// Exclude NO_ENTRY indexes.
|
||||
if (offset == NO_ENTRY) {
|
||||
if (type != null && !mPackage.hasEntrySpec(id, index)) {
|
||||
mMissingEntrySpecs.add(ResId.of(mPackage.getId(), id, index));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
entryOffsets.remove(NO_ENTRY);
|
||||
// Update the entry count for logging.
|
||||
entryCount -= indexes.size();
|
||||
entries[entryCount++] = ((long) offset << 32) | (index & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
// Parse the remaining entries.
|
||||
for (Map.Entry<Integer, List<Integer>> entryOffset : entryOffsets.entrySet()) {
|
||||
int offset = entryOffset.getKey();
|
||||
indexes = entryOffset.getValue();
|
||||
// #3778 - In some apps the entries are unordered and might have to jump backwards.
|
||||
// Sorting orders them by offset, then by index, and entries sharing an offset end up adjacent.
|
||||
Arrays.sort(entries, 0, entryCount);
|
||||
|
||||
// #3428 - In some apps the res entries are padded for alignment, but in #3778 it made sense to align
|
||||
// Parse each offset group once and apply the result to all its entries.
|
||||
for (int i = 0, groupEnd; i < entryCount; i = groupEnd) {
|
||||
int offset = (int) (entries[i] >>> 32);
|
||||
|
||||
// Find the end of the group of entries sharing this offset (exclusive).
|
||||
groupEnd = i + 1;
|
||||
while (groupEnd < entryCount && (int) (entries[groupEnd] >>> 32) == offset) {
|
||||
groupEnd++;
|
||||
}
|
||||
|
||||
// #3428 - In some apps the entries are padded for alignment, but in #3778 it made sense to align
|
||||
// to the start of the entries to handle all cases.
|
||||
long entryStart = parser.chunkStart() + entriesStart + offset;
|
||||
|
||||
// As seen in some recent APKs - there are more entries reported than can fit in the chunk.
|
||||
if (entryStart >= parser.chunkEnd()) {
|
||||
Log.w(TAG, "End of chunk hit. Skipping remaining %s entries in type: %s", entryCount, typeName);
|
||||
Log.w(TAG, "End of chunk hit. Skipping remaining %s entries in type: %s", entryCount - i, typeName);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -485,36 +480,37 @@ public class BinaryResourceParser {
|
||||
int key = entry.getLeft();
|
||||
ResValue value = entry.getRight();
|
||||
|
||||
// Add all entries with the parsed value, or discard them if the config was invalid.
|
||||
if (type != null) {
|
||||
for (int index : indexes) {
|
||||
ResId resId = ResId.of(mPackage.getId(), id, index);
|
||||
|
||||
// #2824 - In some apps the res entries are duplicated with the 2nd being malformed.
|
||||
// AOSP skips this, so we will do the same.
|
||||
if (value == null) {
|
||||
if (!mPackage.hasEntrySpec(id, index)) {
|
||||
mMissingEntrySpecs.add(resId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// The same entry can never be added more than once.
|
||||
if (mPackage.hasEntry(id, index, config, mFlag)) {
|
||||
Log.w(TAG, "Ignoring repeated entry: id=%s, config=%s, flag=%s", resId, config, mFlag);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mPackage.hasEntrySpec(id, index)) {
|
||||
mPackage.addEntrySpec(id, index, mKeyStringPool.getString(key));
|
||||
mMissingEntrySpecs.remove(resId);
|
||||
}
|
||||
mPackage.addEntry(id, index, config, mFlag, value);
|
||||
}
|
||||
// Discard all entries sharing this offset if the config was invalid.
|
||||
if (type == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update the entry count for logging.
|
||||
entryCount -= indexes.size();
|
||||
// Add all entries sharing this offset with the parsed value.
|
||||
for (int j = i; j < groupEnd; j++) {
|
||||
int index = (int) entries[j];
|
||||
ResId resId = ResId.of(mPackage.getId(), id, index);
|
||||
|
||||
// #2824 - In some apps the entries are duplicated with the 2nd being malformed.
|
||||
// AOSP skips this, so we will do the same.
|
||||
if (value == null) {
|
||||
if (!mPackage.hasEntrySpec(id, index)) {
|
||||
mMissingEntrySpecs.add(resId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// The same entry can never be added more than once.
|
||||
if (mPackage.hasEntry(id, index, config, mFlag)) {
|
||||
Log.w(TAG, "Ignoring repeated entry: id=%s, config=%s, flag=%s", resId, config, mFlag);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mPackage.hasEntrySpec(id, index)) {
|
||||
mPackage.addEntrySpec(id, index, mKeyStringPool.getString(key));
|
||||
mMissingEntrySpecs.remove(resId);
|
||||
}
|
||||
mPackage.addEntry(id, index, config, mFlag, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,7 +689,7 @@ public class BinaryResourceParser {
|
||||
int name = mIn.readInt();
|
||||
ResItem value = (ResItem) parseItem(typeName, true);
|
||||
|
||||
// #2824 - In some apps the res entries are duplicated with the 2nd being malformed.
|
||||
// #2824 - In some apps the entries are duplicated with the 2nd being malformed.
|
||||
// AOSP skips this, so we will do the same.
|
||||
if (value == null) {
|
||||
continue;
|
||||
|
||||
+13
-21
@@ -26,7 +26,6 @@ import brut.androlib.res.table.value.ResPrimitive;
|
||||
import brut.androlib.res.table.value.ResString;
|
||||
import brut.common.Log;
|
||||
import brut.util.IOUtils;
|
||||
import brut.zip.ZipUtils;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
@@ -50,24 +49,17 @@ public class ResFileDecoder {
|
||||
}
|
||||
|
||||
public void decode(ResEntry entry, Path outDir, Map<String, String> resFileMap) throws AndrolibException {
|
||||
String value = ((ResFileReference) entry.getValue()).getPath();
|
||||
String name = ((ResFileReference) entry.getValue()).getPath();
|
||||
|
||||
// Some apps have string values where they shouldn't be.
|
||||
// We assumed that they are file references, but if no such file then fall back to a string value.
|
||||
String fileName;
|
||||
try {
|
||||
fileName = ZipUtils.normalize(value);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
fileName = null;
|
||||
}
|
||||
if (fileName == null || !mApkFile.containsFile(fileName)) {
|
||||
entry.setValue(new ResString(value));
|
||||
if (!mApkFile.hasEntry(name)) {
|
||||
entry.setValue(new ResString(name));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get input file extension.
|
||||
String ext = fileName.endsWith(".9.png") ? "9.png"
|
||||
: IOUtils.getFileExtension(fileName).toLowerCase(Locale.ROOT);
|
||||
String ext = name.endsWith(".9.png") ? "9.png" : IOUtils.getFileExtension(name, '/').toLowerCase(Locale.ROOT);
|
||||
|
||||
// Use aapt2-like logic to determine which decoder to use.
|
||||
// TODO: Determine by magic bytes and fill in stripped extensions?
|
||||
@@ -89,35 +81,35 @@ public class ResFileDecoder {
|
||||
+ "/" + entry.getName() + (ext.isEmpty() ? "" : "." + ext);
|
||||
|
||||
// Map input file name to output file name.
|
||||
resFileMap.put(fileName, outFileName);
|
||||
resFileMap.put(name, outFileName);
|
||||
|
||||
Log.d(TAG, "Decoding file " + fileName + " to " + outFileName);
|
||||
Log.d(TAG, "Decoding file " + name + " to " + outFileName);
|
||||
try {
|
||||
Path outFile = outDir.resolve(outFileName);
|
||||
|
||||
if (type != Type.UNKNOWN) {
|
||||
try {
|
||||
decode(type, fileName, outFile);
|
||||
decode(type, name, outFile);
|
||||
return;
|
||||
} catch (RawXmlEncounteredException ignored) {
|
||||
// Assume the file is a raw XML.
|
||||
Log.d(TAG, "Could not decode binary XML file: " + fileName);
|
||||
Log.d(TAG, "Could not decode binary XML file: " + name);
|
||||
} catch (NinePatchNotFoundException ignored) {
|
||||
// Assume the file is a raw PNG.
|
||||
// Some apps contain unprocessed dummy 3x3 9-patch PNGs.
|
||||
// Extract them as-is, let aapt2 process them properly later.
|
||||
Log.d(TAG, "Could not find 9-patch chunk in file: " + fileName);
|
||||
Log.d(TAG, "Could not find 9-patch chunk in file: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
decode(Type.UNKNOWN, fileName, outFile);
|
||||
decode(Type.UNKNOWN, name, outFile);
|
||||
} catch (AndrolibException | IOException ignored) {
|
||||
Log.w(TAG, "Could not decode file, replacing with NULL value: " + fileName);
|
||||
Log.w(TAG, "Could not decode file, replacing with NULL value: " + name);
|
||||
entry.setValue(ResPrimitive.NULL);
|
||||
}
|
||||
}
|
||||
|
||||
private void decode(Type type, String fileName, Path outFile) throws AndrolibException, IOException {
|
||||
private void decode(Type type, String name, Path outFile) throws AndrolibException, IOException {
|
||||
ResStreamDecoder decoder = mDecoders.get(type);
|
||||
if (decoder == null) {
|
||||
throw new IllegalStateException("Undefined decoder for file type: " + type);
|
||||
@@ -127,7 +119,7 @@ public class ResFileDecoder {
|
||||
IOUtils.createParentDirectories(outFile);
|
||||
}
|
||||
try (
|
||||
InputStream in = mApkFile.getFile(fileName).getInputStream();
|
||||
InputStream in = mApkFile.getEntry(name).getInputStream();
|
||||
OutputStream out = Files.newOutputStream(outFile)
|
||||
) {
|
||||
decoder.decode(in, out);
|
||||
|
||||
@@ -98,7 +98,7 @@ public class ResTable {
|
||||
}
|
||||
|
||||
private void loadPackagesFromApk(ApkFile apkFile, boolean isMainPackage) throws AndrolibException {
|
||||
try (InputStream in = apkFile.getFile("resources.arsc").getInputStream()) {
|
||||
try (InputStream in = apkFile.getEntry("resources.arsc").getInputStream()) {
|
||||
BinaryResourceParser parser = isMainPackage
|
||||
? new BinaryResourceParser(this, mConfig.isKeepBrokenResources(), mConfig.isDecodeResolveGreedy())
|
||||
: new BinaryResourceParser(this, true, true);
|
||||
|
||||
@@ -17,23 +17,22 @@
|
||||
package brut.androlib.res.table;
|
||||
|
||||
import brut.androlib.res.table.value.ResItem;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ResTypeSpec {
|
||||
private static final Map<String, Set<String>> STANDARD_TYPE_FORMATS = new HashMap<>();
|
||||
static {
|
||||
STANDARD_TYPE_FORMATS.put("bool", Sets.newHashSet("boolean"));
|
||||
STANDARD_TYPE_FORMATS.put("color", Sets.newHashSet("color"));
|
||||
STANDARD_TYPE_FORMATS.put("dimen", Sets.newHashSet("float", "fraction", "dimension"));
|
||||
STANDARD_TYPE_FORMATS.put("drawable", Sets.newHashSet("color"));
|
||||
STANDARD_TYPE_FORMATS.put("fraction", Sets.newHashSet("float", "fraction", "dimension"));
|
||||
STANDARD_TYPE_FORMATS.put("integer", Sets.newHashSet("integer"));
|
||||
STANDARD_TYPE_FORMATS.put("string", Sets.newHashSet("string"));
|
||||
}
|
||||
private static final Map<String, Set<String>> STANDARD_TYPE_FORMATS = ImmutableMap.<String, Set<String>>builder()
|
||||
.put("bool", ImmutableSet.of("boolean"))
|
||||
.put("color", ImmutableSet.of("color"))
|
||||
.put("dimen", ImmutableSet.of("float", "fraction", "dimension"))
|
||||
.put("drawable", ImmutableSet.of("color"))
|
||||
.put("fraction", ImmutableSet.of("float", "fraction", "dimension"))
|
||||
.put("integer", ImmutableSet.of("integer"))
|
||||
.put("string", ImmutableSet.of("string"))
|
||||
.build();
|
||||
|
||||
private final ResPackage mPackage;
|
||||
private final int mId;
|
||||
|
||||
@@ -24,11 +24,11 @@ import brut.androlib.res.table.ResId;
|
||||
import brut.androlib.res.table.ResPackage;
|
||||
import brut.androlib.res.xml.ResXmlUtils;
|
||||
import brut.common.Log;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class ResStyle extends ResBag {
|
||||
@@ -129,7 +129,7 @@ public class ResStyle extends ResBag {
|
||||
|
||||
ResPackage pkg = mParent.getPackage();
|
||||
boolean skipDuplicates = !pkg.getTable().getConfig().isAnalysisMode();
|
||||
Set<ResId> processedKeys = new HashSet<>();
|
||||
Set<ResId> processedKeys = Sets.newHashSetWithExpectedSize(mItems.length);
|
||||
for (Item item : mItems) {
|
||||
ResReference key = item.getKey();
|
||||
ResEntry keyEntry = key.resolveEntry();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package brut.androlib.smali;
|
||||
|
||||
import brut.androlib.exceptions.AndrolibException;
|
||||
import brut.zip.ZipUtils;
|
||||
import com.android.tools.smali.baksmali.Baksmali;
|
||||
import com.android.tools.smali.baksmali.BaksmaliOptions;
|
||||
import com.android.tools.smali.dexlib2.analysis.InlineMethodResolver;
|
||||
@@ -73,12 +74,8 @@ public class SmaliDecoder {
|
||||
dexFiles.put(1, dexEntry.getDexFile());
|
||||
|
||||
// Add additional dex files if it's a multi-dex container.
|
||||
String prefix = dexName + "/";
|
||||
for (String dexEntryName : mDexContainer.getDexEntryNames()) {
|
||||
if (dexEntryName.equals(dexName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String prefix = dexName + "/";
|
||||
if (!dexEntryName.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
@@ -105,7 +102,8 @@ public class SmaliDecoder {
|
||||
|
||||
String dirName = "smali";
|
||||
if (dexNum > 1 || !dexName.equals("classes.dex")) {
|
||||
dirName += "_" + dexName.substring(0, dexName.lastIndexOf('.')).replace('/', '@');
|
||||
String escapedName = dexName.substring(0, dexName.lastIndexOf('.')).replace('/', '@');
|
||||
dirName += "_" + ZipUtils.normalizeForFs(escapedName);
|
||||
if (dexNum > 1) {
|
||||
dirName += dexNum;
|
||||
}
|
||||
@@ -115,7 +113,7 @@ public class SmaliDecoder {
|
||||
}
|
||||
|
||||
mDexFiles.add(dexName);
|
||||
} catch (IOException ex) {
|
||||
} catch (IllegalArgumentException | IOException ex) {
|
||||
throw new AndrolibException("Could not baksmali file: " + dexName, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,7 +579,7 @@ public class BuildAndDecodeApkTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
public void storedMp3FilesAreNotCompressedTest() throws Exception {
|
||||
assertEquals(0, sTestApk.getFile("res/raw/rain.mp3").getMethod());
|
||||
assertEquals(0, sTestApk.getEntry("res/raw/rain.mp3").getMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -594,7 +594,7 @@ public class BuildAndDecodeApkTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
public void unicodeAssetTest() throws Exception {
|
||||
assumeTrue(!SystemUtils.isWindows());
|
||||
assumeTrue(!SystemUtils.IS_OS_WINDOWS);
|
||||
compareBinaryFolder("assets/unicode-txt");
|
||||
}
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ public class UnknownCompressionTest extends BaseTest {
|
||||
@Test
|
||||
public void pkmExtensionDeflatedTest() throws Exception {
|
||||
String name = "assets/bin/Data/test.pkm";
|
||||
int control = sTestApk.getFile(name).getMethod();
|
||||
int rebuilt = sNewApk.getFile(name).getMethod();
|
||||
int control = sTestApk.getEntry(name).getMethod();
|
||||
int rebuilt = sNewApk.getEntry(name).getMethod();
|
||||
|
||||
// Check that control = rebuilt (both deflated)
|
||||
// Add extra check for checking not equal to 0, just in case control gets broken
|
||||
@@ -66,8 +66,8 @@ public class UnknownCompressionTest extends BaseTest {
|
||||
@Test
|
||||
public void doubleExtensionStoredTest() throws Exception {
|
||||
String name = "assets/bin/Data/two.extension.file";
|
||||
int control = sTestApk.getFile(name).getMethod();
|
||||
int rebuilt = sNewApk.getFile(name).getMethod();
|
||||
int control = sTestApk.getEntry(name).getMethod();
|
||||
int rebuilt = sNewApk.getEntry(name).getMethod();
|
||||
|
||||
// Check that control = rebuilt (both stored)
|
||||
// Add extra check for checking = 0 to enforce check for stored just in case control breaks
|
||||
@@ -78,8 +78,8 @@ public class UnknownCompressionTest extends BaseTest {
|
||||
@Test
|
||||
public void confirmJsonFileIsDeflatedTest() throws Exception {
|
||||
String name = "test.json";
|
||||
int control = sTestApk.getFile(name).getMethod();
|
||||
int rebuilt = sNewApk.getFile(name).getMethod();
|
||||
int control = sTestApk.getEntry(name).getMethod();
|
||||
int rebuilt = sNewApk.getEntry(name).getMethod();
|
||||
|
||||
assertEquals(control, rebuilt);
|
||||
assertEquals(8, rebuilt);
|
||||
@@ -88,8 +88,8 @@ public class UnknownCompressionTest extends BaseTest {
|
||||
@Test
|
||||
public void confirmPngFileIsStoredTest() throws Exception {
|
||||
String name = "950x150.png";
|
||||
int control = sTestApk.getFile(name).getMethod();
|
||||
int rebuilt = sNewApk.getFile(name).getMethod();
|
||||
int control = sTestApk.getEntry(name).getMethod();
|
||||
int rebuilt = sNewApk.getEntry(name).getMethod();
|
||||
|
||||
assertNotEquals(control, rebuilt);
|
||||
assertEquals(0, rebuilt);
|
||||
|
||||
@@ -62,10 +62,6 @@ public final class IOUtils {
|
||||
return (tail != null ? real.resolve(tail) : real).normalize();
|
||||
}
|
||||
|
||||
public static String getFileExtension(String path) {
|
||||
return getFileExtension(Paths.get(path));
|
||||
}
|
||||
|
||||
public static String getFileExtension(Path path) {
|
||||
Path name = path.getFileName();
|
||||
if (name == null) {
|
||||
@@ -76,8 +72,10 @@ public final class IOUtils {
|
||||
return lastDot != -1 ? fileName.substring(lastDot + 1) : "";
|
||||
}
|
||||
|
||||
public static String getNameWithoutExtension(String path) {
|
||||
return getNameWithoutExtension(Paths.get(path));
|
||||
public static String getFileExtension(String path, char separator) {
|
||||
int lastSep = path.lastIndexOf(separator);
|
||||
int lastDot = path.lastIndexOf('.');
|
||||
return lastDot > lastSep ? path.substring(lastDot + 1) : "";
|
||||
}
|
||||
|
||||
public static String getNameWithoutExtension(Path path) {
|
||||
@@ -90,6 +88,12 @@ public final class IOUtils {
|
||||
return lastDot != -1 ? fileName.substring(0, lastDot) : fileName;
|
||||
}
|
||||
|
||||
public static String getNameWithoutExtension(String path, char separator) {
|
||||
int lastSep = path.lastIndexOf(separator);
|
||||
int lastDot = path.lastIndexOf('.');
|
||||
return lastDot > lastSep ? path.substring(lastSep + 1, lastDot) : path.substring(lastSep + 1);
|
||||
}
|
||||
|
||||
public static Path createParentDirectories(Path path, FileAttribute<?>... attrs) throws IOException {
|
||||
Path parent = toCanonicalPath(path).getParent();
|
||||
if (parent != null) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -33,10 +32,22 @@ import java.util.function.Consumer;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
public final class SystemUtils {
|
||||
private static final String OS_NAME = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
private static final boolean IS_64BIT;
|
||||
|
||||
private SystemUtils() {}
|
||||
|
||||
public static final String OS_NAME = System.getProperty("os.name");
|
||||
|
||||
private static boolean isOsNameMatch(String prefix) {
|
||||
return OS_NAME.regionMatches(true, 0, prefix, 0, prefix.length());
|
||||
}
|
||||
|
||||
public static final boolean IS_OS_LINUX = isOsNameMatch("Linux");
|
||||
public static final boolean IS_OS_MAC_OSX = isOsNameMatch("Mac OS X");
|
||||
public static final boolean IS_OS_WINDOWS = isOsNameMatch("Windows");
|
||||
|
||||
public static final boolean IS_64BIT;
|
||||
static {
|
||||
if (isWindows()) {
|
||||
if (IS_OS_WINDOWS) {
|
||||
String arch = System.getenv("PROCESSOR_ARCHITEW6432");
|
||||
if (arch == null || arch.isEmpty()) {
|
||||
arch = System.getenv("PROCESSOR_ARCHITECTURE");
|
||||
@@ -48,29 +59,6 @@ public final class SystemUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private SystemUtils() {}
|
||||
|
||||
public static String getOSName() {
|
||||
return OS_NAME;
|
||||
}
|
||||
|
||||
public static boolean isWindows() {
|
||||
return OS_NAME.contains("win");
|
||||
}
|
||||
|
||||
public static boolean isMac() {
|
||||
return OS_NAME.contains("mac");
|
||||
}
|
||||
|
||||
public static boolean isUnix() {
|
||||
return OS_NAME.contains("nix") || OS_NAME.contains("nux")
|
||||
|| OS_NAME.contains("aix") || OS_NAME.contains("sunos");
|
||||
}
|
||||
|
||||
public static boolean is64Bit() {
|
||||
return IS_64BIT;
|
||||
}
|
||||
|
||||
public static void execute(String[] cmd) throws IOException, InterruptedException {
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
Process process = builder.start();
|
||||
|
||||
@@ -32,30 +32,30 @@ public final class TextUtils {
|
||||
private static final long ROUND_VALUE = 1L << 35;
|
||||
|
||||
private static final long[] POSITIVE_SIGNIFICANDS = {
|
||||
0X0800000000000000L, 0X0A00000000000000L, 0X0C80000000000000L, 0X0FA0000000000000L, 0X09C4000000000000L,
|
||||
0X0C35000000000000L, 0X0F42400000000000L, 0X0989680000000000L, 0X0BEBC20000000000L, 0X0EE6B28000000000L,
|
||||
0X09502F9000000000L, 0X0BA43B7400000000L, 0X0E8D4A5100000000L, 0X09184E72A0000000L, 0X0B5E620F48000000L,
|
||||
0X0E35FA931A000000L, 0X08E1BC9BF0400000L, 0X0B1A2BC2EC500000L, 0X0DE0B6B3A7640000L, 0X08AC7230489E8000L,
|
||||
0X0AD78EBC5AC62000L, 0X0D8D726B7177A800L, 0X0878678326EAC900L, 0X0A968163F0A57B40L, 0X0D3C21BCECCEDA10L,
|
||||
0X084595161401484AL, 0X0A56FA5B99019A5CL, 0X0CECB8F27F4200F3L, 0X0813F3978F894098L, 0X0A18F07D736B90BEL,
|
||||
0X0C9F2C9CD04674EDL, 0X0FC6F7C404581229L, 0X09DC5ADA82B70B59L, 0X0C5371912364CE30L, 0X0F684DF56C3E01BCL,
|
||||
0X09A130B963A6C115L, 0X0C097CE7BC90715BL, 0X0F0BDC21ABB48DB2L, 0X096769950B50D88FL
|
||||
0x0800000000000000L, 0x0A00000000000000L, 0x0C80000000000000L, 0x0FA0000000000000L, 0x09C4000000000000L,
|
||||
0x0C35000000000000L, 0x0F42400000000000L, 0x0989680000000000L, 0x0BEBC20000000000L, 0x0EE6B28000000000L,
|
||||
0x09502F9000000000L, 0x0BA43B7400000000L, 0x0E8D4A5100000000L, 0x09184E72A0000000L, 0x0B5E620F48000000L,
|
||||
0x0E35FA931A000000L, 0x08E1BC9BF0400000L, 0x0B1A2BC2EC500000L, 0x0DE0B6B3A7640000L, 0x08AC7230489E8000L,
|
||||
0x0AD78EBC5AC62000L, 0x0D8D726B7177A800L, 0x0878678326EAC900L, 0x0A968163F0A57B40L, 0x0D3C21BCECCEDA10L,
|
||||
0x084595161401484AL, 0x0A56FA5B99019A5CL, 0x0CECB8F27F4200F3L, 0x0813F3978F894098L, 0x0A18F07D736B90BEL,
|
||||
0x0C9F2C9CD04674EDL, 0x0FC6F7C404581229L, 0x09DC5ADA82B70B59L, 0x0C5371912364CE30L, 0x0F684DF56C3E01BCL,
|
||||
0x09A130B963A6C115L, 0x0C097CE7BC90715BL, 0x0F0BDC21ABB48DB2L, 0x096769950B50D88FL
|
||||
};
|
||||
private static final int[] POSITIVE_SHIFTS = {
|
||||
0, 3, 6, 9, 13, 16, 19, 23, 26, 29, 33, 36, 39, 43, 46, 49, 53, 56, 59, 63, 66, 69, 73, 76, 79, 83, 86, 89,
|
||||
93, 96, 99, 102, 106, 109, 112, 116, 119, 122, 126
|
||||
};
|
||||
private static final long[] NEGATIVE_SIGNIFICANDS = {
|
||||
0X0CCCCCCCCCCCCCCCL, 0X0A3D70A3D70A3D70L, 0X083126E978D4FDF3L, 0X0D1B71758E219652L, 0X0A7C5AC471B47842L,
|
||||
0X08637BD05AF6C69BL, 0X0D6BF94D5E57A42BL, 0X0ABCC77118461CEFL, 0X089705F4136B4A59L, 0X0DBE6FECEBDEDD5BL,
|
||||
0X0AFEBFF0BCB24AAFL, 0X08CBCCC096F5088CL, 0X0E12E13424BB40E1L, 0X0B424DC35095CD80L, 0X0901D7CF73AB0ACDL,
|
||||
0X0E69594BEC44DE15L, 0X0B877AA3236A4B44L, 0X09392EE8E921D5D0L, 0X0EC1E4A7DB69561AL, 0X0BCE5086492111AEL,
|
||||
0X0971DA05074DA7BEL, 0X0F1C90080BAF72CBL, 0X0C16D9A0095928A2L, 0X09ABE14CD44753B5L, 0X0F79687AED3EEC55L,
|
||||
0X0C612062576589DDL, 0X09E74D1B791E07E4L, 0X0FD87B5F28300CA0L, 0X0CAD2F7F5359A3B3L, 0X0A2425FF75E14FC3L,
|
||||
0X081CEB32C4B43FCFL, 0X0CFB11EAD453994BL, 0X0A6274BBDD0FADD6L, 0X084EC3C97DA624ABL, 0X0D4AD2DBFC3D0778L,
|
||||
0X0AA242499697392DL, 0X0881CEA14545C757L, 0X0D9C7DCED53C7225L, 0X0AE397D8AA96C1B7L, 0X08B61313BBABCE2CL,
|
||||
0X0DF01E85F912E37AL, 0X0B267ED1940F1C61L, 0X08EB98A7A9A5B04EL, 0X0E45C10C42A2B3B0L, 0X0B6B00D69BB55C8DL,
|
||||
0X09226712162AB070L, 0X0E9D71B689DDE71AL
|
||||
0x0CCCCCCCCCCCCCCCL, 0x0A3D70A3D70A3D70L, 0x083126E978D4FDF3L, 0x0D1B71758E219652L, 0x0A7C5AC471B47842L,
|
||||
0x08637BD05AF6C69BL, 0x0D6BF94D5E57A42BL, 0x0ABCC77118461CEFL, 0x089705F4136B4A59L, 0x0DBE6FECEBDEDD5BL,
|
||||
0x0AFEBFF0BCB24AAFL, 0x08CBCCC096F5088CL, 0x0E12E13424BB40E1L, 0x0B424DC35095CD80L, 0x0901D7CF73AB0ACDL,
|
||||
0x0E69594BEC44DE15L, 0x0B877AA3236A4B44L, 0x09392EE8E921D5D0L, 0x0EC1E4A7DB69561AL, 0x0BCE5086492111AEL,
|
||||
0x0971DA05074DA7BEL, 0x0F1C90080BAF72CBL, 0x0C16D9A0095928A2L, 0x09ABE14CD44753B5L, 0x0F79687AED3EEC55L,
|
||||
0x0C612062576589DDL, 0x09E74D1B791E07E4L, 0x0FD87B5F28300CA0L, 0x0CAD2F7F5359A3B3L, 0x0A2425FF75E14FC3L,
|
||||
0x081CEB32C4B43FCFL, 0x0CFB11EAD453994BL, 0x0A6274BBDD0FADD6L, 0x084EC3C97DA624ABL, 0x0D4AD2DBFC3D0778L,
|
||||
0x0AA242499697392DL, 0x0881CEA14545C757L, 0x0D9C7DCED53C7225L, 0x0AE397D8AA96C1B7L, 0x08B61313BBABCE2CL,
|
||||
0x0DF01E85F912E37AL, 0x0B267ED1940F1C61L, 0x08EB98A7A9A5B04EL, 0x0E45C10C42A2B3B0L, 0x0B6B00D69BB55C8DL,
|
||||
0x09226712162AB070L, 0x0E9D71B689DDE71AL
|
||||
};
|
||||
private static final int[] NEGATIVE_SHIFTS = {
|
||||
-4, -7, -10, -14, -17, -20, -24, -27, -30, -34, -37, -40, -44, -47, -50, -54, -57, -60, -64, -67, -70, -74,
|
||||
@@ -123,7 +123,7 @@ public final class TextUtils {
|
||||
}
|
||||
|
||||
public static int parseColor(CharSequence text, int start, int end) throws NumberFormatException {
|
||||
if (start > end || end > text.length()) {
|
||||
if (start < 0 || start > end || end > text.length()) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
if (start == end) {
|
||||
@@ -188,7 +188,7 @@ public final class TextUtils {
|
||||
}
|
||||
|
||||
public static int parseInt(CharSequence text, int start, int end) throws NumberFormatException {
|
||||
if (start > end || end > text.length()) {
|
||||
if (start < 0 || start > end || end > text.length()) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
if (start == end) {
|
||||
@@ -255,7 +255,7 @@ public final class TextUtils {
|
||||
}
|
||||
|
||||
public static float parseFloat(CharSequence text, int start, int end) throws NumberFormatException {
|
||||
if (start > end || end > text.length()) {
|
||||
if (start < 0 || start > end || end > text.length()) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
if (start == end) {
|
||||
|
||||
@@ -46,8 +46,8 @@ public class YamlPullParser implements Closeable {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (!mClosed) {
|
||||
mReader.close();
|
||||
mClosed = true;
|
||||
mReader.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ public class YamlSerializer implements Closeable {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (!mClosed) {
|
||||
mWriter.close();
|
||||
mClosed = true;
|
||||
mWriter.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
dependencies {
|
||||
implementation(project(":brut.j.common"))
|
||||
implementation(project(":brut.j.util"))
|
||||
implementation(libs.guava)
|
||||
}
|
||||
@@ -17,13 +17,16 @@
|
||||
package brut.zip;
|
||||
|
||||
import brut.common.Log;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
@@ -31,7 +34,8 @@ import java.util.zip.ZipFile;
|
||||
public class ZipArchive implements Closeable {
|
||||
private final Path mPath;
|
||||
private final ZipFile mZip;
|
||||
private ZipDirEntry mRoot;
|
||||
private Map<String, ZipArchiveEntry> mEntries;
|
||||
private ZipArchiveEntry[] mSortedEntries;
|
||||
private boolean mClosed;
|
||||
|
||||
public ZipArchive(Path path) throws IOException {
|
||||
@@ -54,9 +58,9 @@ public class ZipArchive implements Closeable {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (!mClosed) {
|
||||
mZip.close();
|
||||
mRoot = null;
|
||||
mClosed = true;
|
||||
mEntries = null;
|
||||
mZip.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,96 +68,135 @@ public class ZipArchive implements Closeable {
|
||||
if (mClosed) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (mRoot != null) {
|
||||
if (mEntries != null) {
|
||||
return;
|
||||
}
|
||||
ZipDirEntry root = new ZipDirEntry(this, null, "");
|
||||
Enumeration<? extends ZipEntry> entries = mZip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String name;
|
||||
try {
|
||||
name = ZipUtils.normalize(entry.getName());
|
||||
Paths.get(name); // throws InvalidPathException if the name is invalid on this file system
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Log.w(Log.ROOT, "Skipping invalid ZIP entry: %s (%s)", entry.getName(), ex.getMessage());
|
||||
continue;
|
||||
}
|
||||
ZipDirEntry dir = root;
|
||||
for (int i = 0, segEnd;; i = segEnd + 1) {
|
||||
segEnd = name.indexOf(ZipUtils.SEPARATOR_CHAR, i);
|
||||
boolean lastSeg = segEnd == -1;
|
||||
if (lastSeg) {
|
||||
segEnd = name.length();
|
||||
Map<String, ZipArchiveEntry> entries = Maps.newHashMapWithExpectedSize(mZip.size());
|
||||
Enumeration<? extends ZipEntry> zipEntries = mZip.entries();
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
ZipEntry entry = zipEntries.nextElement();
|
||||
if (!entry.isDirectory()) {
|
||||
String name = entry.getName();
|
||||
if (entries.put(name, new ZipArchiveEntry(this, name, entry)) != null) {
|
||||
Log.w(Log.ROOT, "Duplicate ZIP entry: %s", name);
|
||||
}
|
||||
String key = name.substring(i, segEnd);
|
||||
if (lastSeg) {
|
||||
if (entry.isDirectory()) {
|
||||
dir.getOrCreateDir(key, name);
|
||||
} else {
|
||||
dir.putFile(key, name, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
dir = dir.getOrCreateDir(key, name.substring(0, segEnd));
|
||||
}
|
||||
}
|
||||
mRoot = root;
|
||||
mEntries = entries;
|
||||
}
|
||||
|
||||
public Stream<ZipDirEntry> listDirs() {
|
||||
ensureLoaded();
|
||||
return mRoot.listDirs();
|
||||
private ZipArchiveEntry[] sortedEntries() {
|
||||
if (mSortedEntries == null) {
|
||||
ZipArchiveEntry[] sorted = mEntries.values().toArray(new ZipArchiveEntry[0]);
|
||||
Arrays.sort(sorted, Comparator.comparing(ZipArchiveEntry::getName));
|
||||
mSortedEntries = sorted;
|
||||
}
|
||||
return mSortedEntries;
|
||||
}
|
||||
|
||||
public Stream<ZipDirEntry> walkDirs() {
|
||||
ensureLoaded();
|
||||
return mRoot.walkDirs();
|
||||
private static int lowerBound(ZipArchiveEntry[] sorted, String key) {
|
||||
int low = 0, high = sorted.length;
|
||||
while (low < high) {
|
||||
int mid = (low + high) >>> 1;
|
||||
if (sorted[mid].getName().compareTo(key) < 0) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
public boolean containsDir(String name) {
|
||||
ensureLoaded();
|
||||
return mRoot.containsDir(name);
|
||||
public boolean hasEntries() {
|
||||
return hasEntries(null);
|
||||
}
|
||||
|
||||
public ZipDirEntry getDir(String name) throws ZipNoSuchEntryException {
|
||||
public boolean hasEntries(String prefix) {
|
||||
ensureLoaded();
|
||||
return mRoot.getDir(name);
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
return !mEntries.isEmpty();
|
||||
}
|
||||
ZipArchiveEntry[] sorted = sortedEntries();
|
||||
int i = lowerBound(sorted, prefix);
|
||||
return i < sorted.length && sorted[i].getName().startsWith(prefix);
|
||||
}
|
||||
|
||||
public Stream<ZipFileEntry> listFiles() {
|
||||
ensureLoaded();
|
||||
return mRoot.listFiles();
|
||||
public Stream<ZipArchiveEntry> entries() {
|
||||
return entries(null);
|
||||
}
|
||||
|
||||
public Stream<ZipFileEntry> walkFiles() {
|
||||
public Stream<ZipArchiveEntry> entries(String prefix) {
|
||||
ensureLoaded();
|
||||
return mRoot.walkFiles();
|
||||
ZipArchiveEntry[] sorted = sortedEntries();
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
return Arrays.stream(sorted);
|
||||
}
|
||||
char[] chars = prefix.toCharArray();
|
||||
// End bound would wrap if incremented past \uffff.
|
||||
if (chars[chars.length - 1] == '\uffff') {
|
||||
throw new IllegalArgumentException("Prefix cannot end with \\uffff.");
|
||||
}
|
||||
chars[chars.length - 1]++;
|
||||
int from = lowerBound(sorted, prefix);
|
||||
int to = lowerBound(sorted, new String(chars));
|
||||
return Arrays.stream(sorted, from, to);
|
||||
}
|
||||
|
||||
public boolean containsFile(String name) {
|
||||
public boolean hasEntry(String name) {
|
||||
ensureLoaded();
|
||||
return mRoot.containsFile(name);
|
||||
return mEntries.containsKey(name);
|
||||
}
|
||||
|
||||
public ZipFileEntry getFile(String name) throws ZipNoSuchEntryException {
|
||||
public ZipArchiveEntry getEntry(String name) throws ZipNoSuchEntryException {
|
||||
ensureLoaded();
|
||||
return mRoot.getFile(name);
|
||||
ZipArchiveEntry entry = mEntries.get(name);
|
||||
if (entry == null) {
|
||||
throw new ZipNoSuchEntryException(name);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
public void extract(Path dest) throws IOException {
|
||||
ensureLoaded();
|
||||
mRoot.extract(dest);
|
||||
for (ZipArchiveEntry entry : mEntries.values()) {
|
||||
extractEntry(entry, dest);
|
||||
}
|
||||
}
|
||||
|
||||
public void extract(Path dest, String name) throws IOException {
|
||||
ensureLoaded();
|
||||
mRoot.extract(dest, name);
|
||||
ZipArchiveEntry entry = mEntries.get(name);
|
||||
if (entry == null) {
|
||||
throw new ZipNoSuchEntryException(name);
|
||||
}
|
||||
extractEntry(entry, dest);
|
||||
}
|
||||
|
||||
public void extract(Path dest, String... names) throws IOException {
|
||||
ensureLoaded();
|
||||
mRoot.extract(dest, names);
|
||||
ZipArchiveEntry[] entries = new ZipArchiveEntry[names.length];
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
String name = names[i];
|
||||
ZipArchiveEntry entry = mEntries.get(name);
|
||||
if (entry == null) {
|
||||
throw new ZipNoSuchEntryException(name);
|
||||
}
|
||||
entries[i] = entry;
|
||||
}
|
||||
for (ZipArchiveEntry entry : entries) {
|
||||
extractEntry(entry, dest);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractEntry(ZipArchiveEntry entry, Path dest) throws IOException {
|
||||
String name;
|
||||
try {
|
||||
name = ZipUtils.normalizeForFs(entry.getName());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Log.w(Log.ROOT, "Could not extract ZIP entry: %s (%s)", entry.getName(), ex.getMessage());
|
||||
return;
|
||||
}
|
||||
entry.extract(dest.resolve(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
-13
@@ -17,6 +17,7 @@
|
||||
package brut.zip;
|
||||
|
||||
import brut.util.IOUtils;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
@@ -26,18 +27,16 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public final class ZipFileEntry implements Comparable<ZipFileEntry> {
|
||||
public final class ZipArchiveEntry implements Comparable<ZipArchiveEntry> {
|
||||
public static final int STORED = ZipEntry.STORED;
|
||||
public static final int DEFLATED = ZipEntry.DEFLATED;
|
||||
|
||||
private final ZipArchive mArchive;
|
||||
private final ZipDirEntry mParent;
|
||||
private final String mName;
|
||||
private final ZipEntry mEntry;
|
||||
|
||||
ZipFileEntry(ZipArchive archive, ZipDirEntry parent, String name, ZipEntry entry) {
|
||||
ZipArchiveEntry(ZipArchive archive, String name, ZipEntry entry) {
|
||||
mArchive = archive;
|
||||
mParent = parent;
|
||||
mName = name;
|
||||
mEntry = entry;
|
||||
}
|
||||
@@ -46,10 +45,6 @@ public final class ZipFileEntry implements Comparable<ZipFileEntry> {
|
||||
return mArchive;
|
||||
}
|
||||
|
||||
public ZipDirEntry getParent() {
|
||||
return mParent;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
@@ -110,6 +105,12 @@ public final class ZipFileEntry implements Comparable<ZipFileEntry> {
|
||||
return mArchive.getInputStream(mEntry);
|
||||
}
|
||||
|
||||
public byte[] getData() throws IOException {
|
||||
try (InputStream in = getInputStream()) {
|
||||
return ByteStreams.toByteArray(in);
|
||||
}
|
||||
}
|
||||
|
||||
public void extract(Path dest) throws IOException {
|
||||
IOUtils.createParentDirectories(dest);
|
||||
try (InputStream in = getInputStream()) {
|
||||
@@ -119,7 +120,7 @@ public final class ZipFileEntry implements Comparable<ZipFileEntry> {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ZipFileEntry{archive=%s, name=%s}", mParent, mName);
|
||||
return String.format("ZipArchiveEntry{archive=%s, name=%s}", mArchive, mName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,18 +131,18 @@ public final class ZipFileEntry implements Comparable<ZipFileEntry> {
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ZipFileEntry other = (ZipFileEntry) obj;
|
||||
return mParent.equals(other.mParent)
|
||||
ZipArchiveEntry other = (ZipArchiveEntry) obj;
|
||||
return mArchive.equals(other.mArchive)
|
||||
&& mName.equals(other.mName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * mParent.hashCode() + mName.hashCode();
|
||||
return 31 * mArchive.hashCode() + mName.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ZipFileEntry other) {
|
||||
public int compareTo(ZipArchiveEntry other) {
|
||||
int result = mArchive.getPath().compareTo(other.mArchive.getPath());
|
||||
if (result != 0) {
|
||||
return result;
|
||||
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Ryszard Wiśniewski <[email protected]>
|
||||
* Copyright (C) 2010 Connor Tumbleson <[email protected]>
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public final class ZipDirEntry implements Comparable<ZipDirEntry> {
|
||||
private final ZipArchive mArchive;
|
||||
private final ZipDirEntry mParent;
|
||||
private final String mName;
|
||||
private Map<String, ZipDirEntry> mDirs;
|
||||
private Map<String, ZipFileEntry> mFiles;
|
||||
|
||||
ZipDirEntry(ZipArchive archive, ZipDirEntry parent, String name) {
|
||||
mArchive = archive;
|
||||
mParent = parent;
|
||||
mName = name;
|
||||
}
|
||||
|
||||
ZipDirEntry getOrCreateDir(String key, String name) {
|
||||
if (mDirs == null) {
|
||||
mDirs = new HashMap<>();
|
||||
}
|
||||
return mDirs.computeIfAbsent(key, n -> new ZipDirEntry(mArchive, this, name));
|
||||
}
|
||||
|
||||
void putFile(String key, String name, ZipEntry entry) {
|
||||
if (mFiles == null) {
|
||||
mFiles = new HashMap<>();
|
||||
}
|
||||
mFiles.put(key, new ZipFileEntry(mArchive, this, name, entry));
|
||||
}
|
||||
|
||||
public ZipArchive getArchive() {
|
||||
return mArchive;
|
||||
}
|
||||
|
||||
public ZipDirEntry getParent() {
|
||||
return mParent;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return (mDirs == null || mDirs.isEmpty()) && (mFiles == null || mFiles.isEmpty());
|
||||
}
|
||||
|
||||
public Stream<ZipDirEntry> listDirs() {
|
||||
return mDirs != null ? mDirs.values().stream() : Stream.empty();
|
||||
}
|
||||
|
||||
public Stream<ZipDirEntry> walkDirs() {
|
||||
return mDirs != null
|
||||
? mDirs.values().stream().flatMap(dir -> Stream.concat(Stream.of(dir), dir.walkDirs()))
|
||||
: Stream.empty();
|
||||
}
|
||||
|
||||
public boolean containsDir(String name) {
|
||||
return resolveDir(ZipUtils.normalize(name)) != null;
|
||||
}
|
||||
|
||||
public ZipDirEntry getDir(String name) throws ZipNoSuchEntryException {
|
||||
name = ZipUtils.normalize(name);
|
||||
ZipDirEntry dir = resolveDir(name);
|
||||
if (dir == null) {
|
||||
throw new ZipNoSuchEntryException(mName.isEmpty() ? name : mName + ZipUtils.SEPARATOR + name);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
private ZipDirEntry resolveDir(String name) {
|
||||
ZipDirEntry dir = this;
|
||||
int len = name.length();
|
||||
for (int i = 0, segEnd; dir != null && i < len; i = segEnd + 1) {
|
||||
segEnd = name.indexOf(ZipUtils.SEPARATOR_CHAR, i);
|
||||
if (segEnd == -1) {
|
||||
segEnd = len;
|
||||
}
|
||||
dir = dir.mDirs != null ? dir.mDirs.get(name.substring(i, segEnd)) : null;
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
public Stream<ZipFileEntry> listFiles() {
|
||||
return mFiles != null ? mFiles.values().stream() : Stream.empty();
|
||||
}
|
||||
|
||||
public Stream<ZipFileEntry> walkFiles() {
|
||||
Stream<ZipFileEntry> files = listFiles();
|
||||
return mDirs != null
|
||||
? Stream.concat(files, mDirs.values().stream().flatMap(dir -> dir.walkFiles()))
|
||||
: files;
|
||||
}
|
||||
|
||||
public boolean containsFile(String name) {
|
||||
return resolveFile(ZipUtils.normalize(name)) != null;
|
||||
}
|
||||
|
||||
public ZipFileEntry getFile(String name) throws ZipNoSuchEntryException {
|
||||
name = ZipUtils.normalize(name);
|
||||
ZipFileEntry file = resolveFile(name);
|
||||
if (file == null) {
|
||||
throw new ZipNoSuchEntryException(mName.isEmpty() ? name : mName + ZipUtils.SEPARATOR + name);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private ZipFileEntry resolveFile(String name) {
|
||||
ZipDirEntry dir = this;
|
||||
for (int i = 0, segEnd; dir != null; i = segEnd + 1) {
|
||||
segEnd = name.indexOf(ZipUtils.SEPARATOR_CHAR, i);
|
||||
if (segEnd == -1) {
|
||||
return dir.mFiles != null ? dir.mFiles.get(name.substring(i)) : null;
|
||||
}
|
||||
dir = dir.mDirs != null ? dir.mDirs.get(name.substring(i, segEnd)) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void extract(Path dest) throws IOException {
|
||||
Files.createDirectories(dest);
|
||||
if (mFiles != null) {
|
||||
for (Map.Entry<String, ZipFileEntry> entry : mFiles.entrySet()) {
|
||||
entry.getValue().extract(dest.resolve(entry.getKey()));
|
||||
}
|
||||
}
|
||||
if (mDirs != null) {
|
||||
for (Map.Entry<String, ZipDirEntry> entry : mDirs.entrySet()) {
|
||||
entry.getValue().extract(dest.resolve(entry.getKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void extract(Path dest, String name) throws IOException {
|
||||
name = ZipUtils.normalize(name);
|
||||
ZipDirEntry dir = resolveDir(name);
|
||||
if (dir != null) {
|
||||
dir.extract(dest.resolve(name));
|
||||
return;
|
||||
}
|
||||
ZipFileEntry file = resolveFile(name);
|
||||
if (file != null) {
|
||||
file.extract(dest.resolve(name));
|
||||
}
|
||||
}
|
||||
|
||||
public void extract(Path dest, String... names) throws IOException {
|
||||
for (String name : names) {
|
||||
extract(dest, name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ZipDirEntry{archive=%s, name=%s}", mArchive, mName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ZipDirEntry other = (ZipDirEntry) obj;
|
||||
return mArchive.equals(other.mArchive)
|
||||
&& mName.equals(other.mName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * mArchive.hashCode() + mName.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ZipDirEntry other) {
|
||||
int result = mArchive.getPath().compareTo(other.mArchive.getPath());
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return mName.compareTo(other.mName);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
*/
|
||||
package brut.zip;
|
||||
|
||||
import brut.util.SystemUtils;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -23,56 +25,75 @@ import java.nio.file.Path;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
public final class ZipUtils {
|
||||
public static final String SEPARATOR = "/";
|
||||
public static final char SEPARATOR_CHAR = '/';
|
||||
private static final String[] WINDOWS_RESERVED_NAMES = {
|
||||
"CON", "CONIN$", "CONOUT$", "PRN", "AUX", "NUL", "COM", "LPT"
|
||||
};
|
||||
private static final int WINDOWS_RESERVED_WITH_PORT_INDEX = 6;
|
||||
private static final char PUA_BASE = 0xF000;
|
||||
|
||||
private ZipUtils() {}
|
||||
|
||||
public static String normalize(String name) {
|
||||
public static String normalizeForFs(String name) {
|
||||
int len = name.length();
|
||||
if (len == 0) {
|
||||
throw new IllegalArgumentException("Name is empty.");
|
||||
}
|
||||
char ch = name.charAt(0);
|
||||
if (ch == SEPARATOR_CHAR || ch == '\\' || (len >= 2 && name.charAt(1) == ':'
|
||||
&& ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')))) {
|
||||
if (isAbsolutePathForFs(name)) {
|
||||
throw new IllegalArgumentException("Name represents an absolute path.");
|
||||
}
|
||||
char pathSep = SystemUtils.IS_OS_WINDOWS ? '\\' : '/';
|
||||
StringBuilder sb = null; // lazily initialized
|
||||
boolean prevSep = false;
|
||||
boolean prevSep = true; // starts as true so leading slashes are dropped
|
||||
for (int i = 0; i < len; i++) {
|
||||
ch = name.charAt(i);
|
||||
char ch = name.charAt(i);
|
||||
if (ch == 0) {
|
||||
throw new IllegalArgumentException("Name contains a null character.");
|
||||
}
|
||||
if (ch == SEPARATOR_CHAR || ch == '\\') {
|
||||
// Repeated slashes are dropped and backslashes are rewritten as slashes.
|
||||
if (sb == null && (prevSep || ch == '\\')) {
|
||||
if (ch == '/') {
|
||||
// Repeated slashes are dropped. Windows: Slashes are rewritten as backslashes.
|
||||
if (sb == null && (prevSep || pathSep != '/')) {
|
||||
sb = new StringBuilder(len).append(name, 0, i);
|
||||
}
|
||||
if (sb != null && !prevSep) {
|
||||
sb.append(SEPARATOR_CHAR);
|
||||
sb.append(pathSep);
|
||||
}
|
||||
prevSep = true;
|
||||
continue;
|
||||
}
|
||||
if ((i == 0 || prevSep) && ch == '.') {
|
||||
int next = i + 1 < len ? name.charAt(i + 1) : -1;
|
||||
if (next == -1 || next == SEPARATOR_CHAR || next == '\\') {
|
||||
if (prevSep && ch == '.') {
|
||||
char next;
|
||||
if (i + 1 == len || (next = name.charAt(i + 1)) == '/') {
|
||||
// "." segments are dropped.
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder(len).append(name, 0, i);
|
||||
}
|
||||
prevSep = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (next == '.') {
|
||||
int afterNext = i + 2 < len ? name.charAt(i + 2) : -1;
|
||||
if (afterNext == -1 || afterNext == SEPARATOR_CHAR || afterNext == '\\') {
|
||||
throw new IllegalArgumentException("Name contains a '..' segment.");
|
||||
if (next == '.' && (i + 2 == len || name.charAt(i + 2) == '/')) {
|
||||
throw new IllegalArgumentException("Name contains a '..' segment.");
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
if (prevSep) {
|
||||
int segEnd = name.indexOf('/', i);
|
||||
if (segEnd == -1) {
|
||||
segEnd = len;
|
||||
}
|
||||
// fallthrough
|
||||
if (isWindowsReservedDeviceName(name, i, segEnd)) {
|
||||
throw new IllegalArgumentException("Name contains a reserved device name.");
|
||||
}
|
||||
}
|
||||
// Windows: Map reserved characters to the private-use characters that stand in for them.
|
||||
if (ch >= PUA_BASE && ch < PUA_BASE + 0x100 && isWindowsReservedChar((char) (ch - PUA_BASE))) {
|
||||
throw new IllegalArgumentException("Name contains a reserved private-use character.");
|
||||
}
|
||||
if (isWindowsReservedChar(ch)) {
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder(len).append(name, 0, i);
|
||||
}
|
||||
ch = (char) (PUA_BASE + ch);
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
@@ -87,7 +108,7 @@ public final class ZipUtils {
|
||||
}
|
||||
// Otherwise, trim a trailing slash, if present, and return the rebuilt name.
|
||||
len = sb.length();
|
||||
if (len > 0 && sb.charAt(len - 1) == SEPARATOR_CHAR) {
|
||||
if (len > 0 && sb.charAt(len - 1) == pathSep) {
|
||||
sb.setLength(--len);
|
||||
}
|
||||
if (len == 0) {
|
||||
@@ -96,6 +117,110 @@ public final class ZipUtils {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isAbsolutePathForFs(String name) {
|
||||
char ch = name.charAt(0);
|
||||
return ch == '/' || (SystemUtils.IS_OS_WINDOWS && name.length() >= 2 && name.charAt(1) == ':'
|
||||
&& ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')));
|
||||
}
|
||||
|
||||
private static boolean isWindowsReservedDeviceName(String name, int start, int end) {
|
||||
if (start < 0 || start > end || end > name.length()) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
if (start == end) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < WINDOWS_RESERVED_NAMES.length; i++) {
|
||||
String reserved = WINDOWS_RESERVED_NAMES[i];
|
||||
int len = reserved.length();
|
||||
if (len > end - start) {
|
||||
continue;
|
||||
}
|
||||
int j = 0;
|
||||
for (; j < len; j++) {
|
||||
char ch = name.charAt(start + j);
|
||||
if (ch >= 'a' && ch <= 'z') {
|
||||
ch -= ('a' - 'A');
|
||||
}
|
||||
if (ch != reserved.charAt(j)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j != len) {
|
||||
continue;
|
||||
}
|
||||
j += start;
|
||||
if (i >= WINDOWS_RESERVED_WITH_PORT_INDEX) {
|
||||
if (j == end) {
|
||||
continue;
|
||||
}
|
||||
char ch = name.charAt(j);
|
||||
if (ch < '1' || (ch > '9' && ch != '¹' && ch != '²' && ch != '³')) {
|
||||
continue;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
for (;;) {
|
||||
if (j == end) {
|
||||
return true;
|
||||
}
|
||||
char ch = name.charAt(j++);
|
||||
if (ch == '.') {
|
||||
return true;
|
||||
}
|
||||
if (ch != ' ') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isWindowsReservedChar(char ch) {
|
||||
switch (ch) {
|
||||
case '<':
|
||||
case '>':
|
||||
case ':':
|
||||
case '"':
|
||||
case '/':
|
||||
case '\\':
|
||||
case '|':
|
||||
case '?':
|
||||
case '*':
|
||||
return true;
|
||||
default:
|
||||
return ch < 0x20;
|
||||
}
|
||||
}
|
||||
|
||||
public static String normalizeForZip(String name) {
|
||||
int len = name.length();
|
||||
if (len == 0) {
|
||||
throw new IllegalArgumentException("Name is empty.");
|
||||
}
|
||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
||||
return name;
|
||||
}
|
||||
StringBuilder sb = null; // lazily initialized
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = name.charAt(i);
|
||||
char mapped = ch;
|
||||
if (ch == '\\') {
|
||||
mapped = '/';
|
||||
} else if (ch >= PUA_BASE && ch < PUA_BASE + 0x100 && isWindowsReservedChar((char) (ch - PUA_BASE))) {
|
||||
// Map private-use characters back to the reserved characters they stand in for.
|
||||
mapped = (char) (ch - PUA_BASE);
|
||||
}
|
||||
if (mapped != ch && sb == null) {
|
||||
sb = new StringBuilder(len).append(name, 0, i);
|
||||
}
|
||||
if (sb != null) {
|
||||
sb.append(mapped);
|
||||
}
|
||||
}
|
||||
return sb != null ? sb.toString() : name;
|
||||
}
|
||||
|
||||
public static long computeCrc(Path path) throws IOException {
|
||||
try (InputStream in = Files.newInputStream(path)) {
|
||||
return computeCrc(in);
|
||||
|
||||
Reference in new issue
Block a user