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 javax.annotation.Nonnull; 35import java.io.*; 36import java.nio.CharBuffer; 37import 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 */ 44public 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 52 public ClassFileNameHandler(File path, String fileExtension) { 53 this.top = new PackageNameEntry(path); 54 this.fileExtension = fileExtension; 55 this.modifyWindowsReservedFilenames = testForWindowsReservedFileNames(path); 56 } 57 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 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 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); 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 161 public FileSystemEntry(File file) { 162 this.file = file; 163 } 164 165 public abstract File addUniqueChild(String[] pathElements, int pathElementsIndex); 166 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 177 public PackageNameEntry(File parent, String name) { 178 super(new File(parent, name)); 179 } 180 181 public PackageNameEntry(File path) { 182 super(path); 183 } 184 185 @Override 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 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 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 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 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 373 public FileSystemEntry makeVirtual(File parent) { 374 return this; 375 } 376 } 377 378 private class ClassNameEntry extends FileSystemEntry { 379 public ClassNameEntry(File parent, String name) { 380 super(new File(parent, name)); 381 } 382 383 @Override 384 public File addUniqueChild(String[] pathElements, int pathElementsIndex) { 385 assert false; 386 return file; 387 } 388 } 389} 390