refactor: zip archive revision and perf tweaks (#4253)

This commit is contained in:
Igor Eisberg authored and GitHub committed 2026-10-17 06:44:10 -04:00
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) {
@@ -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;
@@ -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
View File
@@ -1,4 +1,5 @@
dependencies {
implementation(project(":brut.j.common"))
implementation(project(":brut.j.util"))
implementation(libs.guava)
}
+100 -57
View File
@@ -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
@@ -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);
}
}
+147 -22
View File
@@ -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);