1 package org.robolectric.res; 2 3 import static java.util.Arrays.asList; 4 5 import java.io.BufferedInputStream; 6 import java.io.File; 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.net.MalformedURLException; 10 import java.net.URI; 11 import java.net.URL; 12 import java.util.ArrayList; 13 import java.util.Enumeration; 14 import java.util.LinkedHashMap; 15 import java.util.List; 16 import java.util.Map; 17 import java.util.NavigableMap; 18 import java.util.NavigableSet; 19 import java.util.TreeMap; 20 import java.util.jar.JarEntry; 21 import java.util.jar.JarFile; 22 import org.robolectric.util.Join; 23 import org.robolectric.util.Util; 24 25 abstract public class Fs { fromJar(URL url)26 public static Fs fromJar(URL url) { 27 return new JarFs(new File(fixFileURL(url).getPath())); 28 } 29 fixFileURL(URL u)30 private static URI fixFileURL(URL u) { 31 if (!"file".equals(u.getProtocol())) { 32 throw new IllegalArgumentException(); 33 } 34 return new File(u.getPath()).toURI(); 35 } 36 37 /** 38 * @deprecated Use {@link #fromURL(URL)} instead. 39 */ 40 @Deprecated fileFromPath(String urlString)41 public static FsFile fileFromPath(String urlString) { 42 if (urlString.startsWith("jar:")) { 43 String[] parts = urlString.replaceFirst("jar:", "").split("!", 0); 44 Fs fs = new JarFs(new File(parts[0])); 45 return fs.join(parts[1].substring(1)); 46 } else { 47 return new FileFsFile(new File(urlString)); 48 } 49 } 50 fromURL(URL url)51 public static FsFile fromURL(URL url) { 52 switch (url.getProtocol()) { 53 case "file": 54 return new FileFsFile(new File(url.getPath())); 55 case "jar": 56 String[] parts = url.getPath().split("!", 0); 57 try { 58 Fs fs = fromJar(new URL(parts[0])); 59 return fs.join(parts[1].substring(1)); 60 } catch (MalformedURLException e) { 61 throw new IllegalArgumentException(e); 62 } 63 default: 64 throw new IllegalArgumentException("unsupported fs type for '" + url + "'"); 65 } 66 } 67 newFile(File file)68 public static FsFile newFile(File file) { 69 return new FileFsFile(file); 70 } 71 newJarFile(File file)72 public static FsFile newJarFile(File file) { 73 JarFs jarFs = new JarFs(file); 74 return jarFs.new JarFsFile(""); 75 } 76 newFile(String filePath)77 public static FsFile newFile(String filePath) { 78 return new FileFsFile(filePath); 79 } 80 currentDirectory()81 public static FsFile currentDirectory() { 82 return newFile(new File(".")); 83 } 84 85 static class JarFs extends Fs { 86 private static final Map<File, NavigableMap<String, JarEntry>> CACHE = 87 new LinkedHashMap<File, NavigableMap<String, JarEntry>>() { 88 @Override 89 protected boolean removeEldestEntry(Map.Entry<File, NavigableMap<String, JarEntry>> fileNavigableMapEntry) { 90 return size() > 10; 91 } 92 }; 93 94 private final JarFile jarFile; 95 private final NavigableMap<String, JarEntry> jarEntryMap; 96 JarFs(File file)97 public JarFs(File file) { 98 try { 99 jarFile = new JarFile(file); 100 } catch (IOException e) { 101 throw new RuntimeException(e); 102 } 103 104 NavigableMap<String, JarEntry> cachedMap; 105 synchronized (CACHE) { 106 cachedMap = CACHE.get(file.getAbsoluteFile()); 107 } 108 109 if (cachedMap == null) { 110 cachedMap = new TreeMap<>(); 111 Enumeration<JarEntry> entries = jarFile.entries(); 112 while (entries.hasMoreElements()) { 113 JarEntry jarEntry = entries.nextElement(); 114 115 // Add entries for any parent directories that did not have 116 // a JarEntry in the jar file. 117 String name = jarEntry.getName(); 118 int index = name.length(); 119 while ((index = name.lastIndexOf('/', index - 1)) != -1) { 120 String dir = name.substring(0, index+1); 121 if (!cachedMap.containsKey(dir)) { 122 cachedMap.put(dir, new JarEntry(dir)); 123 } 124 } 125 126 cachedMap.put(jarEntry.getName(), jarEntry); 127 } 128 synchronized (CACHE) { 129 CACHE.put(file.getAbsoluteFile(), cachedMap); 130 } 131 } 132 133 jarEntryMap = cachedMap; 134 } 135 join(String folderBaseName)136 @Override public FsFile join(String folderBaseName) { 137 return new JarFsFile(folderBaseName); 138 } 139 140 class JarFsFile implements FsFile { 141 private final String path; 142 JarFsFile(String path)143 public JarFsFile(String path) { 144 this.path = path.replaceAll("^/+", ""); 145 } 146 exists()147 @Override public boolean exists() { 148 return isFile() || isDirectory(); 149 } 150 isDirectory()151 @Override public boolean isDirectory() { 152 return jarEntryMap.containsKey(path + "/"); 153 } 154 isFile()155 @Override public boolean isFile() { 156 return jarEntryMap.containsKey(path); 157 } 158 listFiles()159 @Override public FsFile[] listFiles() { 160 return listFiles(fsFile -> true); 161 } 162 listFiles(Filter filter)163 @Override public FsFile[] listFiles(Filter filter) { 164 NavigableSet<String> strings = jarEntryMap.navigableKeySet(); 165 int startOfFilename = 0; 166 167 if (!path.equals("")) { 168 if (!isDirectory()) { 169 return null; 170 } 171 172 strings = strings.subSet(path + "/", false, path + "0", false); 173 startOfFilename = path.length() + 2; 174 } 175 176 List<FsFile> fsFiles = new ArrayList<>(); 177 for (String string : strings) { 178 int nextSlash = string.indexOf('/', startOfFilename); 179 FsFile fsFile; 180 if (nextSlash == string.length() - 1) { 181 // directory entry 182 fsFile = new JarFsFile(string.substring(0, string.length() - 1)); 183 } else if (nextSlash == -1) { 184 // file entry 185 fsFile = new JarFsFile(string); 186 } else { 187 // file within a nested directory, ignore 188 fsFile = null; 189 } 190 191 if (fsFile != null && filter.accept(fsFile)) { 192 fsFiles.add(fsFile); 193 } 194 } 195 return fsFiles.toArray(new FsFile[fsFiles.size()]); 196 } 197 listFileNames()198 @Override public String[] listFileNames() { 199 List<String> fileNames = new ArrayList<>(); 200 for (FsFile fsFile : listFiles()) { 201 fileNames.add(fsFile.getName()); 202 } 203 return fileNames.toArray(new String[fileNames.size()]); 204 } 205 getParent()206 @Override public FsFile getParent() { 207 int index = path.lastIndexOf('/'); 208 String parent = index != -1 ? path.substring(0, index) : ""; 209 return new JarFsFile(parent); 210 } 211 getName()212 @Override public String getName() { 213 int index = path.lastIndexOf('/'); 214 return index != -1 ? path.substring(index + 1, path.length()) : path; 215 } 216 getInputStream()217 @Override public InputStream getInputStream() throws IOException { 218 return new BufferedInputStream(jarFile.getInputStream(jarEntryMap.get(path))); 219 } 220 getBytes()221 @Override public byte[] getBytes() throws IOException { 222 return Util.readBytes(jarFile.getInputStream(jarEntryMap.get(path))); 223 } 224 join(String... pathParts)225 @Override public FsFile join(String... pathParts) { 226 return new JarFsFile(path + "/" + Join.join("/", asList(pathParts))); 227 } 228 getBaseName()229 @Override public String getBaseName() { 230 String name = getName(); 231 int dotIndex = name.indexOf("."); 232 return dotIndex >= 0 ? name.substring(0, dotIndex) : name; 233 } 234 getPath()235 @Override public String getPath() { 236 return "jar:file:" + getJarFileName() + "!/" + path; 237 } 238 239 @Override length()240 public long length() { 241 return jarFile.getEntry(path).getSize(); 242 } 243 244 @Override equals(Object o)245 public boolean equals(Object o) { 246 if (this == o) return true; 247 if (o == null || getClass() != o.getClass()) return false; 248 249 JarFsFile jarFsFile = (JarFsFile) o; 250 251 if (!getJarFileName().equals(jarFsFile.getJarFileName())) return false; 252 if (!path.equals(jarFsFile.path)) return false; 253 254 return true; 255 } 256 getJarFileName()257 private String getJarFileName() { 258 return jarFile.getName(); 259 } 260 261 @Override hashCode()262 public int hashCode() { 263 return getJarFileName().hashCode() * 31 + path.hashCode(); 264 } 265 toString()266 @Override public String toString() { 267 return getPath(); 268 } 269 } 270 } 271 join(String folderBaseName)272 abstract public FsFile join(String folderBaseName); 273 } 274