diff --git a/.gitignore b/.gitignore index e640ec2c7..5b36b1a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ bin/ # Testing run/ logs/ +quiltloader.log diff --git a/proguard.conf b/proguard.conf index 7116515e5..e3a7e1003 100644 --- a/proguard.conf +++ b/proguard.conf @@ -5,6 +5,6 @@ -keepparameternames -keepattributes * --keep class !org.quiltmc.loader.impl.lib { +-keep class !org.quiltmc.loader.impl.lib.** { *; } \ No newline at end of file diff --git a/src/main/java/org/quiltmc/loader/api/ExtendedFileSystem.java b/src/main/java/org/quiltmc/loader/api/ExtendedFileSystem.java new file mode 100644 index 000000000..c1bb93a5e --- /dev/null +++ b/src/main/java/org/quiltmc/loader/api/ExtendedFileSystem.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.api; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.NotLinkException; +import java.nio.file.Path; + +/** A {@link FileSystem} which may support additional features, beyond those which normal file systems support. Similar + * to regular file systems, you should generally use {@link ExtendedFiles} to perform these operations. */ +public interface ExtendedFileSystem extends FasterFileSystem { + + /** Copies the source file to the target file. If the source file system is read-only then this will + * {@link #mount(Path, Path, MountOption...)} the given file with {@link MountOption#COPY_ON_WRITE}. + * + * @param source A {@link Path}, which might not be in this {@link FileSystem}. + * @param target A {@link Path} which must be from this {@link ExtendedFileSystem} + * @return target */ + default Path copyOnWrite(Path source, Path target, CopyOption... options) throws IOException { + return Files.copy(source, target, options); + } + + /** Mounts the given source file on the target file, such that all reads and writes will actually read and write the + * source file. (The exact behaviour depends on the options given). + *
+ * This is similar to {@link Files#createSymbolicLink(Path, Path, java.nio.file.attribute.FileAttribute...)} except + * the source and target files don't need to be on the same filesystem. + *
+ * Note that this does not support mounting folders. + * + * @param source A path from any {@link FileSystem}. + * @param target A path from this {@link ExtendedFileSystem}. + * @param options Options which control how the file is mounted. + * @throws UnsupportedOperationException if this filesystem doesn't support file mounts. */ + default Path mount(Path source, Path target, MountOption... options) throws IOException { + throw new UnsupportedOperationException(getClass() + " doesn't support ExtendedFileSystem.mount"); + } + + /** @return True if the file has been mounted with {@link #mount(Path, Path, MountOption...)}. */ + default boolean isMountedFile(Path file) { + return false; + } + + /** @return True if the given file was created by {@link #mount(Path, Path, MountOption...)} with + * {@link MountOption#COPY_ON_WRITE}, and the file has not been modified since it was copied. */ + default boolean isCopyOnWrite(Path file) { + return false; + } + + /** Reads the target of a mounted file, if it was created by {@link #mount(Path, Path, MountOption...)}. + * + * @throws NotLinkException if the given file is not a {@link #isMountedFile(Path)}. + * @throws UnsupportedOperationException if this filesystem doesn't support file mounts. */ + default Path readMountTarget(Path file) throws IOException { + throw new UnsupportedOperationException(getClass() + " doesn't support ExtendedFileSystem.mount"); + } +} diff --git a/src/main/java/org/quiltmc/loader/api/ExtendedFiles.java b/src/main/java/org/quiltmc/loader/api/ExtendedFiles.java new file mode 100644 index 000000000..b7883b7cb --- /dev/null +++ b/src/main/java/org/quiltmc/loader/api/ExtendedFiles.java @@ -0,0 +1,91 @@ +/* + * Copyright 2023 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.api; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.Files; +import java.nio.file.NotLinkException; +import java.nio.file.Path; + +/** Similar to {@link Files}, but for {@link ExtendedFileSystem}. Unlike {@link Files}, most operations can take + * {@link Path}s from any file system. */ +public class ExtendedFiles { + + /** Copies the source file to the target file. If the source file system is read-only then the target file may + * become a link to the source file, which is fully copied when it is modified. + *
+ * This method is a safe alternative to {@link #mount(Path, Path, MountOption...)}, when passing them + * {@link MountOption#COPY_ON_WRITE}, in the sense that it will copy the file if the filesystem doesn't support + * mounts. */ + public static Path copyOnWrite(Path source, Path target, CopyOption... options) throws IOException { + if (target.getFileSystem() instanceof ExtendedFileSystem) { + return ((ExtendedFileSystem) target.getFileSystem()).copyOnWrite(source, target, options); + } else { + return Files.copy(source, target, options); + } + } + + /** Attempts to mount the source file onto the target file, such that all reads and writes to the target file + * actually read and write the source file. (The exact behaviour depends on the options given). + *
+ * This is similar to {@link Files#createSymbolicLink(Path, Path, java.nio.file.attribute.FileAttribute...)}, but + * the source file and target file don't need to be on the same filesystem. + *
+ * This does not support mounting folders. + * + * @throws UnsupportedOperationException if the filesystem doesn't support this operation. + * @throws IOException if anything goes wrong while mounting the file. */ + public static Path mount(Path source, Path target, MountOption... options) throws IOException { + if (target.getFileSystem() instanceof ExtendedFileSystem) { + return ((ExtendedFileSystem) target.getFileSystem()).mount(source, target, options); + } else { + throw new UnsupportedOperationException(target.getFileSystem() + " does not support file mounts!"); + } + } + + /** @return True if the file has been mounted with {@link #mount(Path, Path, MountOption...)}. */ + public static boolean isMountedFile(Path file) { + if (file.getFileSystem() instanceof ExtendedFileSystem) { + return ((ExtendedFileSystem) file.getFileSystem()).isMountedFile(file); + } else { + return false; + } + } + + /** @return True if the given file was created by {@link #mount(Path, Path, MountOption...)} with + * {@link MountOption#COPY_ON_WRITE}, and the file has not been modified since it was copied. */ + public static boolean isCopyOnWrite(Path file) { + if (file.getFileSystem() instanceof ExtendedFileSystem) { + return ((ExtendedFileSystem) file.getFileSystem()).isCopyOnWrite(file); + } else { + return false; + } + } + + /** Reads the target of a mounted file, if it was created by {@link #mount(Path, Path, MountOption...)}. + * + * @throws NotLinkException if the given file is not a {@link #isMountedFile(Path)}. + * @throws UnsupportedOperationException if this filesystem doesn't support file mounts. */ + public static Path readMountTarget(Path file) throws IOException { + if (file.getFileSystem() instanceof ExtendedFileSystem) { + return ((ExtendedFileSystem) file.getFileSystem()).readMountTarget(file); + } else { + throw new UnsupportedOperationException(file + " is not a mounted file!"); + } + } +} diff --git a/src/main/java/org/quiltmc/loader/api/FasterFileSystem.java b/src/main/java/org/quiltmc/loader/api/FasterFileSystem.java index 0c6da6409..1fad261bc 100644 --- a/src/main/java/org/quiltmc/loader/api/FasterFileSystem.java +++ b/src/main/java/org/quiltmc/loader/api/FasterFileSystem.java @@ -17,6 +17,7 @@ package org.quiltmc.loader.api; import java.io.IOException; +import java.nio.file.CopyOption; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.LinkOption; @@ -41,6 +42,13 @@ default Path createDirectories(Path dir, FileAttribute>... attrs) throws IOExc return Files.createDirectories(dir, attrs); } + /** @param source A {@link Path}, which might not be in this {@link FileSystem}. + * @param target A {@link Path} which must be from this {@link FileSystem} + * @return target */ + default Path copy(Path source, Path target, CopyOption... options) throws IOException { + return Files.copy(source, target, options); + } + default boolean isSymbolicLink(Path path) { return Files.isSymbolicLink(path); } diff --git a/src/main/java/org/quiltmc/loader/api/FasterFiles.java b/src/main/java/org/quiltmc/loader/api/FasterFiles.java index cc141f022..c5ec55536 100644 --- a/src/main/java/org/quiltmc/loader/api/FasterFiles.java +++ b/src/main/java/org/quiltmc/loader/api/FasterFiles.java @@ -17,6 +17,7 @@ package org.quiltmc.loader.api; import java.io.IOException; +import java.nio.file.CopyOption; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.LinkOption; @@ -57,6 +58,14 @@ public static Path createDirectories(Path dir, FileAttribute>... attrs) throws } } + public static Path copy(Path source, Path target, CopyOption... options) throws IOException { + if (target.getFileSystem() instanceof FasterFileSystem) { + return ((FasterFileSystem) target.getFileSystem()).copy(source, target, options); + } else { + return Files.copy(source, target, options); + } + } + public static boolean isSymbolicLink(Path path) { if (path.getFileSystem() instanceof FasterFileSystem) { return ((FasterFileSystem) path.getFileSystem()).isSymbolicLink(path); diff --git a/src/main/java/org/quiltmc/loader/api/MountOption.java b/src/main/java/org/quiltmc/loader/api/MountOption.java new file mode 100644 index 000000000..22d6e25b2 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/api/MountOption.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.api; + +/** Options for {@link ExtendedFiles#mount(java.nio.file.Path, java.nio.file.Path, MountOption...)} */ +public enum MountOption { + + /** Replace an existing file if it exists when mounting. This cannot replace a non-empty directory. */ + REPLACE_EXISTING, + + /** Indicates that the mounted file will not permit writes. + *
+ * This option is incompatible with {@link #COPY_ON_WRITE} */ + READ_ONLY, + + /** Indicates that the mounted file will copy to a new, separate file when written to. + *
+ * This option is incompatible with {@link #READ_ONLY} */
+ COPY_ON_WRITE,
+}
diff --git a/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java b/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java
index 332ba566d..f3d5ce0c8 100644
--- a/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java
+++ b/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java
@@ -424,6 +424,13 @@ private void setup() throws ModResolutionException {
addMod(modOption.convertToMod(resourceRoot));
}
+ try {
+ transformedModBundle.getFileSystem().close();
+ } catch (IOException e) {
+ // TODO!
+ throw new Error(e);
+ }
+
temporaryPluginSolveResult = null;
temporaryOrderedModList = null;
temporarySourcePaths = null;
diff --git a/src/main/java/org/quiltmc/loader/impl/discovery/RuntimeModRemapper.java b/src/main/java/org/quiltmc/loader/impl/discovery/RuntimeModRemapper.java
index c45504bf7..e315e48b2 100644
--- a/src/main/java/org/quiltmc/loader/impl/discovery/RuntimeModRemapper.java
+++ b/src/main/java/org/quiltmc/loader/impl/discovery/RuntimeModRemapper.java
@@ -20,36 +20,28 @@
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.nio.file.FileSystem;
-import java.nio.file.FileVisitResult;
-import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.UUID;
import java.util.stream.Collectors;
import org.objectweb.asm.commons.Remapper;
+import org.quiltmc.loader.api.ExtendedFiles;
import org.quiltmc.loader.api.FasterFiles;
+import org.quiltmc.loader.api.MountOption;
import org.quiltmc.loader.api.plugin.solver.ModLoadOption;
import org.quiltmc.loader.impl.QuiltLoaderImpl;
-import org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystem;
import org.quiltmc.loader.impl.launch.common.QuiltLauncher;
import org.quiltmc.loader.impl.launch.common.QuiltLauncherBase;
-import org.quiltmc.loader.impl.util.FileSystemUtil;
import org.quiltmc.loader.impl.util.QuiltLoaderInternal;
import org.quiltmc.loader.impl.util.QuiltLoaderInternalType;
import org.quiltmc.loader.impl.util.SystemProperties;
import org.quiltmc.loader.impl.util.mappings.TinyRemapperMappingsHelper;
-import net.fabricmc.accesswidener.AccessWidenerFormatException;
import net.fabricmc.accesswidener.AccessWidenerReader;
import net.fabricmc.accesswidener.AccessWidenerRemapper;
import net.fabricmc.accesswidener.AccessWidenerWriter;
@@ -61,6 +53,8 @@
@QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED)
public final class RuntimeModRemapper {
+ static final boolean COPY_ON_WRITE = true;
+
public static void remap(Path cache, List entries;
+
+ public QuiltMapFileSystem(Class pathClass, String name, boolean uniqueify) {
+ super(filesystemClass, pathClass, name, uniqueify);
+ this.entries = startWithConcurrentMap() ? new ConcurrentHashMap<>() : new HashMap<>();
+ }
+
+ public static void dumpEntries(FileSystem fs, String name) {
+ if (ENABLE_DEBUG_DUMPING && fs instanceof QuiltMapFileSystem) {
+ ((QuiltMapFileSystem, ?>) fs).dumpEntries(name);
+ }
+ }
+
+ public void dumpEntries(String name) {
+ if (!ENABLE_DEBUG_DUMPING) {
+ return;
+ }
+ try (BufferedWriter bw = Files.newBufferedWriter(Paths.get("dbg-map-fs-" + name + ".txt"))) {
+ Set entry : entries.entrySet()) {
+ paths.add(entry.getKey().toString() + " = " + entry.getValue().getClass());
+ }
+ for (String key : paths) {
+ bw.append(key);
+ bw.newLine();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void validate() {
+ if (!ENABLE_VALIDATION) {
+ return;
+ }
+ for (Entry entry : entries.entrySet()) {
+ P path = entry.getKey();
+ QuiltUnifiedEntry e = entry.getValue();
+ if (!path.isRoot()) {
+ QuiltUnifiedEntry parent = entries.get(path.parent);
+ if (parent == null || !(parent instanceof QuiltUnifiedFolder)) {
+ throw new IllegalStateException("Entry " + path + " doesn't have a parent!");
+ }
+ QuiltUnifiedFolder pp = (QuiltUnifiedFolder) parent;
+ if (!pp.getChildren().contains(path)) {
+ throw new IllegalStateException("Entry " + path + " isn't linked to from its parent!");
+ }
+ }
+ }
+ }
+
+ // Construction
+
+ protected abstract boolean startWithConcurrentMap();
+
+ // Subtype helpers
+
+ protected void switchToReadOnly() {
+ for (Map.Entry entry : entries.entrySet()) {
+ entry.setValue(entry.getValue().switchToReadOnly());
+ }
+ }
+
+ // File map access
+
+ protected int getEntryCount() {
+ return entries.size();
+ }
+
+ protected Iterable getEntryPathIterator() {
+ return entries.keySet();
+ }
+
+ protected QuiltUnifiedEntry getEntry(Path path) {
+ if (path.getFileSystem() != this) {
+ throw new IllegalStateException("The given path is for a different filesystem!");
+ }
+ return entries.get(path.toAbsolutePath().normalize());
+ }
+
+ protected void addEntryRequiringParent(QuiltUnifiedEntry newEntry) throws IOException {
+ addEntryRequiringParents0(newEntry, IOException::new);
+ }
+
+ protected void addEntryRequiringParentUnsafe(QuiltUnifiedEntry newEntry) {
+ addEntryRequiringParents0(newEntry, IllegalStateException::new);
+ }
+
+ private keys = new ArrayList<>(entries.keySet());
+ Collections.sort(keys);
+ for (P key : keys) {
+ System.out.println(key + " = " + getEntry(key).getClass());
+ }
+ throw new IOException("Cannot remove an entry if it doesn't exist! " + path);
+ } else {
+ return false;
+ }
+ }
+
+ if (current instanceof QuiltUnifiedFolder) {
+ if (!((QuiltUnifiedFolder) current).getChildren().isEmpty()) {
+ throw new DirectoryNotEmptyException("Cannot remove a non-empty folder!");
+ }
+ }
+
+ QuiltUnifiedEntry parent = getEntry(path.parent);
+ entries.remove(path);
+ if (parent instanceof QuiltUnifiedFolderWriteable) {
+ ((QuiltUnifiedFolderWriteable) parent).children.remove(path);
+ }
+ return true;
+ }
+
+ // General filesystem operations
+
+ @Override
+ public boolean isDirectory(Path path, LinkOption... options) {
+ return getEntry(path) instanceof QuiltUnifiedFolder;
+ }
+
+ @Override
+ public boolean isRegularFile(Path path, LinkOption[] options) {
+ return getEntry(path) instanceof QuiltUnifiedFile;
+ }
+
+ @Override
+ public boolean exists(Path path, LinkOption... options) {
+ return getEntry(path) != null;
+ }
+
+ @Override
+ public boolean notExists(Path path, LinkOption... options) {
+ // Nothing can go wrong, so this is fine
+ return !exists(path, options);
+ }
+
+ @Override
+ public boolean isReadable(Path path) {
+ return exists(path);
+ }
+
+ @Override
+ public boolean isExecutable(Path path) {
+ return exists(path);
+ }
+
+ @Override
+ public boolean isSymbolicLink(Path path) {
+ // Completely unsupported
+ // (File mounts aren't technically symbolic links)
+ return false;
+ }
+
+ @Override
+ public Collection extends Path> getChildren(Path dir) throws IOException {
+ QuiltUnifiedEntry entry = getEntry(dir);
+ if (!(entry instanceof QuiltUnifiedFolder)) {
+ throw new NotDirectoryException(dir.toString());
+ }
+ return Collections.unmodifiableCollection(((QuiltUnifiedFolder) entry).getChildren());
+ }
+
+ @Override
+ public Stream pathClass();
+
+ @Override
+ public FileSystem getFileSystem(URI uri) {
+ return quiltFSP().getFileSystem(uri);
+ }
+
+ protected P toAbsolutePath(Path path) {
+ Class pathClass = pathClass();
+ if (!pathClass.isInstance(path)) {
+ throw new IllegalArgumentException("The given path is not " + pathClass);
+ }
+
+ return pathClass.cast(path).toAbsolutePath().normalize();
+ }
+
+ @Override
+ public P getPath(URI uri) {
+ return quiltFSP().getFileSystem(uri).root.resolve(uri.getPath());
+ }
+
+ @Override
+ public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
+ for (OpenOption o : options) {
+ if (o != StandardOpenOption.READ) {
+ throw new UnsupportedOperationException("'" + o + "' not allowed");
+ }
+ }
+
+ P p = toAbsolutePath(path);
+ QuiltUnifiedEntry entry = p.fs.getEntry(p);
+ if (entry instanceof QuiltUnifiedFile) {
+ return ((QuiltUnifiedFile) entry).createInputStream();
+ } else if (entry != null) {
+ throw new FileSystemException("Cannot open an InputStream on a directory!");
+ } else {
+ throw new NoSuchFileException(path.toString());
+ }
+ }
+
+ @Override
+ public OutputStream newOutputStream(Path pathIn, OpenOption... options) throws IOException {
+ if (options.length == 0) {
+ options = new OpenOption[] { StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE };
+ }
+
+ boolean append = false;
+ boolean truncate = false;
+ boolean create = false;
+ boolean deleteOnClose = false;
+
+ P path = toAbsolutePath(pathIn);
+
+ for (OpenOption option : options) {
+ if (option instanceof LinkOption) {
+ // Okay
+ continue;
+ } else if (option instanceof StandardOpenOption) {
+ switch ((StandardOpenOption) option) {
+ case APPEND:{
+ if (truncate) {
+ throw new IllegalArgumentException("Cannot append and truncate! " + options);
+ } else {
+ append = true;
+ }
+ break;
+ }
+ case TRUNCATE_EXISTING: {
+ if (append) {
+ throw new IllegalArgumentException("Cannot append and truncate! " + options);
+ } else {
+ truncate = true;
+ }
+ break;
+ }
+ case CREATE: {
+ create = true;
+ break;
+ }
+ case CREATE_NEW: {
+ if (path.fs.exists(path)) {
+ throw new IOException(path + " already exists, and CREATE_NEW is specified!");
+ }
+ create = true;
+ break;
+ }
+ case DELETE_ON_CLOSE: {
+ deleteOnClose = true;
+ break;
+ }
+ case READ: {
+ throw new UnsupportedOperationException("Cannot open an OutputStream with StandardOpenOption.READ!");
+ }
+ case WRITE:
+ break;
+ case DSYNC:
+ case SPARSE:
+ case SYNC:
+ default:
+ break;
+ }
+ } else {
+ throw new UnsupportedOperationException("Unknown option " + option);
+ }
+ }
+
+ ensureWriteable(path);
+ QuiltUnifiedEntry current = path.fs.getEntry(path);
+ QuiltUnifiedFile targetFile = null;
+
+ if (create) {
+ if (current != null) {
+ delete(path);
+ }
+
+ path.fs.addEntryRequiringParent(targetFile = new QuiltMemoryFile.ReadWrite(path));
+ } else if (current instanceof QuiltUnifiedFile) {
+ targetFile = (QuiltUnifiedFile) current;
+ } else {
+ throw new IOException("Cannot open an OutputStream on " + current);
+ }
+
+ OutputStream stream = targetFile.createOutputStream(append, truncate);
+
+ if (deleteOnClose) {
+ final OutputStream previous = stream;
+ stream = new OutputStream() {
+
+ @Override
+ public void write(int b) throws IOException {
+ previous.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ previous.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ previous.write(b, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ previous.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ previous.close();
+ delete(path);
+ }
+ };
+ }
+
+ return stream;
+ }
+
+ protected void ensureWriteable(P path) throws IOException {
+ QuiltUnifiedEntry rootEntry = path.fs.getEntry(path.fs.root);
+ if (rootEntry == null) {
+ // Empty, so it's constructing
+ return;
+ }
+
+ if (rootEntry instanceof QuiltUnifiedFolderWriteable) {
+ return;
+ } else if (rootEntry instanceof QuiltUnifiedFolderReadOnly) {
+ throw new IOException(READ_ONLY_EXCEPTION);
+ } else {
+ throw new IllegalStateException("Unexpected root " + rootEntry);
+ }
+ }
+
+ @Override
+ public SeekableByteChannel newByteChannel(Path pathIn, Set extends OpenOption> options, FileAttribute>... attrs)
+ throws IOException {
+
+ P path = toAbsolutePath(pathIn);
+ QuiltUnifiedEntry entry = path.fs.getEntry(path);
+
+ if (!path.getFileSystem().isReadOnly()) {
+ boolean create = false;
+ for (OpenOption o : options) {
+ if (o == StandardOpenOption.CREATE_NEW) {
+ if (entry != null) {
+ throw new IOException("File already exists: " + path);
+ }
+ create = true;
+ } else if (o == StandardOpenOption.CREATE) {
+ create = true;
+ }
+ }
+
+ if (create && entry == null) {
+ if (!path.isAbsolute()) {
+ throw new IOException("Cannot work above the root!");
+ }
+ path.fs.addEntryRequiringParent(entry = new QuiltMemoryFile.ReadWrite(path));
+ }
+ }
+
+ if (entry instanceof QuiltUnifiedFile) {
+ return ((QuiltUnifiedFile) entry).createByteChannel(options);
+ } else if (entry != null) {
+ throw new FileSystemException("Cannot open a ByteChannel on a directory!");
+ } else {
+ throw new NoSuchFileException(path.toString());
+ }
+ }
+
+ @Override
+ public DirectoryStream