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