ClassFileNameHandler.java revision 06bc17a75e0e2d100d60c8f7f08de21630fa9606
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        }
125
126        //let's try one more reserved filename
127        f = new File(path, "con.smali");
128        if (f.exists()) {
129            return false;
130        }
131
132        try {
133            FileWriter writer = new FileWriter(f);
134            writer.write("test");
135            writer.flush();
136            writer.close();
137            f.delete(); //doesn't throw IOException
138            return false;
139        } catch (IOException ex) {
140            //yup, looks like we're on a windows system
141            return true;
142        }
143    }
144
145    private static Pattern reservedFileNameRegex = Pattern.compile("^CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]$",
146            Pattern.CASE_INSENSITIVE);
147    private static boolean isReservedFileName(String className) {
148        return reservedFileNameRegex.matcher(className).matches();
149    }
150
151    private abstract class FileSystemEntry {
152        public final File file;
153
154        public FileSystemEntry(File file) {
155            this.file = file;
156        }
157
158        public abstract File addUniqueChild(String[] pathElements, int pathElementsIndex);
159
160        public FileSystemEntry makeVirtual(File parent) {
161            return new VirtualGroupEntry(this, parent);
162        }
163    }
164
165    private class PackageNameEntry extends FileSystemEntry {
166        //this contains the FileSystemEntries for all of this package's children
167        //the associated keys are all lowercase
168        private RadixTree<FileSystemEntry> children = new RadixTreeImpl<FileSystemEntry>();
169
170        public PackageNameEntry(File parent, String name) {
171            super(new File(parent, name));
172        }
173
174        public PackageNameEntry(File path) {
175            super(path);
176        }
177
178        @Override
179        public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
180            String elementName;
181            String elementNameLower;
182
183            if (pathElementsIndex == pathElements.length - 1) {
184                elementName = pathElements[pathElementsIndex];
185                elementName += fileExtension;
186            } else {
187                elementName = pathElements[pathElementsIndex];
188            }
189            elementNameLower = elementName.toLowerCase();
190
191            FileSystemEntry existingEntry = children.find(elementNameLower);
192            if (existingEntry != null) {
193                FileSystemEntry virtualEntry = existingEntry;
194                //if there is already another entry with the same name but different case, we need to
195                //add a virtual group, and then add the existing entry and the new entry to that group
196                if (!(existingEntry instanceof VirtualGroupEntry)) {
197                    if (existingEntry.file.getName().equals(elementName)) {
198                        if (pathElementsIndex == pathElements.length - 1) {
199                            return existingEntry.file;
200                        } else {
201                            return existingEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
202                        }
203                    } else {
204                        virtualEntry = existingEntry.makeVirtual(file);
205                        children.replace(elementNameLower, virtualEntry);
206                    }
207                }
208
209                return virtualEntry.addUniqueChild(pathElements, pathElementsIndex);
210            }
211
212            if (pathElementsIndex == pathElements.length - 1) {
213                ClassNameEntry classNameEntry = new ClassNameEntry(file, elementName);
214                children.insert(elementNameLower, classNameEntry);
215                return classNameEntry.file;
216            } else {
217                PackageNameEntry packageNameEntry = new PackageNameEntry(file, elementName);
218                children.insert(elementNameLower, packageNameEntry);
219                return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
220            }
221        }
222    }
223
224    /**
225     * A virtual group that groups together file system entries with the same name, differing only in case
226     */
227    private class VirtualGroupEntry extends FileSystemEntry {
228        //this contains the FileSystemEntries for all of the files/directories in this group
229        //the key is the unmodified name of the entry, before it is modified to be made unique (if needed).
230        private RadixTree<FileSystemEntry> groupEntries = new RadixTreeImpl<FileSystemEntry>();
231
232        //whether the containing directory is case sensitive or not.
233        //-1 = unset
234        //0 = false;
235        //1 = true;
236        private int isCaseSensitive = -1;
237
238        public VirtualGroupEntry(FileSystemEntry firstChild, File parent) {
239            super(parent);
240
241            //use the name of the first child in the group as-is
242            groupEntries.insert(firstChild.file.getName(), firstChild);
243        }
244
245        @Override
246        public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
247            String elementName = pathElements[pathElementsIndex];
248
249            if (pathElementsIndex == pathElements.length - 1) {
250                elementName = elementName + fileExtension;
251            }
252
253            FileSystemEntry existingEntry = groupEntries.find(elementName);
254            if (existingEntry != null) {
255                if (pathElementsIndex == pathElements.length - 1) {
256                    return existingEntry.file;
257                } else {
258                    return existingEntry.addUniqueChild(pathElements, pathElementsIndex+1);
259                }
260            }
261
262
263            if (pathElementsIndex == pathElements.length - 1) {
264                String fileName;
265                if (!isCaseSensitive()) {
266                    fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1) + fileExtension;
267                } else {
268                    fileName = elementName;
269                }
270
271                ClassNameEntry classNameEntry = new ClassNameEntry(file, fileName);
272                groupEntries.insert(elementName, classNameEntry);
273                return classNameEntry.file;
274            } else {
275                String fileName;
276                if (!isCaseSensitive()) {
277                    fileName = pathElements[pathElementsIndex] + "." + (groupEntries.getSize()+1);
278                } else {
279                    fileName = elementName;
280                }
281
282                PackageNameEntry packageNameEntry = new PackageNameEntry(file, fileName);
283                groupEntries.insert(elementName, packageNameEntry);
284                return packageNameEntry.addUniqueChild(pathElements, pathElementsIndex + 1);
285            }
286        }
287
288        private boolean isCaseSensitive() {
289            if (isCaseSensitive != -1) {
290                return isCaseSensitive == 1;
291            }
292
293            File path = file;
294
295            if (path.exists() && path.isFile()) {
296                path = path.getParentFile();
297            }
298
299            if ((!file.exists() && !file.mkdirs())) {
300                return false;
301            }
302
303            try {
304                boolean result = testCaseSensitivity(path);
305                isCaseSensitive = result?1:0;
306                return result;
307            } catch (IOException ex) {
308                return false;
309            }
310        }
311
312        private boolean testCaseSensitivity(File path) throws IOException {
313            int num = 1;
314            File f, f2;
315            do {
316                f = new File(path, "test." + num);
317                f2 = new File(path, "TEST." + num++);
318            } while(f.exists() || f2.exists());
319
320            try {
321                try {
322                    FileWriter writer = new FileWriter(f);
323                    writer.write("test");
324                    writer.flush();
325                    writer.close();
326                } catch (IOException ex) {
327                    try {f.delete();} catch (Exception ex2) {}
328                    throw ex;
329                }
330
331                if (f2.exists()) {
332                    return false;
333                }
334
335                if (f2.createNewFile()) {
336                    return true;
337                }
338
339                //the above 2 tests should catch almost all cases. But maybe there was a failure while creating f2
340                //that isn't related to case sensitivity. Let's see if we can open the file we just created using
341                //f2
342                try {
343                    CharBuffer buf = CharBuffer.allocate(32);
344                    FileReader reader = new FileReader(f2);
345
346                    while (reader.read(buf) != -1 && buf.length() < 4);
347                    if (buf.length() == 4 && buf.toString().equals("test")) {
348                        return false;
349                    } else {
350                        //we probably shouldn't get here. If the filesystem was case-sensetive, creating a new
351                        //FileReader should have thrown a FileNotFoundException. Otherwise, we should have opened
352                        //the file and read in the string "test". It's remotely possible that someone else modified
353                        //the file after we created it. Let's be safe and return false here as well
354                        assert(false);
355                        return false;
356                    }
357                } catch (FileNotFoundException ex) {
358                    return true;
359                }
360            } finally {
361                try { f.delete(); } catch (Exception ex) {}
362                try { f2.delete(); } catch (Exception ex) {}
363            }
364        }
365
366        @Override
367        public FileSystemEntry makeVirtual(File parent) {
368            return this;
369        }
370    }
371
372    private class ClassNameEntry extends FileSystemEntry {
373        public ClassNameEntry(File parent, String name) {
374            super(new File(parent, name));
375        }
376
377        @Override
378        public File addUniqueChild(String[] pathElements, int pathElementsIndex) {
379            assert false;
380            return file;
381        }
382    }
383}
384