1 package org.robolectric.res; 2 3 import com.google.errorprone.annotations.InlineMe; 4 import java.io.BufferedInputStream; 5 import java.io.File; 6 import java.io.FileInputStream; 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.net.MalformedURLException; 10 import java.net.URI; 11 import java.net.URISyntaxException; 12 import java.net.URL; 13 import java.nio.file.FileStore; 14 import java.nio.file.FileSystem; 15 import java.nio.file.FileSystems; 16 import java.nio.file.Files; 17 import java.nio.file.Path; 18 import java.nio.file.PathMatcher; 19 import java.nio.file.Paths; 20 import java.nio.file.WatchService; 21 import java.nio.file.attribute.UserPrincipalLookupService; 22 import java.nio.file.spi.FileSystemProvider; 23 import java.util.HashMap; 24 import java.util.Map; 25 import java.util.Set; 26 import java.util.function.Predicate; 27 import java.util.stream.Stream; 28 import javax.annotation.concurrent.GuardedBy; 29 import org.robolectric.util.Util; 30 31 @SuppressWarnings({"NewApi", "AndroidJdkLibsChecker"}) 32 public abstract class Fs { 33 34 @GuardedBy("ZIP_FILESYSTEMS") 35 private static final Map<Path, FsWrapper> ZIP_FILESYSTEMS = new HashMap<>(); 36 37 /** 38 * @deprecated Use {@link File#toPath()} instead. 39 */ 40 @Deprecated 41 @InlineMe(replacement = "file.toPath()") newFile(File file)42 public static Path newFile(File file) { 43 return file.toPath(); 44 } 45 46 /** 47 * @deprecated Use {@link #fromUrl(String)} instead. 48 */ 49 @Deprecated 50 @InlineMe(replacement = "Fs.fromUrl(path)", imports = "org.robolectric.res.Fs") fileFromPath(String path)51 public static Path fileFromPath(String path) { 52 return Fs.fromUrl(path); 53 } 54 forJar(URL url)55 public static FileSystem forJar(URL url) { 56 return forJar(Paths.get(toUri(url))); 57 } 58 forJar(Path jarFile)59 public static FileSystem forJar(Path jarFile) { 60 try { 61 return getJarFs(jarFile); 62 } catch (IOException e) { 63 throw new RuntimeException(e); 64 } 65 } 66 67 /** 68 * Use this method instead of {@link Paths#get(String, String...)} or {@link Paths#get(URI)}. 69 * 70 * <p>Supports "file:path", "jar:file:jarfile.jar!/path", and plain old paths. 71 * 72 * <p>For JAR files, automatically open and cache filesystems. 73 */ fromUrl(String urlString)74 public static Path fromUrl(String urlString) { 75 if (urlString.startsWith("file:") || urlString.startsWith("jar:")) { 76 URL url; 77 try { 78 url = new URL(urlString); 79 } catch (MalformedURLException e) { 80 throw new RuntimeException("Failed to resolve path from " + urlString, e); 81 } 82 return fromUrl(url); 83 } else { 84 return Paths.get(urlString); 85 } 86 } 87 88 /** Isn't this what {@link Paths#get(URI)} should do? */ fromUrl(URL url)89 public static Path fromUrl(URL url) { 90 try { 91 switch (url.getProtocol()) { 92 case "file": 93 return Paths.get(url.toURI()); 94 case "jar": 95 String[] parts = url.getPath().split("!", 0); 96 Path jarFile = Paths.get(new URI(parts[0]).toURL().getFile()); 97 FileSystem fs = getJarFs(jarFile); 98 return fs.getPath(parts[1].substring(1)); 99 default: 100 throw new IllegalArgumentException("unsupported fs type for '" + url + "'"); 101 } 102 } catch (Exception e) { 103 throw new RuntimeException("Failed to resolve path from " + url, e); 104 } 105 } 106 toUri(URL url)107 public static URI toUri(URL url) { 108 try { 109 return url.toURI(); 110 } catch (URISyntaxException e) { 111 throw new IllegalArgumentException("invalid URL: " + url, e); 112 } 113 } 114 baseNameFor(Path path)115 static String baseNameFor(Path path) { 116 String name = path.getFileName().toString(); 117 int dotIndex = name.indexOf("."); 118 return dotIndex >= 0 ? name.substring(0, dotIndex) : name; 119 } 120 getInputStream(Path path)121 public static InputStream getInputStream(Path path) throws IOException { 122 // otherwise we get ClosedByInterruptException, meh 123 if (path.toUri().getScheme().equals("file")) { 124 return new BufferedInputStream(new FileInputStream(path.toFile())); 125 } 126 return new BufferedInputStream(Files.newInputStream(path)); 127 } 128 getBytes(Path path)129 public static byte[] getBytes(Path path) throws IOException { 130 return Util.readBytes(getInputStream(path)); 131 } 132 listFiles(Path path)133 public static Path[] listFiles(Path path) throws IOException { 134 try (Stream<Path> list = Files.list(path)) { 135 return list.toArray(Path[]::new); 136 } 137 } 138 listFiles(Path path, final Predicate<Path> filter)139 public static Path[] listFiles(Path path, final Predicate<Path> filter) throws IOException { 140 try (Stream<Path> list = Files.list(path)) { 141 return list.filter(filter).toArray(Path[]::new); 142 } 143 } 144 listFileNames(Path path)145 public static String[] listFileNames(Path path) { 146 File[] files = path.toFile().listFiles(); 147 if (files == null) return null; 148 String[] strings = new String[files.length]; 149 for (int i = 0; i < files.length; i++) { 150 strings[i] = files[i].getName(); 151 } 152 return strings; 153 } 154 join(Path path, String... pathParts)155 public static Path join(Path path, String... pathParts) { 156 for (String pathPart : pathParts) { 157 path = path.resolve(pathPart); 158 } 159 return path; 160 } 161 externalize(Path path)162 public static String externalize(Path path) { 163 if (path.getFileSystem().provider().getScheme().equals("file")) { 164 return path.toString(); 165 } else { 166 return path.toUri().toString(); 167 } 168 } 169 170 /** Returns a reference-counted Jar FileSystem, possibly one that was previously returned. */ getJarFs(Path jarFile)171 private static FileSystem getJarFs(Path jarFile) throws IOException { 172 Path key = jarFile.toAbsolutePath(); 173 174 synchronized (ZIP_FILESYSTEMS) { 175 FsWrapper fs = ZIP_FILESYSTEMS.get(key); 176 if (fs == null) { 177 fs = new FsWrapper(FileSystems.newFileSystem(key, (ClassLoader) null), key); 178 fs.incrRefCount(); 179 180 ZIP_FILESYSTEMS.put(key, fs); 181 } else { 182 fs.incrRefCount(); 183 } 184 185 return fs; 186 } 187 } 188 189 @SuppressWarnings("NewApi") 190 private static class FsWrapper extends FileSystem { 191 private final FileSystem delegate; 192 private final Path jarFile; 193 194 @GuardedBy("this") 195 private int refCount; 196 FsWrapper(FileSystem delegate, Path jarFile)197 public FsWrapper(FileSystem delegate, Path jarFile) { 198 this.delegate = delegate; 199 this.jarFile = jarFile; 200 } 201 incrRefCount()202 synchronized void incrRefCount() { 203 refCount++; 204 } 205 decrRefCount()206 synchronized void decrRefCount() throws IOException { 207 if (--refCount == 0) { 208 synchronized (ZIP_FILESYSTEMS) { 209 ZIP_FILESYSTEMS.remove(jarFile); 210 } 211 delegate.close(); 212 } 213 } 214 215 @Override provider()216 public FileSystemProvider provider() { 217 return delegate.provider(); 218 } 219 220 @Override close()221 public void close() throws IOException { 222 decrRefCount(); 223 } 224 225 @Override isOpen()226 public boolean isOpen() { 227 return delegate.isOpen(); 228 } 229 230 @Override isReadOnly()231 public boolean isReadOnly() { 232 return delegate.isReadOnly(); 233 } 234 235 @Override getSeparator()236 public String getSeparator() { 237 return delegate.getSeparator(); 238 } 239 240 @Override getRootDirectories()241 public Iterable<Path> getRootDirectories() { 242 return delegate.getRootDirectories(); 243 } 244 245 @Override getFileStores()246 public Iterable<FileStore> getFileStores() { 247 return delegate.getFileStores(); 248 } 249 250 @Override supportedFileAttributeViews()251 public Set<String> supportedFileAttributeViews() { 252 return delegate.supportedFileAttributeViews(); 253 } 254 255 @Override getPath(String first, String... more)256 public Path getPath(String first, String... more) { 257 return delegate.getPath(first, more); 258 } 259 260 @Override getPathMatcher(String syntaxAndPattern)261 public PathMatcher getPathMatcher(String syntaxAndPattern) { 262 return delegate.getPathMatcher(syntaxAndPattern); 263 } 264 265 @Override getUserPrincipalLookupService()266 public UserPrincipalLookupService getUserPrincipalLookupService() { 267 return delegate.getUserPrincipalLookupService(); 268 } 269 270 @Override newWatchService()271 public WatchService newWatchService() throws IOException { 272 return delegate.newWatchService(); 273 } 274 } 275 } 276