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 modList) { List modsToRemap = modList.stream() .filter(modLoadOption -> modLoadOption.namespaceMappingFrom() != null) @@ -92,7 +86,11 @@ public static void remap(Path cache, List modList) { Path dst = modDst.resolve(sub.toString().replace(modSrc.getFileSystem().getSeparator(), modDst.getFileSystem().getSeparator())); try { FasterFiles.createDirectories(dst.getParent()); - Files.copy(path, dst); + if (COPY_ON_WRITE) { + ExtendedFiles.mount(path, dst, MountOption.COPY_ON_WRITE); + } else { + FasterFiles.copy(path, dst); + } } catch (IOException e) { throw new Error(e); } @@ -165,7 +163,6 @@ public static void remap(Path cache, List modList) { if (info.accessWideners != null) { for (Map.Entry entry : info.accessWideners.entrySet()) { - Files.delete(info.outputPath.resolve(entry.getKey())); Files.write(info.outputPath.resolve(entry.getKey()), entry.getValue()); } } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltBasePath.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltBasePath.java index 86d4d88a0..cc9de8aeb 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltBasePath.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltBasePath.java @@ -218,6 +218,65 @@ public String toString() { return sb.toString(); } + /** + * Faster version of {@link #toString()}.{@link #hashCode()} + * @return this.toString().hashCode(). + */ + public int toStringHashCode() { + if (isRoot()) { + return NAME_ROOT.hashCode(); + } + + int hash; + + if (parent != null) { + if (parent.isRoot()) { + hash = '/'; + } else { + hash = 31 * parent.toStringHashCode() + '/'; + } + } else { + hash = 0; + } + + for (int i = 0; i < name.length(); i++) { + hash = 31 * hash + name.charAt(i); + } + + if (toString().hashCode() != hash) { + throw new AssertionError(toString()); + } + return hash; + } + + /** Faster alternative to {@link #toString()}.equals, if both this path and the other path is a QuiltBasePath. + * + * @return this.toString().equals(other.toString()) */ + public boolean isToStringEqual(Path other) { + if (!(other instanceof QuiltBasePath)) { + return toString().equals(other.toString()); + } + boolean should = toString().equals(other.toString()); + boolean result = s2(other); + if (should != result) { + throw new AssertionError(); + } + return result; + } + + private boolean s2(Path other) { + QuiltBasePath o = (QuiltBasePath) other; + if (parent == null || o.parent == null) { + if ((parent == null) != (o.parent == null)) { + return false; + } + } else if (!parent.isToStringEqual(o.parent)) { + return false; + } + return name.equals(o.name); + } + + @Override public int getNameCount() { return data >>> NAME_COUNT_OFFSET; diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltClassPath.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltClassPath.java index 27a09bfd9..5c3bfaade 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltClassPath.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltClassPath.java @@ -16,28 +16,21 @@ package org.quiltmc.loader.impl.filesystem; -import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; -import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.FileVisitResult; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; -import java.nio.file.WatchEvent.Kind; -import java.nio.file.WatchEvent.Modifier; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Deque; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Queue; @@ -62,8 +55,12 @@ public class QuiltClassPath { private static final Queue SCAN_TASKS = new ArrayDeque<>(); private static final Set ACTIVE_SCANNERS = new HashSet<>(); + /** Saves quite a bit of memory to use our own hash table, while also not being too much work (since we already key + * by int hash) */ + private static final boolean USE_CUSTOM_TABLE = true; + private final List roots = new CopyOnWriteArrayList<>(); - private final Map files = new ConcurrentHashMap<>(); + private final FileMap files = USE_CUSTOM_TABLE ? new HashTableFileMap() : new StandardFileMap(); public void addRoot(Path root) { if (root instanceof QuiltJoinedPath) { @@ -80,15 +77,20 @@ public void addRoot(Path root) { Log.warn(LogCategory.GENERAL, "Adding read/write FS root to the classpath, this may slow down class loading: " + fs.name); roots.add(root); } else { - for (Path key : fs.files.keySet()) { + + files.ensureCapacityFor(fs.getEntryCount()); + + for (Path key : fs.getEntryPathIterator()) { putQuickFile(key.toString(), key); } } - } else if (root instanceof QuiltZipPath) { - QuiltZipFileSystem fs = ((QuiltZipPath) root).fs; + } else if (root instanceof QuiltMapPath) { + QuiltMapFileSystem fs = ((QuiltMapPath) root).fs; - for (Path key : fs.entries.keySet()) { + files.ensureCapacityFor(fs.getEntryCount()); + + for (Path key : fs.getEntryPathIterator()) { putQuickFile(key.toString(), key); } @@ -112,21 +114,7 @@ public void addRoot(Path root) { } private void putQuickFile(String fileName, Path file) { - files.compute(fileName, (name, current) -> { - if (current == null) { - return file; - } else if (current instanceof OverlappingPath) { - OverlappingPath multi = (OverlappingPath) current; - multi.paths.add(file); - multi.hasWarned = false; - return multi; - } else { - OverlappingPath multi = new OverlappingPath(name); - multi.paths.add(current); - multi.paths.add(file); - return multi; - } - }); + files.put(file); } private void beginScanning(Path zipRoot) { @@ -230,6 +218,11 @@ public Path findResource(String path) { absolutePath = "/" + path; } Path quick = files.get(absolutePath); + + if (quick instanceof HashCollisionPath) { + quick = ((HashCollisionPath) quick).get(absolutePath); + } + if (quick != null) { if (quick instanceof OverlappingPath) { return ((OverlappingPath) quick).getFirst(); @@ -254,10 +247,15 @@ public List getResources(String path) { } Path quick = files.get(absolutePath); + + if (quick instanceof HashCollisionPath) { + quick = ((HashCollisionPath) quick).get(absolutePath); + } + List paths = new ArrayList<>(); if (quick != null) { if (quick instanceof OverlappingPath) { - paths.addAll(((OverlappingPath)quick).paths); + Collections.addAll(paths, ((OverlappingPath) quick).paths); } else { paths.add(quick); } @@ -273,28 +271,318 @@ public List getResources(String path) { return Collections.unmodifiableList(paths); } + private static boolean isEqualPath(Path in, Path value) { + if (in instanceof OverlappingPath) { + in = ((OverlappingPath) in).paths[0]; + } + if (value instanceof OverlappingPath) { + value = ((OverlappingPath) value).paths[0]; + } + if (in instanceof QuiltBasePath) { + return ((QuiltBasePath) in).isToStringEqual(value); + } + int namesIn = in.getNameCount(); + int namesVal = value.getNameCount(); + if (namesIn != namesVal) { + return false; + } + + for (int i = 0; i < namesIn; i++) { + if (!in.getName(i).toString().equals(value.getName(i).toString())) { + return false; + } + } + + return true; + } + + private static boolean isEqual(String key, Path value) { + + boolean should = key.equals(value.toString()); + + int offset = key.length(); + int names = value.getNameCount(); + for (int part = names - 1; part >= 0; part--) { + String sub = value.getName(part).toString(); + offset -= sub.length(); + if (!key.startsWith(sub, offset)) { + if (should) { + throw new IllegalStateException("Optimized equality test seems to be broken for " + key); + } + return false; + } + offset--; + if (offset < 0) { + if (should) { + throw new IllegalStateException("Optimized equality test seems to be broken for " + key); + } + return false; + } + if (key.charAt(offset) != '/') { + if (should) { + throw new IllegalStateException("Optimized equality test seems to be broken for " + key); + } + return false; + } + } + return true; + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + static abstract class FileMap { + final Path get(String key) { + Path result = get0(key); + + if (result instanceof HashCollisionPath) { + result = ((HashCollisionPath) result).get(key); + } else { + Path compare = result; + if (result instanceof OverlappingPath) { + compare = ((OverlappingPath) result).paths[0]; + } + if (compare != null && !isEqual(key, compare)) { + return null; + } + } + + return result; + } + + abstract Path get0(String key); + + abstract void ensureCapacityFor(int newPathCount); + + abstract void put(Path newPath); + + protected Path computeNewPath(Path current, Path file) { + if (current == null) { + return file; + } else if (current instanceof HashCollisionPath) { + HashCollisionPath collision = (HashCollisionPath) current; + int equalIndex = collision.getEqualPathIndex(file); + if (equalIndex < 0) { + Path[] newArray = new Path[collision.values.length + 1]; + System.arraycopy(collision.values, 0, newArray, 0, collision.values.length); + newArray[collision.values.length] = file; + collision.values = newArray; + } else { + Path equal = collision.values[equalIndex]; + if (equal instanceof OverlappingPath) { + OverlappingPath multi = (OverlappingPath) equal; + multi.addPath(file); + multi.data &= ~OverlappingPath.FLAG_HAS_WARNED; + } else { + OverlappingPath multi = new OverlappingPath(); + multi.paths = new Path[] { equal, file }; + collision.values[equalIndex] = multi; + } + } + return collision; + } else if (current instanceof OverlappingPath) { + if (isEqualPath(file, ((OverlappingPath) current).paths[0])) { + OverlappingPath multi = (OverlappingPath) current; + multi.addPath(file); + multi.data &= ~OverlappingPath.FLAG_HAS_WARNED; + return multi; + } else { + return new HashCollisionPath(current, file); + } + } else { + if (isEqualPath(file, current)) { + OverlappingPath multi = new OverlappingPath(); + multi.paths = new Path[] { current, file }; + return multi; + } else { + return new HashCollisionPath(current, file); + } + } + } + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + static final class StandardFileMap extends FileMap { + final Map files = new ConcurrentHashMap<>(); + + public StandardFileMap() {} + + @Override + void ensureCapacityFor(int newPathCount) { + // NO-OP (only the table version uses this) + } + + @Override + void put(Path path) { + files.compute(path.toString().hashCode(), (a, current) -> computeNewPath(current, path)); + } + + @Override + Path get0(String key) { + return files.get(key.hashCode()); + } + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + static final class HashTableFileMap extends FileMap { + static final double FILL_PERCENT = 0.75; + Path[] table = new Path[128]; + int entryCount; + + public HashTableFileMap() {} + + @Override + Path get0(String key) { + Path[] tbl = table; + // Stored in a variable for thread safety + return tbl[key.hashCode() & tbl.length - 1]; + } + + @Override + synchronized void ensureCapacityFor(int newPathCount) { + int result = entryCount + newPathCount; + int newSize = table.length; + while (newSize * FILL_PERCENT <= result) { + newSize *= 2; + } + if (newSize != table.length) { + rehash(newSize); + } + } + + @Override + synchronized void put(Path newPath) { + entryCount++; + if (table.length * FILL_PERCENT < entryCount) { + rehash(table.length * 2); + } + int index = hashCode(newPath) & table.length - 1; + table[index] = computeNewPath(table[index], newPath); + } + + private static int hashCode(Path path) { + if (path instanceof QuiltBasePath) { + return ((QuiltBasePath) path).toStringHashCode(); + } + return path.toString().hashCode(); + } + + private void rehash(int newSize) { + Path[] oldTable = table; + table = new Path[newSize]; + Path[] array1 = { null }; + Path[] subIter = null; + for (Path sub : oldTable) { + if (sub == null) { + continue; + } + + if (sub instanceof HashCollisionPath) { + HashCollisionPath collision = (HashCollisionPath) sub; + subIter = collision.values; + } else { + array1[0] = sub; + subIter = array1; + } + + for (Path sub2 : subIter) { + final Path hashPath; + if (sub2 instanceof OverlappingPath) { + hashPath = ((OverlappingPath) sub2).paths[0]; + } else { + hashPath = sub2; + } + int index = hashCode(hashPath) & table.length - 1; + table[index] = computeNewPath(table[index], sub2); + } + } + } + } + + /** Used so we don't need to store a full {@link String} for every file we track. */ + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + private static final class HashCollisionPath extends NullPath { + + Path[] values; + + public HashCollisionPath(Path a, Path b) { + if (a instanceof HashCollisionPath || b instanceof HashCollisionPath) { + throw new IllegalStateException("Wrong constructor!"); + } + values = new Path[] { a, b }; + } + + public HashCollisionPath(HashCollisionPath a, Path b) { + values = new Path[a.values.length + 1]; + System.arraycopy(a.values, 0, values, 0, a.values.length); + values[a.values.length] = b; + } + + @Override + protected IllegalStateException illegal() { + IllegalStateException ex = new IllegalStateException( + "QuiltClassPath must NEVER return a HashCollisionPath - something has gone very wrong!" + ); + ex.printStackTrace(); + throw ex; + } + + /** @return The equal path index in {@link #values}, or a negative number if it's not present. */ + public int getEqualPathIndex(Path in) { + for (int i = 0; i < values.length; i++) { + if (isEqualPath(in, values[i])) { + return i; + } + } + return -1; + } + + public Path get(String key) { + for (Path value : values) { + if (value instanceof OverlappingPath) { + value = ((OverlappingPath) value).paths[0]; + } + if (isEqual(key, value)) { + return value; + } + } + return null; + } + } + /** Used when multiple paths are stored as values in {@link QuiltClassPath#files}. */ @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) private static final class OverlappingPath extends NullPath { - final String exposedName; - final List paths = new ArrayList<>(); - boolean hasWarned = false; + static final int FLAG_HAS_WARNED = 1 << 31; + static final int MASK_HASH = Integer.MAX_VALUE; + + int data; + Path[] paths; - public OverlappingPath(String exposedName) { - this.exposedName = exposedName; + public OverlappingPath(int fullHash) { + data = fullHash & MASK_HASH; + } + + public OverlappingPath() {} + + public void addPath(Path file) { + paths = Arrays.copyOf(paths, paths.length + 1); + paths[paths.length - 1] = file; + file.getNameCount(); } @Override protected IllegalStateException illegal() { - throw new IllegalStateException( + IllegalStateException ex = new IllegalStateException( "QuiltClassPath must NEVER return an OverlappingPath - something has gone very wrong!" ); + ex.printStackTrace(); + throw ex; } public Path getFirst() { - if (!hasWarned) { - hasWarned = true; + if ((data & ~FLAG_HAS_WARNED) != 0) { + data |= FLAG_HAS_WARNED; + String exposedName = paths[0].toString(); StringBuilder sb = new StringBuilder(); sb.append("Multiple paths added for '"); sb.append(exposedName); @@ -317,8 +605,7 @@ public Path getFirst() { } Log.warn(LogCategory.GENERAL, sb.toString()); } - return paths.get(0); + return paths[0]; } - } } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltFileAttributes.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltFileAttributes.java index e140b7ab3..d65768891 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltFileAttributes.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltFileAttributes.java @@ -33,9 +33,9 @@ final class QuiltFileAttributes implements BasicFileAttributes { private static final FileTime THE_TIME = FileTime.fromMillis(0); final Object key; - final int size; + final long size; - public QuiltFileAttributes(Object key, int size) { + public QuiltFileAttributes(Object key, long size) { this.key = key == null ? this : key; this.size = size; } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapFileSystem.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapFileSystem.java new file mode 100644 index 000000000..8ec54d626 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapFileSystem.java @@ -0,0 +1,405 @@ +/* + * 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.impl.filesystem; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileStoreAttributeView; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.quiltmc.loader.api.CachedFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFile; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolder; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderWriteable; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; +import org.quiltmc.loader.impl.util.SystemProperties; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public abstract class QuiltMapFileSystem, P extends QuiltMapPath> + extends QuiltBaseFileSystem + implements CachedFileSystem { + + /** Controls {@link #dumpEntries(String)}. */ + private static final boolean ENABLE_DEBUG_DUMPING = Boolean.getBoolean(SystemProperties.DEBUG_DUMP_FILESYSTEM_CONTENTS); + + /** Controls {@link #validate()}. */ + private static final boolean ENABLE_VALIDATION = Boolean.getBoolean(SystemProperties.DEBUG_VALIDATE_FILESYSTEM_CONTENTS); + + private final Map entries; + + public QuiltMapFileSystem(Class filesystemClass, 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 paths = new TreeSet<>(); + for (Map.Entry 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 void addEntryRequiringParents0(QuiltUnifiedEntry newEntry, Function execCtor) throws T { + P path = pathClass.cast(newEntry.path); + P parent = path.parent; + if (parent == null) { + if (root.equals(path)) { + addEntryWithoutParents0(newEntry, execCtor); + return; + } else { + throw new IllegalArgumentException("Somehow obtained a normalised, absolute, path without a parent which isn't root? " + path); + } + } + + QuiltUnifiedEntry entry = getEntry(parent); + if (entry instanceof QuiltUnifiedFolderWriteable) { + addEntryWithoutParents0(newEntry, execCtor); + ((QuiltUnifiedFolderWriteable) entry).children.add(path); + } else if (entry == null) { + throw execCtor.apply("Cannot put entry " + path + " because the parent folder doesn't exist!"); + } else { + throw execCtor.apply("Cannot put entry " + path + " because the parent is not a folder (was " + entry + ")"); + } + + validate(); + } + + protected void addEntryAndParents(QuiltUnifiedEntry newEntry) throws IOException { + addEntryAndParents0(newEntry, IOException::new); + } + + protected void addEntryAndParentsUnsafe(QuiltUnifiedEntry newEntry) { + addEntryAndParents0(newEntry, IllegalStateException::new); + } + + private void addEntryAndParents0(QuiltUnifiedEntry newEntry, Function execCtor) throws T { + P path = addEntryWithoutParents0(newEntry, execCtor); + P parent = path; + P previous = path; + while ((parent = parent.getParent()) != null) { + QuiltUnifiedEntry parentEntry = getEntry(parent); + QuiltUnifiedFolderWriteable parentFolder; + if (parentEntry == null) { + addEntryWithoutParents0(parentFolder = new QuiltUnifiedFolderWriteable(parent), execCtor); + } else if (parentEntry instanceof QuiltUnifiedFolderWriteable) { + parentFolder = (QuiltUnifiedFolderWriteable) parentEntry; + } else { + throw execCtor.apply( + "Cannot make a file into a folder " + parent + " for " + path + ", currently " + parentEntry + ); + } + + if (!parentFolder.children.add(previous)) { + break; + } + + previous = parent; + } + + validate(); + } + + protected void addEntryWithoutParentsUnsafe(QuiltUnifiedEntry newEntry) { + addEntryWithoutParents0(newEntry, IllegalStateException::new); + } + + protected void addEntryWithoutParents(QuiltUnifiedEntry newEntry) throws IOException { + addEntryWithoutParents0(newEntry, IOException::new); + } + + private P addEntryWithoutParents0(QuiltUnifiedEntry newEntry, Function execCtor) throws T { + if (newEntry.path.fs != this) { + throw new IllegalArgumentException("The given entry is for a different filesystem!"); + } + P path = pathClass.cast(newEntry.path); + QuiltUnifiedEntry current = entries.putIfAbsent(path, newEntry); + if (current == null) { + return path; + } else { + throw execCtor.apply("Cannot replace existing entry " + current + " with " + newEntry); + } + } + + protected boolean removeEntry(P path, boolean throwIfMissing) throws IOException { + path = path.toAbsolutePath().normalize(); + if ("/quilt_tags/quilt_tags.accesswidener".equals(path.toString())) { + System.out.println("Removing " + path); + } + QuiltUnifiedEntry current = getEntry(path); + if (current == null) { + if (throwIfMissing) { + List

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 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 list(Path dir) throws IOException { + return getChildren(dir).stream().map(p -> (Path) p); + } + + @Override + public Path createDirectories(Path dir, FileAttribute... attrs) throws IOException { + dir = dir.toAbsolutePath().normalize(); + Deque stack = new ArrayDeque<>(); + Path p = dir; + do { + if (isDirectory(p)) { + break; + } else { + stack.push(p); + } + } while ((p = p.getParent()) != null); + + while (!stack.isEmpty()) { + provider().createDirectory(stack.pop(), attrs); + } + + validate(); + + return dir; + } + + @Override + public Iterable getFileStores() { + FileStore store = new FileStore() { + @Override + public String type() { + return QuiltMapFileSystem.this.getClass().getName(); + } + + @Override + public boolean supportsFileAttributeView(String name) { + return "basic".equals(name); + } + + @Override + public boolean supportsFileAttributeView(Class type) { + return type == BasicFileAttributeView.class; + } + + @Override + public String name() { + return QuiltMapFileSystem.this.name; + } + + @Override + public boolean isReadOnly() { + return QuiltMapFileSystem.this.isReadOnly(); + } + + @Override + public long getUsableSpace() throws IOException { + return 10; + } + + @Override + public long getUnallocatedSpace() throws IOException { + return 0; + } + + @Override + public long getTotalSpace() throws IOException { + return getUsableSpace(); + } + + @Override + public V getFileStoreAttributeView(Class type) { + return null; + } + + @Override + public Object getAttribute(String attribute) throws IOException { + return null; + } + }; + return Collections.singleton(store); + } + + @Override + public Set supportedFileAttributeViews() { + return Collections.singleton("basic"); + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapFileSystemProvider.java new file mode 100644 index 000000000..ebf86d121 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapFileSystemProvider.java @@ -0,0 +1,538 @@ +/* + * 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.impl.filesystem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemException; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileTime; +import java.nio.file.spi.FileSystemProvider; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFile; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderReadOnly; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderWriteable; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public abstract class QuiltMapFileSystemProvider, P extends QuiltMapPath> extends FileSystemProvider { + + static final String READ_ONLY_EXCEPTION = "This FileSystem is read-only"; + + protected abstract QuiltFSP quiltFSP(); + + protected abstract Class fileSystemClass(); + + protected abstract Class

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 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 newDirectoryStream(Path dir, Filter filter) throws IOException { + P qmp = toAbsolutePath(dir); + + final QuiltMapPath[] entries; + QuiltUnifiedEntry entry = qmp.fs.getEntry(qmp); + if (entry instanceof QuiltUnifiedFolderReadOnly) { + entries = ((QuiltUnifiedFolderReadOnly) entry).children; + } else if (entry instanceof QuiltUnifiedFolderWriteable) { + entries = ((QuiltUnifiedFolderWriteable) entry).children + .toArray(new QuiltMapPath[0]); + } else { + throw new NotDirectoryException("Not a directory: " + dir); + } + + return new DirectoryStream() { + + boolean opened = false; + boolean closed = false; + + @Override + public void close() throws IOException { + closed = true; + } + + @Override + public Iterator iterator() { + if (opened) { + throw new IllegalStateException("newDirectoryStream only supports a single iteration!"); + } + opened = true; + + return new Iterator() { + int index = 0; + Path next; + + @Override + public Path next() { + if (next == null) { + if (hasNext()) { + return next; + } else { + throw new NoSuchElementException(); + } + } + Path path = next; + next = null; + return path; + } + + @Override + public boolean hasNext() { + if (closed) { + return false; + } + + if (next != null) { + return true; + } + + for (; index < entries.length; index++) { + Path at = entries[index]; + + try { + if (filter.accept(at)) { + next = at; + index++; + return true; + } + } catch (IOException e) { + throw new DirectoryIteratorException(e); + } + } + + return false; + } + }; + } + }; + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + P path = toAbsolutePath(dir); + if (path.fs.exists(path)) { + throw new FileAlreadyExistsException(path.toString()); + } + ensureWriteable(path); + path.fs.addEntryRequiringParent(new QuiltUnifiedFolderWriteable(path)); + } + + @Override + public void delete(Path path) throws IOException { + delete0(path, true); + } + + @Override + public boolean deleteIfExists(Path path) throws IOException { + return delete0(path, false); + } + + private boolean delete0(Path path, boolean throwIfMissing) throws IOException { + P p = toAbsolutePath(path); + ensureWriteable(p); + return p.fs.removeEntry(p, throwIfMissing); + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + P src = toAbsolutePath(source); + P dst = toAbsolutePath(target); + + if (src.equals(dst)) { + return; + } + + copy0(src, dst, false, options); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + P src = toAbsolutePath(source); + P dst = toAbsolutePath(target); + + if (src.equals(dst)) { + return; + } + + copy0(src, dst, true, options); + } + + private void copy0(P src, P dst, boolean isMove, CopyOption[] options) throws IOException { + if (isMove) { + ensureWriteable(src); + } + ensureWriteable(dst); + + QuiltUnifiedEntry srcEntry = src.fs.getEntry(src); + QuiltUnifiedEntry dstEntry = dst.fs.getEntry(dst); + + if (srcEntry == null) { + throw new NoSuchFileException(src.toString()); + } + + if (!(srcEntry instanceof QuiltUnifiedFile)) { + throw new IOException("Not a file: " + src); + } + + QuiltUnifiedFile srcFile = (QuiltUnifiedFile) srcEntry; + boolean canExist = false; + + for (CopyOption option : options) { + if (option == StandardCopyOption.REPLACE_EXISTING) { + canExist = true; + } + } + + if (canExist) { + delete(dst); + } else if (dstEntry != null) { + throw new FileAlreadyExistsException(dst.toString()); + } + + if (isMove) { + dstEntry = srcEntry.createMovedTo(dst); + } else { + dstEntry = srcEntry.createCopiedTo(dst); + } + + dst.fs.addEntryRequiringParent(dstEntry); + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + path = path.toAbsolutePath().normalize(); + path2 = path2.toAbsolutePath().normalize(); + // We don't support links, so we can just check for equality + return path.equals(path2); + } + + @Override + public boolean isHidden(Path path) throws IOException { + return path.getFileName().toString().startsWith("."); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + P p = toAbsolutePath(path); + for (AccessMode mode : modes) { + if (mode == AccessMode.WRITE) { + ensureWriteable(p); + } + } + QuiltUnifiedEntry entry = p.fs.getEntry(p); + if (entry == null) { + throw new NoSuchFileException(p.toString()); + } + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + P qmp = toAbsolutePath(path); + if (type == BasicFileAttributeView.class) { + BasicFileAttributeView view = new BasicFileAttributeView() { + @Override + public String name() { + return "basic"; + } + + @Override + public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) + throws IOException { + // Unsupported + // Since we don't need to throw we won't + } + + @Override + public BasicFileAttributes readAttributes() throws IOException { + return QuiltMapFileSystemProvider.this.readAttributes(qmp, BasicFileAttributes.class, options); + } + }; + return type.cast(view); + } + + return null; + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + + if (type == BasicFileAttributes.class) { + P p = toAbsolutePath(path); + QuiltUnifiedEntry entry = p.fs.getEntry(p); + if (entry == null) { + if ("/".equals(path.toString())) { + throw new NoSuchFileException(p.fs.getClass() + " [root]"); + } + throw new NoSuchFileException(path.toString()); + } + return type.cast(entry.createAttributes()); + } else { + throw new UnsupportedOperationException("Unsupported attributes " + type); + } + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + return quiltFSP().readAttributes(this, path, attributes, options); + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + throw new IOException("Attributes are unmodifiable"); + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + return toAbsolutePath(path).fs.getFileStores().iterator().next(); + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapPath.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapPath.java new file mode 100644 index 000000000..c0a269656 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMapPath.java @@ -0,0 +1,28 @@ +/* + * 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.impl.filesystem; + +import org.jetbrains.annotations.Nullable; + +public abstract class QuiltMapPath, P extends QuiltMapPath> + extends QuiltBasePath { + + QuiltMapPath(FS fs, @Nullable P parent, String name) { + super(fs, parent, name); + } + +} diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryEntry.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryEntry.java index d21962fd2..25f829337 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryEntry.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryEntry.java @@ -22,6 +22,7 @@ import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) +@Deprecated abstract class QuiltMemoryEntry { final QuiltMemoryPath path; diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFile.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFile.java index 686be7334..6b3bce68b 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFile.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFile.java @@ -32,45 +32,47 @@ import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFile; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; -import org.quiltmc.loader.impl.util.SystemProperties; @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) -abstract class QuiltMemoryFile extends QuiltMemoryEntry { +abstract class QuiltMemoryFile extends QuiltUnifiedFile { - private QuiltMemoryFile(QuiltMemoryPath path) { + private QuiltMemoryFile(QuiltMapPath path) { super(path); } - abstract InputStream createInputStream() throws IOException; - - abstract OutputStream createOutputStream(boolean append) throws IOException; - - abstract SeekableByteChannel createByteChannel(Set options) throws IOException; - - static abstract class ReadOnly extends QuiltMemoryFile { + static final class ReadOnly extends QuiltMemoryFile { + final byte[] bytes; final boolean isCompressed; final int uncompressedSize; - ReadOnly(QuiltMemoryPath path, boolean compressed, int uncompressedSize) { + ReadOnly(QuiltMapPath path, boolean compressed, int uncompressedSize, byte[] bytes) { super(path); this.isCompressed = compressed; this.uncompressedSize = uncompressedSize; + this.bytes = bytes; } - abstract byte[] byteArray(); + final byte[] byteArray() { + return bytes; + } - abstract int bytesOffset(); + final int bytesOffset() { + return 0; + } - abstract int bytesLength(); + final int bytesLength() { + return bytes.length; + } - static QuiltMemoryFile.ReadOnly create(QuiltMemoryPath path, byte[] bytes, boolean compress) { + static QuiltMemoryFile.ReadOnly create(QuiltMapPath path, byte[] bytes, boolean compress) { int size = bytes.length; if (size < 24 || !compress) { - return new QuiltMemoryFile.ReadOnly.Absolute(path, false, size, bytes); + return new QuiltMemoryFile.ReadOnly(path, false, size, bytes); } try { @@ -82,15 +84,20 @@ static QuiltMemoryFile.ReadOnly create(QuiltMemoryPath path, byte[] bytes, boole byte[] c = baos.toByteArray(); if (c.length + 24 < size) { - return new QuiltMemoryFile.ReadOnly.Absolute(path, true, size, c); + return new QuiltMemoryFile.ReadOnly(path, true, size, c); } else { - return new QuiltMemoryFile.ReadOnly.Absolute(path, false, size, bytes); + return new QuiltMemoryFile.ReadOnly(path, false, size, bytes); } } catch (IOException e) { - return new QuiltMemoryFile.ReadOnly.Absolute(path, false, size, bytes); + return new QuiltMemoryFile.ReadOnly(path, false, size, bytes); } } + @Override + protected QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath) { + return new ReadOnly(newPath, isCompressed, uncompressedSize, bytes); + } + @Override protected BasicFileAttributes createAttributes() { return new QuiltFileAttributes(path, uncompressedSize); @@ -141,7 +148,7 @@ private static IOException readOnly() throws IOException { } @Override - OutputStream createOutputStream(boolean append) throws IOException { + OutputStream createOutputStream(boolean append, boolean truncate) throws IOException { throw readOnly(); } @@ -265,56 +272,6 @@ public synchronized long position() throws IOException { return position; } } - - static final class Absolute extends ReadOnly { - private final byte[] bytes; - - Absolute(QuiltMemoryPath path, boolean compressed, int uncompressedSize, byte[] bytes) { - super(path, compressed, uncompressedSize); - this.bytes = bytes; - } - - @Override - byte[] byteArray() { - return bytes; - } - - @Override - int bytesOffset() { - return 0; - } - - @Override - int bytesLength() { - return bytes.length; - } - } - - static final class Relative extends ReadOnly { - private final int byteOffset; - private final int byteLength; - - Relative(QuiltMemoryPath path, boolean compressed, int uncompressedSize, int byteOffset, int byteLength) { - super(path, compressed, uncompressedSize); - this.byteOffset = byteOffset; - this.byteLength = byteLength; - } - - @Override - byte[] byteArray() { - return ((QuiltMemoryFileSystem.ReadOnly) path.fs).packedByteArray; - } - - @Override - int bytesOffset() { - return byteOffset; - } - - @Override - int bytesLength() { - return byteLength; - } - } } static final class ReadWrite extends QuiltMemoryFile { @@ -323,10 +280,16 @@ static final class ReadWrite extends QuiltMemoryFile { private byte[] bytes = null; private int length = 0; - ReadWrite(QuiltMemoryPath path) { + ReadWrite(QuiltMapPath path) { super(path); } + private ReadWrite(QuiltMapPath path, ReadWrite from, boolean copy) { + super(path); + this.bytes = copy ? Arrays.copyOf(from.bytes, from.bytes.length) : from.bytes; + this.length = from.length; + } + private ReadWrite sync() { return this; } @@ -375,6 +338,16 @@ void copyFrom(ReadOnly src) { } } + @Override + protected QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath) { + return new ReadWrite(newPath, this, true); + } + + @Override + protected QuiltUnifiedEntry createMovedTo(QuiltMapPath newPath) { + return new ReadWrite(newPath, this, false); + } + private void expand(int to) throws IOException { if (to > MAX_FILE_SIZE) { throw new IOException("File too big!"); @@ -450,7 +423,10 @@ public int read(byte[] b, int off, int len) throws IOException { } @Override - OutputStream createOutputStream(boolean append) throws IOException { + OutputStream createOutputStream(boolean append, boolean truncate) throws IOException { + if (truncate) { + length = 0; + } return new OutputStream() { private final byte[] tempWriter = new byte[1]; diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystem.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystem.java index 200c9528b..fac274faf 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystem.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystem.java @@ -22,9 +22,7 @@ import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; -import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributeView; @@ -32,10 +30,8 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttributeView; import java.nio.file.attribute.FileTime; -import java.nio.file.spi.FileSystemProvider; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; @@ -44,8 +40,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -53,13 +47,14 @@ import org.jetbrains.annotations.Nullable; import org.quiltmc.loader.api.CachedFileSystem; import org.quiltmc.loader.api.FasterFiles; -import org.quiltmc.loader.api.plugin.NonZipException; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderReadOnly; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderWriteable; import org.quiltmc.loader.impl.util.FileUtil; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) -public abstract class QuiltMemoryFileSystem extends QuiltBaseFileSystem implements CachedFileSystem { +public abstract class QuiltMemoryFileSystem extends QuiltMapFileSystem implements CachedFileSystem { private static final Set FILE_ATTRS = Collections.singleton("basic"); @@ -71,13 +66,10 @@ enum OpenState { CLOSED; } - final Map files; - volatile OpenState openState = OpenState.OPEN; - private QuiltMemoryFileSystem(String name, boolean uniquify, Map fileMap) { + private QuiltMemoryFileSystem(String name, boolean uniquify) { super(QuiltMemoryFileSystem.class, QuiltMemoryPath.class, name, uniquify); - this.files = fileMap; QuiltMemoryFileSystemProvider.PROVIDER.register(this); } @@ -123,63 +115,9 @@ public Set supportedFileAttributeViews() { return FILE_ATTRS; } - // FasterFileSystem - - @Override - public boolean isSymbolicLink(Path path) { - // Not supported - return false; - } - - @Override - public boolean isDirectory(Path path, LinkOption... options) { - return files.get(path.toAbsolutePath().normalize()) instanceof QuiltMemoryFolder; - } - - @Override - public boolean isRegularFile(Path path, LinkOption[] options) { - return files.get(path.toAbsolutePath().normalize()) instanceof QuiltMemoryFile; - } - - @Override - public boolean exists(Path path, LinkOption... options) { - return files.containsKey(path.toAbsolutePath().normalize()); - } - - @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 Stream list(Path dir) throws IOException { - return getChildren(dir).stream().map(p -> (Path) p); - } - - @Override - public Collection getChildren(Path dir) throws IOException { - QuiltMemoryEntry entry = files.get(dir.toAbsolutePath().normalize()); - if (entry instanceof QuiltMemoryFolder) { - return ((QuiltMemoryFolder) entry).getChildren(); - } else { - throw new NotDirectoryException(dir.toString()); - } - } - public BasicFileAttributes readAttributes(QuiltMemoryPath qmp) throws IOException { checkOpen(); - QuiltMemoryEntry entry = files.get(qmp.toAbsolutePath().normalize()); + QuiltUnifiedEntry entry = getEntry(qmp); if (entry != null) { return entry.createAttributes(); @@ -226,16 +164,6 @@ public DirBuildState(QuiltMemoryPath folder) { public static final class ReadOnly extends QuiltMemoryFileSystem implements ReadOnlyFileSystem { - /** Used to store all file data content in a single byte array. Potentially provides performance gains as we - * don't allocate as many objects (plus it's all packed). Comes with the one-time cost of actually coping all - * the bytes out of their original arrays and into the bigger byte array. */ - // TODO: Test: - // 1: how expensive the "packing" is - // 2: how much space we gain or perf we gain? - // Disabled by default after testing - it doesn't save much memory - // and adds a HUGE memory requirement bump during launch. - private static final boolean PACK_FILE_DATA = false; - private static final int STAT_UNCOMPRESSED = 0; private static final int STAT_USED = 1; private static final int STAT_MEMORY = 2; @@ -244,15 +172,12 @@ public static final class ReadOnly extends QuiltMemoryFileSystem implements Read private QuiltMemoryFileStore.ReadOnly fileStore; private Iterable fileStoreItr; - /** Only used if {@link #PACK_FILE_DATA} is true. */ - final byte[] packedByteArray; - /** Creates a new read-only {@link FileSystem} that copies every file in the given directory. * * @param compress if true then all files will be stored in-memory compressed. * @throws IOException if any of the files in the given path could not be read. */ public ReadOnly(String name, boolean uniquify, Path from, boolean compress) throws IOException { - super(name, uniquify, new HashMap<>()); + super(name, uniquify); int[] stats = new int[3]; stats[STAT_MEMORY] = 60; @@ -291,8 +216,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO state.children.add(childPath); QuiltMemoryFile.ReadOnly qmf = QuiltMemoryFile.ReadOnly.create(childPath, Files.readAllBytes(file), compress); putFileStats(stats, qmf); - - files.put(childPath, qmf); + addEntryWithoutParents(qmf); return super.visitFile(file, attrs); } @@ -301,7 +225,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { DirBuildState state = stack.pop(); QuiltMemoryPath[] children = state.children.toArray(new QuiltMemoryPath[0]); - files.put(state.folder, new QuiltMemoryFolder.ReadOnly(state.folder, children)); + addEntryWithoutParents(new QuiltUnifiedFolderReadOnly(state.folder, children)); stats[STAT_MEMORY] += children.length * 4 + 12; @@ -315,45 +239,22 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx uncompressedSize = stats[STAT_UNCOMPRESSED]; usedSize = stats[STAT_USED]; - memorySize = stats[STAT_MEMORY] + ((int) (files.size() * 24 / 0.75f)); - packedByteArray = pack(); + memorySize = stats[STAT_MEMORY] + ((int) (getEntryCount() * 24 / 0.75f)); fileStore = new QuiltMemoryFileStore.ReadOnly(name, usedSize); fileStoreItr = Collections.singleton(fileStore); } + @Override + protected boolean startWithConcurrentMap() { + return false; + } + private static void putFileStats(int[] stats, QuiltMemoryFile.ReadOnly qmf) { stats[STAT_UNCOMPRESSED] += qmf.uncompressedSize; stats[STAT_USED] += qmf.byteArray().length; stats[STAT_MEMORY] += qmf.byteArray().length + 16; } - private byte[] pack() { - if (PACK_FILE_DATA) { - byte[] packed = new byte[usedSize]; - - int pos = 0; - - Iterator> iter = files.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry entry = iter.next(); - if (entry.getValue() instanceof QuiltMemoryFile.ReadOnly.Absolute) { - QuiltMemoryFile.ReadOnly.Absolute abs = (QuiltMemoryFile.ReadOnly.Absolute) entry.getValue(); - int len = abs.bytesLength(); - System.arraycopy(abs.byteArray(), 0, packed, pos, len); - entry.setValue( - new QuiltMemoryFile.ReadOnly.Relative( - abs.path, abs.isCompressed, abs.uncompressedSize, pos, len - ) - ); - pos += len; - } - } - return packed; - } else { - return null; - } - } - /** Creates a new read-only file system that copies every entry of a {@link ZipInputStream} that starts with * "zipPathPrefix". This is effectively the same as * {@link QuiltZipFileSystem#QuiltZipFileSystem(String, Path, String)}, but doesn't open files from their @@ -368,13 +269,13 @@ private byte[] pack() { * @param compress If true then entries will be compressed in-memory. Generally slow. * @throws IOException if {@link ZipInputStream} threw an {@link IOException} while reading entries. */ public ReadOnly(String name, ZipInputStream zipFrom, String zipPathPrefix, boolean compress) throws IOException { - super(name, true, new HashMap<>()); + super(name, true); + + addEntryAndParents(new QuiltUnifiedFolderWriteable(root)); int[] stats = new int[3]; stats[STAT_MEMORY] = 60; - Map> folders = new HashMap<>(); - boolean anyEntries = false; ZipEntry entry; while ((entry = zipFrom.getNextEntry()) != null) { @@ -391,17 +292,14 @@ public ReadOnly(String name, ZipInputStream zipFrom, String zipPathPrefix, boole QuiltMemoryPath path = getPath(entryName); if (entryName.endsWith("/")) { - // Folder, but it might have already been automatically added by a file - folders.computeIfAbsent(path, p -> new HashSet<>()); - putParentFolders(folders, path); + createDirectories(path); } else { // File stats[STAT_MEMORY] += path.name.length() + 28; byte[] bytes = FileUtil.readAllBytes(zipFrom); QuiltMemoryFile.ReadOnly qmf = QuiltMemoryFile.ReadOnly.create(path, bytes, compress); putFileStats(stats, qmf); - files.put(path, qmf); - putParentFolders(folders, path); + addEntryAndParents(qmf); } } @@ -412,17 +310,11 @@ public ReadOnly(String name, ZipInputStream zipFrom, String zipPathPrefix, boole throw new IOException("No zip entries found!"); } - for (Map.Entry> folder : folders.entrySet()) { - QuiltMemoryPath folderPath = folder.getKey(); - QuiltMemoryPath[] entries = folder.getValue().toArray(new QuiltMemoryPath[0]); - stats[STAT_MEMORY] += folderPath.name.length() + entries.length * 4 + 12; - files.put(folderPath, new QuiltMemoryFolder.ReadOnly(folderPath, entries)); - } + switchToReadOnly(); uncompressedSize = stats[STAT_UNCOMPRESSED]; usedSize = stats[STAT_USED]; - memorySize = stats[STAT_MEMORY] + ((int) (files.size() * 24 / 0.75f)); - packedByteArray = pack(); + memorySize = stats[STAT_MEMORY] + ((int) (getEntryCount() * 24 / 0.75f)); fileStore = new QuiltMemoryFileStore.ReadOnly(name, usedSize); fileStoreItr = Collections.singleton(fileStore); @@ -448,11 +340,6 @@ public boolean isReadOnly() { return true; } - @Override - public boolean isPermanentlyReadOnly() { - return false; - } - /** @return The uncompressed size of all files stored in this file system. Since we store file data compressed * this doesn't reflect actual byte usage. */ public int getUncompressedSize() { @@ -492,21 +379,21 @@ public QuiltMemoryFileSystem.ReadWrite replaceWithWritable() { } private void copyPath(QuiltMemoryPath src, QuiltMemoryPath dst) { - QuiltMemoryEntry entrySrc = src.fs.files.get(src); + QuiltUnifiedEntry entrySrc = src.fs.getEntry(src); if (entrySrc instanceof QuiltMemoryFile) { QuiltMemoryFile.ReadOnly fileSrc = (QuiltMemoryFile.ReadOnly) entrySrc; QuiltMemoryFile.ReadWrite fileDst = new QuiltMemoryFile.ReadWrite(dst); fileDst.copyFrom(fileSrc); - dst.fs.files.put(dst, fileDst); + dst.fs.addEntryWithoutParentsUnsafe(fileDst); } else { - QuiltMemoryFolder.ReadOnly folderSrc = (QuiltMemoryFolder.ReadOnly) entrySrc; - QuiltMemoryFolder.ReadWrite folderDst = new QuiltMemoryFolder.ReadWrite(dst); - dst.fs.files.put(dst, folderDst); + QuiltUnifiedFolderReadOnly folderSrc = (QuiltUnifiedFolderReadOnly) entrySrc; + QuiltUnifiedFolderWriteable folderDst = new QuiltUnifiedFolderWriteable(dst); + dst.fs.addEntryWithoutParentsUnsafe(folderDst); - for (QuiltMemoryPath pathSrc : folderSrc.children) { + for (QuiltMapPath pathSrc : folderSrc.children) { QuiltMemoryPath pathDst = dst.resolve(pathSrc.name); folderDst.children.add(pathDst); - copyPath(pathSrc, pathDst); + copyPath((QuiltMemoryPath) pathSrc, pathDst); } } } @@ -518,10 +405,15 @@ public static final class ReadWrite extends QuiltMemoryFileSystem { private Iterable fileStoreItr; public ReadWrite(String name, boolean uniquify) { - super(name, uniquify, new ConcurrentHashMap<>()); + super(name, uniquify); fileStore = new QuiltMemoryFileStore.ReadWrite(name, this); fileStoreItr = Collections.singleton(fileStore); - files.put(root, new QuiltMemoryFolder.ReadWrite(root)); + addEntryAndParentsUnsafe(new QuiltUnifiedFolderWriteable(root)); + } + + @Override + protected boolean startWithConcurrentMap() { + return true; } public ReadWrite(String name, boolean uniquify, Path from) throws IOException { @@ -556,16 +448,16 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO QuiltMemoryPath childPath = state.folder.resolve(fileName); state.children.add(childPath); QuiltMemoryFile.ReadWrite qmf = new QuiltMemoryFile.ReadWrite(childPath); - qmf.createOutputStream(false).write(Files.readAllBytes(file)); - files.put(childPath, qmf); + qmf.createOutputStream(false, false).write(Files.readAllBytes(file)); + addEntryWithoutParents(qmf); return super.visitFile(file, attrs); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { DirBuildState state = stack.pop(); - QuiltMemoryFolder.ReadWrite qmf = new QuiltMemoryFolder.ReadWrite(state.folder); - files.put(state.folder, qmf); + QuiltUnifiedFolderWriteable qmf = new QuiltUnifiedFolderWriteable(state.folder); + addEntryWithoutParents(qmf); qmf.children.addAll(state.children); return super.postVisitDirectory(dir, exc); } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java index 32e56ad16..0ec541b41 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java @@ -17,47 +17,19 @@ package org.quiltmc.loader.impl.filesystem; import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.WeakReference; import java.net.URI; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.AccessMode; -import java.nio.file.CopyOption; -import java.nio.file.DirectoryIteratorException; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.DirectoryStream; -import java.nio.file.DirectoryStream.Filter; -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.LinkOption; -import java.nio.file.NoSuchFileException; -import java.nio.file.NotDirectoryException; -import java.nio.file.OpenOption; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributeView; -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.HashMap; -import java.util.Iterator; import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import org.jetbrains.annotations.Nullable; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; -@SuppressWarnings("unchecked") // TODO make more specific @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) -public final class QuiltMemoryFileSystemProvider extends FileSystemProvider { +public final class QuiltMemoryFileSystemProvider extends QuiltMapFileSystemProvider { public QuiltMemoryFileSystemProvider() {} public static final String SCHEME = "quilt.mfs"; @@ -75,402 +47,32 @@ public static QuiltMemoryFileSystemProvider instance() { } @Override - public String getScheme() { - return SCHEME; + protected QuiltFSP quiltFSP() { + return PROVIDER; } @Override - public FileSystem newFileSystem(URI uri, Map env) throws IOException { - throw new IOException("Only direct creation is supported"); - } - - @Override - public FileSystem getFileSystem(URI uri) { - throw new FileSystemNotFoundException("Only direct creation is supported"); - } - - @Override - public QuiltMemoryPath getPath(URI uri) { - return PROVIDER.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"); - } - } - - if (path instanceof QuiltMemoryPath) { - QuiltMemoryPath p = (QuiltMemoryPath) path; - QuiltMemoryEntry entry = p.fs.files.get(p.toAbsolutePath().normalize()); - if (entry instanceof QuiltMemoryFile) { - return ((QuiltMemoryFile) entry).createInputStream(); - } else if (entry != null) { - throw new FileSystemException("Cannot open an InputStream on a directory!"); - } else { - throw new NoSuchFileException(path.toString()); - } - } else { - throw new IllegalArgumentException("The given path is not a QuiltMemoryPath!"); - } + protected Class fileSystemClass() { + return QuiltMemoryFileSystem.class; } @Override - public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) - throws IOException { - - if (!(path instanceof QuiltMemoryPath)) { - throw new IllegalArgumentException("The given path is not a QuiltMemoryPath!"); - } - - QuiltMemoryPath p = (QuiltMemoryPath) path; - QuiltMemoryPath normalizedPath = p.toAbsolutePath().normalize(); - QuiltMemoryEntry entry = p.fs.files.get(normalizedPath); - - 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: " + p); - } - create = true; - } else if (o == StandardOpenOption.CREATE) { - create = true; - } - } - - if (create && entry == null) { - if (!normalizedPath.isAbsolute()) { - throw new IOException("Cannot work above the root!"); - } - - QuiltMemoryEntry parent = p.fs.files.get(normalizedPath.getParent()); - if (parent == null) { - throw new IOException("Cannot create a file if it's parent directory doesn't exist!"); - } - if (!(parent instanceof QuiltMemoryFolder.ReadWrite)) { - throw new IOException("Cannot create a file if it's parent is not a directory!"); - } - p.fs.files.put(normalizedPath, entry = new QuiltMemoryFile.ReadWrite(normalizedPath)); - ((QuiltMemoryFolder.ReadWrite) parent).children.add(normalizedPath); - } - } - - if (entry instanceof QuiltMemoryFile) { - return ((QuiltMemoryFile) entry).createByteChannel(options); - } else if (entry != null) { - throw new FileSystemException("Cannot open an InputStream on a directory!"); - } else { - throw new NoSuchFileException(path.toString()); - } + protected Class pathClass() { + return QuiltMemoryPath.class; } @Override - public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { - QuiltMemoryPath qmp = (QuiltMemoryPath) dir; - - final QuiltMemoryPath[] entries; - QuiltMemoryEntry entry = qmp.fs.files.get(qmp); - if (entry instanceof QuiltMemoryFolder.ReadOnly) { - entries = ((QuiltMemoryFolder.ReadOnly) entry).children; - } else if (entry instanceof QuiltMemoryFolder.ReadWrite) { - entries = ((QuiltMemoryFolder.ReadWrite) entry).children - .toArray(new QuiltMemoryPath[0]); - } else { - throw new NotDirectoryException("Not a directory: " + dir); - } - - return new DirectoryStream() { - - boolean opened = false; - boolean closed = false; - - @Override - public void close() throws IOException { - closed = true; - } - - @Override - public Iterator iterator() { - if (opened) { - throw new IllegalStateException("newDirectoryStream only supports a single iteration!"); - } - opened = true; - - return new Iterator() { - int index = 0; - Path next; - - @Override - public Path next() { - if (next == null) { - if (hasNext()) { - return next; - } else { - throw new NoSuchElementException(); - } - } - Path path = next; - next = null; - return path; - } - - @Override - public boolean hasNext() { - if (closed) { - return false; - } - - if (next != null) { - return true; - } - - for (; index < entries.length; index++) { - Path at = entries[index]; - - try { - if (filter.accept(at)) { - next = at; - index++; - return true; - } - } catch (IOException e) { - throw new DirectoryIteratorException(e); - } - } - - return false; - } - }; - } - }; - } - - private static QuiltMemoryPath toAbsQuiltPath(Path path) { - Path p = path.toAbsolutePath().normalize(); - if (p instanceof QuiltMemoryPath) { - return (QuiltMemoryPath) p; - } else { - throw new IllegalArgumentException("Only 'QuiltMemoryPath' is supported!"); - } - } - - @Override - public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { - if (dir.getFileSystem().isReadOnly()) { - throw new IOException(READ_ONLY_EXCEPTION); - } - QuiltMemoryPath d = toAbsQuiltPath(dir); - QuiltMemoryPath parent = d.parent; - - QuiltMemoryEntry entry = d.fs.files.get(d); - QuiltMemoryEntry parentEntry = d.fs.files.get(parent); - - if (entry != null) { - throw new FileAlreadyExistsException(d.toString()); - } - - if (parentEntry instanceof QuiltMemoryFolder.ReadWrite) { - - QuiltMemoryFolder.ReadWrite parentFolder = (QuiltMemoryFolder.ReadWrite) parentEntry; - d.fs.files.put(d, new QuiltMemoryFolder.ReadWrite(d)); - parentFolder.children.add(d); - - } else if (parentEntry != null) { - throw new IOException("Parent file is not a directory " + parent); - } else { - throw new IOException("Parent is missing! " + parent); - } - } - - @Override - public void delete(Path path) throws IOException { - delete0(path, true); - } - - @Override - public boolean deleteIfExists(Path path) throws IOException { - return delete0(path, false); - } - - private static boolean delete0(Path path, boolean throwIfMissing) throws IOException { - if (path.getFileSystem().isReadOnly()) { - throw new IOException(READ_ONLY_EXCEPTION); - } - QuiltMemoryPath p = toAbsQuiltPath(path); - QuiltMemoryEntry entry = p.fs.files.get(p); - if (entry == null) { - if (throwIfMissing) { - throw new NoSuchFileException(p.toString()); - } else { - return false; - } - } - QuiltMemoryEntry pEntry = p.fs.files.get(p.parent); - - if (entry instanceof QuiltMemoryFolder.ReadWrite) { - QuiltMemoryFolder.ReadWrite folder = (QuiltMemoryFolder.ReadWrite) entry; - if (!folder.children.isEmpty()) { - throw new DirectoryNotEmptyException(p.toString()); - } - } - - if (pEntry == null) { - throw new IOException("Missing parent folder to delete " + p + " from!"); - } - - ((QuiltMemoryFolder.ReadWrite) pEntry).children.remove(p); - p.fs.files.remove(p); - return true; - } - - @Override - public void copy(Path source, Path target, CopyOption... options) throws IOException { - if (target.getFileSystem().isReadOnly()) { - throw new IOException(READ_ONLY_EXCEPTION); - } - QuiltMemoryPath src = toAbsQuiltPath(source); - QuiltMemoryPath dst = toAbsQuiltPath(target); - - if (src.equals(dst)) { - return; - } - - QuiltMemoryEntry srcEntry = src.fs.files.get(src); - QuiltMemoryEntry dstEntry = dst.fs.files.get(dst); - - if (srcEntry == null) { - throw new NoSuchFileException(src.toString()); - } - - if (!(srcEntry instanceof QuiltMemoryFile)) { - throw new IOException("Not a file: " + src); - } - - QuiltMemoryFile srcFile = (QuiltMemoryFile) srcEntry; - boolean canExist = false; - - for (CopyOption option : options) { - if (option == StandardCopyOption.REPLACE_EXISTING) { - canExist = true; - } - } - - QuiltMemoryFile.ReadWrite dstFile; - - if (canExist) { - if (dstEntry instanceof QuiltMemoryFolder.ReadWrite) { - QuiltMemoryFolder.ReadWrite dstFolder = (QuiltMemoryFolder.ReadWrite) dstEntry; - if (!dstFolder.children.isEmpty()) { - throw new DirectoryNotEmptyException(dstEntry.path.toString()); - } - dstEntry = dstFile = new QuiltMemoryFile.ReadWrite(dst); - dst.fs.files.put(dst, dstEntry); - } else if (dstEntry instanceof QuiltMemoryFile.ReadWrite) { - dstFile = (QuiltMemoryFile.ReadWrite) dstEntry; - } else if (dstEntry != null) { - throw new IllegalStateException("Not a RW folder or file: " + dstEntry.getClass()); - } else { - dstFile = null; - } - } else if (dstEntry != null) { - throw new FileAlreadyExistsException(dst.toString()); - } else { - dstFile = null; - } - - if (dstFile == null) { - QuiltMemoryEntry parent = dst.fs.files.get(dst.parent); - if (parent == null) { - throw new IOException("Missing parent folder! " + dst); - } else if (parent instanceof QuiltMemoryFolder.ReadWrite) { - QuiltMemoryFolder.ReadWrite parentFolder = (QuiltMemoryFolder.ReadWrite) parent; - parentFolder.children.add(dst); - } else { - throw new IOException("Parent is not a folder! " + dst); - } - - dstEntry = dstFile = new QuiltMemoryFile.ReadWrite(dst); - dst.fs.files.put(dst, dstEntry); - } - - dstFile.copyFrom(srcFile); - } - - @Override - public void move(Path source, Path target, CopyOption... options) throws IOException { - if (target.getFileSystem().isReadOnly()) { - throw new IOException(READ_ONLY_EXCEPTION); - } - - if (isSameFile(source, target)) { - return; - } - - copy(source, target, options); - delete(source); - } - - @Override - public boolean isSameFile(Path path, Path path2) throws IOException { - path = path.toAbsolutePath().normalize(); - path2 = path2.toAbsolutePath().normalize(); - // We don't support links, so we can just check for equality - return path.equals(path2); + public String getScheme() { + return SCHEME; } @Override - public boolean isHidden(Path path) throws IOException { - return path.getFileName().toString().startsWith("."); + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + throw new IOException("Only direct creation is supported"); } @Override public FileStore getFileStore(Path path) throws IOException { return ((QuiltMemoryPath) path).fs.getFileStores().iterator().next(); } - - @Override - public void checkAccess(Path path, AccessMode... modes) throws IOException { - for (AccessMode mode : modes) { - if (mode == AccessMode.WRITE && path.getFileSystem().isReadOnly()) { - throw new IOException(READ_ONLY_EXCEPTION); - } - } - QuiltMemoryPath quiltPath = toAbsQuiltPath(path); - QuiltMemoryEntry entry = quiltPath.fs.files.get(quiltPath); - if (entry == null) { - throw new NoSuchFileException(quiltPath.toString()); - } - } - - @Override - public V getFileAttributeView(Path path, Class type, LinkOption... options) { - QuiltMemoryPath qmp = toAbsQuiltPath(path); - return qmp.fs.getFileAttributeView(qmp, type); - } - - @Override - public A readAttributes(Path path, Class type, LinkOption... options) - throws IOException { - - if (type == BasicFileAttributes.class) { - QuiltMemoryPath qmp = (QuiltMemoryPath) path; - return (A) qmp.fs.readAttributes(qmp); - } else { - throw new UnsupportedOperationException("Unsupported attributes " + type); - } - } - - @Override - public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { - return PROVIDER.readAttributes(this, path, attributes, options); - } - - @Override - public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { - throw new IOException(READ_ONLY_EXCEPTION); - } } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFolder.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFolder.java index 16b0299a5..32f777d9c 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFolder.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFolder.java @@ -28,6 +28,7 @@ import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) +@Deprecated abstract class QuiltMemoryFolder extends QuiltMemoryEntry { private QuiltMemoryFolder(QuiltMemoryPath path) { @@ -41,6 +42,7 @@ protected BasicFileAttributes createAttributes() { protected abstract Collection getChildren(); + @Deprecated public static final class ReadOnly extends QuiltMemoryFolder { final QuiltMemoryPath[] children; @@ -55,6 +57,7 @@ protected Collection getChildren() { } } + @Deprecated public static final class ReadWrite extends QuiltMemoryFolder { final Set children = Collections.newSetFromMap(new ConcurrentHashMap<>()); diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryPath.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryPath.java index 5f5dd817d..db6e86a9c 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryPath.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryPath.java @@ -21,7 +21,7 @@ import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) -public final class QuiltMemoryPath extends QuiltBasePath { +public final class QuiltMemoryPath extends QuiltMapPath { QuiltMemoryPath(@NotNull QuiltMemoryFileSystem fs, QuiltMemoryPath parent, String name) { super(fs, parent, name); diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedEntry.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedEntry.java new file mode 100644 index 000000000..34d07e946 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedEntry.java @@ -0,0 +1,250 @@ +/* + * 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.impl.filesystem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public abstract /* sealed */ class QuiltUnifiedEntry /* permits QuiltUnifiedFolder, QuiltUnifiedFile */ { + + // We don't actually need generics at this point + final QuiltMapPath path; + + private QuiltUnifiedEntry(QuiltMapPath path) { + this.path = path.toAbsolutePath().normalize(); + } + + @Override + public String toString() { + return path + " " + getClass().getName(); + } + + protected abstract BasicFileAttributes createAttributes() throws IOException; + + protected QuiltUnifiedEntry switchToReadOnly() { + return this; + } + + /** @return A new entry which has been copied to the new path. Might not be on the same filesystem. */ + protected abstract QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath); + + /** Like {@link #createCopiedTo(QuiltMapPath)}, but used when the original file will be deleted - which allows some entries to + * be shallow copied. */ + protected QuiltUnifiedEntry createMovedTo(QuiltMapPath newPath) { + return createCopiedTo(newPath); + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + public static abstract class QuiltUnifiedFolder extends QuiltUnifiedEntry { + private QuiltUnifiedFolder(QuiltMapPath path) { + super(path); + } + + @Override + protected BasicFileAttributes createAttributes() { + return new QuiltFileAttributes(path, QuiltFileAttributes.SIZE_DIRECTORY); + } + + protected abstract Collection getChildren(); + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + public static final class QuiltUnifiedFolderReadOnly extends QuiltUnifiedFolder { + public final QuiltMapPath[] children; + + public QuiltUnifiedFolderReadOnly(QuiltMapPath path, QuiltMapPath[] children) { + super(path); + this.children = children; + } + + @Override + protected Collection getChildren() { + return Arrays.asList(children); + } + + @Override + protected QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath) { + return new QuiltUnifiedFolderReadOnly(newPath, new QuiltMapPath[0]); + } + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + public static final class QuiltUnifiedFolderWriteable extends QuiltUnifiedFolder { + public final Set> children = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public QuiltUnifiedFolderWriteable(QuiltMapPath path) { + super(path); + } + + @Override + protected Collection getChildren() { + return children; + } + + @Override + protected QuiltUnifiedEntry switchToReadOnly() { + return new QuiltUnifiedFolderReadOnly(path, children.toArray(new QuiltMapPath[0])); + } + + @Override + protected QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath) { + return new QuiltUnifiedFolderWriteable(newPath); + } + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + public static abstract class QuiltUnifiedFile extends QuiltUnifiedEntry { + public QuiltUnifiedFile(QuiltMapPath path) { + super(path); + } + + abstract InputStream createInputStream() throws IOException; + + abstract OutputStream createOutputStream(boolean append, boolean truncate) throws IOException; + + abstract SeekableByteChannel createByteChannel(Set options) throws IOException; + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + public static class QuiltUnifiedMountedFile extends QuiltUnifiedFile { + + public final Path to; + public final boolean readOnly; + + public QuiltUnifiedMountedFile(QuiltMapPath path, Path to, boolean readOnly) { + super(path); + this.to = to; + this.readOnly = readOnly; + } + + @Override + InputStream createInputStream() throws IOException { + return Files.newInputStream(to); + } + + @Override + OutputStream createOutputStream(boolean append, boolean truncate) throws IOException { + if (readOnly) { + throw new IOException("ReadOnly"); + } + List options = new ArrayList<>(3); + options.add(StandardOpenOption.WRITE); + if (append) { + options.add(StandardOpenOption.APPEND); + } + if (truncate) { + options.add(StandardOpenOption.TRUNCATE_EXISTING); + } + return Files.newOutputStream(to, options.toArray(new OpenOption[0])); + } + + @Override + SeekableByteChannel createByteChannel(Set options) throws IOException { + for (OpenOption option : options) { + if (option != StandardOpenOption.READ && readOnly) { + throw new IOException("ReadOnly"); + } + } + + return Files.newByteChannel(to, options); + } + + @Override + protected BasicFileAttributes createAttributes() throws IOException { + BasicFileAttributes attrs = Files.readAttributes(to, BasicFileAttributes.class); + return new QuiltFileAttributes(this, attrs.size()); + } + + @Override + protected QuiltUnifiedEntry switchToReadOnly() { + if (readOnly) { + return this; + } else { + return new QuiltUnifiedMountedFile(path, to, true); + } + } + + @Override + protected QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath) { + return new QuiltUnifiedMountedFile(newPath, to, readOnly); + } + } + + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + public static class QuiltUnifiedCopyOnWriteFile extends QuiltUnifiedMountedFile { + public QuiltUnifiedCopyOnWriteFile(QuiltMapPath path, Path to) { + super(path, to, false); +// System.out.println("NEW copy-on-write " + path + " -> " + to); + } + + @Override + protected QuiltUnifiedEntry switchToReadOnly() { + // If we're still present then we haven't been modified. + return new QuiltUnifiedMountedFile(path, to, true); + } + + @Override + protected QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath) { + return new QuiltUnifiedCopyOnWriteFile(newPath, to); + } + + private QuiltUnifiedFile deepCopy(boolean truncate) throws IOException { + System.out.println("REMOVED copy-on-write " + path); + path.fs.provider().delete(path); + QuiltMemoryFile.ReadWrite file = new QuiltMemoryFile.ReadWrite(path); + if (!truncate) { + try (OutputStream dst = file.createOutputStream(true, true)) { + Files.copy(path, dst); + } + } + path.fs.addEntryRequiringParent(file); + return file; + } + + @Override + OutputStream createOutputStream(boolean append, boolean truncate) throws IOException { + return deepCopy(truncate).createOutputStream(append, truncate); + } + + @Override + SeekableByteChannel createByteChannel(Set options) throws IOException { + if (options.contains(StandardOpenOption.WRITE)) { + boolean truncate = options.contains(StandardOpenOption.TRUNCATE_EXISTING); + return deepCopy(truncate).createByteChannel(options); + } + return super.createByteChannel(options); + } + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystem.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystem.java new file mode 100644 index 000000000..a69acdc67 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystem.java @@ -0,0 +1,196 @@ +/* + * 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.impl.filesystem; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.NotLinkException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; +import org.quiltmc.loader.api.CachedFileSystem; +import org.quiltmc.loader.api.ExtendedFileSystem; +import org.quiltmc.loader.api.MountOption; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedCopyOnWriteFile; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderWriteable; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedMountedFile; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +/** General-purpose {@link FileSystem}, used when building the transform cache. Also intended to replace the various + * zip/memory file systems currently in use. */ +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public class QuiltUnifiedFileSystem extends QuiltMapFileSystem implements ExtendedFileSystem { + + private boolean readOnly = false; + + public QuiltUnifiedFileSystem(String name, boolean uniqueify) { + super(QuiltUnifiedFileSystem.class, QuiltUnifiedPath.class, name, uniqueify); + addEntryAndParentsUnsafe(new QuiltUnifiedFolderWriteable(root)); + } + + @Override + protected boolean startWithConcurrentMap() { + return true; + } + + @Override + QuiltUnifiedPath createPath(@Nullable QuiltUnifiedPath parent, String name) { + return new QuiltUnifiedPath(this, parent, name); + } + + @Override + public QuiltUnifiedFileSystemProvider provider() { + return QuiltUnifiedFileSystemProvider.instance(); + } + + /** Disallows all modification. */ + @Override + public void switchToReadOnly() { + super.switchToReadOnly(); + readOnly = true; + } + + @Override + public boolean isPermanentlyReadOnly() { + return readOnly; + } + + @Override + public void close() throws IOException { + + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public boolean isReadOnly() { + return isPermanentlyReadOnly(); + } + + @Override + public Path copyOnWrite(Path source, Path target, CopyOption... options) throws IOException { + FileSystem srcFS = source.getFileSystem(); + if (srcFS instanceof CachedFileSystem) { + CachedFileSystem cached = (CachedFileSystem) srcFS; + if (!cached.isPermanentlyReadOnly()) { + System.out.println("Cannot copy-on-write " + cached.getClass() + " " + source + " " + source.getClass()); + return copy(source, target, options); + } + } else { + System.out.println("Cannot copy-on-write other " + source + " " + source.getClass()); + return copy(source, target, options); + } + QuiltUnifiedPath dst = provider().toAbsolutePath(target); + QuiltUnifiedEntry dstEntry = getEntry(dst); + + boolean canExist = false; + + for (CopyOption option : options) { + if (option == StandardCopyOption.REPLACE_EXISTING) { + canExist = true; + } + } + + if (canExist) { + provider().delete(dst); + } else if (dstEntry != null) { + throw new FileAlreadyExistsException(dst.toString()); + } + + addEntryRequiringParent(new QuiltUnifiedCopyOnWriteFile(dst, source)); + return dst; + } + + @Override + public Path mount(Path source, Path target, MountOption... options) throws IOException { + QuiltUnifiedPath dst = provider().toAbsolutePath(target); + QuiltUnifiedEntry dstEntry = getEntry(dst); + + boolean canExist = false; + boolean readOnly = false; + boolean copyOnWrite = false; + + for (MountOption option : options) { + switch (option) { + case REPLACE_EXISTING: { + canExist = true; + break; + } + case COPY_ON_WRITE: { + copyOnWrite = true; + break; + } + case READ_ONLY: { + readOnly = true; + break; + } + default: { + throw new IllegalStateException("Unknown MountOption " + option); + } + } + } + + if (copyOnWrite && readOnly) { + throw new IllegalArgumentException("Can't specify both READ_ONLY and COPY_ON_WRITE : " + Arrays.toString(options)); + } + + + if (canExist) { + provider().delete(dst); + } else if (dstEntry != null) { + throw new FileAlreadyExistsException(dst.toString()); + } + + if (copyOnWrite) { + dstEntry = new QuiltUnifiedCopyOnWriteFile(dst, source); + } else { + dstEntry = new QuiltUnifiedMountedFile(dst, source, readOnly); + } + addEntryRequiringParent(dstEntry); + return dst; + } + + @Override + public boolean isMountedFile(Path file) { + return getEntry(file) instanceof QuiltUnifiedMountedFile; + } + + @Override + public boolean isCopyOnWrite(Path file) { + return getEntry(file) instanceof QuiltUnifiedCopyOnWriteFile; + } + + @Override + public Path readMountTarget(Path file) throws IOException { + QuiltUnifiedEntry entry = getEntry(file); + if (entry instanceof QuiltUnifiedMountedFile) { + return ((QuiltUnifiedMountedFile) entry).to; + } else { + throw new NotLinkException(file.toString() + " is not a mounted file!"); + } + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystemProvider.java new file mode 100644 index 000000000..0be9874a6 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystemProvider.java @@ -0,0 +1,88 @@ +/* + * 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.impl.filesystem; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +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 org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public class QuiltUnifiedFileSystemProvider extends QuiltMapFileSystemProvider { + public QuiltUnifiedFileSystemProvider() {} + + public static final String SCHEME = "quilt.ufs"; + + static final String READ_ONLY_EXCEPTION = "This FileSystem is read-only"; + static final QuiltFSP PROVIDER = new QuiltFSP<>(SCHEME); + + public static QuiltUnifiedFileSystemProvider instance() { + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { + if (provider instanceof QuiltUnifiedFileSystemProvider) { + return (QuiltUnifiedFileSystemProvider) provider; + } + } + throw new IllegalStateException("Unable to load QuiltUnifiedFileSystemProvider via services!"); + } + + @Override + public String getScheme() { + return SCHEME; + } + + @Override + protected QuiltFSP quiltFSP() { + return PROVIDER; + } + + @Override + protected Class fileSystemClass() { + return QuiltUnifiedFileSystem.class; + } + + @Override + protected Class pathClass() { + return QuiltUnifiedPath.class; + } + + @Override + public QuiltUnifiedPath getPath(URI uri) { + return PROVIDER.getFileSystem(uri).root.resolve(uri.getPath()); + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + throw new IOException("Only direct creation is supported"); + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedPath.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedPath.java new file mode 100644 index 000000000..d2463d328 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedPath.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.impl.filesystem; + +import org.jetbrains.annotations.Nullable; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public class QuiltUnifiedPath extends QuiltMapPath { + + QuiltUnifiedPath(QuiltUnifiedFileSystem fs, @Nullable QuiltUnifiedPath parent, String name) { + super(fs, parent, name); + } + + @Override + QuiltUnifiedPath getThisPath() { + return this; + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipCustomCompressedWriter.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipCustomCompressedWriter.java index 14a35b714..bed79e46b 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipCustomCompressedWriter.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipCustomCompressedWriter.java @@ -16,17 +16,18 @@ package org.quiltmc.loader.impl.filesystem; -import java.io.BufferedOutputStream; +import java.io.BufferedWriter; import java.io.DataOutputStream; import java.io.IOException; import java.io.InterruptedIOException; -import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; -import java.nio.file.OpenOption; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; @@ -34,23 +35,23 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPOutputStream; -import org.quiltmc.loader.impl.util.CountingOutputStream; +import org.quiltmc.loader.impl.util.ExposedByteArrayOutputStream; /** Writer class that implements * {@link QuiltZipFileSystem#writeQuiltCompressedFileSystem(java.nio.file.Path, java.nio.file.Path)}. */ final class QuiltZipCustomCompressedWriter { static final Charset UTF8 = StandardCharsets.UTF_8; - static final byte[] HEADER = "quiltmczipcmpv1".getBytes(UTF8); + static final byte[] HEADER = "quiltmczipcmpv2".getBytes(UTF8); static final byte[] PARTIAL_HEADER = Arrays.copyOf("PARTIAL!PARTIAL!PARTIAL!".getBytes(UTF8), HEADER.length); private static final AtomicInteger WRITER_THREAD_INDEX = new AtomicInteger(); @@ -58,6 +59,8 @@ final class QuiltZipCustomCompressedWriter { final Path src, dst; final LinkedBlockingQueue sourceFiles = new LinkedBlockingQueue<>(); + final Map files = new ConcurrentHashMap<>(); + final AtomicInteger currentOffset = new AtomicInteger(); volatile boolean interrupted; volatile boolean aborted = false; @@ -70,14 +73,14 @@ final class QuiltZipCustomCompressedWriter { /** @see QuiltZipFileSystem#writeQuiltCompressedFileSystem(Path, Path) */ void write() throws IOException { - try { - write0(); + try (FileChannel channel = FileChannel.open(dst, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + write0(channel); } finally { aborted = true; } } - private void write0() throws IOException { + private void write0(FileChannel channel) throws IOException { // Steps: // 1: Find all folders and files @@ -89,10 +92,15 @@ private void write0() throws IOException { // Spin up the other threads now int mainIndex = WRITER_THREAD_INDEX.incrementAndGet(); + channel.write(ByteBuffer.wrap(PARTIAL_HEADER)); + // 4 bytes: Directory pointer + channel.write(ByteBuffer.allocate(4)); + currentOffset.set((int) channel.position()); + int threadCount = Runtime.getRuntime().availableProcessors(); WriterThread[] threads = new WriterThread[threadCount]; for (int i = 0; i < threadCount; i++) { - threads[i] = new WriterThread(mainIndex, i); + threads[i] = new WriterThread(mainIndex, i, channel); threads[i].setDaemon(true); threads[i].start(); } @@ -212,58 +220,23 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } } - // Compute the real offsets - Map realOffsets = new HashMap<>(); - // Real offset starts from the end of the directory list, which makes everything a lot simpler - int offset = 0; - for (WriterThread writer : threads) { - if (offset == 0) { - // Everything is already aligned - realOffsets.putAll(writer.files); - } else { - for (Map.Entry entry : writer.files.entrySet()) { - FileEntry from = entry.getValue(); - realOffsets.put( - entry.getKey(), new FileEntry( - offset + from.offset, from.uncompressedLength, from.compressedLength - ) - ); - } - } - - offset += writer.currentOffset(); - } - - final byte[] tmpHeader = PARTIAL_HEADER; - - int offsetStart; - - // Now to write the actual file - OpenOption[] options = { StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE }; - try (OutputStream stream = new BufferedOutputStream(Files.newOutputStream(dst, options))) { - CountingOutputStream counter = new CountingOutputStream(stream); - counter.write(tmpHeader); - counter.write(new byte[4]); // File offset start - GZIPOutputStream gzip = new GZIPOutputStream(counter); - writeDirectory(stack.pop(), realOffsets, new DataOutputStream(gzip)); - gzip.finish(); - offsetStart = counter.getBytesWritten(); - - for (WriterThread thread : threads) { - int arrayCount = thread.arrays.size(); - for (int i = 0; i < arrayCount; i++) { - byte[] array = thread.arrays.get(i); - int length = i == arrayCount - 1 ? thread.currentArrayIndex : array.length; - stream.write(array, 0, length); - } - } - } - - // And rewrite the header since we're complete - try (OutputStream stream = Files.newOutputStream(dst, StandardOpenOption.WRITE)) { - stream.write(HEADER); - new DataOutputStream(stream).writeInt(offsetStart); - } + // Write the directory + int directoryOffset = currentOffset.get(); + ExposedByteArrayOutputStream baos = new ExposedByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(baos); + writeDirectory(stack.pop(), files, new DataOutputStream(gzip)); + gzip.finish(); + channel.write(baos.wrapIntoBuffer(), directoryOffset); + + // Write the directory offset + baos = new ExposedByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeInt(directoryOffset); + channel.write(baos.wrapIntoBuffer(), HEADER.length); + channel.force(false); + + // and the finished header + channel.write(ByteBuffer.wrap(HEADER), 0); } private void writeDirectory(Directory directory, Map fileMap, DataOutputStream to) @@ -300,22 +273,12 @@ public Directory(String folderName) { private final class WriterThread extends Thread { - private static final int ARRAY_LENGTH = 512 * 1024; - - /** List of 512k byte arrays. */ - private final List arrays = new ArrayList<>(); - - /** Index in the current byte array, not the index of the current byte array (which is always the last - * array). */ - int currentArrayIndex = ARRAY_LENGTH; - - final ExpandingOutputStream outputStream = new ExpandingOutputStream(); - final Map files = new HashMap<>(); - + final FileChannel channel; Deflater deflater; - public WriterThread(int mainIndex, int subIndex) { + public WriterThread(int mainIndex, int subIndex, FileChannel channel) { super("QuiltZipWriter-" + mainIndex + "." + subIndex); + this.channel = channel; } @Override @@ -332,15 +295,22 @@ public void run() { break; } - int offset = currentOffset(); - int uncompressedLength; if (deflater == null) { deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true); } else { deflater.reset(); } - try (DeflaterOutputStream compressor = new DeflaterOutputStream(outputStream, deflater)) { - uncompressedLength = (int) Files.copy(next, compressor); + + try { + int uncompressedLength; + ExposedByteArrayOutputStream baos = new ExposedByteArrayOutputStream(); + try (DeflaterOutputStream compressor = new DeflaterOutputStream(baos, deflater)) { + uncompressedLength = (int) Files.copy(next, compressor); + } + int offset = currentOffset.getAndAdd(baos.size()); + int length = baos.size(); + channel.write(ByteBuffer.wrap(baos.getArray(), 0, length), offset); + files.put(next, new FileEntry(offset, uncompressedLength, length)); } catch (IOException e) { e = new IOException("Failed to copy " + next, e); synchronized (QuiltZipCustomCompressedWriter.this) { @@ -357,45 +327,12 @@ public void run() { break; } } - int length = currentOffset() - offset; - files.put(next, new FileEntry(offset, uncompressedLength, length)); } if (deflater != null) { deflater.end(); } } - - private int currentOffset() { - return (arrays.size() - 1) * ARRAY_LENGTH + currentArrayIndex; - } - - final class ExpandingOutputStream extends OutputStream { - final byte[] singleArray = new byte[1]; - - @Override - public void write(int b) throws IOException { - singleArray[0] = (byte) b; - write(singleArray, 0, 1); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - while (len > 0) { - if (currentArrayIndex == ARRAY_LENGTH) { - arrays.add(new byte[ARRAY_LENGTH]); - currentArrayIndex = 0; - } - byte[] to = arrays.get(arrays.size() - 1); - int available = to.length - currentArrayIndex; - int toCopy = Math.min(available, len); - System.arraycopy(b, off, to, currentArrayIndex, toCopy); - off += toCopy; - currentArrayIndex += toCopy; - len -= toCopy; - } - } - } } static final class FileEntry { diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java index 97d9a3057..e228df01d 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java @@ -17,37 +17,35 @@ package org.quiltmc.loader.impl.filesystem; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.PushbackInputStream; import java.io.UncheckedIOException; +import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.FileStore; +import java.nio.file.FileSystems; import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.NotDirectoryException; +import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttributeView; import java.nio.file.attribute.FileStoreAttributeView; import java.nio.file.spi.FileSystemProvider; import java.util.Arrays; -import java.util.Collection; 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 java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -55,7 +53,9 @@ import java.util.zip.ZipInputStream; import org.jetbrains.annotations.Nullable; -import org.quiltmc.loader.api.CachedFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFile; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderReadOnly; +import org.quiltmc.loader.impl.util.ExposedByteArrayOutputStream; import org.quiltmc.loader.impl.util.FileUtil; import org.quiltmc.loader.impl.util.LimitedInputStream; import org.quiltmc.loader.impl.util.QuiltLoaderCleanupTasks; @@ -71,26 +71,42 @@ * only use this if the backing path supports efficient random access (generally {@link QuiltMemoryFileSystem} supports * this if it's not read-only, or the "compress" constructor argument is false). */ @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) -public class QuiltZipFileSystem extends QuiltBaseFileSystem +public class QuiltZipFileSystem extends QuiltMapFileSystem implements ReadOnlyFileSystem { - final Map entries = new HashMap<>(); - final SharedByteChannels channels; + static final boolean DEBUG_TEST_READING = false; + + final WeakReference thisRef = new WeakReference<>(this); + final ZipSource source; public QuiltZipFileSystem(String name, Path zipFrom, String zipPathPrefix) throws IOException { super(QuiltZipFileSystem.class, QuiltZipPath.class, name, true); - channels = new SharedByteChannels(this, zipFrom); + if (DEBUG_TEST_READING) { + System.out.println("new QuiltZipFileSystem ( " + name + ", from " + zipFrom + " )"); + } + + if (zipFrom.getFileSystem() == FileSystems.getDefault()) { + source = new SharedByteChannels(this, zipFrom); + } else { + source = new InMemorySource(Files.newInputStream(zipFrom)); + } // Check for our header byte[] header = new byte[QuiltZipCustomCompressedWriter.HEADER.length]; - try (InputStream fileStream = Files.newInputStream(zipFrom, StandardOpenOption.READ)) { + try (InputStream fileStream = source.openConstructingStream()) { BufferedInputStream pushback = new BufferedInputStream(fileStream); pushback.mark(header.length); int readLength = pushback.read(header); if (readLength == header.length && Arrays.equals(header, QuiltZipCustomCompressedWriter.HEADER)) { - int start = new DataInputStream(pushback).readInt(); - readDirectory(root, start, new DataInputStream(new GZIPInputStream(pushback)), zipPathPrefix); + if (!(source instanceof SharedByteChannels)) { + throw new IOException("Cannot read a custom compressed stream that isn't on the default file system!"); + } + int directoryStart = new DataInputStream(pushback).readInt(); + int dataStart = readLength + 4; + try (GZIPInputStream src = new GZIPInputStream(source.stream(directoryStart))) { + readDirectory(root, new DataInputStream(src), zipPathPrefix); + } } else if (readLength == header.length && Arrays.equals(header, QuiltZipCustomCompressedWriter.PARTIAL_HEADER)) { throw new PartiallyWrittenIOException(); } else { @@ -99,7 +115,22 @@ public QuiltZipFileSystem(String name, Path zipFrom, String zipPathPrefix) throw } } + source.build(); + + switchToReadOnly(); + QuiltZipFileSystemProvider.PROVIDER.register(this); + validate(); + dumpEntries(name); + + if (!isDirectory(root)) { + throw new IllegalStateException("Missing root???"); + } + } + + @Override + protected boolean startWithConcurrentMap() { + return false; } private void initializeFromZip(InputStream fileStream, String zipPathPrefix) throws IOException { @@ -121,19 +152,18 @@ private void initializeFromZip(InputStream fileStream, String zipPathPrefix) thr QuiltZipPath path = getPath(entryName); if (entryName.endsWith("/")) { - // Folder, but it might already have been automatically added by a file - entries.putIfAbsent(path, new QuiltZipFolder()); + createDirectories(path); } else { - putFile(path, new QuiltZipFile(channels, entry, zip), IOException::new); + addEntryAndParents(new QuiltZipFile(path, source, entry, zip)); } } } } - private void readDirectory(QuiltZipPath path, int start, DataInputStream stream, String zipPathPrefix) throws IOException { + private void readDirectory(QuiltZipPath path, DataInputStream stream, String zipPathPrefix) throws IOException { String pathString = path.toString(); if (pathString.startsWith(zipPathPrefix) || zipPathPrefix.startsWith(pathString)) { - entries.computeIfAbsent(path, p -> new QuiltZipFolder()); + createDirectories(path); } int childFiles = stream.readUnsignedShort(); for (int i = 0; i < childFiles; i++) { @@ -141,12 +171,11 @@ private void readDirectory(QuiltZipPath path, int start, DataInputStream stream, byte[] nameBytes = new byte[length]; stream.readFully(nameBytes); QuiltZipPath filePath = path.resolve(new String(nameBytes, StandardCharsets.UTF_8)); - int offset = start + stream.readInt(); + int offset = stream.readInt(); int uncompressedSize = stream.readInt(); int compressedSize = stream.readInt(); if (filePath.toString().startsWith(zipPathPrefix)) { - QuiltZipFile file = new QuiltZipFile(filePath.toString(), channels, offset, compressedSize, uncompressedSize, true); - putFile(filePath, file, IOException::new); + addEntryAndParents(new QuiltZipFile(filePath, source, offset, compressedSize, uncompressedSize, true)); } } @@ -156,7 +185,7 @@ private void readDirectory(QuiltZipPath path, int start, DataInputStream stream, byte[] nameBytes = new byte[length]; stream.readFully(nameBytes); String name = new String(nameBytes, StandardCharsets.UTF_8); - readDirectory(path.resolve(name), start, stream, zipPathPrefix); + readDirectory(path.resolve(name), stream, zipPathPrefix); } } @@ -165,44 +194,38 @@ private void readDirectory(QuiltZipPath path, int start, DataInputStream stream, public QuiltZipFileSystem(String name, QuiltZipPath newRoot) { super(QuiltZipFileSystem.class, QuiltZipPath.class, name, true); - channels = newRoot.fs.channels; - channels.open(this); + source = newRoot.fs.source; + source.open(this); addFolder(newRoot, getRoot()); QuiltZipFileSystemProvider.PROVIDER.register(this); + + if (!isDirectory(root)) { + throw new IllegalStateException("Missing root???"); + } } private void addFolder(QuiltZipPath src, QuiltZipPath dst) { QuiltZipFileSystem srcFS = src.fs; - QuiltZipEntry entryFrom = srcFS.entries.get(src); - if (entryFrom instanceof QuiltZipFolder) { + QuiltUnifiedEntry entryFrom = srcFS.getEntry(src); + if (entryFrom instanceof QuiltUnifiedFolderReadOnly) { // QuiltZipFolder does store subfolders that are part of the original FS, so we need to fully copy it - entries.put(dst, new QuiltZipFolder()); - for (Map.Entry child : ((QuiltZipFolder) entryFrom).children.entrySet()) { - addFolder(child.getValue(), dst.resolve(child.getKey())); - } + QuiltMapPath[] srcChildren = ((QuiltUnifiedFolderReadOnly) entryFrom).children; + QuiltMapPath[] dstChildren = new QuiltMapPath[srcChildren.length]; + for (int i = 0; i < srcChildren.length; i++) { + QuiltMapPath srcChild = srcChildren[i]; + QuiltZipPath dstChild = dst.resolve(srcChild.name); + addFolder((QuiltZipPath) srcChild, dstChild); + dstChildren[i] = dstChild; + } + addEntryWithoutParentsUnsafe(new QuiltUnifiedFolderReadOnly(dst, dstChildren)); } else if (entryFrom instanceof QuiltZipFile) { - // QuiltZipFile doesn't store anything that directly relates it to the source file system, so we can just - // reuse the object - putFile(dst, (QuiltZipFile) entryFrom, IllegalStateException::new); + QuiltZipFile from = (QuiltZipFile) entryFrom; + addEntryWithoutParentsUnsafe(new QuiltZipFile(dst, source, from.offset, from.compressedSize, from.uncompressedSize, from.isCompressed)); } else { // This isn't meant to happen, it means something got constructed badly - } - } - - private void putFile(QuiltZipPath path, QuiltZipFile file, Function exCtor) - throws T { - entries.put(path, file); - QuiltZipPath parent = path; - QuiltZipPath previous = path; - while ((parent = parent.getParent()) != null) { - QuiltZipEntry newEntry = entries.computeIfAbsent(parent, f -> new QuiltZipFolder()); - if (!(newEntry instanceof QuiltZipFolder)) { - throw exCtor.apply("Cannot make a file into a folder " + parent + " for " + path); - } - ((QuiltZipFolder) newEntry).children.put(previous.name, previous); - previous = parent; + throw new IllegalArgumentException("Unknown source entry " + entryFrom); } } @@ -229,12 +252,12 @@ public FileSystemProvider provider() { @Override public void close() throws IOException { - channels.close(this); + source.close(this); } @Override public boolean isOpen() { - return channels.isOpen; + return source.isOpen(); } @Override @@ -244,119 +267,11 @@ public boolean isReadOnly() { // FasterFileSystem - @Override - public boolean isSymbolicLink(Path path) { - // Symbolic links are not supported in zips - return false; - } - - @Override - public boolean isDirectory(Path path, LinkOption... options) { - return entries.get(path.toAbsolutePath().normalize()) instanceof QuiltZipFolder; - } - - @Override - public boolean isRegularFile(Path path, LinkOption[] options) { - return entries.get(path.toAbsolutePath().normalize()) instanceof QuiltZipFile; - } - - @Override - public boolean exists(Path path, LinkOption... options) { - return entries.containsKey(path.toAbsolutePath().normalize()); - } - - @Override - public boolean notExists(Path path, LinkOption... options) { - // Normally you're not meant to do this - // But it's fine, since there's nothing that could prevent us from checking if the file exists - return !exists(path, options); - } - - @Override - public boolean isReadable(Path path) { - return exists(path); - } - @Override public boolean isExecutable(Path path) { return exists(path); } - @Override - public Stream list(Path dir) throws IOException { - return getChildren(dir).stream().map(p -> (Path) p); - } - - @Override - public Collection getChildren(Path dir) throws IOException { - QuiltZipEntry entry = entries.get(dir.toAbsolutePath().normalize()); - if (!(entry instanceof QuiltZipFolder)) { - throw new NotDirectoryException(dir.toString()); - } - return Collections.unmodifiableCollection(((QuiltZipFolder) entry).children.values()); - } - - @Override - public Iterable getFileStores() { - FileStore store = new FileStore() { - @Override - public String type() { - return "QuiltZipFileSystem"; - } - - @Override - public boolean supportsFileAttributeView(String name) { - return "basic".equals(name); - } - - @Override - public boolean supportsFileAttributeView(Class type) { - return type == BasicFileAttributeView.class; - } - - @Override - public String name() { - return "QuiltZipFileSystem"; - } - - @Override - public boolean isReadOnly() { - return true; - } - - @Override - public long getUsableSpace() throws IOException { - return 10; - } - - @Override - public long getUnallocatedSpace() throws IOException { - return 0; - } - - @Override - public long getTotalSpace() throws IOException { - return getUsableSpace(); - } - - @Override - public V getFileStoreAttributeView(Class type) { - return null; - } - - @Override - public Object getAttribute(String attribute) throws IOException { - return null; - } - }; - return Collections.singleton(store); - } - - @Override - public Set supportedFileAttributeViews() { - return Collections.singleton("basic"); - } - // Custom classes to grab the real offset while reading the zip static final class CountingInputStream extends InputStream { @@ -419,10 +334,114 @@ public long getOffset() { } } + static abstract class ZipSource { + + abstract InputStream openConstructingStream() throws IOException; + + abstract ZipSource forIndividualFile(long offset, int length); + + abstract void build() throws IOException; + + abstract boolean isOpen(); + + abstract void open(QuiltZipFileSystem fs); + + abstract void close(QuiltZipFileSystem fs) throws IOException; + + abstract InputStream stream(long position) throws IOException; + + abstract SeekableByteChannel channel() throws IOException; + } + + static final class InMemorySource extends ZipSource { + + private InputStream from; + private ExposedByteArrayOutputStream baos; + private int negativeOffset; + private byte[] bytes; + + InMemorySource(InputStream from) { + this.from = from; + this.baos = new ExposedByteArrayOutputStream(); + } + + InMemorySource(long negativeOffset, byte[] bytes) { + this.negativeOffset = (int) negativeOffset; + this.bytes = bytes; + } + + @Override + InputStream openConstructingStream() throws IOException { + return new InputStream() { + @Override + public int read() throws IOException { + int read = from.read(); + if (read >= 0) { + baos.write(read); + } + return read; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = from.read(b, off, len); + if (read > 0) { + baos.write(b, off, read); + } + return read; + } + + @Override + public void close() throws IOException { + from.close(); + } + }; + } + + @Override + ZipSource forIndividualFile(long offset, int length) { + int pos = (int) offset; + return new InMemorySource(pos, Arrays.copyOfRange(baos.getArray(), pos, pos + length)); + } + + @Override + void build() throws IOException { + from.close(); + from = null; + baos = null; + } + + @Override + boolean isOpen() { + return true; + } + + @Override + void open(QuiltZipFileSystem fs) { + // NO-OP + } + + @Override + void close(QuiltZipFileSystem fs) throws IOException { + // NO-OP + } + + @Override + InputStream stream(long position) throws IOException { + int pos = (int) (position - negativeOffset); + return new ByteArrayInputStream(bytes, pos, bytes.length - pos); + } + + @Override + SeekableByteChannel channel() throws IOException { + return new ByteArrayChannel(bytes, negativeOffset); + } + } + /** Used to cache {@link SeekableByteChannel} per-thread, since it's an expensive operation to open them. */ - static final class SharedByteChannels { + static final class SharedByteChannels extends ZipSource { final Path zipFrom; - final Set fileSystems = new HashSet<>(); + final Set> fileSystems = new HashSet<>(); // This is not very nice: we want to use a ThreadLocal // but we can't since we need to close every channel afterwards @@ -436,12 +455,34 @@ static final class SharedByteChannels { QuiltLoaderCleanupTasks.addCleanupTask(this, this::removeDeadThreads); } + @Override + public InputStream openConstructingStream() throws IOException { + return Files.newInputStream(zipFrom); + } + + @Override + ZipSource forIndividualFile(long offset, int length) { + return this; + } + + @Override + void build() throws IOException { + // NO-OP + } + + @Override + boolean isOpen() { + return isOpen; + } + + @Override synchronized void open(QuiltZipFileSystem fs) { - fileSystems.add(fs); + fileSystems.add(fs.thisRef); } + @Override synchronized void close(QuiltZipFileSystem fs) throws IOException { - fileSystems.remove(fs); + fileSystems.remove(fs.thisRef); if (fileSystems.isEmpty()) { isOpen = false; for (SeekableByteChannel channel : channels.values()) { @@ -452,6 +493,12 @@ synchronized void close(QuiltZipFileSystem fs) throws IOException { } } + @Override + InputStream stream(long position) throws IOException { + return new ByteChannel2Stream(channel(), position); + } + + @Override SeekableByteChannel channel() throws IOException { try { return channels.computeIfAbsent(Thread.currentThread(), t -> { @@ -481,6 +528,13 @@ private synchronized void removeDeadThreads() { } } + Iterator> iter = fileSystems.iterator(); + while (iter.hasNext()) { + if (iter.next().get() == null) { + iter.remove(); + } + } + if (fileSystems.isEmpty()) { QuiltLoaderCleanupTasks.removeCleanupTask(this); } @@ -524,27 +578,83 @@ public long skip(long n) throws IOException { } } - static abstract class QuiltZipEntry { - protected abstract BasicFileAttributes createAttributes(QuiltZipPath path); - } + static final class ByteArrayChannel implements SeekableByteChannel { + + private final byte[] bytes; + private final int negativeOffset; + private int position; + + ByteArrayChannel(byte[] bytes, int negativeOffset) { + this.bytes = bytes; + this.negativeOffset = negativeOffset; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void close() throws IOException { + // NO-OP + } + + @Override + public int read(ByteBuffer dst) throws IOException { + if (position >= bytes.length) { + return -1; + } + if (position < 0) { + return -1; + } + int length = Math.min(bytes.length - position, dst.remaining()); + dst.put(bytes, position, length); + position += length; + return length; + } - static final class QuiltZipFolder extends QuiltZipEntry { - final Map children = new HashMap<>(); + @Override + public int write(ByteBuffer src) throws IOException { + throw new IOException("read only"); + } @Override - protected BasicFileAttributes createAttributes(QuiltZipPath path) { - return new QuiltFileAttributes(path, QuiltFileAttributes.SIZE_DIRECTORY); + public long position() throws IOException { + return position + negativeOffset; + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + if (newPosition < 0) { + throw new IllegalArgumentException("position < 0"); + } + this.position = (int) Math.min(Integer.MAX_VALUE / 2, newPosition) - negativeOffset; + return this; + } + + @Override + public long size() throws IOException { + return bytes.length; + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + if (size >= bytes.length) { + return this; + } else { + throw new IOException("read only"); + } } } - static final class QuiltZipFile extends QuiltZipEntry { - final SharedByteChannels channels; + static final class QuiltZipFile extends QuiltUnifiedFile { + final ZipSource source; final long offset; final int compressedSize, uncompressedSize; final boolean isCompressed; - QuiltZipFile(SharedByteChannels channels, ZipEntry entry, CustomZipInputStream zip) throws IOException { - this.channels = channels; + QuiltZipFile(QuiltZipPath path, ZipSource source, ZipEntry entry, CustomZipInputStream zip) throws IOException { + super(path); this.offset = zip.getOffset(); int method = entry.getMethod(); if (method == ZipEntry.DEFLATED) { @@ -573,11 +683,20 @@ static final class QuiltZipFile extends QuiltZipEntry { compressed = (int) (zip.getOffset() - offset); uncompressed = outputLength; time = System.nanoTime() - start; + } else { + while (true) { + int skipped = (int) zip.skip(1 << 16); + if (skipped == 0) { + break; + } + } } this.compressedSize = compressed; this.uncompressedSize = uncompressed; + this.source = source.forIndividualFile(offset, compressedSize); + if (Boolean.getBoolean("alexiil.temp.dump_zip_file_system_entries")) { StringBuilder sb = new StringBuilder(); sb.append(entry.getName()); @@ -596,24 +715,29 @@ static final class QuiltZipFile extends QuiltZipEntry { System.out.println(sb.toString()); } -// testReading(entry.toString()); + if (DEBUG_TEST_READING) { + testReading(entry.toString()); + } } - QuiltZipFile(String path, SharedByteChannels channels, long offset, int compressedSize, int uncompressedSize, + QuiltZipFile(QuiltZipPath path, ZipSource source, long offset, int compressedSize, int uncompressedSize, boolean isCompressed) { - this.channels = channels; + super(path); + + this.source = source; this.offset = offset; this.compressedSize = compressedSize; this.uncompressedSize = uncompressedSize; this.isCompressed = isCompressed; -// testReading(path); + if (DEBUG_TEST_READING) { + testReading(path.toString()); + } } - private void testReading(String path) { - if (!path.endsWith(".json") && !path.endsWith(".txt")) { + if (!path.endsWith(".json") && !path.endsWith(".txt") && !"META-INF/MANIFEST.MF".equals(path)) { return; } System.out.println(path + " @ " + Integer.toHexString((int) offset)); @@ -664,10 +788,16 @@ private void testReading(String path) { } @Override - protected BasicFileAttributes createAttributes(QuiltZipPath path) { + protected QuiltUnifiedEntry createCopiedTo(QuiltMapPath newPath) { + return new QuiltZipFile((QuiltZipPath) newPath, source, offset, compressedSize, uncompressedSize, isCompressed); + } + + @Override + protected BasicFileAttributes createAttributes() { return new QuiltFileAttributes(path, uncompressedSize); } + @Override InputStream createInputStream() throws IOException { InputStream stream = createUncompressingInputStream(); if (isCompressed) { @@ -684,14 +814,28 @@ InputStream createInputStream() throws IOException { } private InputStream createUncompressingInputStream() throws IOException, IOException { - ByteChannel2Stream channel = new ByteChannel2Stream(channels.channel(), offset); - return new LimitedInputStream(channel, compressedSize); + return new LimitedInputStream(source.stream(offset), compressedSize); + } + + @Override + OutputStream createOutputStream(boolean append, boolean truncate) throws IOException { + throw new IOException(READ_ONLY_ERROR_MESSAGE); + } + + @Override + SeekableByteChannel createByteChannel(Set options) throws IOException { + for (OpenOption option : options) { + if (option != StandardOpenOption.READ) { + throw new IOException(READ_ONLY_ERROR_MESSAGE); + } + } + + return createByteChannel(); } SeekableByteChannel createByteChannel() throws IOException { if (!isCompressed) { - Path path = channels.zipFrom; - return new OffsetSeekableByteChannel(Files.newByteChannel(path, StandardOpenOption.READ)); + return new OffsetSeekableByteChannel(source.channel()); } else { return new InflaterSeekableByteChannel(); } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java index 313b48f93..5aaba14a3 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java @@ -17,42 +17,18 @@ package org.quiltmc.loader.impl.filesystem; import java.io.IOException; -import java.io.InputStream; import java.net.URI; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.AccessMode; -import java.nio.file.CopyOption; -import java.nio.file.DirectoryStream; -import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileStore; import java.nio.file.FileSystem; -import java.nio.file.FileSystemException; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.LinkOption; -import java.nio.file.NoSuchFileException; -import java.nio.file.NotDirectoryException; -import java.nio.file.OpenOption; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributeView; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.FileAttributeView; -import java.nio.file.attribute.FileTime; import java.nio.file.spi.FileSystemProvider; -import java.util.Iterator; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem.QuiltZipEntry; -import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem.QuiltZipFile; -import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem.QuiltZipFolder; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) -public class QuiltZipFileSystemProvider extends FileSystemProvider { +public class QuiltZipFileSystemProvider extends QuiltMapFileSystemProvider { public static final String SCHEME = "quilt.zfs"; static final String READ_ONLY_EXCEPTION = "This FileSystem is read-only"; @@ -68,116 +44,28 @@ public static QuiltZipFileSystemProvider instance() { } @Override - public String getScheme() { - return SCHEME; + protected QuiltFSP quiltFSP() { + return PROVIDER; } @Override - public FileSystem newFileSystem(URI uri, Map env) throws IOException { - throw new IOException("Only direct creation is supported"); + protected Class fileSystemClass() { + return QuiltZipFileSystem.class; } @Override - public FileSystem getFileSystem(URI uri) { - throw new FileSystemNotFoundException("Only direct creation is supported"); + protected Class pathClass() { + return QuiltZipPath.class; } @Override - public QuiltZipPath getPath(URI uri) { - return PROVIDER.getFileSystem(uri).root.resolve(uri.getPath()); - } - - @Override - public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) - throws IOException { - for (OpenOption o : options) { - if (o != StandardOpenOption.READ) { - throw new UnsupportedOperationException("'" + o + "' not allowed"); - } - } - - if (path instanceof QuiltZipPath) { - QuiltZipPath p = (QuiltZipPath) path; - QuiltZipEntry entry = p.fs.entries.get(p.toAbsolutePath()); - if (entry instanceof QuiltZipFolder) { - throw new FileSystemException("Cannot open an InputStream on a directory!"); - } else if (entry instanceof QuiltZipFile) { - return ((QuiltZipFile) entry).createByteChannel(); - } else { - throw new NoSuchFileException(path.toString()); - } - } else { - throw new IllegalArgumentException("The given path is not a QuiltZipPath!"); - } - } - - @Override - public InputStream newInputStream(Path path, OpenOption... options) throws IOException { - for (OpenOption o : options) { - if (o != StandardOpenOption.READ) { - throw new UnsupportedOperationException("'" + o + "' not allowed"); - } - } - - if (path instanceof QuiltZipPath) { - QuiltZipPath p = (QuiltZipPath) path; - QuiltZipEntry entry = p.fs.entries.get(p.toAbsolutePath()); - if (entry instanceof QuiltZipFolder) { - throw new FileSystemException("Cannot open an InputStream on a directory!"); - } else if (entry instanceof QuiltZipFile) { - return ((QuiltZipFile) entry).createInputStream(); - } else { - throw new NoSuchFileException(path.toString()); - } - } else { - throw new IllegalArgumentException("The given path is not a QuiltZipPath!"); - } + public String getScheme() { + return SCHEME; } @Override - public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { - QuiltZipPath from = toAbsQuiltPath(dir); - - QuiltZipEntry entry = from.fs.entries.get(from); - final Map entries; - if (entry instanceof QuiltZipFolder) { - entries = ((QuiltZipFolder) entry).children; - } else { - throw new NotDirectoryException("Not a directory: " + dir); - } - - return new DirectoryStream() { - - boolean opened = false; - boolean closed = false; - - @Override - public void close() throws IOException { - closed = true; - } - - @Override - public Iterator iterator() { - if (opened) { - throw new IllegalStateException("newDirectoryStream only supports a single iteration!"); - } - opened = true; - - return new Iterator() { - final Iterator entryIter = entries.values().iterator(); - - @Override - public boolean hasNext() { - return entryIter.hasNext(); - } - - @Override - public Path next() { - return entryIter.next(); - } - }; - } - }; + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + throw new IOException("Only direct creation is supported"); } private static QuiltZipPath toAbsQuiltPath(Path path) { @@ -189,107 +77,8 @@ private static QuiltZipPath toAbsQuiltPath(Path path) { } } - @Override - public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { - throw new IOException(READ_ONLY_EXCEPTION); - } - - @Override - public void delete(Path path) throws IOException { - throw new IOException(READ_ONLY_EXCEPTION); - } - - @Override - public void copy(Path source, Path target, CopyOption... options) throws IOException { - throw new IOException(READ_ONLY_EXCEPTION); - } - - @Override - public void move(Path source, Path target, CopyOption... options) throws IOException { - throw new IOException(READ_ONLY_EXCEPTION); - } - - @Override - public boolean isSameFile(Path path, Path path2) throws IOException { - path = path.toAbsolutePath().normalize(); - path2 = path2.toAbsolutePath().normalize(); - // We don't support links, so we can just check for equality - return path.equals(path2); - } - - @Override - public boolean isHidden(Path path) throws IOException { - return path.getFileName().toString().startsWith("."); - } - @Override public FileStore getFileStore(Path path) throws IOException { return ((QuiltZipPath) path).fs.getFileStores().iterator().next(); } - - @Override - public void checkAccess(Path path, AccessMode... modes) throws IOException { - for (AccessMode mode : modes) { - if (mode == AccessMode.WRITE) { - throw new IOException(READ_ONLY_EXCEPTION); - } - } - QuiltZipPath quiltPath = toAbsQuiltPath(path); - QuiltZipEntry entry = quiltPath.fs.entries.get(quiltPath); - if (entry == null) { - throw new NoSuchFileException(quiltPath.toString()); - } - } - - @Override - public V getFileAttributeView(Path path, Class type, LinkOption... options) { - if (type == BasicFileAttributeView.class) { - BasicFileAttributeView view = new BasicFileAttributeView() { - @Override - public String name() { - return "basic"; - } - - @Override - public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { - // Unsupported - // Since we don't need to throw we won't - } - - @Override - public BasicFileAttributes readAttributes() throws IOException { - return QuiltZipFileSystemProvider.this.readAttributes(path, BasicFileAttributes.class, options); - } - }; - return type.cast(view); - } - return null; - } - - @Override - public A readAttributes(Path path, Class type, LinkOption... options) - throws IOException { - - if (type != BasicFileAttributes.class) { - throw new UnsupportedOperationException("Unsupported attributes " + type); - } - - QuiltZipPath zipPath = toAbsQuiltPath(path); - QuiltZipEntry entry = zipPath.fs.entries.get(zipPath); - if (entry != null) { - return type.cast(entry.createAttributes(zipPath)); - } else { - throw new NoSuchFileException(zipPath.toString()); - } - } - - @Override - public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { - return PROVIDER.readAttributes(this, path, attributes, options); - } - - @Override - public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { - throw new IOException(READ_ONLY_EXCEPTION); - } } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipPath.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipPath.java index abbe77928..429a4f6b3 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipPath.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipPath.java @@ -20,7 +20,7 @@ import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) -public class QuiltZipPath extends QuiltBasePath { +public class QuiltZipPath extends QuiltMapPath { QuiltZipPath(QuiltZipFileSystem fs, QuiltZipPath parent, String name) { super(fs, parent, name); diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java index 9c11cd84b..3deb2df08 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java @@ -23,10 +23,7 @@ import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; -import java.nio.file.Files; -import java.nio.file.Path; -import org.quiltmc.loader.impl.filesystem.QuiltBasePath; import org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystem; import org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystemProvider; import org.quiltmc.loader.impl.filesystem.QuiltMemoryPath; diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/ufs/Handler.java b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/ufs/Handler.java new file mode 100644 index 000000000..56aa6b5a5 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/ufs/Handler.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022, 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.impl.filesystem.quilt.ufs; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedFileSystemProvider; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedPath; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +/** {@link URLStreamHandler} for {@link QuiltUnifiedFileSystem}. */ +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public class Handler extends URLStreamHandler { + @Override + protected URLConnection openConnection(URL u) throws IOException { + QuiltUnifiedPath path; + try { + path = QuiltUnifiedFileSystemProvider.instance().getPath(u.toURI()); + } catch (URISyntaxException e) { + throw new IOException(e); + } + + return new URLConnection(u) { + @Override + public void connect() { + // No-op + } + + @Override + public InputStream getInputStream() throws IOException { + return path.openUrlInputStream(); + } + }; + } + + @Override + protected InetAddress getHostAddress(URL u) { + return null; + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java b/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java index afe13322f..925948762 100644 --- a/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java +++ b/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java @@ -61,8 +61,6 @@ import org.quiltmc.loader.api.QuiltLoader; import org.quiltmc.loader.api.Version; import org.quiltmc.loader.api.VersionFormatException; -import org.quiltmc.loader.api.VersionInterval; -import org.quiltmc.loader.api.VersionRange; import org.quiltmc.loader.api.gui.QuiltDisplayedError; import org.quiltmc.loader.api.gui.QuiltLoaderGui; import org.quiltmc.loader.api.gui.QuiltLoaderText; @@ -94,11 +92,12 @@ import org.quiltmc.loader.impl.filesystem.QuiltJoinedPath; import org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystem; import org.quiltmc.loader.impl.filesystem.QuiltMemoryPath; +import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltZipPath; import org.quiltmc.loader.impl.game.GameProvider; import org.quiltmc.loader.impl.gui.GuiManagerImpl; import org.quiltmc.loader.impl.gui.QuiltJsonGuiMessage; import org.quiltmc.loader.impl.metadata.qmj.V1ModMetadataReader; -import org.quiltmc.loader.impl.metadata.qmj.VersionConstraintImpl; import org.quiltmc.loader.impl.plugin.base.InternalModContainerBase; import org.quiltmc.loader.impl.plugin.fabric.StandardFabricPlugin; import org.quiltmc.loader.impl.plugin.gui.TempQuilt2OldStatusNode; @@ -113,9 +112,9 @@ import org.quiltmc.loader.impl.solver.ModSolveResultImpl.LoadOptionResult; import org.quiltmc.loader.impl.solver.Sat4jWrapper; import org.quiltmc.loader.impl.util.AsciiTableGenerator; -import org.quiltmc.loader.impl.util.HashUtil; import org.quiltmc.loader.impl.util.AsciiTableGenerator.AsciiTableColumn; import org.quiltmc.loader.impl.util.AsciiTableGenerator.AsciiTableRow; +import org.quiltmc.loader.impl.util.HashUtil; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; import org.quiltmc.loader.impl.util.SystemProperties; @@ -245,8 +244,8 @@ public QuiltPluginTask loadZip(Path zip) { private Path loadZip0(Path zip) throws IOException, NonZipException { String name = zip.getFileName().toString(); - try (ZipInputStream zipFrom = new ZipInputStream(Files.newInputStream(zip))) { - QuiltMemoryPath qRoot = new QuiltMemoryFileSystem.ReadOnly(name, zipFrom, "", false).getRoot(); + try { + QuiltZipPath qRoot = new QuiltZipFileSystem(name, zip, "").getRoot(); pathParents.put(qRoot, zip); return qRoot; } catch (IOException e) { diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java index b45ba9583..bab901a99 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java @@ -42,8 +42,9 @@ import org.quiltmc.loader.impl.discovery.ModResolutionException; import org.quiltmc.loader.impl.discovery.RuntimeModRemapper; import org.quiltmc.loader.impl.filesystem.PartiallyWrittenIOException; -import org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystem; -import org.quiltmc.loader.impl.filesystem.QuiltMemoryPath; +import org.quiltmc.loader.impl.filesystem.QuiltMapFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedPath; import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem; import org.quiltmc.loader.impl.filesystem.QuiltZipPath; import org.quiltmc.loader.impl.util.FilePreloadHelper; @@ -256,9 +257,10 @@ private static QuiltZipPath createTransformCache(Path transformCacheFile, String } if (!Boolean.getBoolean(SystemProperties.DISABLE_OPTIMIZED_COMPRESSED_TRANSFORM_CACHE)) { - try (QuiltMemoryFileSystem.ReadWrite rw = new QuiltMemoryFileSystem.ReadWrite("transform-cache", true)) { - QuiltMemoryPath root = rw.getRoot(); + try (QuiltUnifiedFileSystem fs = new QuiltUnifiedFileSystem("transform-cache", true)) { + QuiltUnifiedPath root = fs.getRoot(); populateTransformCache(root, modList, result); + fs.dumpEntries("after-populate"); Files.write(root.resolve("options.txt"), options.getBytes(StandardCharsets.UTF_8)); Files.createFile(root.resolve(FILE_TRANSFORM_COMPLETE)); QuiltZipFileSystem.writeQuiltCompressedFileSystem(root, transformCacheFile); @@ -304,6 +306,8 @@ private static void populateTransformCache(Path root, List modLis RuntimeModRemapper.remap(root, modList); + QuiltMapFileSystem.dumpEntries(root.getFileSystem(), "after-remap"); + if (Boolean.getBoolean(SystemProperties.ENABLE_EXPERIMENTAL_CHASM)) { ChasmInvoker.applyChasm(root, modList, solveResult); } diff --git a/src/main/java/org/quiltmc/loader/impl/util/ExposedByteArrayOutputStream.java b/src/main/java/org/quiltmc/loader/impl/util/ExposedByteArrayOutputStream.java new file mode 100644 index 000000000..71ab11333 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/util/ExposedByteArrayOutputStream.java @@ -0,0 +1,31 @@ +/* + * 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.impl.util; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public final class ExposedByteArrayOutputStream extends ByteArrayOutputStream { + public byte[] getArray() { + return buf; + } + + public ByteBuffer wrapIntoBuffer() { + return ByteBuffer.wrap(buf, 0, count); + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java b/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java index bbd9ca6e2..2e6bb4438 100644 --- a/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java +++ b/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java @@ -77,6 +77,8 @@ public final class SystemProperties { // enable useTempFile in ZipFileSystem, reduces memory usage when writing transform cache at the cost of speed public static final String USE_ZIPFS_TEMP_FILE = "loader.zipfs.use_temp_file"; public static final String DISABLE_BEACON = "loader.disable_beacon"; + public static final String DEBUG_DUMP_FILESYSTEM_CONTENTS = "loader.debug.filesystem.dump_contents"; + public static final String DEBUG_VALIDATE_FILESYSTEM_CONTENTS = "loader.debug.filesystem.validate_constantly"; private SystemProperties() { } diff --git a/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider index e0a093e58..484d52caf 100644 --- a/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider +++ b/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -1,3 +1,4 @@ org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystemProvider org.quiltmc.loader.impl.filesystem.QuiltJoinedFileSystemProvider -org.quiltmc.loader.impl.filesystem.QuiltZipFileSystemProvider \ No newline at end of file +org.quiltmc.loader.impl.filesystem.QuiltZipFileSystemProvider +org.quiltmc.loader.impl.filesystem.QuiltUnifiedFileSystemProvider \ No newline at end of file