xref: /aosp_15_r20/external/robolectric/resources/src/main/java/org/robolectric/res/Fs.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
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