1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-12-24 17:10:31 +00:00

Hoist some ArchiveMount logic into a new superclass

- Add AbstractInMemoryMount, which contains all of ArchiveMount's file
   tree logic, but not the caching functionality.

 - Convert MemoryMount to inherit from AbstractInMemoryMount.

 - Add a helper method to add a file to an AbstractInMemoryMount, and
   use that within {Resource,Jar}Mount.

There's definitely more work to be done here - it might be nice to split
FileEntry into separate Directory and File interfaces, or at least make
them slightly more immutable, but that's definitely a future job.
This commit is contained in:
Jonathan Coates 2023-09-21 09:54:02 +01:00
parent ae71eb3cae
commit 0d6c6e7ae7
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
7 changed files with 242 additions and 294 deletions

View File

@ -29,8 +29,6 @@ import java.util.Map;
public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
private static final Logger LOG = LoggerFactory.getLogger(ResourceMount.class);
private static final byte[] TEMP_BUFFER = new byte[8192];
/**
* Maintain a cache of currently loaded resource mounts. This cache is invalidated when currentManager changes.
*/
@ -68,7 +66,11 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?
var localPath = FileSystem.toLocal(file.getPath(), subPath);
create(newRoot, localPath);
try {
getOrCreateChild(newRoot, localPath, this::createEntry);
} catch (ResourceLocationException e) {
LOG.warn("Cannot create resource location for {} ({})", localPath, e.getMessage());
}
hasAny = true;
}
@ -83,52 +85,12 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
}
}
private void create(FileEntry lastEntry, String path) {
var lastIndex = 0;
while (lastIndex < path.length()) {
var nextIndex = path.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = path.length();
var part = path.substring(lastIndex, nextIndex);
if (lastEntry.children == null) lastEntry.children = new HashMap<>();
var nextEntry = lastEntry.children.get(part);
if (nextEntry == null) {
ResourceLocation childPath;
try {
childPath = new ResourceLocation(namespace, subPath + "/" + path);
} catch (ResourceLocationException e) {
LOG.warn("Cannot create resource location for {} ({})", part, e.getMessage());
return;
}
lastEntry.children.put(part, nextEntry = new FileEntry(path, childPath));
}
lastEntry = nextEntry;
lastIndex = nextIndex + 1;
}
private FileEntry createEntry(String path) {
return new FileEntry(path, new ResourceLocation(namespace, subPath + "/" + path));
}
@Override
public long getSize(FileEntry file) {
var resource = manager.getResource(file.identifier).orElse(null);
if (resource == null) return 0;
try (var stream = resource.open()) {
int total = 0, read = 0;
do {
total += read;
read = stream.read(TEMP_BUFFER);
} while (read > 0);
return total;
} catch (IOException e) {
return 0;
}
}
@Override
public byte[] getContents(FileEntry file) throws IOException {
public byte[] getFileContents(FileEntry file) throws IOException {
var resource = manager.getResource(file.identifier).orElse(null);
if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);

View File

@ -12,25 +12,30 @@ import java.time.Instant;
/**
* A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes.
*
* @param isDirectory Whether this filesystem entry is a directory.
* @param size The size of the file.
* @param isDirectory Whether this filesystem entry is a directory.
* @param size The size of the file.
* @param creationTime The time the file was created.
* @param lastModifiedTime The time the file was last modified.
*/
public record FileAttributes(boolean isDirectory, long size) implements BasicFileAttributes {
public record FileAttributes(
boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime
) implements BasicFileAttributes {
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
@Override
public FileTime lastModifiedTime() {
return EPOCH;
/**
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
* {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch.
*
* @param isDirectory Whether the filesystem entry is a directory.
* @param size The size of the file.
*/
public FileAttributes(boolean isDirectory, long size) {
this(isDirectory, size, EPOCH, EPOCH);
}
@Override
public FileTime lastAccessTime() {
return EPOCH;
}
@Override
public FileTime creationTime() {
return EPOCH;
return lastModifiedTime();
}
@Override

View File

@ -17,7 +17,6 @@ import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.metrics.Metrics;
import javax.annotation.Nullable;
import java.nio.file.attribute.FileTime;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@ -488,9 +487,9 @@ public class FSAPI implements ILuaAPI {
try (var ignored = environment.time(Metrics.FS_OPS)) {
var attributes = getFileSystem().getAttributes(path);
Map<String, Object> result = new HashMap<>();
result.put("modification", getFileTime(attributes.lastModifiedTime()));
result.put("modified", getFileTime(attributes.lastModifiedTime()));
result.put("created", getFileTime(attributes.creationTime()));
result.put("modification", attributes.lastModifiedTime().toMillis());
result.put("modified", attributes.lastModifiedTime().toMillis());
result.put("created", attributes.creationTime().toMillis());
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
result.put("isDir", attributes.isDirectory());
result.put("isReadOnly", getFileSystem().isReadOnly(path));
@ -499,8 +498,4 @@ public class FSAPI implements ILuaAPI {
throw new LuaException(e.getMessage());
}
}
private static long getFileTime(@Nullable FileTime time) {
return time == null ? 0 : time.toMillis();
}
}

View File

@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.FileAttributes;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.Mount;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* An abstract mount which stores its file tree in memory.
*
* @param <T> The type of file.
*/
public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.FileEntry<T>> implements Mount {
protected static final String NO_SUCH_FILE = "No such file";
@Nullable
protected T root;
private @Nullable T get(String path) {
var lastEntry = root;
var lastIndex = 0;
while (lastEntry != null && lastIndex < path.length()) {
var nextIndex = path.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = path.length();
lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
lastIndex = nextIndex + 1;
}
return lastEntry;
}
@Override
public final boolean exists(String path) {
return get(path) != null;
}
@Override
public final boolean isDirectory(String path) {
var file = get(path);
return file != null && file.isDirectory();
}
@Override
public final void list(String path, List<String> contents) throws IOException {
var file = get(path);
if (file == null || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
file.list(contents);
}
@Override
public final long getSize(String path) throws IOException {
var file = get(path);
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
return getSize(file);
}
/**
* Get the size of a file.
*
* @param file The file to get the size of.
* @return The size of the file. This should be 0 for directories, and equal to {@code openForRead(_).size()} for files.
* @throws IOException If the size could not be read.
*/
protected abstract long getSize(T file) throws IOException;
@Override
public final SeekableByteChannel openForRead(String path) throws IOException {
var file = get(path);
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
return openForRead(file);
}
/**
* Open a file for reading.
*
* @param file The file to read. This will not be a directory.
* @return The channel for this file.
*/
protected abstract SeekableByteChannel openForRead(T file) throws IOException;
@Override
public final BasicFileAttributes getAttributes(String path) throws IOException {
var file = get(path);
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
return getAttributes(file);
}
/**
* Get all attributes of the file.
*
* @param file The file to compute attributes for.
* @return The file's attributes.
* @throws IOException If the attributes could not be read.
*/
protected BasicFileAttributes getAttributes(T file) throws IOException {
return new FileAttributes(file.isDirectory(), getSize(file));
}
protected T getOrCreateChild(T lastEntry, String localPath, Function<String, T> factory) {
var lastIndex = 0;
while (lastIndex < localPath.length()) {
var nextIndex = localPath.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = localPath.length();
var part = localPath.substring(lastIndex, nextIndex);
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);
var nextEntry = lastEntry.children.get(part);
if (nextEntry == null || !nextEntry.isDirectory()) {
lastEntry.children.put(part, nextEntry = factory.apply(localPath.substring(0, nextIndex)));
}
lastEntry = nextEntry;
lastIndex = nextIndex + 1;
}
return lastEntry;
}
protected static class FileEntry<T extends FileEntry<T>> {
public final String path;
@Nullable
public Map<String, T> children;
protected FileEntry(String path) {
this.path = path;
}
public boolean isDirectory() {
return children != null;
}
protected void list(List<String> contents) {
if (children != null) contents.addAll(children.keySet());
}
}
}

View File

@ -6,25 +6,21 @@ package dan200.computercraft.core.filesystem;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import dan200.computercraft.api.filesystem.FileAttributes;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* An abstract mount based on some archive of files, such as a Zip or Minecraft's resources.
* <p>
* We assume that we cannot create {@link SeekableByteChannel}s directly from the archive, and so maintain a (shared)
* cache of recently read files and their contents.
*
* @param <T> The type of file.
*/
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implements Mount {
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends AbstractInMemoryMount<T> {
protected static final String NO_SUCH_FILE = "No such file";
/**
@ -44,82 +40,38 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implemen
.<FileEntry<?>, byte[]>weigher((k, v) -> v.length)
.build();
@Nullable
protected T root;
private @Nullable T get(String path) {
var lastEntry = root;
var lastIndex = 0;
while (lastEntry != null && lastIndex < path.length()) {
var nextIndex = path.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = path.length();
lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
lastIndex = nextIndex + 1;
}
return lastEntry;
}
@Override
public final boolean exists(String path) {
return get(path) != null;
}
@Override
public final boolean isDirectory(String path) {
var file = get(path);
return file != null && file.isDirectory();
}
@Override
public final void list(String path, List<String> contents) throws IOException {
var file = get(path);
if (file == null || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
file.list(contents);
}
@Override
public final long getSize(String path) throws IOException {
var file = get(path);
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
return getCachedSize(file);
}
private long getCachedSize(T file) throws IOException {
protected final long getSize(T file) throws IOException {
if (file.size != -1) return file.size;
if (file.isDirectory()) return file.size = 0;
var contents = CONTENTS_CACHE.getIfPresent(file);
if (contents != null) return file.size = contents.length;
return file.size = getSize(file);
return file.size = contents != null ? contents.length : getFileSize(file);
}
/**
* Get the size of a file.
* <p>
* This should only be called once per file, as the result is cached in {@link #getSize(String)}.
* Get the size of the file by reading it (or its metadata) from disk.
*
* @param file The file to compute the size of. This will not be a directory.
* @return The size of the file.
* @throws IOException If the size could not be read.
* @param file The file to get the size of.
* @return The file's size.
* @throws IOException If the size could not be computed.
*/
protected abstract long getSize(T file) throws IOException;
protected long getFileSize(T file) throws IOException {
return getContents(file).length;
}
@Override
public SeekableByteChannel openForRead(String path) throws IOException {
var file = get(path);
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
protected final SeekableByteChannel openForRead(T file) throws IOException {
return new ArrayByteChannel(getContents(file));
}
private byte[] getContents(T file) throws IOException {
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
if (cachedContents != null) return new ArrayByteChannel(cachedContents);
if (cachedContents != null) return cachedContents;
var contents = getContents(file);
var contents = getFileContents(file);
CONTENTS_CACHE.put(file, contents);
return new ArrayByteChannel(contents);
return contents;
}
/**
@ -128,44 +80,13 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implemen
* @param file The file to read into memory. This will not be a directory.
* @return The contents of the file.
*/
protected abstract byte[] getContents(T file) throws IOException;
@Override
public final BasicFileAttributes getAttributes(String path) throws IOException {
var file = get(path);
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
return getAttributes(file);
}
/**
* Get all attributes of the file.
*
* @param file The file to compute attributes for. This will not be a directory.
* @return The file's attributes.
* @throws IOException If the attributes could not be read.
*/
protected BasicFileAttributes getAttributes(T file) throws IOException {
return new FileAttributes(file.isDirectory(), getCachedSize(file));
}
protected static class FileEntry<T extends ArchiveMount.FileEntry<T>> {
public final String path;
@Nullable
public Map<String, T> children;
protected abstract byte[] getFileContents(T file) throws IOException;
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
long size = -1;
protected FileEntry(String path) {
this.path = path;
}
protected boolean isDirectory() {
return children != null;
}
protected void list(List<String> contents) {
if (children != null) contents.addAll(children.keySet());
super(path);
}
}
}

View File

@ -4,8 +4,8 @@
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.FileAttributes;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.core.util.Nullability;
import javax.annotation.Nullable;
import java.io.Closeable;
@ -42,7 +42,7 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
}
// Read in all the entries
root = new FileEntry("");
var root = this.root = new FileEntry("");
var zipEntries = zip.entries();
while (zipEntries.hasMoreElements()) {
var entry = zipEntries.nextElement();
@ -51,41 +51,18 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
if (!entryPath.startsWith(subPath)) continue;
var localPath = FileSystem.toLocal(entryPath, subPath);
create(entry, localPath);
getOrCreateChild(root, localPath, FileEntry::new).setup(entry);
}
}
private void create(ZipEntry entry, String localPath) {
var lastEntry = Nullability.assertNonNull(root);
var lastIndex = 0;
while (lastIndex < localPath.length()) {
var nextIndex = localPath.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = localPath.length();
var part = localPath.substring(lastIndex, nextIndex);
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);
var nextEntry = lastEntry.children.get(part);
if (nextEntry == null || !nextEntry.isDirectory()) {
lastEntry.children.put(part, nextEntry = new FileEntry(localPath.substring(0, nextIndex)));
}
lastEntry = nextEntry;
lastIndex = nextIndex + 1;
}
lastEntry.setup(entry);
}
@Override
protected long getSize(FileEntry file) throws FileOperationException {
protected long getFileSize(FileEntry file) throws FileOperationException {
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
return file.zipEntry.getSize();
}
@Override
protected byte[] getContents(FileEntry file) throws FileOperationException {
protected byte[] getFileContents(FileEntry file) throws FileOperationException {
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
try (var stream = zip.getInputStream(file.zipEntry)) {
@ -97,9 +74,10 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
}
@Override
public BasicFileAttributes getAttributes(FileEntry file) throws FileOperationException {
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
return new ZipEntryAttributes(file.zipEntry);
protected BasicFileAttributes getAttributes(FileEntry file) throws IOException {
return file.zipEntry == null ? super.getAttributes(file) : new FileAttributes(
file.isDirectory(), getSize(file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
);
}
@Override
@ -117,69 +95,13 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
void setup(ZipEntry entry) {
zipEntry = entry;
size = entry.getSize();
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
}
}
private static class ZipEntryAttributes implements BasicFileAttributes {
private final ZipEntry entry;
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
ZipEntryAttributes(ZipEntry entry) {
this.entry = entry;
}
@Override
public FileTime lastModifiedTime() {
return orEpoch(entry.getLastModifiedTime());
}
@Override
public FileTime lastAccessTime() {
return orEpoch(entry.getLastAccessTime());
}
@Override
public FileTime creationTime() {
var time = entry.getCreationTime();
return time == null ? lastModifiedTime() : time;
}
@Override
public boolean isRegularFile() {
return !entry.isDirectory();
}
@Override
public boolean isDirectory() {
return entry.isDirectory();
}
@Override
public boolean isSymbolicLink() {
return false;
}
@Override
public boolean isOther() {
return false;
}
@Override
public long size() {
return entry.getSize();
}
@Nullable
@Override
public Object fileKey() {
return null;
}
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
private static FileTime orEpoch(@Nullable FileTime time) {
return time == null ? EPOCH : time;
}
private static FileTime orEpoch(@Nullable FileTime time) {
return time == null ? EPOCH : time;
}
}

View File

@ -7,53 +7,44 @@ package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.filesystem.AbstractInMemoryMount;
import dan200.computercraft.core.util.Nullability;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
*/
public class MemoryMount implements Mount {
private final Map<String, byte[]> files = new HashMap<>();
private final Set<String> directories = new HashSet<>();
public class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> {
public MemoryMount() {
directories.add("");
}
@Override
public boolean exists(String path) {
return files.containsKey(path) || directories.contains(path);
}
@Override
public boolean isDirectory(String path) {
return directories.contains(path);
}
@Override
public void list(String path, List<String> files) {
for (var file : this.files.keySet()) {
if (file.startsWith(path)) files.add(file.substring(path.length() + 1));
}
}
@Override
public long getSize(String path) {
throw new RuntimeException("Not implemented");
}
@Override
public SeekableByteChannel openForRead(String path) throws FileOperationException {
var file = files.get(path);
if (file == null) throw new FileOperationException(path, "File not found");
return new ArrayByteChannel(file);
root = new FileEntry("");
}
public MemoryMount addFile(String file, String contents) {
files.put(file, contents.getBytes(StandardCharsets.UTF_8));
getOrCreateChild(Nullability.assertNonNull(root), file, FileEntry::new).contents = contents.getBytes(StandardCharsets.UTF_8);
return this;
}
@Override
protected long getSize(FileEntry file) {
return file.contents == null ? 0 : file.contents.length;
}
@Override
protected SeekableByteChannel openForRead(FileEntry file) throws IOException {
if (file.contents == null) throw new FileOperationException(file.path, "File is a directory");
return new ArrayByteChannel(file.contents);
}
protected static class FileEntry extends AbstractInMemoryMount.FileEntry<FileEntry> {
@Nullable
byte[] contents;
protected FileEntry(String path) {
super(path);
}
}
}