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