From 69313883df54a36b51a3ebb9f32a8a890d8be20f Mon Sep 17 00:00:00 2001 From: jovanstevanovic Date: Wed, 17 Mar 2021 17:52:15 +0100 Subject: [PATCH] Improve resource support. Implemenation of resource in-memory file system. --- substratevm/mx.substratevm/suite.py | 1 + .../JDKVersionSpecificResourceBuilder.java | 66 + .../JDKVersionSpecificResourceBuilder.java | 64 + .../java.nio.file.spi.FileSystemProvider | 1 + .../com/oracle/svm/core/hub/DynamicHub.java | 38 +- .../JDKVersionSpecificResourceBuilder.java | 37 + .../svm/core/jdk/JavaLangSubstitutions.java | 24 - .../svm/core/jdk/JavaNetSubstitutions.java | 32 +- .../com/oracle/svm/core/jdk/Resources.java | 107 +- .../jdk/Target_java_lang_ClassLoader.java | 239 ++-- .../svm/core/jdk/Target_java_lang_Module.java | 13 +- ...rget_java_lang_module_ModuleReference.java | 34 + .../core/jdk/resources/ByteArrayChannel.java | 269 ++++ .../NativeImageResourceDirectoryStream.java | 100 ++ .../NativeImageResourceFileAttributes.java | 108 ++ ...NativeImageResourceFileAttributesView.java | 170 +++ .../NativeImageResourceFileStore.java | 105 ++ .../NativeImageResourceFileSystem.java | 1220 +++++++++++++++++ ...ativeImageResourceFileSystemException.java | 36 + ...NativeImageResourceFileSystemProvider.java | 250 ++++ .../NativeImageResourceFileSystemUtil.java | 78 ++ .../resources/NativeImageResourcePath.java | 866 ++++++++++++ .../jdk/resources/ResourceStorageEntry.java | 48 + .../jdk/resources/ResourceURLConnection.java | 100 ++ ..._sun_nio_zipfs_ZipUtils_JDK8OrEarlier.java | 38 + ...t_jdk_nio_zipfs_ZipUtils_JDK11OrLater.java | 38 + .../oracle/svm/hosted/ResourcesFeature.java | 170 ++- ...veImageResourceFileSystemProviderTest.java | 577 ++++++++ .../src/resources/resource-test1.txt | 1 + .../src/resources/resource-test2.txt | 1 + .../oracle/svm/truffle/tck/resources/jre.json | 3 +- 31 files changed, 4549 insertions(+), 285 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.core.jdk11/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java create mode 100644 substratevm/src/com.oracle.svm.core.jdk8/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java create mode 100644 substratevm/src/com.oracle.svm.core/src/META-INF/services/java.nio.file.spi.FileSystemProvider create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_module_ModuleReference.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ByteArrayChannel.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceDirectoryStream.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributes.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributesView.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileStore.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemException.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemProvider.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourcePath.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_com_sun_nio_zipfs_ZipUtils_JDK8OrEarlier.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_jdk_nio_zipfs_ZipUtils_JDK11OrLater.java create mode 100644 substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java create mode 100644 substratevm/src/com.oracle.svm.test/src/resources/resource-test1.txt create mode 100644 substratevm/src/com.oracle.svm.test/src/resources/resource-test2.txt diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index d96dc9334b74b..0c134a65cdb7c 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -206,6 +206,7 @@ "jdk.internal.module", "jdk.internal.misc", "jdk.internal.logger", + "jdk.internal.loader", "sun.util.resources", "sun.text.spi", "jdk.internal.perf", diff --git a/substratevm/src/com.oracle.svm.core.jdk11/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java b/substratevm/src/com.oracle.svm.core.jdk11/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java new file mode 100644 index 0000000000000..3c3e8db542480 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.jdk11/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class JDKVersionSpecificResourceBuilder { + + public static Object buildResource(String name, URL url, URLConnection urlConnection) { + return new jdk.internal.loader.Resource() { + + @Override + public String getName() { + return name; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public URL getCodeSourceURL() { + // We are deleting resource URL class path during native image build, + // so in runtime we don't have this information. + return null; + } + + @Override + public InputStream getInputStream() throws IOException { + return urlConnection.getInputStream(); + } + + @Override + public int getContentLength() throws IOException { + return urlConnection.getContentLength(); + } + }; + } +} diff --git a/substratevm/src/com.oracle.svm.core.jdk8/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java b/substratevm/src/com.oracle.svm.core.jdk8/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java new file mode 100644 index 0000000000000..b319fbbc6a493 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.jdk8/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class JDKVersionSpecificResourceBuilder { + + public static Object buildResource(String name, URL url, URLConnection urlConnection) { + return new sun.misc.Resource() { + + @Override + public String getName() { + return name; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public URL getCodeSourceURL() { + return null; + } + + @Override + public InputStream getInputStream() throws IOException { + return urlConnection.getInputStream(); + } + + @Override + public int getContentLength() throws IOException { + return urlConnection.getContentLength(); + } + }; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/META-INF/services/java.nio.file.spi.FileSystemProvider b/substratevm/src/com.oracle.svm.core/src/META-INF/services/java.nio.file.spi.FileSystemProvider new file mode 100644 index 0000000000000..2c8308d652d37 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -0,0 +1 @@ +com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystemProvider \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index 81b5880932c4c..6864a09498921 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -26,7 +26,6 @@ //Checkstyle: allow reflection -import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.io.Serializable; @@ -81,7 +80,6 @@ import com.oracle.svm.core.jdk.JDK16OrLater; import com.oracle.svm.core.jdk.JDK8OrEarlier; import com.oracle.svm.core.jdk.Package_jdk_internal_reflect; -import com.oracle.svm.core.jdk.Resources; import com.oracle.svm.core.jdk.Target_java_lang_Module; import com.oracle.svm.core.jdk.Target_jdk_internal_reflect_Reflection; import com.oracle.svm.core.meta.SharedType; @@ -720,34 +718,18 @@ public Enum[] getEnumConstantsShared() { return (Enum[]) enumConstantsReference; } - @Substitute - private InputStream getResourceAsStream(String resourceName) { - final String path = resolveName(getName(), resourceName); - List arr = Resources.get(path); - return arr == null ? null : new ByteArrayInputStream(arr.get(0)); - } + @KeepOriginal + public native URL getResource(String resourceName); - @Substitute - private URL getResource(String resourceName) { - final String path = resolveName(getName(), resourceName); - List arr = Resources.get(path); - return arr == null ? null : Resources.createURL(path, arr.get(0)); - } + @KeepOriginal + public native InputStream getResourceAsStream(String resourceName); - private String resolveName(String baseName, String resourceName) { - if (resourceName == null) { - return resourceName; - } - if (resourceName.startsWith("/")) { - return resourceName.substring(1); - } - int index = baseName.lastIndexOf('.'); - if (index != -1) { - return baseName.substring(0, index).replace('.', '/') + "/" + resourceName; - } else { - return resourceName; - } - } + @KeepOriginal + private native String resolveName(String resourceName); + + @KeepOriginal + @TargetElement(name = "isOpenToCaller", onlyWith = JDK11OrLater.class) + private native boolean isOpenToCaller(String resourceName, Class caller); @KeepOriginal private native ClassLoader getClassLoader(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java new file mode 100644 index 0000000000000..a9390de78eb59 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKVersionSpecificResourceBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk; + +import java.net.URL; +import java.net.URLConnection; + +public class JDKVersionSpecificResourceBuilder { + + @SuppressWarnings("unused") + public static Object buildResource(String name, URL url, URLConnection urlConnection) { + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java index 3f325032ce916..6b164b0140b9a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java @@ -29,11 +29,9 @@ import static com.oracle.svm.core.snippets.KnownIntrinsics.readHub; import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.net.URL; -import java.util.Enumeration; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; @@ -726,28 +724,6 @@ private static boolean hasClassPath() { return true; } - @SuppressWarnings("unused") - @Substitute - private static URL findResource(String mn, String name) { - return ClassLoader.getSystemClassLoader().getResource(name); - } - - @SuppressWarnings("unused") - @Substitute - private static InputStream findResourceAsStream(String mn, String name) { - return ClassLoader.getSystemClassLoader().getResourceAsStream(name); - } - - @Substitute - private static URL findResource(String name) { - return ClassLoader.getSystemClassLoader().getResource(name); - } - - @Substitute - private static Enumeration findResources(String name) throws IOException { - return ClassLoader.getSystemClassLoader().getResources(name); - } - /** * All ClassLoaderValue are reset at run time for now. See also * {@link Target_java_lang_ClassLoader#classLoaderValueMap} for resetting of individual class diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaNetSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaNetSubstitutions.java index e33e9f5b80210..b0d5c9080d81b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaNetSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaNetSubstitutions.java @@ -26,10 +26,6 @@ // Checkstyle: allow reflection -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; @@ -58,6 +54,7 @@ import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.c.CGlobalData; import com.oracle.svm.core.c.CGlobalDataFactory; +import com.oracle.svm.core.jdk.resources.ResourceURLConnection; import com.oracle.svm.core.option.OptionUtils; import com.oracle.svm.core.option.SubstrateOptionsParser; import com.oracle.svm.core.util.VMError; @@ -240,31 +237,8 @@ static URLStreamHandler getURLStreamHandler(String protocol) throws MalformedURL static URLStreamHandler createResourcesURLStreamHandler() { return new URLStreamHandler() { @Override - protected URLConnection openConnection(URL url) throws IOException { - return new URLConnection(url) { - private InputStream in; - - @Override - public void connect() throws IOException { - if (connected) { - return; - } - connected = true; - // remove "resource:" from url to get the resource name - String resName = url.toString().substring(1 + JavaNetSubstitutions.RESOURCE_PROTOCOL.length()); - final List bytes = Resources.get(resName); - if (bytes == null || bytes.size() < 1) { - throw new FileNotFoundException(url.toString()); - } - in = new ByteArrayInputStream(bytes.get(0)); - } - - @Override - public InputStream getInputStream() throws IOException { - connect(); - return in; - } - }; + protected URLConnection openConnection(URL url) { + return new ResourceURLConnection(url); } }; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index 42d3de41be90f..538a1027fe1c4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -24,7 +24,6 @@ */ package com.oracle.svm.core.jdk; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; @@ -32,8 +31,12 @@ import java.net.URLConnection; import java.net.URLStreamHandler; import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; import java.util.List; +import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; +import com.oracle.svm.core.jdk.resources.ResourceURLConnection; import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; @@ -53,23 +56,21 @@ */ public final class Resources { - static Resources singleton() { + public static Resources singleton() { return ImageSingletons.lookup(Resources.class); } /** The hosted map used to collect registered resources. */ - private final EconomicMap> resources = ImageHeapMap.create(); + private final EconomicMap resources = ImageHeapMap.create(); Resources() { } - public EconomicMap> resources() { + public EconomicMap resources() { return resources; } - @Platforms(Platform.HOSTED_ONLY.class) - public static void registerResource(String name, InputStream is) { - + public static byte[] inputStreamToByteArray(InputStream is) { byte[] arr = new byte[4096]; int pos = 0; try { @@ -89,71 +90,71 @@ public static void registerResource(String name, InputStream is) { throw VMError.shouldNotReachHere(ex); } - byte[] res = new byte[pos]; - System.arraycopy(arr, 0, res, 0, pos); + byte[] data = new byte[pos]; + System.arraycopy(arr, 0, data, 0, pos); + return data; + } + private static void addEntry(String resourceName, boolean isDirectory, byte[] data) { Resources support = singleton(); - List list = support.resources.get(name); - if (list == null) { - list = new ArrayList<>(); - support.resources.put(name, list); + ResourceStorageEntry entry = support.resources.get(resourceName); + if (entry == null) { + entry = new ResourceStorageEntry(isDirectory); + support.resources.put(resourceName, entry); } - list.add(res); + entry.getData().add(data); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static void registerResource(String resourceName, InputStream is) { + addEntry(resourceName, false, inputStreamToByteArray(is)); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerDirectoryResource(String dir, String content) { + public static void registerDirectoryResource(String resourceDirName, String content) { /* * A directory content represents the names of all files and subdirectories located in the * specified directory, separated with new line delimiter and joined into one string which * is later converted into a byte array and placed into the resources map. */ - Resources support = singleton(); - - byte[] arr = content.getBytes(); - List list = support.resources.get(dir); - if (list == null) { - list = new ArrayList<>(); - support.resources.put(dir, list); - } - list.add(arr); + addEntry(resourceDirName, true, content.getBytes()); } - public static List get(String name) { + public static ResourceStorageEntry get(String name) { return singleton().resources.get(name); } - public static URL createURL(String name, byte[] resourceBytes) { - class Conn extends URLConnection { - Conn(URL url) { - super(url); - } + private static URL createURL(String resourceName, int index) { + try { + return new URL(JavaNetSubstitutions.RESOURCE_PROTOCOL, null, -1, resourceName, + new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL url) { + return new ResourceURLConnection(url, index); + } + }); + } catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } - @Override - public void connect() throws IOException { - } + } - @Override - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(resourceBytes); - } + public static URL createURL(String resourceName) { + Enumeration urls = createURLs(resourceName); + return urls.hasMoreElements() ? urls.nextElement() : null; + } - @Override - public long getContentLengthLong() { - return resourceBytes.length; - } + public static Enumeration createURLs(String resourceName) { + ResourceStorageEntry entry = Resources.get(resourceName); + if (entry == null) { + return Collections.emptyEnumeration(); } - - try { - return new URL("resource", null, -1, name, new URLStreamHandler() { - @Override - protected URLConnection openConnection(URL u) throws IOException { - return new Conn(u); - } - }); - } catch (MalformedURLException ex) { - throw new IllegalStateException(ex); + int numberOfResources = entry.getData().size(); + List resourcesURLs = new ArrayList<>(numberOfResources); + for (int index = 0; index < numberOfResources; index++) { + resourcesURLs.add(createURL(resourceName, index)); } + return Collections.enumeration(resourcesURLs); } } @@ -172,8 +173,8 @@ public void afterCompilation(AfterCompilationAccess access) { * of lazily initialized fields. Only the byte[] arrays themselves can be safely made * read-only. */ - for (List resourceList : Resources.singleton().resources().getValues()) { - for (byte[] resource : resourceList) { + for (ResourceStorageEntry resourceList : Resources.singleton().resources().getValues()) { + for (byte[] resource : resourceList.getData()) { access.registerAsImmutable(resource); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java index 6ac33fac00adf..ae54e9dd6cdf9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java @@ -24,13 +24,13 @@ */ package com.oracle.svm.core.jdk; -import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; +import java.net.URLConnection; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Collections; @@ -58,8 +58,75 @@ import jdk.vm.ci.meta.MetaAccessProvider; import jdk.vm.ci.meta.ResolvedJavaField; +@TargetClass(className = "jdk.internal.loader.Resource", onlyWith = JDK11OrLater.class) +@SuppressWarnings("unused") +final class Target_jdk_internal_loader_Resource_JDK11OrLater { + +} + +@TargetClass(className = "sun.misc.Resource", onlyWith = JDK8OrEarlier.class) +@SuppressWarnings("unused") +final class Target_sun_misc_Resource_JDK8OrEarlier { + +} + +@SuppressWarnings("unchecked") +class ResourcesHelper { + + private static T urlToResource(String resourceName, URL url) { + try { + if (url == null) { + return null; + } + URLConnection urlConnection = url.openConnection(); + Object resource = JDKVersionSpecificResourceBuilder.buildResource(resourceName, url, urlConnection); + VMError.guarantee(resource != null); + return (T) resource; + } catch (IOException e) { + return null; + } catch (ClassCastException classCastException) { + throw VMError.shouldNotReachHere(classCastException); + } + } + + static T nameToResource(String resourceName) { + return urlToResource(resourceName, nameToResourceURL(resourceName)); + } + + static Enumeration nameToResources(String resourceName) { + Enumeration urls = Resources.createURLs(resourceName); + List resourceURLs = new ArrayList<>(); + while (urls.hasMoreElements()) { + resourceURLs.add(urlToResource(resourceName, urls.nextElement())); + } + return Collections.enumeration(resourceURLs); + } + + static URL nameToResourceURL(String resourceName) { + return Resources.createURL(resourceName); + } + + static InputStream nameToResourceInputStream(String resourceName) throws IOException { + URL url = nameToResourceURL(resourceName); + return url != null ? url.openStream() : null; + } + + static List nameToResourceListURLs(String resourcesName) { + Enumeration urls = Resources.createURLs(resourcesName); + List resourceURLs = new ArrayList<>(); + while (urls.hasMoreElements()) { + resourceURLs.add(urls.nextElement()); + } + return resourceURLs; + } + + static Enumeration nameToResourceEnumerationURLs(String resourcesName) { + return Collections.enumeration(nameToResourceListURLs(resourcesName)); + } +} + @TargetClass(classNameProvider = Package_jdk_internal_loader.class, className = "URLClassPath") -@SuppressWarnings("static-method") +@SuppressWarnings({"unused", "static-method"}) final class Target_jdk_internal_loader_URLClassPath { /* Reset fields that can store a Zip file via sun.misc.URLClassPath$JarLoader.jar. */ @@ -91,20 +158,50 @@ final class Target_jdk_internal_loader_URLClassPath { @Delete @TargetElement(onlyWith = JDK8OrEarlier.class) private static native boolean knownToNotExist0(ClassLoader loader, String className); + + @Substitute + public URL findResource(String name, boolean check) { + return Resources.createURL(name); + } + + @Substitute + public Enumeration findResources(final String name, final boolean check) { + return Resources.createURLs(name); + } + + @Substitute + @TargetElement(name = "getResource", onlyWith = JDK11OrLater.class) + public Target_jdk_internal_loader_Resource_JDK11OrLater getResourceJDK11OrLater(String name, boolean check) { + return ResourcesHelper.nameToResource(name); + } + + @Substitute + @TargetElement(name = "getResources", onlyWith = JDK11OrLater.class) + public Enumeration getResourcesJDK11OrLater(final String name, + final boolean check) { + return ResourcesHelper.nameToResources(name); + } + + @Substitute + @TargetElement(name = "getResource", onlyWith = JDK8OrEarlier.class) + public Target_sun_misc_Resource_JDK8OrEarlier getResourceJDK8OrEarlier(String name, boolean check) { + return ResourcesHelper.nameToResource(name); + } + + @Substitute + @TargetElement(name = "getResources", onlyWith = JDK8OrEarlier.class) + public Enumeration getResourcesJDK8OrEarlier(final String name, + final boolean check) { + return ResourcesHelper.nameToResources(name); + } } @TargetClass(URLClassLoader.class) -@SuppressWarnings("static-method") +@SuppressWarnings({"unused", "static-method"}) final class Target_java_net_URLClassLoader { @Alias @RecomputeFieldValue(kind = Kind.NewInstance, declClass = WeakHashMap.class)// private WeakHashMap closeables; - @Substitute - private InputStream getResourceAsStream(String name) { - List arr = Resources.get(name); - return arr == null ? null : new ByteArrayInputStream(arr.get(0)); - } - @Substitute @SuppressWarnings("unused") protected Class findClass(final String name) { @@ -113,38 +210,47 @@ protected Class findClass(final String name) { } @TargetClass(className = "jdk.internal.loader.BuiltinClassLoader", onlyWith = JDK11OrLater.class) -@SuppressWarnings("static-method") +@SuppressWarnings({"unused", "static-method"}) final class Target_jdk_internal_loader_BuiltinClassLoader { @Substitute - public URL findResource(@SuppressWarnings("unused") String mn, String name) { - List arr = Resources.get(name); - return arr == null ? null : Resources.createURL(name, arr.get(0)); + public URL findResource(String mn, String name) { + return ResourcesHelper.nameToResourceURL(name); } @Substitute - public URL findResource(String name) { - List arr = Resources.get(name); - return arr == null ? null : Resources.createURL(name, arr.get(0)); + public InputStream findResourceAsStream(String mn, String name) throws IOException { + return ResourcesHelper.nameToResourceInputStream(name); } @Substitute - public InputStream findResourceAsStream(@SuppressWarnings("unused") String mn, String name) { - List arr = Resources.get(name); - return arr == null ? null : new ByteArrayInputStream(arr.get(0)); + public URL findResource(String name) { + return ResourcesHelper.nameToResourceURL(name); } @Substitute public Enumeration findResources(String name) { - List arr = Resources.get(name); - if (arr == null) { - return Collections.emptyEnumeration(); - } - List res = new ArrayList<>(arr.size()); - for (byte[] data : arr) { - res.add(Resources.createURL(name, data)); - } - return Collections.enumeration(res); + return ResourcesHelper.nameToResourceEnumerationURLs(name); + } + + @Substitute + private List findMiscResource(String name) { + return ResourcesHelper.nameToResourceListURLs(name); + } + + @Substitute + private URL findResource(Target_java_lang_module_ModuleReference mref, String name) { + return ResourcesHelper.nameToResourceURL(name); + } + + @Substitute + private URL findResourceOnClassPath(String name) { + return ResourcesHelper.nameToResourceURL(name); + } + + @Substitute + private Enumeration findResourcesOnClassPath(String name) { + return ResourcesHelper.nameToResourceEnumerationURLs(name); } } @@ -153,43 +259,39 @@ public Enumeration findResources(String name) { final class Target_jdk_internal_loader_Loader { @Substitute - private URL findResource(String mn, String name) { - return findResource(name); + protected URL findResource(String mn, String name) { + return ResourcesHelper.nameToResourceURL(name); } @Substitute - private URL findResource(String name) { - List arr = Resources.get(name); - return arr == null ? null : Resources.createURL(name, arr.get(0)); + public URL findResource(String name) { + return ResourcesHelper.nameToResourceURL(name); } @Substitute - private Enumeration findResources(String name) { - List arr = Resources.get(name); - if (arr == null) { - return Collections.emptyEnumeration(); - } - List res = new ArrayList<>(arr.size()); - for (byte[] data : arr) { - res.add(Resources.createURL(name, data)); - } - return Collections.enumeration(res); + public Enumeration findResources(String name) { + return ResourcesHelper.nameToResourceEnumerationURLs(name); } @Substitute - private URL getResource(String name) { - return findResource(name); + public URL getResource(String name) { + return ResourcesHelper.nameToResourceURL(name); } @Substitute - private Enumeration getResources(String name) { - return findResources(name); + public Enumeration getResources(String name) throws IOException { + return ResourcesHelper.nameToResourceEnumerationURLs(name); + } + + @Substitute + private List findResourcesAsList(String name) { + return ResourcesHelper.nameToResourceListURLs(name); } } @TargetClass(ClassLoader.class) @SuppressWarnings("static-method") -final class Target_java_lang_ClassLoader { +public final class Target_java_lang_ClassLoader { /** * This field can be safely deleted, but that would require substituting the entire constructor @@ -233,42 +335,12 @@ public static ClassLoader getSystemClassLoader() { @Substitute private URL getResource(String name) { - return getSystemResource(name); - } - - @Substitute - private InputStream getResourceAsStream(String name) { - return getSystemResourceAsStream(name); + return ResourcesHelper.nameToResourceURL(name); } @Substitute private Enumeration getResources(String name) { - return getSystemResources(name); - } - - @Substitute - private static URL getSystemResource(String name) { - List arr = Resources.get(name); - return arr == null ? null : Resources.createURL(name, arr.get(0)); - } - - @Substitute - private static InputStream getSystemResourceAsStream(String name) { - List arr = Resources.get(name); - return arr == null ? null : new ByteArrayInputStream(arr.get(0)); - } - - @Substitute - private static Enumeration getSystemResources(String name) { - List arr = Resources.get(name); - if (arr == null) { - return Collections.emptyEnumeration(); - } - List res = new ArrayList<>(arr.size()); - for (byte[] data : arr) { - res.add(Resources.createURL(name, data)); - } - return Collections.enumeration(res); + return ResourcesHelper.nameToResourceEnumerationURLs(name); } @Substitute @@ -338,13 +410,6 @@ private boolean trySetObjectField(String name, Object obj) { throw VMError.unsupportedFeature("JDK11OrLater: Target_java_lang_ClassLoader.trySetObjectField(String name, Object obj)"); } - @Substitute // - @TargetElement(onlyWith = JDK11OrLater.class) // - @SuppressWarnings({"unused"}) - protected URL findResource(String moduleName, String name) throws IOException { - throw VMError.unsupportedFeature("JDK11OrLater: Target_java_lang_ClassLoader.findResource(String, String)"); - } - @Substitute // @SuppressWarnings({"unused"}) Object getClassLoadingLock(String className) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java index 85afea183b0a2..4e1b03662c8c9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java @@ -26,21 +26,24 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.util.List; import com.oracle.svm.core.annotate.Delete; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; -import com.oracle.svm.core.annotate.TargetElement; +import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; @TargetClass(className = "java.lang.Module", onlyWith = JDK11OrLater.class) public final class Target_java_lang_Module { + @SuppressWarnings("static-method") @Substitute - @TargetElement(name = "getResourceAsStream") public InputStream getResourceAsStream(String name) { - List arr = Resources.get(name); - return arr == null ? null : new ByteArrayInputStream(arr.get(0)); + ResourceStorageEntry entry = Resources.get(name); + if (entry == null) { + return null; + } else { + return new ByteArrayInputStream(entry.getData().get(0)); + } } /* diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_module_ModuleReference.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_module_ModuleReference.java new file mode 100644 index 0000000000000..fe7bb7485eebf --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_module_ModuleReference.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk; + +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "java.lang.module.ModuleReference", onlyWith = JDK11OrLater.class) +@SuppressWarnings("unused") +public final class Target_java_lang_module_ModuleReference { + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ByteArrayChannel.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ByteArrayChannel.java new file mode 100644 index 0000000000000..b4941b2eb8a06 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ByteArrayChannel.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; +import java.util.Arrays; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + *

+ * This class is copy of class jdk.nio.zipfs.ByteArrayChannel. There are two reasons why we cannot + * substitute this class: + *

    + *
  • This class doesn't exists on JDK8.
  • + *
  • On JDK11, we need to extends this class, and in case of substitution, it's not possible + * because substituted class is final
  • + *
+ *

+ */ +public class ByteArrayChannel implements SeekableByteChannel { + + private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); + private byte[] buf; + + /** + * The current position of this channel. + */ + private int pos; + + /** + * The index that is one greater than the last valid byte in the channel. + */ + private int last; + + private boolean closed; + private final boolean readonly; + + /** + * Creates a {@code ByteArrayChannel} with size {@code sz}. + */ + ByteArrayChannel(int sz, boolean readonly) { + this.buf = new byte[sz]; + this.pos = this.last = 0; + this.readonly = readonly; + } + + /** + * Creates a ByteArrayChannel with its 'pos' at 0 and its 'last' at buf's end. Note: no + * defensive copy of the 'buf', used directly. + */ + ByteArrayChannel(byte[] buf, boolean readonly) { + this.buf = buf; + this.pos = 0; + this.last = buf.length; + this.readonly = readonly; + } + + @Override + public boolean isOpen() { + return !closed; + } + + @Override + public long position() throws IOException { + beginRead(); + try { + ensureOpen(); + return pos; + } finally { + endRead(); + } + } + + @Override + public SeekableByteChannel position(long position) throws IOException { + beginWrite(); + try { + ensureOpen(); + if (position < 0 || position >= Integer.MAX_VALUE) { + throw new IllegalArgumentException("Illegal position " + position); + } + this.pos = Math.min((int) position, last); + return this; + } finally { + endWrite(); + } + } + + @Override + public int read(ByteBuffer dst) throws IOException { + beginWrite(); + try { + ensureOpen(); + if (pos == last) { + return -1; + } + int n = Math.min(dst.remaining(), last - pos); + dst.put(buf, pos, n); + pos += n; + return n; + } finally { + endWrite(); + } + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + if (readonly) { + throw new NonWritableChannelException(); + } + ensureOpen(); + throw new UnsupportedOperationException(); + } + + @Override + public int write(ByteBuffer src) throws IOException { + if (readonly) { + throw new NonWritableChannelException(); + } + beginWrite(); + try { + ensureOpen(); + int n = src.remaining(); + ensureCapacity(pos + n); + src.get(buf, pos, n); + pos += n; + if (pos > last) { + last = pos; + } + return n; + } finally { + endWrite(); + } + } + + @Override + public long size() throws IOException { + beginRead(); + try { + ensureOpen(); + return last; + } finally { + endRead(); + } + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + + beginWrite(); + try { + closed = true; + buf = null; + pos = 0; + last = 0; + } finally { + endWrite(); + } + } + + /** + * Creates a newly allocated byte array. Its size is the current size of this channel and the + * valid contents of the buffer have been copied into it. + * + * @return the current contents of this channel, as a byte array. + */ + public byte[] toByteArray() { + beginRead(); + try { + // avoid copy if last == bytes.length? + return Arrays.copyOf(buf, last); + } finally { + endRead(); + } + } + + private void ensureOpen() throws IOException { + if (closed) { + throw new ClosedChannelException(); + } + } + + private void beginWrite() { + rwlock.writeLock().lock(); + } + + private void endWrite() { + rwlock.writeLock().unlock(); + } + + private void beginRead() { + rwlock.readLock().lock(); + } + + private void endRead() { + rwlock.readLock().unlock(); + } + + private void ensureCapacity(int minCapacity) { + // overflow-conscious code + if (minCapacity - buf.length > 0) { + grow(minCapacity); + } + } + + /** + * The maximum size of array to allocate. Some VMs reserve some header words in an array. + * Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size + * exceeds VM limit + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + /** + * Increases the capacity to ensure that it can hold at least the number of elements specified + * by the minimum capacity argument. + * + * @param minCapacity the desired minimum capacity + */ + private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = buf.length; + int newCapacity = oldCapacity << 1; + if (newCapacity - minCapacity < 0) { + newCapacity = minCapacity; + } + if (newCapacity - MAX_ARRAY_SIZE > 0) { + newCapacity = hugeCapacity(minCapacity); + } + buf = Arrays.copyOf(buf, newCapacity); + } + + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) { // overflow + throw new OutOfMemoryError(); + } + return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceDirectoryStream.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceDirectoryStream.java new file mode 100644 index 0000000000000..bd7451b1c7095 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceDirectoryStream.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.io.IOException; +import java.nio.file.ClosedDirectoryStreamException; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class NativeImageResourceDirectoryStream implements DirectoryStream { + + private final NativeImageResourceFileSystem fileSystem; + private final DirectoryStream.Filter filter; + private final NativeImageResourcePath dir; + private volatile boolean isClosed = false; + private Iterator directoryIterator; + + public NativeImageResourceDirectoryStream(NativeImageResourcePath dir, Filter filter) throws IOException { + this.fileSystem = dir.getFileSystem(); + this.dir = dir; + this.filter = filter; + if (!fileSystem.isDirectory(dir.getResolvedPath())) { + throw new NotDirectoryException(dir.toString()); + } + } + + @Override + public Iterator iterator() { + if (isClosed) { + throw new ClosedDirectoryStreamException(); + } + if (directoryIterator != null) { + throw new IllegalStateException("Iterator has already been returned"); + } + + try { + directoryIterator = fileSystem.iteratorOf(dir, filter); + } catch (IOException ioException) { + throw new DirectoryIteratorException(ioException); + } + + return new Iterator() { + + @Override + public boolean hasNext() { + if (isClosed) { + return false; + } + return directoryIterator.hasNext(); + } + + @Override + public Path next() { + if (isClosed) { + throw new NoSuchElementException(); + } + return directoryIterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public void close() throws IOException { + if (!isClosed) { + isClosed = true; + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributes.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributes.java new file mode 100644 index 0000000000000..6d8a175d295f3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributes.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.Formatter; + +public class NativeImageResourceFileAttributes implements BasicFileAttributes { + + private final NativeImageResourceFileSystem fileSystem; + private final NativeImageResourceFileSystem.Entry entry; + + public NativeImageResourceFileAttributes(NativeImageResourceFileSystem fileSystem, NativeImageResourceFileSystem.Entry entry) { + this.fileSystem = fileSystem; + this.entry = entry; + } + + public String getName() { + return fileSystem.getString(entry.name); + } + + @Override + public FileTime lastModifiedTime() { + return FileTime.fromMillis(entry.lastModifiedTime); + } + + @Override + public FileTime lastAccessTime() { + return FileTime.fromMillis(entry.lastAccessTime); + } + + @Override + public FileTime creationTime() { + return FileTime.fromMillis(entry.createTime); + } + + @Override + public boolean isRegularFile() { + return !entry.isDirectory(); + } + + @Override + public boolean isDirectory() { + return entry.isDirectory(); + } + + @Override + public boolean isSymbolicLink() { + return false; + } + + @Override + public boolean isOther() { + return false; + } + + @Override + public long size() { + return entry.size(); + } + + @Override + public Object fileKey() { + return null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(1024); + Formatter fm = new Formatter(sb); + fm.format(" name : %s%n", getName()); + fm.format(" creationTime : %tc%n", creationTime().toMillis()); + fm.format(" lastAccessTime : %tc%n", lastAccessTime().toMillis()); + fm.format(" lastModifiedTime: %tc%n", lastModifiedTime().toMillis()); + fm.format(" isRegularFile : %b%n", isRegularFile()); + fm.format(" isDirectory : %b%n", isDirectory()); + fm.format(" isSymbolicLink : %b%n", isSymbolicLink()); + fm.format(" isOther : %b%n", isOther()); + fm.format(" fileKey : %s%n", fileKey()); + fm.format(" size : %d%n", size()); + fm.close(); + return sb.toString(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributesView.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributesView.java new file mode 100644 index 0000000000000..dd06a8727922e --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileAttributesView.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.io.IOException; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileTime; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +public class NativeImageResourceFileAttributesView implements BasicFileAttributeView { + + private enum AttributeID { + size, + creationTime, + lastAccessTime, + lastModifiedTime, + isDirectory, + isRegularFile, + isSymbolicLink, + isOther, + fileKey; + + private static final Set attributeValues = new HashSet<>(); + + static { + for (AttributeID choice : AttributeID.values()) { + attributeValues.add(choice.name()); + } + } + + public static boolean contains(String value) { + return attributeValues.contains(value); + } + } + + private final NativeImageResourcePath path; + private final boolean isBasic; + + public NativeImageResourceFileAttributesView(NativeImageResourcePath path, boolean isBasic) { + this.path = path; + this.isBasic = isBasic; + } + + @SuppressWarnings("unchecked") + static V get(NativeImageResourcePath path, Class type) { + if (type == null) { + throw new NullPointerException(); + } + if (type == BasicFileAttributeView.class) { + return (V) new NativeImageResourceFileAttributesView(path, true); + } + if (type == NativeImageResourceFileAttributesView.class) { + return (V) new NativeImageResourceFileAttributesView(path, false); + } + return null; + } + + static NativeImageResourceFileAttributesView get(NativeImageResourcePath path, String type) { + if (type == null) { + throw new NullPointerException(); + } + if (type.equals("basic")) { + return new NativeImageResourceFileAttributesView(path, true); + } + if (type.equals("resource")) { + return new NativeImageResourceFileAttributesView(path, false); + } + return null; + } + + @Override + public String name() { + return isBasic ? "basic" : "resource"; + } + + @Override + public NativeImageResourceFileAttributes readAttributes() throws IOException { + return path.getAttributes(); + } + + public void setAttribute(String attribute, Object value) throws IOException { + try { + if (AttributeID.valueOf(attribute) == AttributeID.lastModifiedTime) { + setTimes((FileTime) value, null, null); + } + if (AttributeID.valueOf(attribute) == AttributeID.lastAccessTime) { + setTimes(null, (FileTime) value, null); + } + if (AttributeID.valueOf(attribute) == AttributeID.creationTime) { + setTimes(null, null, (FileTime) value); + } + } catch (IllegalArgumentException x) { + throw new UnsupportedOperationException("'" + attribute + "' is unknown or read-only attribute"); + } + } + + Map readAttributes(String attributes) throws IOException { + NativeImageResourceFileAttributes nativeImageResourceFileAttributes = readAttributes(); + LinkedHashMap map = new LinkedHashMap<>(); + if ("*".equals(attributes)) { + for (AttributeID id : AttributeID.values()) { + map.put(id.name(), attribute(id, nativeImageResourceFileAttributes)); + } + } else { + String[] as = attributes.split(","); + for (String a : as) { + if (AttributeID.contains(a)) { + map.put(a, attribute(AttributeID.valueOf(a), nativeImageResourceFileAttributes)); + } + } + } + return map; + } + + @Override + public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { + path.setTimes(lastModifiedTime, lastAccessTime, createTime); + } + + Object attribute(AttributeID id, NativeImageResourceFileAttributes nativeImageResourceFileAttributes) { + switch (id) { + case size: + return nativeImageResourceFileAttributes.size(); + case creationTime: + return nativeImageResourceFileAttributes.creationTime(); + case lastAccessTime: + return nativeImageResourceFileAttributes.lastAccessTime(); + case lastModifiedTime: + return nativeImageResourceFileAttributes.lastModifiedTime(); + case isDirectory: + return nativeImageResourceFileAttributes.isDirectory(); + case isRegularFile: + return nativeImageResourceFileAttributes.isRegularFile(); + case isSymbolicLink: + return nativeImageResourceFileAttributes.isSymbolicLink(); + case isOther: + return nativeImageResourceFileAttributes.isOther(); + case fileKey: + return nativeImageResourceFileAttributes.fileKey(); + } + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileStore.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileStore.java new file mode 100644 index 0000000000000..8578981bd3a5c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileStore.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileStoreAttributeView; + +public class NativeImageResourceFileStore extends FileStore { + + private final FileSystem resourceFileSystem; + + NativeImageResourceFileStore(Path path) { + this.resourceFileSystem = path.getFileSystem(); + } + + @Override + public String name() { + return resourceFileSystem.toString() + resourceFileSystem.getSeparator(); + } + + @Override + public String type() { + return "rfs"; + } + + @Override + public boolean isReadOnly() { + return resourceFileSystem.isReadOnly(); + } + + @Override + public long getTotalSpace() { + throw new UnsupportedOperationException("This operation is not support for native image resource file system!"); + } + + @Override + public long getUsableSpace() { + throw new UnsupportedOperationException("This operation is not support for native image resource file system!"); + } + + @Override + public long getUnallocatedSpace() { + throw new UnsupportedOperationException("This operation is not support for native image resource file system!"); + } + + @Override + public boolean supportsFileAttributeView(Class type) { + return type == BasicFileAttributeView.class || type == NativeImageResourceFileAttributesView.class; + } + + @Override + public boolean supportsFileAttributeView(String name) { + return name.equals("basic") || name.equals("resource"); + } + + @Override + public V getFileStoreAttributeView(Class type) { + if (type == null) { + throw new NullPointerException(); + } + return null; + } + + @Override + public Object getAttribute(String attribute) throws IOException { + if (attribute.equals("totalSpace")) { + return getTotalSpace(); + } + if (attribute.equals("usableSpace")) { + return getUsableSpace(); + } + if (attribute.equals("unallocatedSpace")) { + return getUnallocatedSpace(); + } + throw new UnsupportedOperationException("Attribute isn't supported!"); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java new file mode 100644 index 0000000000000..77878fd75a232 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystem.java @@ -0,0 +1,1220 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import static com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystemUtil.toRegexPattern; +import static java.lang.Boolean.TRUE; +import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.nio.file.StandardOpenOption.APPEND; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.nio.file.StandardOpenOption.READ; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.ClosedFileSystemException; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.StandardOpenOption; +import java.nio.file.WatchService; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Pattern; + +import org.graalvm.collections.MapCursor; + +/** + *

+ * Some parts of code from this class are copied from jdk.nio.zipfs.ZipFileSystem with small tweaks. + * The main reason why we cannot reuse this class (without any code copying) is that we cannot + * extend jdk.nio.zipfs.ZipPath which is the central class in the Zip file system. + *

+ */ +public class NativeImageResourceFileSystem extends FileSystem { + + private static final String GLOB_SYNTAX = "glob"; + private static final String REGEX_SYNTAX = "regex"; + + private static final int DEFAULT_BUFFER_SIZE = 8192; + + private static final Set supportedFileAttributeViews = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("basic", "resource"))); + + private final Set inputStreams = Collections.synchronizedSet(new HashSet<>()); + private final Set outputStreams = Collections.synchronizedSet(new HashSet<>()); + private final Set tmpPaths = Collections.synchronizedSet(new HashSet<>()); + + private final long defaultTimestamp = System.currentTimeMillis(); + + private final NativeImageResourceFileSystemProvider provider; + private final Path resourcePath; + private final NativeImageResourcePath root; + private boolean isOpen = true; + private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); + + private static final byte[] ROOT_PATH = new byte[]{'/'}; + private final IndexNode lookupKey = new IndexNode(null, true); + private final LinkedHashMap inodes = new LinkedHashMap<>(10); + + public NativeImageResourceFileSystem(NativeImageResourceFileSystemProvider provider, Path resourcePath, Map env) { + this.provider = provider; + this.resourcePath = resourcePath; + this.root = new NativeImageResourcePath(this, new byte[]{'/'}); + if (!isTrue(env)) { + throw new FileSystemNotFoundException(resourcePath.toString()); + } + readAllEntries(); + } + + // Returns true if there is a name=true/"true" setting in env. + private static boolean isTrue(Map env) { + return "true".equals(env.get("create")) || TRUE.equals(env.get("create")); + } + + private void ensureOpen() { + if (!isOpen) { + throw new ClosedFileSystemException(); + } + } + + private void beginWrite() { + rwlock.writeLock().lock(); + } + + private void endWrite() { + rwlock.writeLock().unlock(); + } + + private void beginRead() { + rwlock.readLock().lock(); + } + + private void endRead() { + rwlock.readLock().unlock(); + } + + byte[] getBytes(String path) { + return path.getBytes(StandardCharsets.UTF_8); + } + + String getString(byte[] path) { + return new String(path, StandardCharsets.UTF_8); + } + + @Override + public FileSystemProvider provider() { + return provider; + } + + @Override + public void close() throws IOException { + beginWrite(); + try { + if (!isOpen) { + return; + } + isOpen = false; + } finally { + endWrite(); + } + + if (!inputStreams.isEmpty()) { // Unlock and close all remaining input streams. + Set copy = new HashSet<>(inputStreams); + for (InputStream is : copy) { + is.close(); + } + } + + if (!outputStreams.isEmpty()) { // Unlock and close all remaining output streams. + Set copy = new HashSet<>(outputStreams); + for (OutputStream os : copy) { + os.close(); + } + } + + provider.removeFileSystem(); + } + + @Override + public boolean isOpen() { + return isOpen; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public String getSeparator() { + return "/"; + } + + @Override + public Iterable getRootDirectories() { + return Collections.singleton(root); + } + + @Override + public Iterable getFileStores() { + return Collections.singleton(new NativeImageResourceFileStore(root)); + } + + @Override + public Set supportedFileAttributeViews() { + return supportedFileAttributeViews; + } + + Path getResourcePath() { + return resourcePath; + } + + @Override + public String toString() { + return resourcePath.toString(); + } + + @Override + public Path getPath(String first, String... more) { + String path; + if (more.length == 0) { + path = first; + } else { + StringBuilder sb = new StringBuilder(); + sb.append(first); + for (String segment : more) { + if (segment.length() > 0) { + if (sb.length() > 0) { + sb.append('/'); + } + sb.append(segment); + } + } + path = sb.toString(); + } + return new NativeImageResourcePath(this, getBytes(path)); + } + + @Override + public PathMatcher getPathMatcher(String syntaxAndPattern) { + int pos = syntaxAndPattern.indexOf(':'); + if (pos <= 0 || pos == syntaxAndPattern.length()) { + throw new IllegalArgumentException(); + } + String syntax = syntaxAndPattern.substring(0, pos); + String input = syntaxAndPattern.substring(pos + 1); + String expr; + if (syntax.equals(GLOB_SYNTAX)) { + expr = toRegexPattern(input); + } else { + if (syntax.equals(REGEX_SYNTAX)) { + expr = input; + } else { + throw new UnsupportedOperationException("Syntax '" + syntax + "' not recognized"); + } + } + + Pattern pattern = Pattern.compile(expr); + return path -> pattern.matcher(path.toString()).matches(); + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + throw new UnsupportedOperationException(); + } + + @Override + public WatchService newWatchService() { + throw new UnsupportedOperationException(); + } + + NativeImageResourceFileAttributes getFileAttributes(byte[] path) { + Entry entry; + beginRead(); + try { + ensureOpen(); + entry = getEntry(path); + if (entry == null) { + IndexNode inode = getInode(path); + if (inode == null) { + return null; + } + entry = new Entry(inode.name, inode.isDir); + entry.lastModifiedTime = entry.lastAccessTime = entry.createTime = defaultTimestamp; + } + } finally { + endRead(); + } + return new NativeImageResourceFileAttributes(this, entry); + } + + static void checkOptions(Set options) { + for (OpenOption option : options) { + if (option == null) { + throw new NullPointerException(); + } + if (!(option instanceof StandardOpenOption)) { + throw new IllegalArgumentException(); + } + } + if (options.contains(APPEND) && options.contains(TRUNCATE_EXISTING)) { + throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING are not allowed!"); + } + } + + SeekableByteChannel newByteChannel(byte[] path, Set options) throws IOException { + checkOptions(options); + if (options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.APPEND)) { + beginRead(); // Only need a read lock, the "update()" will obtain the write lock when + // the channel is closed. + try { + Entry e = getEntry(path); + if (e != null) { + if (e.isDir() || options.contains(CREATE_NEW)) { + throw new FileAlreadyExistsException(getString(path)); + } + SeekableByteChannel sbc = new EntryOutputChannel(new Entry(e, Entry.NEW)); + if (options.contains(APPEND)) { + try (InputStream is = getInputStream(e)) { + sbc.write(ByteBuffer.wrap(NativeImageResourceFileSystemUtil.inputStreamToByteArray(is))); + } + } + return sbc; + } + if (!options.contains(CREATE) && !options.contains(CREATE_NEW)) { + throw new NoSuchFileException(getString(path)); + } + checkParents(path); + return new EntryOutputChannel(new Entry(path, false)); + } finally { + endRead(); + } + } else { + beginRead(); + try { + ensureOpen(); + Entry e = getEntry(path); + if (e == null || e.isDir()) { + throw new NoSuchFileException(getString(path)); + } + try (InputStream is = getInputStream(e)) { + return new ByteArrayChannel(NativeImageResourceFileSystemUtil.inputStreamToByteArray(is), true); + } + } finally { + endRead(); + } + } + } + + boolean exists(byte[] path) { + beginRead(); + try { + ensureOpen(); + return getInode(path) != null; + } finally { + endRead(); + } + } + + static FileStore getFileStore(NativeImageResourcePath path) { + return new NativeImageResourceFileStore(path); + } + + void checkAccess(byte[] path) throws NoSuchFileException { + beginRead(); + try { + ensureOpen(); + if (getInode(path) == null) { + throw new NoSuchFileException(toString()); + } + } finally { + endRead(); + } + } + + void setTimes(byte[] path, FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws NoSuchFileException { + beginWrite(); + try { + ensureOpen(); + Entry e = getEntry(path); + if (e == null) { + throw new NoSuchFileException(getString(path)); + } + if (lastModifiedTime != null) { + e.lastModifiedTime = lastModifiedTime.toMillis(); + } + if (lastAccessTime != null) { + e.lastAccessTime = lastAccessTime.toMillis(); + } + if (createTime != null) { + e.createTime = createTime.toMillis(); + } + update(e); + } finally { + endWrite(); + } + } + + boolean isDirectory(byte[] path) { + beginRead(); + try { + IndexNode n = getInode(path); + return n != null && n.isDir(); + } finally { + endRead(); + } + } + + void createDirectory(byte[] dir) throws IOException { + beginWrite(); + try { + ensureOpen(); + if (dir.length == 0 || exists(dir)) { + throw new FileAlreadyExistsException(getString(dir)); + } + checkParents(dir); + Entry e = new Entry(dir, true); + update(e); + } finally { + endWrite(); + } + } + + boolean deleteFile(byte[] path, boolean failIfNotExists) throws IOException { + IndexNode inode = getInode(path); + if (inode == null) { + if (path.length == 0) { + throw new NativeImageResourceFileSystemException("Root directory can't be deleted!"); + } + if (failIfNotExists) { + throw new NoSuchFileException(getString(path)); + } else { + return false; + } + } else { + if (inode.isDir() && inode.child != null) { + throw new DirectoryNotEmptyException(getString(path)); + } + updateDelete(inode); + } + return true; + } + + private Path createTempFileInSameDirectoryAs() throws IOException { + Path tmpPath = Files.createTempFile("rfs_tmp", null); + tmpPaths.add(tmpPath); + return tmpPath; + } + + private Path getTempPathForEntry(byte[] path) throws IOException { + Path tmpPath = createTempFileInSameDirectoryAs(); + if (path != null) { + Entry e = getEntry(path); + if (e != null) { + try (InputStream is = newInputStream(path)) { + Files.copy(is, tmpPath, REPLACE_EXISTING); + } + } + } + + return tmpPath; + } + + private void removeTempPathForEntry(Path path) throws IOException { + Files.delete(path); + tmpPaths.remove(path); + } + + void copyFile(boolean deleteSource, byte[] src, byte[] dst, CopyOption[] options) throws IOException { + if (Arrays.equals(src, dst)) { + return; // Do nothing, src and dst are the same. + } + + beginWrite(); + try { + ensureOpen(); + Entry eSrc = getEntry(src); // ensureOpen checked + + if (eSrc == null) { + throw new NoSuchFileException(getString(src)); + } + if (eSrc.isDir()) { // spec says to create dst dir + createDirectory(dst); + return; + } + boolean hasReplace = false; + boolean hasCopyAttrs = false; + for (CopyOption opt : options) { + if (opt == REPLACE_EXISTING) { + hasReplace = true; + } else if (opt == COPY_ATTRIBUTES) { + hasCopyAttrs = true; + } + } + Entry eDst = getEntry(dst); + if (eDst != null) { + if (!hasReplace) { + throw new FileAlreadyExistsException(getString(dst)); + } + } else { + checkParents(dst); + } + Entry target = new Entry(eSrc, Entry.COPY); // Copy eSrc entry. + target.name(dst); // Change name. + if (eSrc.type == Entry.NEW || eSrc.type == Entry.FILE_CH) { + target.type = eSrc.type; // Make it the same type. + if (deleteSource) { // If it's a "rename", take the data + target.bytes = eSrc.bytes; + target.file = eSrc.file; + } else { // If it's not "rename", copy the data. + if (eSrc.bytes != null) { + target.bytes = Arrays.copyOf(eSrc.bytes, eSrc.bytes.length); + } else if (eSrc.file != null) { + target.file = getTempPathForEntry(null); + Files.copy(eSrc.file, target.file, REPLACE_EXISTING); + } + } + } + if (!hasCopyAttrs) { + target.lastModifiedTime = target.lastAccessTime = target.createTime = System.currentTimeMillis(); + } + update(target); + if (deleteSource) { + updateDelete(eSrc); + } + } finally { + endWrite(); + } + } + + private void checkParents(byte[] pathBytes) throws IOException { + beginRead(); + try { + byte[] path = pathBytes; + while ((path = getParent(path)) != null && path != ROOT_PATH) { + if (!inodes.containsKey(IndexNode.keyOf(path))) { + throw new NoSuchFileException(getString(path)); + } + } + } finally { + endRead(); + } + } + + IndexNode getInode(byte[] path) { + if (path == null) { + throw new NullPointerException("Path is null!"); + } + return inodes.get(IndexNode.keyOf(path)); + } + + Entry getEntry(byte[] path) { + IndexNode inode = getInode(path); + if (inode instanceof Entry) { + return (Entry) inode; + } + if (inode == null) { + return null; + } + return new Entry(inode.name, inode.isDir); + } + + static byte[] getParent(byte[] path) { + int off = getParentOff(path); + if (off <= 1) { + return ROOT_PATH; + } + return Arrays.copyOf(path, off); + } + + private static int getParentOff(byte[] path) { + int off = path.length - 1; + if (off > 0 && path[off] == '/') { + off--; + } + while (off > 0 && path[off] != '/') { + off--; + } + return off; + } + + private void removeFromTree(IndexNode inode) { + IndexNode parent = inodes.get(lookupKey.as(getParent(inode.name))); + IndexNode child = parent.child; + if (child.equals(inode)) { + parent.child = child.sibling; + } else { + IndexNode last = child; + while ((child = child.sibling) != null) { + if (child.equals(inode)) { + last.sibling = child.sibling; + break; + } else { + last = child; + } + } + } + } + + private void updateDelete(IndexNode inode) { + beginWrite(); + try { + removeFromTree(inode); + inodes.remove(inode); + } finally { + endWrite(); + } + } + + private void update(Entry e) { + beginWrite(); + try { + IndexNode old = inodes.put(e, e); + if (old != null) { + removeFromTree(old); + } + if (e.type == Entry.NEW || e.type == Entry.FILE_CH || e.type == Entry.COPY) { + IndexNode parent = inodes.get(lookupKey.as(getParent(e.name))); + e.sibling = parent.child; + parent.child = e; + } + } finally { + endWrite(); + } + } + + private void readAllEntries() { + MapCursor entries = NativeImageResourceFileSystemUtil.iterator(); + while (entries.advance()) { + byte[] name = getBytes(entries.getKey()); + if (!entries.getValue().isDirectory()) { + IndexNode newIndexNode = new IndexNode(name, false); + inodes.put(newIndexNode, newIndexNode); + } + } + buildNodeTree(); + } + + private void buildNodeTree() { + beginWrite(); + try { + IndexNode rootIndex = inodes.get(lookupKey.as(ROOT_PATH)); + if (rootIndex == null) { + rootIndex = new IndexNode(ROOT_PATH, true); + } else { + inodes.remove(rootIndex); + } + IndexNode[] nodes = inodes.keySet().toArray(new IndexNode[0]); + inodes.put(rootIndex, rootIndex); + ParentLookup lookup = new ParentLookup(); + for (IndexNode controlNode : nodes) { + IndexNode parent; + IndexNode node = controlNode; + while (true) { + int off = getParentOff(node.name); + if (off <= 1) { // Parent is root. + node.sibling = rootIndex.child; + rootIndex.child = node; + break; + } + lookup = lookup.as(node.name, off); + if (inodes.containsKey(lookup)) { + parent = inodes.get(lookup); + node.sibling = parent.child; + parent.child = node; + break; + } + // Add new pseudo directory entry. + parent = new IndexNode(Arrays.copyOf(node.name, off), true); + inodes.put(parent, parent); + node.sibling = parent.child; + parent.child = node; + node = parent; + } + } + } finally { + endWrite(); + } + } + + private InputStream getInputStream(Entry e) throws IOException { + InputStream eis = null; + if (e.type == Entry.NEW || e.type == Entry.COPY) { + byte[] bytes = e.getBytes(true); + if (bytes != null) { + eis = new ByteArrayInputStream(bytes); + } else { + if (e.file != null) { + eis = Files.newInputStream(e.file); + } else { + throw new NativeImageResourceFileSystemException("Entry data is missing!"); + } + } + } else { + if (e.type == Entry.FILE_CH) { + eis = Files.newInputStream(e.file); + return eis; + } + } + inputStreams.add(eis); + return eis; + } + + private OutputStream getOutputStream(Entry e) { + e.getBytes(false); + if (e.lastModifiedTime == -1) { + e.lastModifiedTime = System.currentTimeMillis(); + } + OutputStream os = new EntryOutputStream(e, new ByteArrayOutputStream((e.size > 0) ? e.size : DEFAULT_BUFFER_SIZE)); + outputStreams.add(os); + return os; + } + + InputStream newInputStream(byte[] path) throws IOException { + beginRead(); + try { + ensureOpen(); + Entry entry = getEntry(path); + if (entry == null) { + throw new NoSuchFileException(getString(path)); + } + if (entry.isDir()) { + throw new FileSystemException(getString(path), "is a directory", null); + } + return getInputStream(entry); + } finally { + endRead(); + } + } + + OutputStream newOutputStream(byte[] path, OpenOption... options) throws IOException { + boolean hasCreateNew = false; + boolean hasCreate = false; + boolean hasAppend = false; + boolean hasTruncate = false; + for (OpenOption opt : options) { + if (opt == READ) { + throw new IllegalArgumentException("READ not allowed!"); + } + if (opt == CREATE_NEW) { + hasCreateNew = true; + } + if (opt == CREATE) { + hasCreate = true; + } + if (opt == APPEND) { + hasAppend = true; + } + if (opt == TRUNCATE_EXISTING) { + hasTruncate = true; + } + } + + if (hasAppend && hasTruncate) { + throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING are not allowed!"); + } + + beginRead(); // Only need a read lock, the update will + try { // try to obtain a write lock when the os is + ensureOpen(); // being closed. + Entry e = getEntry(path); + if (e != null) { + if (e.isDir() || hasCreateNew) { + throw new FileAlreadyExistsException(getString(path)); + } + if (hasAppend) { + try (InputStream is = getInputStream(e)) { + OutputStream os = getOutputStream(new Entry(e, Entry.NEW)); + byte[] bytes = NativeImageResourceFileSystemUtil.inputStreamToByteArray(is); + os.write(bytes, 0, bytes.length); + return os; + } + } + return getOutputStream(new Entry(e, Entry.NEW)); + } else { + if (!hasCreate && !hasCreateNew) { + throw new NoSuchFileException(getString(path)); + } + checkParents(path); + return getOutputStream(new Entry(path, false)); + } + } finally { + endRead(); + } + } + + FileChannel newFileChannel(byte[] path, Set options, FileAttribute... attrs) throws IOException { + checkOptions(options); + boolean forWrite = (options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.APPEND)); + beginRead(); + try { + ensureOpen(); + Entry e = getEntry(path); + if (forWrite) { + if (e == null) { + if (!options.contains(StandardOpenOption.CREATE) && !options.contains(StandardOpenOption.CREATE_NEW)) { + throw new NoSuchFileException(getString(path)); + } + } else { + if (options.contains(StandardOpenOption.CREATE_NEW)) { + throw new FileAlreadyExistsException(getString(path)); + } + if (e.isDir()) { + throw new FileAlreadyExistsException("Directory <" + getString(path) + "> exists!"); + } + } + } else if (e == null || e.isDir()) { + throw new NoSuchFileException(getString(path)); + } + + final boolean isFCH = (e != null && e.type == Entry.FILE_CH); + final Path tmpFile = isFCH ? e.file : getTempPathForEntry(path); + final FileChannel fch = tmpFile.getFileSystem().provider().newFileChannel(tmpFile, options, attrs); + final Entry target = isFCH ? e : new Entry(path, tmpFile, Entry.FILE_CH); + return new FileChannel() { + + @Override + public int write(ByteBuffer src) throws IOException { + return fch.write(src); + } + + @Override + public long write(ByteBuffer[] src, int offset, int length) throws IOException { + return fch.write(src, offset, length); + } + + @Override + public long position() throws IOException { + return fch.position(); + } + + @Override + public FileChannel position(long newPosition) throws IOException { + fch.position(newPosition); + return this; + } + + @Override + public long size() throws IOException { + return fch.size(); + } + + @Override + public FileChannel truncate(long size) throws IOException { + fch.truncate(size); + return this; + } + + @Override + public void force(boolean metaData) throws IOException { + fch.force(metaData); + } + + @Override + public long transferTo(long position, long count, WritableByteChannel byteChannel) + throws IOException { + + return fch.transferTo(position, count, byteChannel); + } + + @Override + public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { + return fch.transferFrom(src, position, count); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + return fch.read(dst); + } + + @Override + public int read(ByteBuffer dst, long position) throws IOException { + return fch.read(dst, position); + } + + @Override + public long read(ByteBuffer[] dst, int offset, int length) throws IOException { + return fch.read(dst, offset, length); + } + + @Override + public int write(ByteBuffer src, long position) throws IOException { + return fch.write(src, position); + } + + @Override + public MappedByteBuffer map(MapMode mode, long position, long size) { + throw new UnsupportedOperationException(); + } + + @Override + public FileLock lock(long position, long size, boolean shared) throws IOException { + return fch.lock(position, size, shared); + } + + @Override + public FileLock tryLock(long position, long size, boolean shared) throws IOException { + return fch.tryLock(position, size, shared); + } + + @Override + protected void implCloseChannel() throws IOException { + fch.close(); + if (forWrite) { + target.lastModifiedTime = System.currentTimeMillis(); + target.size = (int) Files.size(target.file); + + update(target); + } else { + if (!isFCH) { + removeTempPathForEntry(tmpFile); + } + } + } + }; + } finally { + endRead(); + } + } + + Iterator iteratorOf(NativeImageResourcePath dir, DirectoryStream.Filter filter) throws IOException { + beginWrite(); + try { + ensureOpen(); + byte[] path = dir.getResolvedPath(); + IndexNode inode = getInode(path); + if (inode == null) { + throw new NotDirectoryException(getString(path)); + } + List list = new ArrayList<>(); + IndexNode child = inode.child; + while (child != null) { + byte[] childName = child.name; + NativeImageResourcePath childPath = new NativeImageResourcePath(this, childName, true); + Path childFileName = childPath.getFileName(); + Path dirPath = null; + if (childFileName != null) { + dirPath = dir.resolve(childFileName); + } + if (filter == null || (dirPath != null && filter.accept(dirPath))) { + list.add(dirPath); + } + child = child.sibling; + } + return list.iterator(); + } finally { + endWrite(); + } + } + + private static class IndexNode { + + private static final ThreadLocal cachedKey = new ThreadLocal<>(); + + byte[] name; + int hashcode; + boolean isDir; + + IndexNode child; + IndexNode sibling; + + IndexNode() { + } + + IndexNode(byte[] n) { + name(n); + } + + IndexNode(byte[] n, boolean isDir) { + name(n); + this.isDir = isDir; + } + + static IndexNode keyOf(byte[] n) { + IndexNode key = cachedKey.get(); + if (key == null) { + key = new IndexNode(n); + cachedKey.set(key); + } + return key.as(n); + } + + final void name(byte[] n) { + this.name = n; + this.hashcode = Arrays.hashCode(n); + } + + final IndexNode as(byte[] n) { + name(n); + return this; + } + + boolean isDir() { + return isDir; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof IndexNode)) { + return false; + } + if (other instanceof ParentLookup) { + return other.equals(this); + } + return Arrays.equals(name, ((IndexNode) other).name); + } + + @Override + public int hashCode() { + return hashcode; + } + } + + // For parent lookup, so we don't have to copy the parent name every time. + private static class ParentLookup extends IndexNode { + + private int length; + + ParentLookup() { + } + + ParentLookup as(byte[] n, int len) { + name(n, len); + return this; + } + + void name(byte[] n, int len) { + this.name = n; + this.length = len; + int result = 1; + for (int i = 0; i < len; i++) { + result = 31 * result + n[i]; + } + this.hashcode = result; + } + + boolean isEquals(byte[] name1, int endIndex1, byte[] name2, int endIndex2) { + if (name1.length < endIndex1 || name2.length < endIndex2) { + return false; + } + int lenToCmp = Math.min(endIndex1, endIndex2); + for (int index = 0; index < lenToCmp; index++) { + if (name1[index] != name2[index]) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof IndexNode)) { + return false; + } + byte[] otherName = ((IndexNode) other).name; + return isEquals(name, length, otherName, otherName.length); + } + } + + class Entry extends IndexNode { + + private static final int NEW = 1; // Updated contents in bytes or file. + private static final int FILE_CH = 2; // File channel update in file. + private static final int COPY = 3; // Copy entry. + + public int size; + public int type; + public long lastModifiedTime; + public long lastAccessTime; + public long createTime; + + private boolean copyOnWrite; + private byte[] bytes; + public Path file; + + Entry(byte[] name, Path file, int type) { + this(name, type, false); + this.file = file; + } + + void initTimes() { + this.lastModifiedTime = this.lastAccessTime = this.createTime = System.currentTimeMillis(); + } + + void initData() { + this.bytes = NativeImageResourceFileSystemUtil.getBytes(getString(name), true); + this.size = !isDir ? this.bytes.length : 0; + } + + byte[] getBytes(boolean readOnly) { + if (!readOnly) { + // Copy On Write technique. + if (!copyOnWrite) { + copyOnWrite = true; + this.bytes = NativeImageResourceFileSystemUtil.getBytes(getString(name), false); + } + } + return this.bytes; + } + + Entry(byte[] name, boolean isDir) { + name(name); + this.type = Entry.NEW; + this.isDir = isDir; + initData(); + initTimes(); + } + + Entry(byte[] name, int type, boolean isDir) { + name(name); + this.type = type; + this.isDir = isDir; + initData(); + initTimes(); + } + + Entry(Entry other, int type) { + name(other.name); + this.lastModifiedTime = other.lastModifiedTime; + this.lastAccessTime = other.lastAccessTime; + this.createTime = other.createTime; + this.isDir = other.isDir; + this.size = other.size; + this.bytes = other.bytes; + this.type = type; + this.copyOnWrite = true; + } + + boolean isDirectory() { + return isDir; + } + + long size() { + return size; + } + } + + class EntryOutputChannel extends ByteArrayChannel { + + Entry e; + + EntryOutputChannel(Entry e) { + super(e.size > 0 ? e.size : DEFAULT_BUFFER_SIZE, false); + this.e = e; + if (e.lastModifiedTime == -1) { + e.lastModifiedTime = System.currentTimeMillis(); + } + } + + @Override + public void close() throws IOException { + // This will update entry. + try (OutputStream os = getOutputStream(e)) { + os.write(toByteArray()); + } + super.close(); + } + } + + private class EntryOutputStream extends FilterOutputStream { + private final Entry e; + private int written; + private boolean isClosed; + + EntryOutputStream(Entry e, OutputStream os) { + super(os); + this.e = Objects.requireNonNull(e, "Entry is null!"); + } + + // Checkstyle: stop + @Override + public synchronized void write(int b) throws IOException { + out.write(b); + written += 1; + } + + @Override + public synchronized void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + written += len; + } + + @Override + public synchronized void close() throws IOException { + if (isClosed) { + return; + } + isClosed = true; + e.size = written; + if (out instanceof ByteArrayOutputStream) { + e.bytes = ((ByteArrayOutputStream) out).toByteArray(); + } + super.close(); + update(e); + } + // Checkstyle: resume + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemException.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemException.java new file mode 100644 index 0000000000000..d17553befd054 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.io.IOException; + +public class NativeImageResourceFileSystemException extends IOException { + private static final long serialVersionUID = 8000196834066748623L; + + public NativeImageResourceFileSystemException(String message) { + super(message); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemProvider.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemProvider.java new file mode 100644 index 0000000000000..883f56643fbdb --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemProvider.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import com.oracle.svm.core.jdk.JavaNetSubstitutions; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.ProviderMismatchException; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.spi.FileSystemProvider; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class NativeImageResourceFileSystemProvider extends FileSystemProvider { + + private final String resourcePath = "file:/resources"; + private NativeImageResourceFileSystem fileSystem; + private final Lock writeLock; + private final Lock readLock; + + private Path uriToPath(URI uri) { + String scheme = uri.getScheme(); + if ((scheme == null) || !scheme.equalsIgnoreCase(getScheme())) { + throw new IllegalArgumentException("URI scheme is not '" + getScheme() + "'"); + } + + try { + // Syntax for URI resource is resource:file:resources!/{entry} + return Paths.get(new URI(resourcePath)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + public NativeImageResourceFileSystemProvider() { + ReadWriteLock rwlock = new ReentrantReadWriteLock(); + this.writeLock = rwlock.writeLock(); + this.readLock = rwlock.readLock(); + } + + private static NativeImageResourcePath toResourcePath(Path path) { + if (path == null) { + throw new NullPointerException(); + } + if (!(path instanceof NativeImageResourcePath)) { + throw new ProviderMismatchException(); + } + return (NativeImageResourcePath) path; + } + + @Override + public String getScheme() { + return JavaNetSubstitutions.RESOURCE_PROTOCOL; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) { + try { + writeLock.lock(); + Path path = uriToPath(uri); + if (fileSystem != null) { + throw new FileSystemAlreadyExistsException(); + } + fileSystem = new NativeImageResourceFileSystem(this, path, env); + return fileSystem; + } finally { + writeLock.unlock(); + } + } + + @Override + public FileSystem newFileSystem(Path path, Map env) { + try { + writeLock.lock(); + if (fileSystem != null) { + throw new FileSystemAlreadyExistsException(); + } + fileSystem = new NativeImageResourceFileSystem(this, path, env); + return fileSystem; + } finally { + writeLock.unlock(); + } + } + + @Override + public FileSystem getFileSystem(URI uri) { + try { + readLock.lock(); + if (fileSystem == null) { + throw new FileSystemNotFoundException(); + } + return fileSystem; + } finally { + readLock.unlock(); + } + } + + @Override + public Path getPath(URI uri) { + return getFileSystem(uri).getPath(uri.getSchemeSpecificPart()); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + return toResourcePath(path).newByteChannel(options); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + return toResourcePath(dir).newDirectoryStream(filter); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + toResourcePath(dir).createDirectory(); + } + + @Override + public void delete(Path path) throws IOException { + toResourcePath(path).delete(); + } + + @Override + public boolean deleteIfExists(Path path) throws IOException { + return toResourcePath(path).deleteIfExists(); + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + toResourcePath(source).copy(toResourcePath(target), options); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + toResourcePath(source).move(toResourcePath(target), options); + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + return toResourcePath(path).isSameFile(path2); + } + + @Override + public boolean isHidden(Path path) { + return toResourcePath(path).isHidden(); + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + return toResourcePath(path).getFileStore(); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + toResourcePath(path).checkAccess(modes); + } + + @Override + public InputStream newInputStream(Path path, OpenOption... options) throws IOException { + return toResourcePath(path).newInputStream(options); + } + + @Override + public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException { + return toResourcePath(path).newOutputStream(options); + } + + @Override + public FileChannel newFileChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + return toResourcePath(path).newFileChannel(options, attrs); + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + return NativeImageResourceFileAttributesView.get(toResourcePath(path), type); + } + + @Override + @SuppressWarnings("unchecked") + public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { + if (type == BasicFileAttributes.class || type == NativeImageResourceFileAttributes.class) { + return (A) toResourcePath(path).getAttributes(); + } + return null; + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + return toResourcePath(path).readAttributes(attributes); + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + toResourcePath(path).setAttribute(attribute, value); + } + + void removeFileSystem() { + try { + writeLock.lock(); + fileSystem = null; + } finally { + writeLock.unlock(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java new file mode 100644 index 0000000000000..e3debb494652f --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourceFileSystemUtil.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.io.InputStream; +import java.util.Arrays; + +import org.graalvm.collections.MapCursor; +import org.graalvm.compiler.serviceprovider.JavaVersionUtil; + +import com.oracle.svm.core.jdk.Resources; + +public final class NativeImageResourceFileSystemUtil { + + private NativeImageResourceFileSystemUtil() { + } + + public static MapCursor iterator() { + return Resources.singleton().resources().getEntries(); + } + + public static byte[] getBytes(String resourceName, boolean readOnly) { + ResourceStorageEntry entry = Resources.singleton().resources().get(resourceName); + if (entry == null) { + return new byte[0]; + } + byte[] bytes = entry.getData().get(0); + if (readOnly) { + return bytes; + } else { + return Arrays.copyOf(bytes, bytes.length); + } + } + + public static int getSize(String resourceName) { + ResourceStorageEntry entry = Resources.singleton().resources().get(resourceName); + if (entry == null) { + return 0; + } else { + return entry.getData().get(0).length; + } + } + + public static String toRegexPattern(String globPattern) { + if (JavaVersionUtil.JAVA_SPEC >= 11) { + return Target_jdk_nio_zipfs_ZipUtils_JDK11OrLater.toRegexPattern(globPattern); + } else { + return Target_com_sun_nio_zipfs_ZipUtils_JDK8OrEarlier.toRegexPattern(globPattern); + } + } + + public static byte[] inputStreamToByteArray(InputStream is) { + return Resources.inputStreamToByteArray(is); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourcePath.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourcePath.java new file mode 100644 index 0000000000000..3c7bccdd5bac5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/NativeImageResourcePath.java @@ -0,0 +1,866 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.READ; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessDeniedException; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.ProviderMismatchException; +import java.nio.file.ReadOnlyFileSystemException; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + *

+ * Most of the code from this class is a copy of jdk.nio.zipfs.ZipPath with small tweaks. The main + * reason why we cannot reuse this class is that this class is final in its original implementation. + *

+ */ +public class NativeImageResourcePath implements Path { + + private final NativeImageResourceFileSystem fileSystem; + private final byte[] path; + private volatile int[] offsets; + private byte[] resolved; + private int hashcode = 0; + + public NativeImageResourcePath(NativeImageResourceFileSystem fileSystem, byte[] resourcePath) { + this(fileSystem, resourcePath, false); + } + + public NativeImageResourcePath(NativeImageResourceFileSystem fileSystem, byte[] resourcePath, boolean normalized) { + this.fileSystem = fileSystem; + if (normalized) { + this.path = resourcePath; + } else { + this.path = normalize(resourcePath); + } + } + + private void initOffsets() { + if (this.offsets == null) { + int count = 0; + int index = 0; + while (index < path.length) { + byte c = path[index++]; + if (c != '/') { + count++; + while (index < path.length && path[index] != '/') { + index++; + } + } + } + int[] result = new int[count]; + count = 0; + index = 0; + while (index < path.length) { + int m = path[index]; + if (m == '/') { + index++; + } else { + result[count++] = index++; + while (index < path.length && path[index] != '/') { + index++; + } + } + } + + // Checkstyle: stop + synchronized (this) { + if (offsets == null) { + offsets = result; + } + } + // Checkstyle: resume + } + } + + @Override + public NativeImageResourceFileSystem getFileSystem() { + return fileSystem; + } + + @Override + public boolean isAbsolute() { + return (this.path.length > 0 && path[0] == '/'); + } + + @Override + public NativeImageResourcePath getRoot() { + if (isAbsolute()) { + return new NativeImageResourcePath(fileSystem, new byte[]{this.path[0]}); + } + return null; + } + + @Override + public Path getFileName() { + initOffsets(); + int nbOffsets = offsets.length; + if (nbOffsets == 0) { + return null; + } + if (nbOffsets == 1 && path[0] != '/') { + return this; + } + int offset = offsets[nbOffsets - 1]; + int length = path.length - offset; + byte[] newPath = new byte[length]; + System.arraycopy(this.path, offset, newPath, 0, length); + return new NativeImageResourcePath(fileSystem, newPath); + } + + @Override + public NativeImageResourcePath getParent() { + initOffsets(); + int nbOffsets = offsets.length; + if (nbOffsets == 0) { + return null; + } + int length = offsets[nbOffsets - 1] - 1; + if (length <= 0) { + return getRoot(); + } + byte[] newPath = new byte[length]; + System.arraycopy(this.path, 0, newPath, 0, length); + return new NativeImageResourcePath(fileSystem, newPath); + } + + @Override + public int getNameCount() { + initOffsets(); + return offsets.length; + } + + @Override + public Path getName(int index) { + initOffsets(); + if (index < 0 || index >= offsets.length) { + throw new IllegalArgumentException(); + } + int begin = offsets[index]; + int len; + if (index == (offsets.length - 1)) { + len = path.length - begin; + } else { + len = offsets[index + 1] - begin - 1; + } + + byte[] result = new byte[len]; + System.arraycopy(path, begin, result, 0, len); + return new NativeImageResourcePath(fileSystem, result); + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + initOffsets(); + if (beginIndex < 0 || + beginIndex >= offsets.length || + endIndex > offsets.length || + beginIndex >= endIndex) { + throw new IllegalArgumentException(); + } + + int begin = offsets[beginIndex]; + int len; + if (endIndex == offsets.length) { + len = path.length - begin; + } else { + len = offsets[endIndex] - begin - 1; + } + + byte[] result = new byte[len]; + System.arraycopy(path, begin, result, 0, len); + return new NativeImageResourcePath(fileSystem, result); + } + + @Override + public boolean startsWith(Path other) { + NativeImageResourcePath p1 = this; + NativeImageResourcePath p2 = checkPath(other); + if (p1.isAbsolute() != p2.isAbsolute() || p1.path.length < p2.path.length) { + return false; + } + int length = p2.path.length; + for (int idx = 0; idx < length; idx++) { + if (p1.path[idx] != p2.path[idx]) { + return false; + } + } + return p1.path.length == p2.path.length || p2.path[length - 1] == '/' || p1.path[length] == '/'; + } + + @Override + public boolean startsWith(String other) { + return startsWith(getFileSystem().getPath(other)); + } + + @Override + public boolean endsWith(Path other) { + NativeImageResourcePath p1 = this; + NativeImageResourcePath p2 = checkPath(other); + int i1 = p1.path.length - 1; + if (i1 > 0 && p1.path[i1] == '/') { + i1--; + } + int i2 = p2.path.length - 1; + if (i2 > 0 && p2.path[i2] == '/') { + i2--; + } + if (i2 == -1) { + return i1 == -1; + } + if ((p2.isAbsolute() && (!isAbsolute() || i2 != i1)) || (i1 < i2)) { + return false; + } + for (; i2 >= 0; i1--) { + if (p2.path[i2] != p1.path[i1]) { + return false; + } + i2--; + } + return (p2.path[i2 + 1] == '/'); + } + + @Override + public boolean endsWith(String other) { + return endsWith(getFileSystem().getPath(other)); + } + + @Override + public Path normalize() { + byte[] p = getResolved(); + if (p == this.path) { + return this; + } + return new NativeImageResourcePath(fileSystem, p, true); + } + + @Override + public Path resolve(Path other) { + NativeImageResourcePath p1 = this; + NativeImageResourcePath p2 = checkPath(other); + if (p2.isAbsolute()) { + return p2; + } + byte[] result; + if (p1.path[p1.path.length - 1] == '/') { + result = new byte[p1.path.length + p2.path.length]; + System.arraycopy(p1.path, 0, result, 0, p1.path.length); + System.arraycopy(p2.path, 0, result, p1.path.length, p2.path.length); + } else { + result = new byte[p1.path.length + 1 + p2.path.length]; + System.arraycopy(p1.path, 0, result, 0, p1.path.length); + result[p1.path.length] = '/'; + System.arraycopy(p2.path, 0, result, p1.path.length + 1, p2.path.length); + } + return new NativeImageResourcePath(fileSystem, result); + } + + private static NativeImageResourcePath checkPath(Path paramPath) { + if (paramPath == null) { + throw new NullPointerException(); + } + if (!(paramPath instanceof NativeImageResourcePath)) { + throw new ProviderMismatchException(); + } + return (NativeImageResourcePath) paramPath; + } + + @Override + public Path resolve(String other) { + return resolve(getFileSystem().getPath(other)); + } + + @Override + public Path resolveSibling(Path other) { + if (other == null) { + throw new NullPointerException(); + } + NativeImageResourcePath parent = getParent(); + return parent == null ? other : parent.resolve(other); + } + + @Override + public Path resolveSibling(String other) { + return resolveSibling(getFileSystem().getPath(other)); + } + + @Override + public Path relativize(Path other) { + NativeImageResourcePath p1 = this; + NativeImageResourcePath p2 = checkPath(other); + if (p2.equals(p1)) { + return new NativeImageResourcePath(fileSystem, new byte[0], true); + } + if (p1.isAbsolute() != p2.isAbsolute()) { + throw new IllegalArgumentException(); + } + // Check how many segments are common. + int nbNames1 = p1.getNameCount(); + int nbNames2 = p2.getNameCount(); + int l = Math.min(nbNames1, nbNames2); + int nbCommon = 0; + while (nbCommon < l && equalsNameAt(p1, p2, nbCommon)) { + nbCommon++; + } + int nbUp = nbNames1 - nbCommon; + // Compute the resulting length. + int length = nbUp * 3 - 1; + if (nbCommon < nbNames2) { + length += p2.path.length - p2.offsets[nbCommon] + 1; + } + // Compute result. + byte[] result = new byte[length]; + int idx = 0; + while (nbUp-- > 0) { + result[idx++] = '.'; + result[idx++] = '.'; + if (idx < length) { + result[idx++] = '/'; + } + } + // Copy remaining segments. + if (nbCommon < nbNames2) { + System.arraycopy(p2.path, p2.offsets[nbCommon], result, idx, p2.path.length - p2.offsets[nbCommon]); + } + return new NativeImageResourcePath(fileSystem, result); + } + + @Override + public URI toUri() { + try { + return new URI( + "resource", + fileSystem.getResourcePath().toUri() + + "!" + + fileSystem.getString(toAbsolutePath().path), + null); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Override + public NativeImageResourcePath toAbsolutePath() { + if (isAbsolute()) { + return this; + } + byte[] result = new byte[path.length + 1]; + result[0] = '/'; + System.arraycopy(path, 0, result, 1, path.length); + return new NativeImageResourcePath(fileSystem, result, true); + } + + @Override + public Path toRealPath(LinkOption... options) throws IOException { + NativeImageResourcePath absolute = new NativeImageResourcePath(fileSystem, getResolvedPath()).toAbsolutePath(); + fileSystem.provider().checkAccess(absolute); + return absolute; + } + + @Override + public File toFile() { + throw new UnsupportedOperationException(); + } + + @Override + public WatchKey register(WatchService watcher, WatchEvent.Kind[] events, WatchEvent.Modifier... modifiers) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public WatchKey register(WatchService watcher, WatchEvent.Kind... events) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator() { + return new Iterator() { + private int i = 0; + + @Override + public boolean hasNext() { + return (i < getNameCount()); + } + + @Override + public Path next() { + if (i < getNameCount()) { + Path result = getName(i); + i++; + return result; + } else { + throw new NoSuchElementException(); + } + } + + @Override + public void remove() { + throw new ReadOnlyFileSystemException(); + } + }; + } + + @Override + public int compareTo(Path other) { + NativeImageResourcePath p1 = this; + NativeImageResourcePath p2 = checkPath(other); + byte[] a1 = p1.path; + byte[] a2 = p2.path; + int l1 = a1.length; + int l2 = a2.length; + for (int i = 0, l = Math.min(l1, l2); i < l; i++) { + int b1 = a1[i] & 0xFF; + int b2 = a2[i] & 0xFF; + if (b1 != b2) { + return b1 - b2; + } + } + return l1 - l2; + } + + @Override + public int hashCode() { + int h = hashcode; + if (h == 0) { + hashcode = h = Arrays.hashCode(path); + } + return h; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof NativeImageResourcePath && + this.fileSystem == ((NativeImageResourcePath) obj).fileSystem && + compareTo((Path) obj) == 0; + } + + @Override + public String toString() { + return fileSystem.getString(path); + } + + SeekableByteChannel newByteChannel(Set options) throws IOException { + return fileSystem.newByteChannel(getResolvedPath(), options); + } + + DirectoryStream newDirectoryStream(DirectoryStream.Filter filter) throws IOException { + return new NativeImageResourceDirectoryStream(this, filter); + } + + NativeImageResourceFileAttributes getAttributes() throws NoSuchFileException { + NativeImageResourceFileAttributes nativeImageResourceFileAttributes = fileSystem.getFileAttributes(getResolvedPath()); + if (nativeImageResourceFileAttributes == null) { + throw new NoSuchFileException(toString()); + } + return nativeImageResourceFileAttributes; + } + + Map readAttributes(String attributes) throws IOException { + String view; + String attrs; + int colonPos = attributes.indexOf(':'); + if (colonPos == -1) { + view = "basic"; + attrs = attributes; + } else { + view = attributes.substring(0, colonPos++); + attrs = attributes.substring(colonPos); + } + NativeImageResourceFileAttributesView raw = NativeImageResourceFileAttributesView.get(this, view); + if (raw == null) { + throw new UnsupportedOperationException("View is not supported!"); + } + return raw.readAttributes(attrs); + } + + byte[] getResolvedPath() { + byte[] r = resolved; + if (r == null) { + if (isAbsolute()) { + r = getResolved(); + } else { + r = toAbsolutePath().getResolvedPath(); + } + if (r[0] == '/') { + r = Arrays.copyOfRange(r, 1, r.length); + } + resolved = r; + } + return resolved; + } + + private byte[] normalize(byte[] resourcePath) { + if (resourcePath.length == 0) { + return resourcePath; + } + int i = 0; + for (int j = 0; j < resourcePath.length; j++) { + int k = resourcePath[j]; + if (k == '\\') { + return normalize(resourcePath, j); + } + if ((k == '/') && (i == '/')) { + return normalize(resourcePath, j - 1); + } + if (k == 0) { + throw new InvalidPathException(fileSystem.getString(resourcePath), "Path: nul character not allowed"); + } + i = k; + } + return resourcePath; + } + + private byte[] normalize(byte[] resourcePath, int index) { + byte[] arrayOfByte = new byte[resourcePath.length]; + int i = 0; + while (i < index) { + arrayOfByte[i] = resourcePath[i]; + i++; + } + int j = i; + int k = 0; + while (i < resourcePath.length) { + int m = resourcePath[i++]; + if (m == '\\') { + m = '/'; + } + if ((m != '/') || (k != '/')) { + if (m == 0) { + throw new InvalidPathException(fileSystem.getString(resourcePath), "Path: nul character not allowed"); + } + arrayOfByte[j++] = (byte) m; + k = m; + } + } + if ((j > 1) && (arrayOfByte[j - 1] == '/')) { + j--; + } + return j == arrayOfByte.length ? arrayOfByte : Arrays.copyOf(arrayOfByte, j); + } + + private byte[] getResolved() { + if (path.length == 0) { + return path; + } + for (byte c : path) { + if (c == '.') { + return doGetResolved(this); + } + } + return path; + } + + private static byte[] doGetResolved(NativeImageResourcePath p) { + int nc = p.getNameCount(); + byte[] path = p.path; + int[] offsets = p.offsets; + byte[] to = new byte[path.length]; + int[] lastM = new int[nc]; + int lastMOff = -1; + int m = 0; + for (int i = 0; i < nc; i++) { + int n = offsets[i]; + int len = (i == offsets.length - 1) ? (path.length - n) : (offsets[i + 1] - n - 1); + if (len == 1 && path[n] == (byte) '.') { + if (m == 0 && path[0] == '/') { // absolute path + to[m++] = '/'; + } + continue; + } + if (len == 2 && path[n] == '.' && path[n + 1] == '.') { + if (lastMOff >= 0) { + m = lastM[lastMOff--]; // retreat + continue; + } + if (path[0] == '/') { // "/../xyz" skip + if (m == 0) { + to[m++] = '/'; + } + } else { // "../xyz" -> "../xyz" + if (m != 0 && to[m - 1] != '/') { + to[m++] = '/'; + } + while (len-- > 0) { + to[m++] = path[n++]; + } + } + continue; + } + if (m == 0 && path[0] == '/' || // absolute path + m != 0 && to[m - 1] != '/') { // not the first name + to[m++] = '/'; + } + lastM[++lastMOff] = m; + while (len-- > 0) { + to[m++] = path[n++]; + } + } + if (m > 1 && to[m - 1] == '/') { + m--; + } + return (m == to.length) ? to : Arrays.copyOf(to, m); + } + + private static boolean equalsNameAt(NativeImageResourcePath p1, NativeImageResourcePath p2, int index) { + int beg1 = p1.offsets[index]; + int len1; + if (index == p1.offsets.length - 1) { + len1 = p1.path.length - beg1; + } else { + len1 = p1.offsets[index + 1] - beg1 - 1; + } + int beg2 = p2.offsets[index]; + int len2; + if (index == p2.offsets.length - 1) { + len2 = p2.path.length - beg2; + } else { + len2 = p2.offsets[index + 1] - beg2 - 1; + } + if (len1 != len2) { + return false; + } + for (int n = 0; n < len1; n++) { + if (p1.path[beg1 + n] != p2.path[beg2 + n]) { + return false; + } + } + return true; + } + + boolean exists() { + return fileSystem.exists(getResolvedPath()); + } + + FileStore getFileStore() throws IOException { + if (exists()) { + return NativeImageResourceFileSystem.getFileStore(this); + } + throw new NoSuchFileException(fileSystem.getString(path)); + } + + boolean isSameFile(Path other) throws IOException { + if (this.equals(other)) { + return true; + } + if (other == null || this.getFileSystem() != other.getFileSystem()) { + return false; + } + this.checkAccess(); + ((NativeImageResourcePath) other).checkAccess(); + return Arrays.equals(this.getResolvedPath(), ((NativeImageResourcePath) other).getResolvedPath()); + } + + void checkAccess(AccessMode... modes) throws IOException { + boolean x = false; + for (AccessMode mode : modes) { + switch (mode) { + case READ: + case WRITE: + break; + case EXECUTE: + x = true; + break; + default: + throw new UnsupportedOperationException(); + } + } + fileSystem.checkAccess(getResolvedPath()); + if (x) { + throw new AccessDeniedException(toString()); + } + } + + void setAttribute(String attribute, Object value) throws IOException { + String type; + String attr; + int colonPos = attribute.indexOf(':'); + if (colonPos == -1) { + type = "basic"; + attr = attribute; + } else { + type = attribute.substring(0, colonPos++); + attr = attribute.substring(colonPos); + } + NativeImageResourceFileAttributesView view = NativeImageResourceFileAttributesView.get(this, type); + if (view == null) { + throw new UnsupportedOperationException("View is not supported"); + } + view.setAttribute(attr, value); + } + + void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws NoSuchFileException { + fileSystem.setTimes(getResolvedPath(), lastModifiedTime, lastAccessTime, createTime); + } + + void createDirectory() throws IOException { + fileSystem.createDirectory(getResolvedPath()); + } + + void delete() throws IOException { + fileSystem.deleteFile(getResolvedPath(), true); + } + + boolean deleteIfExists() throws IOException { + return fileSystem.deleteFile(getResolvedPath(), false); + } + + void move(NativeImageResourcePath target, CopyOption... options) throws IOException { + if (Files.isSameFile(this.fileSystem.getResourcePath(), target.fileSystem.getResourcePath())) { + fileSystem.copyFile(true, getResolvedPath(), target.getResolvedPath(), options); + } else { + copyToTarget(target, options); + delete(); + } + } + + void copy(NativeImageResourcePath target, CopyOption... options) throws IOException { + if (Files.isSameFile(this.fileSystem.getResourcePath(), target.fileSystem.getResourcePath())) { + fileSystem.copyFile(false, getResolvedPath(), target.getResolvedPath(), options); + } else { + copyToTarget(target, options); + } + } + + private void copyToTarget(NativeImageResourcePath target, CopyOption... options) throws IOException { + boolean replaceExisting = false; + boolean copyAttrs = false; + for (CopyOption opt : options) { + if (opt == REPLACE_EXISTING) { + replaceExisting = true; + } else if (opt == COPY_ATTRIBUTES) { + copyAttrs = true; + } + } + // Attributes of source file. + NativeImageResourceFileAttributes nativeImageResourceFileAttributes = getAttributes(); + + // Check if target exists. + boolean exists; + if (replaceExisting) { + try { + target.deleteIfExists(); + exists = false; + } catch (DirectoryNotEmptyException x) { + exists = true; + } + } else { + exists = target.exists(); + } + if (exists) { + throw new FileAlreadyExistsException(target.toString()); + } + + if (nativeImageResourceFileAttributes.isDirectory()) { + // Create directory or file. + target.createDirectory(); + } else { + try (InputStream is = fileSystem.newInputStream(getResolvedPath())) { + try (OutputStream os = target.newOutputStream()) { + byte[] buf = new byte[8192]; + int n; + while ((n = is.read(buf)) != -1) { + os.write(buf, 0, n); + } + } + } + } + if (copyAttrs) { + BasicFileAttributeView attributeView = NativeImageResourceFileAttributesView.get(target, BasicFileAttributeView.class); + try { + attributeView.setTimes(nativeImageResourceFileAttributes.lastModifiedTime(), nativeImageResourceFileAttributes.lastAccessTime(), + nativeImageResourceFileAttributes.creationTime()); + } catch (IOException e) { + try { + target.delete(); + } catch (IOException ignore) { + } + throw e; + } + } + } + + InputStream newInputStream(OpenOption... options) throws IOException { + if (options.length > 0) { + for (OpenOption opt : options) { + if (opt != READ) { + throw new UnsupportedOperationException("'" + opt + "' not allowed"); + } + } + } + return fileSystem.newInputStream(getResolvedPath()); + } + + OutputStream newOutputStream(OpenOption... options) throws IOException { + if (options.length == 0) { + return fileSystem.newOutputStream(getResolvedPath(), CREATE, TRUNCATE_EXISTING, WRITE); + } + return fileSystem.newOutputStream(getResolvedPath(), options); + } + + FileChannel newFileChannel(Set options, FileAttribute... attrs) throws IOException { + return fileSystem.newFileChannel(getResolvedPath(), options, attrs); + } + + boolean isHidden() { + return false; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java new file mode 100644 index 0000000000000..701e192fe7c2d --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceStorageEntry.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.util.ArrayList; +import java.util.List; + +public class ResourceStorageEntry { + + private final boolean isDirectory; + private final List data; + + public ResourceStorageEntry(boolean isDirectory) { + this.isDirectory = isDirectory; + this.data = new ArrayList<>(); + } + + public boolean isDirectory() { + return isDirectory; + } + + public List getData() { + return data; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java new file mode 100644 index 0000000000000..3aa271c5f30d4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/ResourceURLConnection.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.List; + +import com.oracle.svm.core.jdk.Resources; + +public class ResourceURLConnection extends URLConnection { + + private final URL url; + private final int index; + private byte[] data; + + public ResourceURLConnection(URL url) { + this(url, 0); + } + + public ResourceURLConnection(URL url, int index) { + super(url); + this.url = url; + this.index = index; + } + + private static String resolveName(String resourceName) { + return resourceName.startsWith("/") ? resourceName.substring(1) : resourceName; + } + + @Override + public void connect() { + if (connected) { + return; + } + connected = true; + + String resourceName = resolveName(url.getPath()); + ResourceStorageEntry entry = Resources.get(resourceName); + if (entry != null) { + List bytes = entry.getData(); + if (index < bytes.size()) { + this.data = bytes.get(index); + } else { + // This will happen only in case that we are creating one URL with the second URL as + // a context. + this.data = bytes.get(0); + } + } else { + this.data = null; + } + } + + @Override + public InputStream getInputStream() throws IOException { + // Operations that depend on being connected will implicitly perform the connection, if + // necessary. + connect(); + if (data == null) { + throw new FileNotFoundException(url.toString()); + } + return new ByteArrayInputStream(data); + } + + @Override + public long getContentLengthLong() { + // Operations that depend on being connected will implicitly perform the connection, if + // necessary. + connect(); + return data != null ? data.length : -1L; + } + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_com_sun_nio_zipfs_ZipUtils_JDK8OrEarlier.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_com_sun_nio_zipfs_ZipUtils_JDK8OrEarlier.java new file mode 100644 index 0000000000000..bfb0171e6fa28 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_com_sun_nio_zipfs_ZipUtils_JDK8OrEarlier.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jdk.JDK8OrEarlier; + +@TargetClass(className = "com.sun.nio.zipfs.ZipUtils", onlyWith = JDK8OrEarlier.class) +@SuppressWarnings({"unused", "static-method"}) +final class Target_com_sun_nio_zipfs_ZipUtils_JDK8OrEarlier { + + @Alias + public static native String toRegexPattern(String globPattern); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_jdk_nio_zipfs_ZipUtils_JDK11OrLater.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_jdk_nio_zipfs_ZipUtils_JDK11OrLater.java new file mode 100644 index 0000000000000..2d8d5d3639f9d --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/Target_jdk_nio_zipfs_ZipUtils_JDK11OrLater.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jdk.resources; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jdk.JDK11OrLater; + +@TargetClass(className = "jdk.nio.zipfs.ZipUtils", onlyWith = JDK11OrLater.class) +@SuppressWarnings({"unused", "static-method"}) +final class Target_jdk_nio_zipfs_ZipUtils_JDK11OrLater { + + @Alias + public static native String toRegexPattern(String globPattern); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java index ca26a030d23e0..a14ea09baa9ce 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java @@ -32,12 +32,15 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.FileSystem; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -51,6 +54,7 @@ import org.graalvm.compiler.options.Option; import org.graalvm.compiler.options.OptionType; import org.graalvm.compiler.serviceprovider.JavaVersionUtil; +import org.graalvm.home.HomeFinder; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; @@ -61,6 +65,10 @@ import com.oracle.svm.core.configure.ResourcesRegistry; import com.oracle.svm.core.jdk.Resources; import com.oracle.svm.core.jdk.localization.LocalizationFeature; +import com.oracle.svm.core.jdk.resources.NativeImageResourceFileAttributes; +import com.oracle.svm.core.jdk.resources.NativeImageResourceFileAttributesView; +import com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystem; +import com.oracle.svm.core.jdk.resources.NativeImageResourceFileSystemProvider; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.option.LocatableMultiOptionValue; import com.oracle.svm.core.util.UserError; @@ -69,6 +77,34 @@ import com.oracle.svm.hosted.config.ConfigurationParserUtils; import com.oracle.svm.util.ModuleSupport; +/** + *

+ * Resources are collected at build time in this feature and stored in a hash map in + * {@link Resources} class. + *

+ * + *

+ * {@link NativeImageResourceFileSystemProvider } is a core class for building a custom file system + * on top of resources in the native image. + *

+ * + *

+ * The {@link NativeImageResourceFileSystemProvider} provides most of the functionality of a + * {@link FileSystem}. It is an in-memory file system that upon creation contains a copy of the + * resources included in the native-image. Note that changes to files do not affect actual resources + * returned by resource manipulation methods like `Class.getResource`. Upon being closed, all + * changes are discarded. + *

+ * + *

+ * As with other file system providers, these methods provide a low-level interface and are not + * meant for direct usage - see {@link java.nio.file.Files} + *

+ * + * @see NativeImageResourceFileSystem + * @see NativeImageResourceFileAttributes + * @see NativeImageResourceFileAttributesView + */ @AutomaticFeature public final class ResourcesFeature implements Feature { @@ -81,21 +117,21 @@ public static class Options { } private boolean sealed = false; - private Set newResources = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private Set ignoredResources = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set resourcePatternWorkSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set excludedResourcePatterns = Collections.newSetFromMap(new ConcurrentHashMap<>()); private int loadedConfigurations; private class ResourcesRegistryImpl implements ResourcesRegistry { @Override public void addResources(String pattern) { UserError.guarantee(!sealed, "Resources added too late: %s", pattern); - newResources.add(pattern); + resourcePatternWorkSet.add(pattern); } @Override public void ignoreResources(String pattern) { UserError.guarantee(!sealed, "Resources ignored too late: %s", pattern); - ignoredResources.add(pattern); + excludedResourcePatterns.add(pattern); } @Override @@ -117,20 +153,20 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { ConfigurationFiles.Options.ResourceConfigurationFiles, ConfigurationFiles.Options.ResourceConfigurationResources, ConfigurationFile.RESOURCES.getFileName()); - newResources.addAll(Options.IncludeResources.getValue().values()); - ignoredResources.addAll(Options.ExcludeResources.getValue().values()); + resourcePatternWorkSet.addAll(Options.IncludeResources.getValue().values()); + excludedResourcePatterns.addAll(Options.ExcludeResources.getValue().values()); } @Override public void duringAnalysis(DuringAnalysisAccess access) { - if (newResources.isEmpty()) { + if (resourcePatternWorkSet.isEmpty()) { return; } access.requireAnalysisIteration(); DebugContext debugContext = ((DuringAnalysisAccessImpl) access).getDebugContext(); - final Pattern[] includePatterns = compilePatterns(newResources); - final Pattern[] excludePatterns = compilePatterns(ignoredResources); + final Pattern[] includePatterns = compilePatterns(resourcePatternWorkSet); + final Pattern[] excludePatterns = compilePatterns(excludedResourcePatterns); if (JavaVersionUtil.JAVA_SPEC > 8) { try { @@ -151,32 +187,40 @@ public void duringAnalysis(DuringAnalysisAccess access) { * @formatter:on */ - final Set todo = new HashSet<>(); - // Checkstyle: stop final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + final LinkedHashSet userClasspathFiles = new LinkedHashSet<>(); + final LinkedHashSet supportLibraries = new LinkedHashSet<>(); + String homeFolder = HomeFinder.getInstance().getHomeFolder().toString(); if (contextClassLoader instanceof URLClassLoader) { for (URL url : ((URLClassLoader) contextClassLoader).getURLs()) { try { final File file = new File(url.toURI()); - todo.add(file); + // Make sure the user resources are the first to be registered. + if (file.getAbsolutePath().startsWith(homeFolder)) { + supportLibraries.add(file); + } else { + userClasspathFiles.add(file); + } } catch (URISyntaxException | IllegalArgumentException e) { - throw UserError.abort("Unable to handle imagecp element '%s'. Make sure that all imagecp entries are either directories or valid jar files.", url.toExternalForm()); + throw UserError.abort("Unable to handle image classpath element '%s'. Make sure that all image classpath entries are either directories or valid jar files.", url.toExternalForm()); } } } - // Checkstyle: resume - for (File element : todo) { + + userClasspathFiles.addAll(supportLibraries); + for (File classpathFile : userClasspathFiles) { try { - if (element.isDirectory()) { - scanDirectory(debugContext, element, "", includePatterns, excludePatterns); + if (classpathFile.isDirectory()) { + scanDirectory(debugContext, classpathFile, includePatterns, excludePatterns); } else { - scanJar(debugContext, element, includePatterns, excludePatterns); + scanJar(debugContext, classpathFile, includePatterns, excludePatterns); } } catch (IOException ex) { - throw UserError.abort("Unable to handle classpath element '%s'. Make sure that all classpath entries are either directories or valid jar files.", element); + throw UserError.abort("Unable to handle classpath element '%s'. Make sure that all classpath entries are either directories or valid jar files.", classpathFile); } } - newResources.clear(); + + resourcePatternWorkSet.clear(); } private static Pattern[] compilePatterns(Set patterns) { @@ -203,45 +247,36 @@ public void beforeCompilation(BeforeCompilationAccess access) { } } - private void scanDirectory(DebugContext debugContext, File f, String relativePath, Pattern[] includePatterns, Pattern[] excludePatterns) throws IOException { - if (f.isDirectory()) { - File[] files = f.listFiles(); - if (files == null) { - throw UserError.abort("Cannot scan directory %s", f); - } else { - for (File ch : files) { - scanDirectory(debugContext, ch, relativePath.isEmpty() ? ch.getName() : relativePath + "/" + ch.getName(), includePatterns, excludePatterns); - } - } - } else { - if (matches(includePatterns, excludePatterns, relativePath)) { - try (FileInputStream is = new FileInputStream(f)) { - registerResource(debugContext, relativePath, is); - } - } - } - } - - private static void scanJar(DebugContext debugContext, File element, Pattern[] includePatterns, Pattern[] excludePatterns) throws IOException { - JarFile jf = new JarFile(element); - Enumeration en = jf.entries(); - + private static void scanDirectory(DebugContext debugContext, File root, Pattern[] includePatterns, Pattern[] excludePatterns) throws IOException { Map> matchedDirectoryResources = new HashMap<>(); Set allEntries = new HashSet<>(); - while (en.hasMoreElements()) { - JarEntry e = en.nextElement(); - if (e.isDirectory()) { - String dirName = e.getName().substring(0, e.getName().length() - 1); - allEntries.add(dirName); - if (matches(includePatterns, excludePatterns, dirName)) { - matchedDirectoryResources.put(dirName, new ArrayList<>()); - } - continue; + ArrayList queue = new ArrayList<>(); + + queue.add(root); + while (!queue.isEmpty()) { + File file = queue.remove(0); + String relativeFilePath = ""; + if (file != root) { + relativeFilePath = file.getAbsolutePath().substring(root.getAbsolutePath().length() + 1); } - allEntries.add(e.getName()); - if (matches(includePatterns, excludePatterns, e.getName())) { - try (InputStream is = jf.getInputStream(e)) { - registerResource(debugContext, e.getName(), is); + if (file.isDirectory()) { + if (!relativeFilePath.isEmpty()) { + allEntries.add(relativeFilePath); + } + if (matches(includePatterns, excludePatterns, relativeFilePath)) { + matchedDirectoryResources.put(relativeFilePath, new ArrayList<>()); + } + File[] files = file.listFiles(); + if (files == null) { + throw UserError.abort("Cannot scan directory %s", file); + } + queue.addAll(Arrays.asList(files)); + } else { + allEntries.add(relativeFilePath); + if (matches(includePatterns, excludePatterns, relativeFilePath)) { + try (InputStream is = new FileInputStream(file)) { + registerResource(debugContext, relativeFilePath, is); + } } } } @@ -251,7 +286,7 @@ private static void scanJar(DebugContext debugContext, File element, Pattern[] i String key = last == -1 ? "" : entry.substring(0, last); List dirContent = matchedDirectoryResources.get(key); if (dirContent != null && !dirContent.contains(entry)) { - dirContent.add(entry.substring(last + 1, entry.length())); + dirContent.add(entry.substring(last + 1)); } } @@ -261,6 +296,27 @@ private static void scanJar(DebugContext debugContext, File element, Pattern[] i }); } + private static void scanJar(DebugContext debugContext, File root, Pattern[] includePatterns, Pattern[] excludePatterns) throws IOException { + JarFile jf = new JarFile(root); + Enumeration entries = jf.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + String dirName = entry.getName().substring(0, entry.getName().length() - 1); + if (matches(includePatterns, excludePatterns, dirName)) { + // Directory content is empty in JAR scanning. + registerDirectoryResource(debugContext, dirName, ""); + } + } else { + if (matches(includePatterns, excludePatterns, entry.getName())) { + try (InputStream is = jf.getInputStream(entry)) { + registerResource(debugContext, entry.getName(), is); + } + } + } + } + } + private static boolean matches(Pattern[] includePatterns, Pattern[] excludePatterns, String relativePath) { for (Pattern p : excludePatterns) { if (p.matcher(relativePath).matches()) { diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java new file mode 100644 index 0000000000000..27288f5da2cf1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java @@ -0,0 +1,577 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.test; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.configure.ResourcesRegistry; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.junit.Assert; +import org.junit.Test; + +public class NativeImageResourceFileSystemProviderTest { + + private static final String RESOURCE_DIR = "/resources"; + private static final String RESOURCE_FILE_1 = RESOURCE_DIR + "/resource-test1.txt"; + private static final String RESOURCE_FILE_2 = RESOURCE_DIR + "/resource-test2.txt"; + private static final String NEW_DIRECTORY = RESOURCE_DIR + "/tmp"; + + private static final int TIME_SPAN = 1_000_000; + + // Register resources. + @SuppressWarnings("unused") + @AutomaticFeature + private static final class RegisterResourceFeature implements Feature { + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + RuntimeClassInitialization.initializeAtBuildTime(NativeImageResourceFileSystemProviderTest.class); + ResourcesRegistry registry = ImageSingletons.lookup(ResourcesRegistry.class); + registry.addResources(RESOURCE_FILE_1); + registry.addResources(RESOURCE_FILE_2); + } + } + + private static URL resourceNameToURL(String resourceName) { + URL resource = NativeImageResourceFileSystemProviderTest.class.getResource(resourceName); + Assert.assertNotNull("Resource " + resourceName + " is not found!", resource); + return resource; + } + + private static URI resourceNameToURI(String resourceName) { + try { + return resourceNameToURL(resourceName).toURI(); + } catch (URISyntaxException e) { + Assert.fail("Bad URI syntax!"); + } + return null; + } + + private static Path resourceNameToPath(String resourceName) { + return Paths.get(resourceNameToURI(resourceName)); + } + + private static boolean compareTwoURLs(URL url1, URL url2) throws IOException { + URLConnection url1Connection = url1.openConnection(); + URLConnection url2Connection = url2.openConnection(); + + try (InputStream is1 = url1Connection.getInputStream(); InputStream is2 = url2Connection.getInputStream()) { + Assert.assertNotNull("First input stream is null!", is1); + Assert.assertNotNull("Second input stream is null!", is2); + int nextByte1 = is1.read(); + int nextByte2 = is2.read(); + while (nextByte1 != -1 && nextByte2 != -1) { + if (nextByte1 != nextByte2) { + return false; + } + + nextByte1 = is1.read(); + nextByte2 = is2.read(); + } + return nextByte1 == -1 && nextByte2 == -1; + } + } + + private static FileSystem createNewFileSystem() { + URI resource = resourceNameToURI(RESOURCE_FILE_1); + + Map env = new HashMap<>(); + env.put("create", "true"); + + FileSystem fileSystem = null; + boolean exceptionThrown = false; + try { + // Try to get file system. This should raise exception. + fileSystem = FileSystems.getFileSystem(resource); + } catch (FileSystemNotFoundException e) { + // File system not found. Create new one. + exceptionThrown = true; + try { + fileSystem = FileSystems.newFileSystem(resource, env); + } catch (IOException ioException) { + Assert.fail("Error during creating a new file system!"); + } + } + + Assert.assertTrue("File system is already created!", exceptionThrown); + Assert.assertNotNull("File system is not created!", fileSystem); + + return fileSystem; + } + + private static void closeFileSystem(FileSystem fileSystem) { + try { + fileSystem.close(); + } catch (IOException e) { + Assert.fail("Exception occurs during closing file system!"); + } + } + + /** + *

+ * Combining URL and resource operations. + *

+ * + *

+ * Description: Test inspired by issues:
+ *

    + *
  1. 1349
  2. + *
  3. 2291
  4. + *
+ *

+ */ + @Test + public void githubIssues() { + try { + URL url1 = resourceNameToURL(RESOURCE_FILE_1); + URL url2 = new URL(url1, RESOURCE_FILE_2); + Assert.assertNotNull("Second URL is null!", url2); + Assert.assertFalse("Two URLs are same!", compareTwoURLs(url1, url2)); + } catch (IOException e) { + Assert.fail("Exception occurs during URL operations!"); + } + } + + /** + *

+ * Reading from file using {@link java.nio.channels.ByteChannel}. + *

+ * + *

+ * Description: We are doing next operations:
+ *

    + *
  1. Create new file system
  2. + *
  3. Reading from file
  4. + *
  5. Closing file system
  6. + *
+ *

+ */ + @Test + public void readingFileByteChannel() { + // 1. Creating new file system. + FileSystem fileSystem = createNewFileSystem(); + + Path resourceDirectory = fileSystem.getPath(RESOURCE_DIR); + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1); + + // 2. Reading from file. + try (SeekableByteChannel channel = Files.newByteChannel(resourceDirectory, StandardOpenOption.READ)) { + ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer); + Assert.fail("Trying to read from directory as a file!"); + } catch (IOException ignored) { + } + + try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ)) { + ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer); + String content = new String(byteBuffer.array()); + Assert.assertTrue("Nothing has been read from file!", content.length() > 0); + } catch (IOException ioException) { + Assert.fail("Exception occurs during reading from file!"); + } + + // 3. Closing file system. + closeFileSystem(fileSystem); + } + + /** + *

+ * Writing into file using {@link java.nio.channels.ByteChannel}. + *

+ * + *

+ * Description: We are doing next operations:
+ *

    + *
  1. Create new file system
  2. + *
  3. Writing into file
  4. + *
  5. Closing file system
  6. + *
+ *

+ */ + @Test + public void writingFileByteChannel() { + // 1. Creating new file system. + FileSystem fileSystem = createNewFileSystem(); + + Path resourceDirectory = fileSystem.getPath(RESOURCE_DIR); + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1); + + // 2. Writing into file. + try { + Files.newByteChannel(resourceDirectory, StandardOpenOption.WRITE); + Assert.fail("Trying to write into directory as a file!"); + } catch (IOException ignored) { + } + + try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ)) { + ByteBuffer byteBuffer = ByteBuffer.wrap("test string".getBytes()); + channel.write(byteBuffer); + Assert.fail("Wrong write permissions!"); + } catch (IOException | NonWritableChannelException ignored) { + } + + try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ, StandardOpenOption.WRITE)) { + ByteBuffer byteBuffer = ByteBuffer.wrap("test string#".getBytes()); + channel.write(byteBuffer); + ByteBuffer byteBuffer2 = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer2); + String content = new String(byteBuffer.array()); + Assert.assertTrue("Nothing has been writen into file!", content.length() > 0); + Assert.assertTrue("Content has been writen into file improperly!", content.startsWith("test string#")); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + + // 3. Closing file system. + closeFileSystem(fileSystem); + } + + /** + *

+ * Reading from file using {@link java.nio.channels.FileChannel}. + *

+ * + *

+ * Description: We are doing next operations:
+ *

    + *
  1. Create new file system
  2. + *
  3. Reading from file
  4. + *
  5. Closing file system
  6. + *
+ *

+ */ + @Test + public void readingFileFileChannel() { + // 1. Creating new file system. + FileSystem fileSystem = createNewFileSystem(); + + Path resourceDirectory = fileSystem.getPath(RESOURCE_DIR); + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1); + + // 2. Reading from file. + Set permissions = Collections.singleton(StandardOpenOption.READ); + try (SeekableByteChannel channel = fileSystem.provider().newFileChannel(resourceDirectory, permissions)) { + ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer); + Assert.fail("Trying to read from directory as a file!"); + } catch (IOException ignored) { + } + + try (SeekableByteChannel channel = fileSystem.provider().newFileChannel(resourceFile1, permissions)) { + ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer); + String content = new String(byteBuffer.array()); + Assert.assertTrue("Nothing has been read from file!", content.length() > 0); + } catch (IOException ioException) { + Assert.fail("Exception occurs during reading from file!"); + } + + // 3. Closing file system. + closeFileSystem(fileSystem); + } + + /** + *

+ * Writing into file using {@link java.nio.channels.FileChannel}. + *

+ * + *

+ * Description: We are doing next operations:
+ *

    + *
  1. Create new file system
  2. + *
  3. Writing into file
  4. + *
  5. Closing file system
  6. + *
+ *

+ */ + @Test + public void writingFileFileChannel() { + // 1. Creating new file system. + FileSystem fileSystem = createNewFileSystem(); + FileSystemProvider provider = fileSystem.provider(); + + Path resourceDirectory = fileSystem.getPath(RESOURCE_DIR); + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1); + + Set readPermissions = Collections.singleton(StandardOpenOption.READ); + Set writePermissions = Collections.singleton(StandardOpenOption.WRITE); + Set readWritePermissions = new HashSet<>(Collections.emptySet()); + readWritePermissions.addAll(readPermissions); + readWritePermissions.addAll(writePermissions); + + // 2. Writing into file. + try { + provider.newFileChannel(resourceDirectory, writePermissions); + Assert.fail("Trying to write into directory as a file!"); + } catch (IOException ignored) { + } + + try (SeekableByteChannel channel = provider.newFileChannel(resourceFile1, readPermissions)) { + ByteBuffer byteBuffer = ByteBuffer.wrap("test string".getBytes()); + channel.write(byteBuffer); + Assert.fail("Wrong write permissions!"); + } catch (IOException | NonWritableChannelException ignored) { + } + + try (SeekableByteChannel channel = provider.newFileChannel(resourceFile1, readWritePermissions)) { + ByteBuffer byteBuffer = ByteBuffer.wrap("test string#".getBytes()); + channel.write(byteBuffer); + ByteBuffer byteBuffer2 = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer2); + String content = new String(byteBuffer.array()); + Assert.assertTrue("Nothing has been writen into file!", content.length() > 0); + Assert.assertTrue("Content has been writen into file improperly!", content.startsWith("test string#")); + } catch (IOException ioException) { + Assert.fail("Exception occurs during writing into file!"); + } + + // 3. Closing file system. + closeFileSystem(fileSystem); + } + + /** + *

+ * Basic file system operations. + *

+ * + *

+ * Description: We are doing next operations:
+ *

    + *
  1. Create new file system
  2. + *
  3. Creating new directory
  4. + *
  5. Copy file to newly create directory
  6. + *
  7. Moving file to newly create directory
  8. + *
  9. Listing newly create directory
  10. + *
  11. Deleting file from newly created directory
  12. + *
  13. Closing file system
  14. + *
+ *

+ */ + @Test + public void fileSystemOperations() { + // 1. Creating new file system. + FileSystem fileSystem = createNewFileSystem(); + + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1); + Path resourceFile2 = resourceNameToPath(RESOURCE_FILE_2); + + // 2. Creating new directory. + Path newDirectory = fileSystem.getPath(NEW_DIRECTORY); + try { + Files.createDirectory(newDirectory); + } catch (IOException ioException) { + Assert.fail("Exception occurs during creating new directory!"); + } + + // 3. Copy file to newly create directory. + Path destination = fileSystem.getPath(newDirectory.toString(), + resourceFile1.getName(resourceFile1.getNameCount() - 1).toString()); + try { + Files.copy(resourceFile1, destination, StandardCopyOption.REPLACE_EXISTING); + try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ)) { + ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer); + String content = new String(byteBuffer.array()); + Assert.assertTrue("Nothing has been read from new file!", content.length() > 0); + } + } catch (IOException ioException) { + Assert.fail("Exception occurs during copying file into new directory!"); + } + + // 4. Moving file to newly create directory. + destination = fileSystem.getPath(newDirectory.toString(), + resourceFile2.getName(resourceFile2.getNameCount() - 1).toString()); + try { + Files.move(resourceFile2, destination, StandardCopyOption.REPLACE_EXISTING); + try (SeekableByteChannel channel = Files.newByteChannel(destination, StandardOpenOption.READ)) { + ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size()); + channel.read(byteBuffer); + String content = new String(byteBuffer.array()); + Assert.assertTrue("Nothing has been read from new file!", content.length() > 0); + } + } catch (IOException ioException) { + Assert.fail("Exception occurs during moving file into new directory!"); + } + + try { + Files.newByteChannel(resourceFile2, StandardOpenOption.READ); + Assert.fail("File is still existing after deletion!"); + } catch (IOException ignored) { + } + + // 5. Listing newly create directory. + try (DirectoryStream directoryStream = Files.newDirectoryStream(newDirectory)) { + Iterator iterator = directoryStream.iterator(); + boolean anyEntry = false; + while (iterator.hasNext()) { + Path path = iterator.next(); + Assert.assertNotNull("Path is null!", path); + anyEntry = true; + } + Assert.assertTrue("New directory is empty!", anyEntry); + } catch (IOException e) { + Assert.fail("Exception occurs during listing new directory!"); + } + + // 6. Deleting file from newly created directory. + Path target = fileSystem.getPath(newDirectory.toString(), + resourceFile1.getName(resourceFile1.getNameCount() - 1).toString()); + try { + Files.delete(target); + try { + Files.newByteChannel(target, StandardOpenOption.READ); + Assert.fail("File is still existing after deletion!"); + } catch (IOException ignored) { + } + } catch (IOException ignored) { + Assert.fail("Exception occurs during file deletion!"); + } + + // 7. Closing file system. + closeFileSystem(fileSystem); + } + + /** + *

+ * Reading file/directory attributes. + *

+ * + *

+ * Description: We are doing next operations:
+ *

    + *
  1. Create new file system
  2. + *
  3. Reading file attributes
  4. + *
  5. Reading directory attributes
  6. + *
  7. Closing file system
  8. + *
+ *

+ */ + @Test + public void readingFileAttributes() { + // 1. Creating new file system. + FileSystem fileSystem = createNewFileSystem(); + + Path resourceDirectory = fileSystem.getPath(RESOURCE_DIR); + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1); + + try { + // 2. Reading file attributes. + BasicFileAttributes fileAttributes = Files.readAttributes(resourceFile1, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + Assert.assertNotNull("fileAttributes.lastAccessTime is null!", fileAttributes.lastAccessTime()); + Assert.assertNotNull("fileAttributes.creationTime is null!", fileAttributes.creationTime()); + Assert.assertNotNull("fileAttributes.lastModifiedTime is null!", fileAttributes.lastModifiedTime()); + Assert.assertFalse("fileAttributes.isDirectory is true!", fileAttributes.isDirectory()); + Assert.assertTrue("fileAttributes.isRegularFile is false!", fileAttributes.isRegularFile()); + Assert.assertFalse("fileAttributes.isOther is true!", fileAttributes.isOther()); + Assert.assertFalse("fileAttributes.isSymbolicLink is true!", fileAttributes.isSymbolicLink()); + Assert.assertNull("fileAttributes.fileKey is not null!", fileAttributes.fileKey()); + + // 3. Reading directory attributes. + BasicFileAttributes directoryAttributes = Files.readAttributes(resourceDirectory, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + Assert.assertNotNull("directoryAttributes.lastAccessTime is null!", directoryAttributes.lastAccessTime()); + Assert.assertNotNull("directoryAttributes.creationTime is null!", directoryAttributes.creationTime()); + Assert.assertNotNull("directoryAttributes.lastModifiedTime is null!", directoryAttributes.lastModifiedTime()); + Assert.assertTrue("directoryAttributes.isDirectory is false!", directoryAttributes.isDirectory()); + Assert.assertFalse("directoryAttributes.isRegularFile is true!", directoryAttributes.isRegularFile()); + Assert.assertFalse("directoryAttributes.isOther is true!", directoryAttributes.isOther()); + Assert.assertFalse("directoryAttributes.isSymbolicLink is true!", directoryAttributes.isSymbolicLink()); + Assert.assertNull("directoryAttributes.fileKey is not null!", directoryAttributes.fileKey()); + } catch (IOException e) { + Assert.fail("Exception occurs during attributes operations!"); + } + + // 4. Closing file system. + closeFileSystem(fileSystem); + } + + /** + *

+ * Writing file attributes. + *

+ * + *

+ * Description: We are doing next operations:
+ *

    + *
  1. Create new file system
  2. + *
  3. Writing file attributes
  4. + *
  5. Closing file system
  6. + *
+ *

+ */ + @Test + public void writingFileAttributes() { + // 1. Creating new file system. + FileSystem fileSystem = createNewFileSystem(); + Path resourceFile1 = resourceNameToPath(RESOURCE_FILE_1); + + try { + // 2. Writing file attributes. + long lastModifiedTime = System.currentTimeMillis() + TIME_SPAN; + Files.setAttribute(resourceFile1, "lastModifiedTime", FileTime.fromMillis(lastModifiedTime), + LinkOption.NOFOLLOW_LINKS); + BasicFileAttributes fileAttributes = Files.readAttributes(resourceFile1, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + Assert.assertEquals("lastModifiedTime is not set properly!", fileAttributes.lastModifiedTime(), + FileTime.fromMillis(lastModifiedTime)); + } catch (IOException e) { + Assert.fail("Exception occurs during attributes operations!"); + } + + // 3. Closing file system. + closeFileSystem(fileSystem); + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/resources/resource-test1.txt b/substratevm/src/com.oracle.svm.test/src/resources/resource-test1.txt new file mode 100644 index 0000000000000..f16f70c912708 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/resources/resource-test1.txt @@ -0,0 +1 @@ +Java is awesome! \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.test/src/resources/resource-test2.txt b/substratevm/src/com.oracle.svm.test/src/resources/resource-test2.txt new file mode 100644 index 0000000000000..549dea6322311 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/resources/resource-test2.txt @@ -0,0 +1 @@ +Native image is awesome! \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.truffle.tck/src/com/oracle/svm/truffle/tck/resources/jre.json b/substratevm/src/com.oracle.svm.truffle.tck/src/com/oracle/svm/truffle/tck/resources/jre.json index 24d75e18ab79f..379395e023b0e 100644 --- a/substratevm/src/com.oracle.svm.truffle.tck/src/com/oracle/svm/truffle/tck/resources/jre.json +++ b/substratevm/src/com.oracle.svm.truffle.tck/src/com/oracle/svm/truffle/tck/resources/jre.json @@ -214,8 +214,7 @@ "methods" : [ { "name" : "createURL" , "parameterTypes" : [ - "java.lang.String", - "byte[]" + "java.lang.String" ]} ] },