• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * [The "BSD licence"]
3  * Copyright (c) 2010 Ben Gruver
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  * 3. The name of the author may not be used to endorse or promote products
15  *    derived from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28 
29 package org.jf.util;
30 
31 import ds.tree.RadixTree;
32 import ds.tree.RadixTreeImpl;
33 
34 import javax.annotation.Nonnull;
35 import java.io.*;
36 import java.nio.CharBuffer;
37 import java.util.regex.Pattern;
38 
39 /**
40  * This class checks for case-insensitive file systems, and generates file names based on a given class name, that are
41  * guaranteed to be unique. When "colliding" class names are found, it appends a numeric identifier to the end of the
42  * class name to distinguish it from another class with a name that differes only by case. i.e. a.smali and a_2.smali
43  */
44 public class ClassFileNameHandler {
45     // we leave an extra 10 characters to allow for a numeric suffix to be added, if it's needed
46     private static final int MAX_FILENAME_LENGTH = 245;
47 
48     private PackageNameEntry top;
49     private String fileExtension;
50     private boolean modifyWindowsReservedFilenames;
51 
ClassFileNameHandler(File path, String fileExtension)52     public ClassFileNameHandler(File path, String fileExtension) {
53         this.top = new PackageNameEntry(path);
54         this.fileExtension = fileExtension;
55         this.modifyWindowsReservedFilenames = testForWindowsReservedFileNames(path);
56     }
57 
getUniqueFilenameForClass(String className)58     public File getUniqueFilenameForClass(String className) {
59         //class names should be passed in the normal dalvik style, with a leading L, a trailing ;, and using
60         //'/' as a separator.
61         if (className.charAt(0) != 'L' || className.charAt(className.length()-1) != ';') {
62             throw new RuntimeException("Not a valid dalvik class name");
63         }
64 
65         int packageElementCount = 1;
66         for (int i=1; i<className.length()-1; i++) {
67             if (className.charAt(i) == '/') {
68                 packageElementCount++;
69             }
70         }
71 
72         String packageElement;
73         String[] packageElements = new String[packageElementCount];
74         int elementIndex = 0;
75         int elementStart = 1;
76         for (int i=1; i<className.length()-1; i++) {
77             if (className.charAt(i) == '/') {
78                 //if the first char after the initial L is a '/', or if there are
79                 //two consecutive '/'
80                 if (i-elementStart==0) {
81                     throw new RuntimeException("Not a valid dalvik class name");
82                 }
83 
84                 packageElement = className.substring(elementStart, i);
85 
86                 if (modifyWindowsReservedFilenames && isReservedFileName(packageElement)) {
87                     packageElement += "#";
88                 }
89 
90                 if (packageElement.length() > MAX_FILENAME_LENGTH) {
91                     packageElement = shortenPathComponent(packageElement, MAX_FILENAME_LENGTH);
92                 }
93 
94                 packageElements[elementIndex++] = packageElement;
95                 elementStart = ++i;
96             }
97         }
98 
99         //at this point, we have added all the package elements to packageElements, but still need to add
100         //the final class name. elementStart should point to the beginning of the class name
101 
102         //this will be true if the class ends in a '/', i.e. Lsome/package/className/;
103         if (elementStart >= className.length()-1) {
104             throw new RuntimeException("Not a valid dalvik class name");
105         }
106 
107         packageElement = className.substring(elementStart, className.length()-1);
108         if (modifyWindowsReservedFilenames && isReservedFileName(packageElement)) {
109             packageElement += "#";
110         }
111 
112         if ((packageElement.length() + fileExtension.length()) > MAX_FILENAME_LENGTH) {
113             packageElement = shortenPathComponent(packageElement, MAX_FILENAME_LENGTH - fileExtension.length());
114         }
115 
116         packageElements[elementIndex] = packageElement;
117 
118         return top.addUniqueChild(packageElements, 0);
119     }
120 
121     @Nonnull
shortenPathComponent(@onnull String pathComponent, int maxLength)122     static String shortenPathComponent(@Nonnull String pathComponent, int maxLength) {
123         int toRemove = pathComponent.length() - maxLength + 1;
124 
125         int firstIndex = (pathComponent.length()/2) - (toRemove/2);
126         return pathComponent.substring(0, firstIndex) + "#" + pathComponent.substring(firstIndex+toRemove);
127     }
128 
testForWindowsReservedFileNames(File path)129     private static boolean testForWindowsReservedFileNames(File path) {
130         String[] reservedNames = new String[]{"aux", "con", "com1", "com9", "lpt1", "com9"};
131 
132         for (String reservedName: reservedNames) {
133             File f = new File(path, reservedName + ".smali");
134             if (f.exists()) {
135                 continue;
136             }
137 
138             try {
139                 FileWriter writer = new FileWriter(f);
140                 writer.write("test");
141                 writer.flush();
142                 writer.close();
143                 f.delete(); //doesn't throw IOException
144             } catch (IOException ex) {
145                 //if an exception occurred, it's likely that we're on a windows system.
146                 return true;
147             }
148         }
149         return false;
150     }
151 
152     private static Pattern reservedFileNameRegex = Pattern.compile("^CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]$",
153             Pattern.CASE_INSENSITIVE);
isReservedFileName(String className)154     private static boolean isReservedFileName(String className) {
155         return reservedFileNameRegex.matcher(className).matches();
156     }
157 
158     private abstract class FileSystemEntry {
159         public final File file;
160 
FileSystemEntry(File file)161         public FileSystemEntry(File file) {
162             this.file = file;
163         }
164 
addUniqueChild(String[] pathElements, int pathElementsIndex)165         public abstract File addUniqueChild(String[] pathElements, int pathElementsIndex);
166 
makeVirtual(File parent)167         public FileSystemEntry makeVirtual(File parent) {
168             return new VirtualGroupEntry(this, parent);
169         }
170     }
171 
172     private class PackageNameEntry extends FileSystemEntry {
173         //this contains the FileSystemEntries for all of this package's children
174         //the associated keys are all lowercase
175         private RadixTree<FileSystemEntry> children = new RadixTreeImpl<FileSystemEntry>();
176 
PackageNameEntry(File parent, String name)177         public PackageNameEntry(File parent, String name) {
178             super(new File(parent, name));
179         }
180 
PackageNameEntry(File path)181         public PackageNameEntry(File path) {
182             super(path);
183         }
184 
185         @Override
addUniqueChild(String[] pathElements, int pathElementsIndex)186         public synchronized File addUniqueChild(String[] pathElements, int pathElementsIndex) {
187             String elementName;
188             String elementNameLower;
189 
190             if (pathElementsIndex == pathElements.length - 1) {
191                 elementName = pathElements[pathElementsIndex];
192                 elementName += fileExtension;
193             } else {
194                 elementName = pathElements[pathElementsIndex];
195             }
196             elementNameLower = elementName.toLowerCase();
197 
198             FileSystemEntry existingEntry = children.find(elementNameLower);
199             if (existingEntry != null) {
200                 FileSystemEntry virtualEntry = existingEntry;
201                 //if there is already another entry with the same name but different case, we need to
202                 //add a virtual group, and then add the existing entry and the new entry to that group
203                 if (!(existingEntry instanceof VirtualGroupEntry)) {
204                     if (existingEntry.file.getName().equals(elementName)) {
205                         if (pathElementsIndex == pathElements.length - 1) {
206                             return existingEntry.file;
207                         } else {
208                             return existingEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
209                         }
210                     } else {
211                         virtualEntry = existingEntry.makeVirtual(file);
212                         children.replace(elementNameLower, virtualEntry);
213                     }
214                 }
215 
216                 return virtualEntry.addUniqueChild(pathElements, pathElementsIndex);
217             }
218 
219             if (pathElementsIndex == pathElements.length - 1) {
220                 ClassNameEntry classNameEntry = new ClassNameEntry(file, elementName);
221                 children.insert(elementNameLower, classNameEntry);
222                 return classNameEntry.file;
223             } else {
224                 PackageNameEntry packageNameEntry = new PackageNameEntry(file, elementName);
225                 children.insert(elementNameLower, packageNameEntry);
226                 return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
227             }
228         }
229     }
230 
231     /**
232      * A virtual group that groups together file system entries with the same name, differing only in case
233      */
234     private class VirtualGroupEntry extends FileSystemEntry {
235         //this contains the FileSystemEntries for all of the files/directories in this group
236         //the key is the unmodified name of the entry, before it is modified to be made unique (if needed).
237         private RadixTree<FileSystemEntry> groupEntries = new RadixTreeImpl<FileSystemEntry>();
238 
239         //whether the containing directory is case sensitive or not.
240         //-1 = unset
241         //0 = false;
242         //1 = true;
243         private int isCaseSensitive = -1;
244 
VirtualGroupEntry(FileSystemEntry firstChild, File parent)245         public VirtualGroupEntry(FileSystemEntry firstChild, File parent) {
246             super(parent);
247 
248             //use the name of the first child in the group as-is
249             groupEntries.insert(firstChild.file.getName(), firstChild);
250         }
251 
252         @Override
addUniqueChild(String[] pathElements, int pathElementsIndex)253         public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
254             String elementName = pathElements[pathElementsIndex];
255 
256             if (pathElementsIndex == pathElements.length - 1) {
257                 elementName = elementName + fileExtension;
258             }
259 
260             FileSystemEntry existingEntry = groupEntries.find(elementName);
261             if (existingEntry != null) {
262                 if (pathElementsIndex == pathElements.length - 1) {
263                     return existingEntry.file;
264                 } else {
265                     return existingEntry.addUniqueChild(pathElements, pathElementsIndex+1);
266                 }
267             }
268 
269             if (pathElementsIndex == pathElements.length - 1) {
270                 String fileName;
271                 if (!isCaseSensitive()) {
272                     fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1) + fileExtension;
273                 } else {
274                     fileName = elementName;
275                 }
276 
277                 ClassNameEntry classNameEntry = new ClassNameEntry(file, fileName);
278                 groupEntries.insert(elementName, classNameEntry);
279                 return classNameEntry.file;
280             } else {
281                 String fileName;
282                 if (!isCaseSensitive()) {
283                     fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1);
284                 } else {
285                     fileName = elementName;
286                 }
287 
288                 PackageNameEntry packageNameEntry = new PackageNameEntry(file, fileName);
289                 groupEntries.insert(elementName, packageNameEntry);
290                 return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
291             }
292         }
293 
isCaseSensitive()294         private boolean isCaseSensitive() {
295             if (isCaseSensitive != -1) {
296                 return isCaseSensitive == 1;
297             }
298 
299             File path = file;
300 
301             if (path.exists() && path.isFile()) {
302                 path = path.getParentFile();
303             }
304 
305             if ((!file.exists() && !file.mkdirs())) {
306                 return false;
307             }
308 
309             try {
310                 boolean result = testCaseSensitivity(path);
311                 isCaseSensitive = result?1:0;
312                 return result;
313             } catch (IOException ex) {
314                 return false;
315             }
316         }
317 
testCaseSensitivity(File path)318         private boolean testCaseSensitivity(File path) throws IOException {
319             int num = 1;
320             File f, f2;
321             do {
322                 f = new File(path, "test." + num);
323                 f2 = new File(path, "TEST." + num++);
324             } while(f.exists() || f2.exists());
325 
326             try {
327                 try {
328                     FileWriter writer = new FileWriter(f);
329                     writer.write("test");
330                     writer.flush();
331                     writer.close();
332                 } catch (IOException ex) {
333                     try {f.delete();} catch (Exception ex2) {}
334                     throw ex;
335                 }
336 
337                 if (f2.exists()) {
338                     return false;
339                 }
340 
341                 if (f2.createNewFile()) {
342                     return true;
343                 }
344 
345                 //the above 2 tests should catch almost all cases. But maybe there was a failure while creating f2
346                 //that isn't related to case sensitivity. Let's see if we can open the file we just created using
347                 //f2
348                 try {
349                     CharBuffer buf = CharBuffer.allocate(32);
350                     FileReader reader = new FileReader(f2);
351 
352                     while (reader.read(buf) != -1 && buf.length() < 4);
353                     if (buf.length() == 4 && buf.toString().equals("test")) {
354                         return false;
355                     } else {
356                         //we probably shouldn't get here. If the filesystem was case-sensetive, creating a new
357                         //FileReader should have thrown a FileNotFoundException. Otherwise, we should have opened
358                         //the file and read in the string "test". It's remotely possible that someone else modified
359                         //the file after we created it. Let's be safe and return false here as well
360                         assert(false);
361                         return false;
362                     }
363                 } catch (FileNotFoundException ex) {
364                     return true;
365                 }
366             } finally {
367                 try { f.delete(); } catch (Exception ex) {}
368                 try { f2.delete(); } catch (Exception ex) {}
369             }
370         }
371 
372         @Override
makeVirtual(File parent)373         public FileSystemEntry makeVirtual(File parent) {
374             return this;
375         }
376     }
377 
378     private class ClassNameEntry extends FileSystemEntry {
ClassNameEntry(File parent, String name)379         public ClassNameEntry(File parent, String name) {
380             super(new File(parent, name));
381         }
382 
383         @Override
addUniqueChild(String[] pathElements, int pathElementsIndex)384         public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
385             assert false;
386             return file;
387         }
388     }
389 }
390