ClassFileNameHandler.java revision 4374e7babc6c25968f532a352e6999e9f21dcf8d
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
29package org.jf.util;
30
31import ds.tree.RadixTree;
32import ds.tree.RadixTreeImpl;
33
34import java.io.*;
35import java.nio.CharBuffer;
36import java.util.regex.Pattern;
37
38/**
39 * This class checks for case-insensitive file systems, and generates file names based on a given class name, that are
40 * guaranteed to be unique. When "colliding" class names are found, it appends a numeric identifier to the end of the
41 * class name to distinguish it from another class with a name that differes only by case. i.e. a.smali and a_2.smali
42 */
43public class ClassFileNameHandler {
44    private PackageNameEntry top;
45    private String fileExtension;
46    private boolean modifyWindowsReservedFilenames;
47
48    public ClassFileNameHandler(File path, String fileExtension) {
49        this.top = new PackageNameEntry(path);
50        this.fileExtension = fileExtension;
51        this.modifyWindowsReservedFilenames = testForWindowsReservedFileNames(path);
52    }
53
54    public File getUniqueFilenameForClass(String className) {
55        //class names should be passed in the normal dalvik style, with a leading L, a trailing ;, and using
56        //'/' as a separator.
57        if (className.charAt(0) != 'L' || className.charAt(className.length()-1) != ';') {
58            throw new RuntimeException("Not a valid dalvik class name");
59        }
60
61        int packageElementCount = 1;
62        for (int i=1; i<className.length()-1; i++) {
63            if (className.charAt(i) == '/') {
64                packageElementCount++;
65            }
66        }
67
68        String packageElement;
69        String[] packageElements = new String[packageElementCount];
70        int elementIndex = 0;
71        int elementStart = 1;
72        for (int i=1; i<className.length()-1; i++) {
73            if (className.charAt(i) == '/') {
74                //if the first char after the initial L is a '/', or if there are
75                //two consecutive '/'
76                if (i-elementStart==0) {
77                    throw new RuntimeException("Not a valid dalvik class name");
78                }
79
80                packageElement = className.substring(elementStart, i);
81
82                if (modifyWindowsReservedFilenames && isReservedFileName(packageElement)) {
83                    packageElement += "#";
84                }
85
86                packageElements[elementIndex++] = packageElement;
87                elementStart = ++i;
88            }
89        }
90
91        //at this point, we have added all the package elements to packageElements, but still need to add
92        //the final class name. elementStart should point to the beginning of the class name
93
94        //this will be true if the class ends in a '/', i.e. Lsome/package/className/;
95        if (elementStart >= className.length()-1) {
96            throw new RuntimeException("Not a valid dalvik class name");
97        }
98
99        packageElement = className.substring(elementStart, className.length()-1);
100        if (modifyWindowsReservedFilenames && isReservedFileName(packageElement)) {
101            packageElement += "#";
102        }
103
104        packageElements[elementIndex] = packageElement;
105
106        return top.addUniqueChild(packageElements, 0);
107    }
108
109    private static boolean testForWindowsReservedFileNames(File path) {
110        File f = new File(path, "aux.smali");
111        if (f.exists()) {
112            return false;
113        }
114
115        try {
116            FileWriter writer = new FileWriter(f);
117            writer.write("test");
118            writer.flush();
119            writer.close();
120            f.delete(); //doesn't throw IOException
121            return false;
122        } catch (IOException ex) {
123            //if an exception occured, it's likely that we're on a windows system.
124            return true;
125        }
126    }
127
128    private static Pattern reservedFileNameRegex = Pattern.compile("^CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]$",
129            Pattern.CASE_INSENSITIVE);
130    private static boolean isReservedFileName(String className) {
131        return reservedFileNameRegex.matcher(className).matches();
132    }
133
134    private abstract class FileSystemEntry {
135        public final File file;
136
137        public FileSystemEntry(File file) {
138            this.file = file;
139        }
140
141        public abstract File addUniqueChild(String[] pathElements, int pathElementsIndex);
142
143        public FileSystemEntry makeVirtual(File parent) {
144            return new VirtualGroupEntry(this, parent);
145        }
146    }
147
148    private class PackageNameEntry extends FileSystemEntry {
149        //this contains the FileSystemEntries for all of this package's children
150        //the associated keys are all lowercase
151        private RadixTree<FileSystemEntry> children = new RadixTreeImpl<FileSystemEntry>();
152
153        public PackageNameEntry(File parent, String name) {
154            super(new File(parent, name));
155        }
156
157        public PackageNameEntry(File path) {
158            super(path);
159        }
160
161        @Override
162        public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
163            String elementName;
164            String elementNameLower;
165
166            if (pathElementsIndex == pathElements.length - 1) {
167                elementName = pathElements[pathElementsIndex];
168                elementName += fileExtension;
169            } else {
170                elementName = pathElements[pathElementsIndex];
171            }
172            elementNameLower = elementName.toLowerCase();
173
174            FileSystemEntry existingEntry = children.find(elementNameLower);
175            if (existingEntry != null) {
176                FileSystemEntry virtualEntry = existingEntry;
177                //if there is already another entry with the same name but different case, we need to
178                //add a virtual group, and then add the existing entry and the new entry to that group
179                if (!(existingEntry instanceof VirtualGroupEntry)) {
180                    if (existingEntry.file.getName().equals(elementName)) {
181                        if (pathElementsIndex == pathElements.length - 1) {
182                            return existingEntry.file;
183                        } else {
184                            return existingEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
185                        }
186                    } else {
187                        virtualEntry = existingEntry.makeVirtual(file);
188                        children.replace(elementNameLower, virtualEntry);
189                    }
190                }
191
192                return virtualEntry.addUniqueChild(pathElements, pathElementsIndex);
193            }
194
195            if (pathElementsIndex == pathElements.length - 1) {
196                ClassNameEntry classNameEntry = new ClassNameEntry(file, elementName);
197                children.insert(elementNameLower, classNameEntry);
198                return classNameEntry.file;
199            } else {
200                PackageNameEntry packageNameEntry = new PackageNameEntry(file, elementName);
201                children.insert(elementNameLower, packageNameEntry);
202                return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
203            }
204        }
205    }
206
207    /**
208     * A virtual group that groups together file system entries with the same name, differing only in case
209     */
210    private class VirtualGroupEntry extends FileSystemEntry {
211        //this contains the FileSystemEntries for all of the files/directories in this group
212        //the key is the unmodified name of the entry, before it is modified to be made unique (if needed).
213        private RadixTree<FileSystemEntry> groupEntries = new RadixTreeImpl<FileSystemEntry>();
214
215        //whether the containing directory is case sensitive or not.
216        //-1 = unset
217        //0 = false;
218        //1 = true;
219        private int isCaseSensitive = -1;
220
221        public VirtualGroupEntry(FileSystemEntry firstChild, File parent) {
222            super(parent);
223
224            //use the name of the first child in the group as-is
225            groupEntries.insert(firstChild.file.getName(), firstChild);
226        }
227
228        @Override
229        public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
230            String elementName = pathElements[pathElementsIndex];
231
232            if (pathElementsIndex == pathElements.length - 1) {
233                elementName = elementName + fileExtension;
234            }
235
236            FileSystemEntry existingEntry = groupEntries.find(elementName);
237            if (existingEntry != null) {
238                if (pathElementsIndex == pathElements.length - 1) {
239                    return existingEntry.file;
240                } else {
241                    return existingEntry.addUniqueChild(pathElements, pathElementsIndex+1);
242                }
243            }
244
245
246            if (pathElementsIndex == pathElements.length - 1) {
247                String fileName;
248                if (!isCaseSensitive()) {
249                    fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1) + fileExtension;
250                } else {
251                    fileName = elementName;
252                }
253
254                ClassNameEntry classNameEntry = new ClassNameEntry(file, fileName);
255                groupEntries.insert(elementName, classNameEntry);
256                return classNameEntry.file;
257            } else {
258                String fileName;
259                if (!isCaseSensitive()) {
260                    fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1);
261                } else {
262                    fileName = elementName;
263                }
264
265                PackageNameEntry packageNameEntry = new PackageNameEntry(file, fileName);
266                groupEntries.insert(elementName, packageNameEntry);
267                return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
268            }
269        }
270
271        private boolean isCaseSensitive() {
272            if (isCaseSensitive != -1) {
273                return isCaseSensitive == 1;
274            }
275
276            File path = file;
277
278            if (path.exists() && path.isFile()) {
279                path = path.getParentFile();
280            }
281
282            if ((!file.exists() && !file.mkdirs())) {
283                return false;
284            }
285
286            try {
287                boolean result = testCaseSensitivity(path);
288                isCaseSensitive = result?1:0;
289                return result;
290            } catch (IOException ex) {
291                return false;
292            }
293        }
294
295        private boolean testCaseSensitivity(File path) throws IOException {
296            int num = 1;
297            File f, f2;
298            do {
299                f = new File(path, "test." + num);
300                f2 = new File(path, "TEST." + num++);
301            } while(f.exists() || f2.exists());
302
303            try {
304                try {
305                    FileWriter writer = new FileWriter(f);
306                    writer.write("test");
307                    writer.flush();
308                    writer.close();
309                } catch (IOException ex) {
310                    try {f.delete();} catch (Exception ex2) {}
311                    throw ex;
312                }
313
314                if (f2.exists()) {
315                    return false;
316                }
317
318                if (f2.createNewFile()) {
319                    return true;
320                }
321
322                //the above 2 tests should catch almost all cases. But maybe there was a failure while creating f2
323                //that isn't related to case sensitivity. Let's see if we can open the file we just created using
324                //f2
325                try {
326                    CharBuffer buf = CharBuffer.allocate(32);
327                    FileReader reader = new FileReader(f2);
328
329                    while (reader.read(buf) != -1 && buf.length() < 4);
330                    if (buf.length() == 4 && buf.toString().equals("test")) {
331                        return false;
332                    } else {
333                        //we probably shouldn't get here. If the filesystem was case-sensetive, creating a new
334                        //FileReader should have thrown a FileNotFoundException. Otherwise, we should have opened
335                        //the file and read in the string "test". It's remotely possible that someone else modified
336                        //the file after we created it. Let's be safe and return false here as well
337                        assert(false);
338                        return false;
339                    }
340                } catch (FileNotFoundException ex) {
341                    return true;
342                }
343            } finally {
344                try { f.delete(); } catch (Exception ex) {}
345                try { f2.delete(); } catch (Exception ex) {}
346            }
347        }
348
349        @Override
350        public FileSystemEntry makeVirtual(File parent) {
351            return this;
352        }
353    }
354
355    private class ClassNameEntry extends FileSystemEntry {
356        public ClassNameEntry(File parent, String name) {
357            super(new File(parent, name));
358        }
359
360        @Override
361        public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
362            assert false;
363            return file;
364        }
365    }
366}
367