mirror of
https://github.com/revanced/smali.git
synced 2025-05-09 10:54:29 +02:00
384 lines
15 KiB
Java
384 lines
15 KiB
Java
/*
|
|
* [The "BSD licence"]
|
|
* Copyright (c) 2010 Ben Gruver
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. The name of the author may not be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
package org.jf.util;
|
|
|
|
import ds.tree.RadixTree;
|
|
import ds.tree.RadixTreeImpl;
|
|
|
|
import java.io.*;
|
|
import java.nio.CharBuffer;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* This class checks for case-insensitive file systems, and generates file names based on a given class name, that are
|
|
* guaranteed to be unique. When "colliding" class names are found, it appends a numeric identifier to the end of the
|
|
* class name to distinguish it from another class with a name that differes only by case. i.e. a.smali and a_2.smali
|
|
*/
|
|
public class ClassFileNameHandler {
|
|
private PackageNameEntry top;
|
|
private String fileExtension;
|
|
private boolean modifyWindowsReservedFilenames;
|
|
|
|
public ClassFileNameHandler(File path, String fileExtension) {
|
|
this.top = new PackageNameEntry(path);
|
|
this.fileExtension = fileExtension;
|
|
this.modifyWindowsReservedFilenames = testForWindowsReservedFileNames(path);
|
|
}
|
|
|
|
public File getUniqueFilenameForClass(String className) {
|
|
//class names should be passed in the normal dalvik style, with a leading L, a trailing ;, and using
|
|
//'/' as a separator.
|
|
if (className.charAt(0) != 'L' || className.charAt(className.length()-1) != ';') {
|
|
throw new RuntimeException("Not a valid dalvik class name");
|
|
}
|
|
|
|
int packageElementCount = 1;
|
|
for (int i=1; i<className.length()-1; i++) {
|
|
if (className.charAt(i) == '/') {
|
|
packageElementCount++;
|
|
}
|
|
}
|
|
|
|
String packageElement;
|
|
String[] packageElements = new String[packageElementCount];
|
|
int elementIndex = 0;
|
|
int elementStart = 1;
|
|
for (int i=1; i<className.length()-1; i++) {
|
|
if (className.charAt(i) == '/') {
|
|
//if the first char after the initial L is a '/', or if there are
|
|
//two consecutive '/'
|
|
if (i-elementStart==0) {
|
|
throw new RuntimeException("Not a valid dalvik class name");
|
|
}
|
|
|
|
packageElement = className.substring(elementStart, i);
|
|
|
|
if (modifyWindowsReservedFilenames && isReservedFileName(packageElement)) {
|
|
packageElement += "#";
|
|
}
|
|
|
|
packageElements[elementIndex++] = packageElement;
|
|
elementStart = ++i;
|
|
}
|
|
}
|
|
|
|
//at this point, we have added all the package elements to packageElements, but still need to add
|
|
//the final class name. elementStart should point to the beginning of the class name
|
|
|
|
//this will be true if the class ends in a '/', i.e. Lsome/package/className/;
|
|
if (elementStart >= className.length()-1) {
|
|
throw new RuntimeException("Not a valid dalvik class name");
|
|
}
|
|
|
|
packageElement = className.substring(elementStart, className.length()-1);
|
|
if (modifyWindowsReservedFilenames && isReservedFileName(packageElement)) {
|
|
packageElement += "#";
|
|
}
|
|
|
|
packageElements[elementIndex] = packageElement;
|
|
|
|
return top.addUniqueChild(packageElements, 0);
|
|
}
|
|
|
|
private static boolean testForWindowsReservedFileNames(File path) {
|
|
File f = new File(path, "aux.smali");
|
|
if (f.exists()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
FileWriter writer = new FileWriter(f);
|
|
writer.write("test");
|
|
writer.flush();
|
|
writer.close();
|
|
f.delete(); //doesn't throw IOException
|
|
return false;
|
|
} catch (IOException ex) {
|
|
//if an exception occured, it's likely that we're on a windows system.
|
|
}
|
|
|
|
//let's try one more reserved filename
|
|
f = new File(path, "con.smali");
|
|
if (f.exists()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
FileWriter writer = new FileWriter(f);
|
|
writer.write("test");
|
|
writer.flush();
|
|
writer.close();
|
|
f.delete(); //doesn't throw IOException
|
|
return false;
|
|
} catch (IOException ex) {
|
|
//yup, looks like we're on a windows system
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static Pattern reservedFileNameRegex = Pattern.compile("^CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]$",
|
|
Pattern.CASE_INSENSITIVE);
|
|
private static boolean isReservedFileName(String className) {
|
|
return reservedFileNameRegex.matcher(className).matches();
|
|
}
|
|
|
|
private abstract class FileSystemEntry {
|
|
public final File file;
|
|
|
|
public FileSystemEntry(File file) {
|
|
this.file = file;
|
|
}
|
|
|
|
public abstract File addUniqueChild(String[] pathElements, int pathElementsIndex);
|
|
|
|
public FileSystemEntry makeVirtual(File parent) {
|
|
return new VirtualGroupEntry(this, parent);
|
|
}
|
|
}
|
|
|
|
private class PackageNameEntry extends FileSystemEntry {
|
|
//this contains the FileSystemEntries for all of this package's children
|
|
//the associated keys are all lowercase
|
|
private RadixTree<FileSystemEntry> children = new RadixTreeImpl<FileSystemEntry>();
|
|
|
|
public PackageNameEntry(File parent, String name) {
|
|
super(new File(parent, name));
|
|
}
|
|
|
|
public PackageNameEntry(File path) {
|
|
super(path);
|
|
}
|
|
|
|
@Override
|
|
public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
|
|
String elementName;
|
|
String elementNameLower;
|
|
|
|
if (pathElementsIndex == pathElements.length - 1) {
|
|
elementName = pathElements[pathElementsIndex];
|
|
elementName += fileExtension;
|
|
} else {
|
|
elementName = pathElements[pathElementsIndex];
|
|
}
|
|
elementNameLower = elementName.toLowerCase();
|
|
|
|
FileSystemEntry existingEntry = children.find(elementNameLower);
|
|
if (existingEntry != null) {
|
|
FileSystemEntry virtualEntry = existingEntry;
|
|
//if there is already another entry with the same name but different case, we need to
|
|
//add a virtual group, and then add the existing entry and the new entry to that group
|
|
if (!(existingEntry instanceof VirtualGroupEntry)) {
|
|
if (existingEntry.file.getName().equals(elementName)) {
|
|
if (pathElementsIndex == pathElements.length - 1) {
|
|
return existingEntry.file;
|
|
} else {
|
|
return existingEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
|
|
}
|
|
} else {
|
|
virtualEntry = existingEntry.makeVirtual(file);
|
|
children.replace(elementNameLower, virtualEntry);
|
|
}
|
|
}
|
|
|
|
return virtualEntry.addUniqueChild(pathElements, pathElementsIndex);
|
|
}
|
|
|
|
if (pathElementsIndex == pathElements.length - 1) {
|
|
ClassNameEntry classNameEntry = new ClassNameEntry(file, elementName);
|
|
children.insert(elementNameLower, classNameEntry);
|
|
return classNameEntry.file;
|
|
} else {
|
|
PackageNameEntry packageNameEntry = new PackageNameEntry(file, elementName);
|
|
children.insert(elementNameLower, packageNameEntry);
|
|
return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A virtual group that groups together file system entries with the same name, differing only in case
|
|
*/
|
|
private class VirtualGroupEntry extends FileSystemEntry {
|
|
//this contains the FileSystemEntries for all of the files/directories in this group
|
|
//the key is the unmodified name of the entry, before it is modified to be made unique (if needed).
|
|
private RadixTree<FileSystemEntry> groupEntries = new RadixTreeImpl<FileSystemEntry>();
|
|
|
|
//whether the containing directory is case sensitive or not.
|
|
//-1 = unset
|
|
//0 = false;
|
|
//1 = true;
|
|
private int isCaseSensitive = -1;
|
|
|
|
public VirtualGroupEntry(FileSystemEntry firstChild, File parent) {
|
|
super(parent);
|
|
|
|
//use the name of the first child in the group as-is
|
|
groupEntries.insert(firstChild.file.getName(), firstChild);
|
|
}
|
|
|
|
@Override
|
|
public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
|
|
String elementName = pathElements[pathElementsIndex];
|
|
|
|
if (pathElementsIndex == pathElements.length - 1) {
|
|
elementName = elementName + fileExtension;
|
|
}
|
|
|
|
FileSystemEntry existingEntry = groupEntries.find(elementName);
|
|
if (existingEntry != null) {
|
|
if (pathElementsIndex == pathElements.length - 1) {
|
|
return existingEntry.file;
|
|
} else {
|
|
return existingEntry.addUniqueChild(pathElements, pathElementsIndex+1);
|
|
}
|
|
}
|
|
|
|
|
|
if (pathElementsIndex == pathElements.length - 1) {
|
|
String fileName;
|
|
if (!isCaseSensitive()) {
|
|
fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1) + fileExtension;
|
|
} else {
|
|
fileName = elementName;
|
|
}
|
|
|
|
ClassNameEntry classNameEntry = new ClassNameEntry(file, fileName);
|
|
groupEntries.insert(elementName, classNameEntry);
|
|
return classNameEntry.file;
|
|
} else {
|
|
String fileName;
|
|
if (!isCaseSensitive()) {
|
|
fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1);
|
|
} else {
|
|
fileName = elementName;
|
|
}
|
|
|
|
PackageNameEntry packageNameEntry = new PackageNameEntry(file, fileName);
|
|
groupEntries.insert(elementName, packageNameEntry);
|
|
return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
|
|
}
|
|
}
|
|
|
|
private boolean isCaseSensitive() {
|
|
if (isCaseSensitive != -1) {
|
|
return isCaseSensitive == 1;
|
|
}
|
|
|
|
File path = file;
|
|
|
|
if (path.exists() && path.isFile()) {
|
|
path = path.getParentFile();
|
|
}
|
|
|
|
if ((!file.exists() && !file.mkdirs())) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
boolean result = testCaseSensitivity(path);
|
|
isCaseSensitive = result?1:0;
|
|
return result;
|
|
} catch (IOException ex) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private boolean testCaseSensitivity(File path) throws IOException {
|
|
int num = 1;
|
|
File f, f2;
|
|
do {
|
|
f = new File(path, "test." + num);
|
|
f2 = new File(path, "TEST." + num++);
|
|
} while(f.exists() || f2.exists());
|
|
|
|
try {
|
|
try {
|
|
FileWriter writer = new FileWriter(f);
|
|
writer.write("test");
|
|
writer.flush();
|
|
writer.close();
|
|
} catch (IOException ex) {
|
|
try {f.delete();} catch (Exception ex2) {}
|
|
throw ex;
|
|
}
|
|
|
|
if (f2.exists()) {
|
|
return false;
|
|
}
|
|
|
|
if (f2.createNewFile()) {
|
|
return true;
|
|
}
|
|
|
|
//the above 2 tests should catch almost all cases. But maybe there was a failure while creating f2
|
|
//that isn't related to case sensitivity. Let's see if we can open the file we just created using
|
|
//f2
|
|
try {
|
|
CharBuffer buf = CharBuffer.allocate(32);
|
|
FileReader reader = new FileReader(f2);
|
|
|
|
while (reader.read(buf) != -1 && buf.length() < 4);
|
|
if (buf.length() == 4 && buf.toString().equals("test")) {
|
|
return false;
|
|
} else {
|
|
//we probably shouldn't get here. If the filesystem was case-sensetive, creating a new
|
|
//FileReader should have thrown a FileNotFoundException. Otherwise, we should have opened
|
|
//the file and read in the string "test". It's remotely possible that someone else modified
|
|
//the file after we created it. Let's be safe and return false here as well
|
|
assert(false);
|
|
return false;
|
|
}
|
|
} catch (FileNotFoundException ex) {
|
|
return true;
|
|
}
|
|
} finally {
|
|
try { f.delete(); } catch (Exception ex) {}
|
|
try { f2.delete(); } catch (Exception ex) {}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public FileSystemEntry makeVirtual(File parent) {
|
|
return this;
|
|
}
|
|
}
|
|
|
|
private class ClassNameEntry extends FileSystemEntry {
|
|
public ClassNameEntry(File parent, String name) {
|
|
super(new File(parent, name));
|
|
}
|
|
|
|
@Override
|
|
public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
|
|
assert false;
|
|
return file;
|
|
}
|
|
}
|
|
}
|