package org.jruby.util;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.Channel;
import java.nio.channels.Channels;
import java.nio.file.attribute.FileTime;
import java.util.*;
import jnr.constants.platform.Errno;
import jnr.posix.FileStat;
import org.jruby.Ruby;
import org.jruby.RubyInstanceConfig;
public class URLResource implements FileResource, DummyResourceStat.FileResourceExt {
public static String URI = "uri:";
public static String CLASSLOADER = "classloader:";
public static String URI_CLASSLOADER = URI + CLASSLOADER;
private final String uri;
private final String[] list;
private final URL url;
private final String pathname;
private final ClassLoader cl;
URLResource(String uri, URL url, String[] files) {
this(uri, url, null, null, files);
}
URLResource(String uri, ClassLoader cl, String pathname, String[] files) {
this(uri, null, cl, pathname, files);
}
private URLResource(String uri, URL url, ClassLoader cl, String pathname, String[] files) {
this.uri = uri;
this.list = files;
this.url = url;
this.cl = cl;
this.pathname = pathname;
}
@Override
public String absolutePath() {
return uri;
}
public String canonicalPath() {
return uri;
}
@Override
public boolean exists() {
return url != null || pathname != null || list != null;
}
@Override
public boolean isDirectory() {
return list != null;
}
@Override
public boolean isFile() {
return list == null && (url != null || pathname != null);
}
@Override
public long length() {
long totalRead = 0;
InputStream is = null;
try {
is = openInputStream();
byte[] buf = new byte[8096];
int amountRead;
while ((amountRead = is.read(buf)) != -1) {
totalRead += amountRead;
}
is.close();
}
catch (IOException e) { close(is); }
return totalRead;
}
@Override
public boolean canRead() { return exists(); }
@Override
public boolean canWrite() { return false; }
@Override
public boolean canExecute() { return false; }
@Override
public String[] list() { return list; }
@Override
public boolean isSymLink() { return false; }
@Override
public long lastModified() {
return 0L;
}
public FileTime lastModifiedTime() {
return null;
}
public FileTime lastAccessTime() {
return null;
}
public FileTime creationTime() {
return null;
}
@Override
public int errno() {
return Errno.ENOENT.intValue();
}
@Override
public FileStat stat() {
return new DummyResourceStat(this);
}
@Override
public FileStat lstat() {
return stat();
}
@Override
public <T> T unwrap(Class<T> type) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public InputStream openInputStream() throws IOException {
if (pathname != null) {
return cl.getResourceAsStream(pathname);
}
return url.openStream();
}
@Override
public Channel openChannel( int flags, int perm ) throws IOException {
return Channels.newChannel(openInputStream());
}
@Override
public boolean equals(Object obj) {
if (obj instanceof URLResource) {
return uri.equals(((URLResource) obj).uri);
}
return false;
}
@Override
public int hashCode() {
return 17 * uri.hashCode();
}
@Override
public String toString() {
return getClass().getName() + '{' + absolutePath() + '}';
}
public static FileResource createClassloaderURI(Ruby runtime, String pathname, boolean asFile) {
ClassLoader cl = runtime != null ? runtime.getJRubyClassLoader() : RubyInstanceConfig.defaultClassLoader();
try {
pathname = new URI(pathname.replaceFirst("^/*", "/"))
.normalize().getPath().replaceAll("^/([.][.]/)*", "");
}
catch (URISyntaxException e) {
pathname = pathname.replaceAll("^[.]?/*", "");
}
final URL url = cl.getResource(pathname);
String[] files = null;
if (!asFile) {
files = listClassLoaderFiles(cl, pathname);
if (files == null) {
boolean isDirectory = false;
Set<String> list = new LinkedHashSet<>();
list.add("."); list.add("..");
try {
Enumeration<URL> urls = cl.getResources(pathname);
while (urls.hasMoreElements()){
isDirectory = addDirectoryEntries(list, urls.nextElement(), isDirectory);
}
if (runtime != null) {
isDirectory = addDirectoriesFromClassloader(cl, list, pathname, isDirectory);
isDirectory = addDirectoriesFromClassloader(cl.getParent(), list, pathname, isDirectory);
}
else {
isDirectory = addDirectoriesFromClassloader(cl, list, pathname, isDirectory);
}
if (isDirectory) files = list.toArray(new String[list.size()]);
}
catch (IOException e) { }
}
}
return new URLResource(URI_CLASSLOADER + '/' + pathname, cl, url == null ? null : pathname, files);
}
private static boolean addDirectoriesFromClassloader(ClassLoader cl, Set<String> list, String pathname, boolean isDirectory) throws IOException {
if (cl instanceof URLClassLoader ) {
for (URL u : ((URLClassLoader) cl).getURLs()) {
if (u.getFile().endsWith(".jar") && u.getProtocol().equals("file")) {
u = new URL("jar:" + u + "!/" + pathname);
isDirectory = addDirectoryEntries(list, u, isDirectory);
}
}
}
return isDirectory;
}
private static boolean addDirectoryEntries(Set<String> entries, URL url, boolean isDirectory) {
switch (url.getProtocol()) {
case "jar":
FileResource jar = JarResource.create(url.toString());
if (jar != null && jar.isDirectory()) {
if (!isDirectory) isDirectory = true;
entries.addAll(Arrays.asList(jar.list()));
}
break;
case "file":
File file = new File(url.getPath());
if (file.isDirectory()) {
if (!isDirectory) isDirectory = true;
entries.addAll(Arrays.asList(file.list()));
}
break;
default:
}
return isDirectory;
}
public static FileResource create(Ruby runtime, String pathname, boolean asFile) {
if (!pathname.startsWith(URI)) {
return null;
}
pathname = pathname.substring(URI.length()).replace('\\', '/');
if (pathname.startsWith(CLASSLOADER)) {
return createClassloaderURI(runtime, pathname.substring(CLASSLOADER.length()), asFile);
}
return createRegularURI(pathname, asFile);
}
private static FileResource createRegularURI(String pathname, boolean asFile) {
URL url;
try {
pathname = pathname.replaceFirst( "file:/([^/])", "file:///$1" );
pathname = pathname.replaceFirst( ":/([^/])", "://$1" );
url = new URL(pathname);
if (url.getProtocol().startsWith("http")) return null;
}
catch (MalformedURLException e) {
return new URLResource(URI + pathname, null, null);
}
String[] files = asFile ? null : listFiles(pathname);
if (files != null) {
return new URLResource(URI + pathname, null, files);
}
try {
InputStream is = url.openStream();
if (is != null) {
is.close();
}
else {
url = null;
}
return new URLResource(URI + pathname, url, null);
}
catch (IOException e) {
return new URLResource(URI + pathname, null, null);
}
}
private static String[] listFilesFromInputStream(InputStream is) {
BufferedReader reader = null;
try {
List<String> files = new ArrayList<>();
reader = new BufferedReader(new InputStreamReader(is));
String line = reader.readLine();
while (line != null) {
files.add(line);
line = reader.readLine();
}
return files.toArray(new String[files.size()]);
}
catch (IOException e) {
return null;
}
finally {
close(reader);
}
}
private static String[] listClassLoaderFiles(ClassLoader classloader, String pathname) {
try {
String path = pathname + (pathname.isEmpty() || pathname.endsWith("/") ? ".jrubydir" : "/.jrubydir");
Enumeration<URL> urls = classloader.getResources(path);
if (!urls.hasMoreElements()) return null;
Set<String> result = new LinkedHashSet<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
for (String entry: listFilesFromInputStream(url.openStream())) {
if (!result.contains(entry)) result.add(entry);
}
}
return result.toArray(new String[result.size()]);
}
catch (IOException e) {
return null;
}
}
private static String[] listFiles(String pathname) {
try {
InputStream is = new URL(pathname + "/.jrubydir").openStream();
if (is != null) {
return listFilesFromInputStream(is);
}
return null;
}
catch (IOException e) {
return null;
}
}
private static void close(final Closeable resource) {
if (resource != null) {
try { resource.close(); } catch (IOException ex) {}
}
}
public static URL getResourceURL(Ruby runtime, String location) {
if (location.startsWith(URI_CLASSLOADER)) {
return runtime.getJRubyClassLoader().getResource(location.substring(URI_CLASSLOADER.length() + 1));
}
try {
return new URL(location.replaceFirst("^" + URI, ""));
}
catch (MalformedURLException e) {
throw new AssertionError("BUG in " + URLResource.class, e);
}
}
}