Skip to content

Commit

Permalink
Make mount error messages a bit more consistent
Browse files Browse the repository at this point in the history
 - Move most error message constants to a new MountHelpers class.
 - Be a little more consistent in when we throw "No such file" vs "Not a
   file/directory" messages.
  • Loading branch information
SquidDev committed Oct 22, 2023
1 parent cab66a2 commit 09e5217
Show file tree
Hide file tree
Showing 16 changed files with 197 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.HashMap;
import java.util.Map;

import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;

/**
* A mount backed by Minecraft's {@link ResourceManager}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
import java.util.Map;
import java.util.function.Function;

import static dan200.computercraft.core.filesystem.MountHelpers.*;

/**
* 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;

Expand Down Expand Up @@ -57,7 +57,8 @@ public final boolean isDirectory(String path) {
@Override
public final void list(String path, List<String> contents) throws IOException {
var file = get(path);
if (file == null || file.children == null) throw new FileOperationException(path, "Not a directory");
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
if (file.children == null) throw new FileOperationException(path, NOT_A_DIRECTORY);

contents.addAll(file.children.keySet());
}
Expand All @@ -82,7 +83,8 @@ public final long getSize(String path) 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);
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
if (file.isDirectory()) throw new FileOperationException(path, NOT_A_FILE);
return openForRead(path, file);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
* @param <T> The type of file.
*/
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends AbstractInMemoryMount<T> {
protected static final String NO_SUCH_FILE = "No such file";

/**
* Limit the entire cache to 64MiB.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import java.util.List;
import java.util.Set;

import static dan200.computercraft.core.filesystem.MountHelpers.NOT_A_FILE;
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;

/**
* A {@link Mount} implementation which provides read-only access to a directory.
*/
Expand Down Expand Up @@ -84,7 +87,9 @@ public BasicFileAttributes getAttributes(String path) throws FileOperationExcept
@Override
public SeekableByteChannel openForRead(String path) throws FileOperationException {
var file = resolvePath(path);
if (!Files.isRegularFile(file)) throw new FileOperationException(path, "No such file");
if (!Files.isRegularFile(file)) {
throw new FileOperationException(path, Files.exists(file) ? NOT_A_FILE : NO_SUCH_FILE);
}

try {
return Files.newByteChannel(file, READ_OPTIONS);
Expand All @@ -103,7 +108,7 @@ public SeekableByteChannel openForRead(String path) throws FileOperationExceptio
protected FileOperationException remapException(String fallbackPath, IOException exn) {
return exn instanceof FileSystemException fsExn
? remapException(fallbackPath, fsExn)
: new FileOperationException(fallbackPath, exn.getMessage() == null ? "Access denied" : exn.getMessage());
: new FileOperationException(fallbackPath, exn.getMessage() == null ? MountHelpers.ACCESS_DENIED : exn.getMessage());
}

/**
Expand All @@ -115,7 +120,7 @@ protected FileOperationException remapException(String fallbackPath, IOException
* @return The wrapped exception.
*/
protected FileOperationException remapException(String fallbackPath, FileSystemException exn) {
var reason = getReason(exn);
var reason = MountHelpers.getReason(exn);

var failedFile = exn.getFile();
if (failedFile == null) return new FileOperationException(fallbackPath, reason);
Expand All @@ -125,20 +130,4 @@ protected FileOperationException remapException(String fallbackPath, FileSystemE
? new FileOperationException(Joiner.on('/').join(root.relativize(failedPath)), reason)
: new FileOperationException(fallbackPath, reason);
}

/**
* Get the user-friendly reason for a {@link FileSystemException}.
*
* @param exn The exception that occurred.
* @return The friendly reason for this exception.
*/
protected String getReason(FileSystemException exn) {
if (exn instanceof FileAlreadyExistsException) return "File exists";
if (exn instanceof NoSuchFileException) return "No such file";
if (exn instanceof NotDirectoryException) return "Not a directory";
if (exn instanceof AccessDeniedException) return "Access denied";

var reason = exn.getReason();
return reason != null ? reason.trim() : "Operation failed";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.function.Function;
import java.util.regex.Pattern;

import static dan200.computercraft.core.filesystem.MountHelpers.*;

public class FileSystem {
/**
* Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into.
Expand Down Expand Up @@ -206,9 +208,9 @@ public synchronized void move(String sourcePath, String destPath) throws FileSys
sourcePath = sanitizePath(sourcePath);
destPath = sanitizePath(destPath);

if (isReadOnly(sourcePath) || isReadOnly(destPath)) throw new FileSystemException("Access denied");
if (!exists(sourcePath)) throw new FileSystemException("No such file");
if (exists(destPath)) throw new FileSystemException("File exists");
if (isReadOnly(sourcePath) || isReadOnly(destPath)) throw new FileSystemException(ACCESS_DENIED);
if (!exists(sourcePath)) throw new FileSystemException(NO_SUCH_FILE);
if (exists(destPath)) throw new FileSystemException(FILE_EXISTS);
if (contains(sourcePath, destPath)) throw new FileSystemException("Can't move a directory inside itself");

var mount = getMount(sourcePath);
Expand All @@ -223,15 +225,9 @@ public synchronized void move(String sourcePath, String destPath) throws FileSys
public synchronized void copy(String sourcePath, String destPath) throws FileSystemException {
sourcePath = sanitizePath(sourcePath);
destPath = sanitizePath(destPath);
if (isReadOnly(destPath)) {
throw new FileSystemException("/" + destPath + ": Access denied");
}
if (!exists(sourcePath)) {
throw new FileSystemException("/" + sourcePath + ": No such file");
}
if (exists(destPath)) {
throw new FileSystemException("/" + destPath + ": File exists");
}
if (isReadOnly(destPath)) throw new FileSystemException("/" + destPath + ": " + ACCESS_DENIED);
if (!exists(sourcePath)) throw new FileSystemException("/" + sourcePath + ": " + NO_SUCH_FILE);
if (exists(destPath)) throw new FileSystemException("/" + destPath + ": " + FILE_EXISTS);
if (contains(sourcePath, destPath)) {
throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself");
}
Expand Down Expand Up @@ -264,7 +260,7 @@ private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMo
// Copy bytes as fast as we can
ByteStreams.copy(source, destination);
} catch (AccessDeniedException e) {
throw new FileSystemException("Access denied");
throw new FileSystemException(ACCESS_DENIED);
} catch (IOException e) {
throw FileSystemException.of(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.io.IOException;
import java.io.Serial;

import static dan200.computercraft.core.filesystem.MountHelpers.ACCESS_DENIED;

public class FileSystemException extends Exception {
@Serial
private static final long serialVersionUID = -2500631644868104029L;
Expand All @@ -20,6 +22,6 @@ public static FileSystemException of(IOException e) {
}

public static String getMessage(IOException e) {
return e.getMessage() == null ? "Access denied" : e.getMessage();
return e.getMessage() == null ? ACCESS_DENIED : e.getMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
import java.io.IOException;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.HashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;

/**
* A mount which reads zip/jar files.
*/
Expand Down Expand Up @@ -95,9 +96,7 @@ void setup(ZipEntry entry) {
}
}

private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);

private static FileTime orEpoch(@Nullable FileTime time) {
return time == null ? EPOCH : time;
return time == null ? MountHelpers.EPOCH : time;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@
import java.time.Instant;
import java.util.*;

import static dan200.computercraft.core.filesystem.WritableFileMount.MINIMUM_FILE_SIZE;
import static dan200.computercraft.core.filesystem.MountHelpers.*;

/**
* A basic {@link Mount} which stores files and directories in-memory.
*/
public final class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> implements WritableMount {
private static final byte[] EMPTY = new byte[0];
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);

private final long capacity;

Expand Down Expand Up @@ -80,7 +79,7 @@ protected long getSize(String path, FileEntry file) {

@Override
protected SeekableByteChannel openForRead(String path, FileEntry file) throws IOException {
if (file.contents == null) throw new FileOperationException(path, "File is a directory");
if (file.contents == null) throw new FileOperationException(path, NOT_A_FILE);
return new EntryChannel(file, 0);
}

Expand Down Expand Up @@ -119,7 +118,7 @@ public void makeDirectory(String path) throws IOException {
if (nextEntry == null) {
lastEntry.children.put(part, nextEntry = FileEntry.newDir());
} else if (nextEntry.children == null) {
throw new FileOperationException(path, "File exists");
throw new FileOperationException(path, FILE_EXISTS);
}

lastEntry = nextEntry;
Expand All @@ -129,7 +128,7 @@ public void makeDirectory(String path) throws IOException {

@Override
public void delete(String path) throws IOException {
if (path.isEmpty()) throw new AccessDeniedException("Access denied");
if (path.isEmpty()) throw new AccessDeniedException(ACCESS_DENIED);
var node = getParentAndName(path);
if (node != null) node.parent().remove(node.name());
}
Expand All @@ -139,23 +138,23 @@ public void rename(String source, String dest) throws IOException {
if (dest.startsWith(source)) throw new FileOperationException(source, "Cannot move a directory inside itself");

var sourceParent = getParentAndName(source);
if (sourceParent == null || !sourceParent.exists()) throw new FileOperationException(source, "No such file");
if (sourceParent == null || !sourceParent.exists()) throw new FileOperationException(source, NO_SUCH_FILE);

var destParent = getParentAndName(dest);
if (destParent == null) throw new FileOperationException(dest, "Parent directory does not exist");
if (destParent.exists()) throw new FileOperationException(dest, "File exists");
if (destParent.exists()) throw new FileOperationException(dest, FILE_EXISTS);

destParent.put(sourceParent.parent().remove(sourceParent.name()));
}

private FileEntry getForWrite(String path) throws FileOperationException {
if (path.isEmpty()) throw new FileOperationException(path, "Cannot write to directory");
if (path.isEmpty()) throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY);

var parent = getParentAndName(path);
if (parent == null) throw new FileOperationException(path, "Parent directory does not exist");

var file = parent.get();
if (file != null && file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
if (file != null && file.isDirectory()) throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY);
if (file == null) parent.put(file = FileEntry.newFile());

return file;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0

package dan200.computercraft.core.filesystem;

import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;

import java.nio.file.FileSystemException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.List;

/**
* Useful constants and helper functions for working with mounts.
*/
public final class MountHelpers {
/**
* A {@link FileTime} set to the Unix EPOCH, intended for {@link BasicFileAttributes}'s file times.
*/
public static final FileTime EPOCH = FileTime.from(Instant.EPOCH);

/**
* The minimum size of a file for file {@linkplain WritableMount#getCapacity() capacity calculations}.
*/
public static final long MINIMUM_FILE_SIZE = 500;

/**
* The error message used when the file does not exist.
*/
public static final String NO_SUCH_FILE = "No such file";

/**
* The error message used when trying to use a file as a directory (for instance when
* {@linkplain Mount#list(String, List) listing its contents}).
*/
public static final String NOT_A_DIRECTORY = "Not a directory";

/**
* The error message used when trying to use a directory as a file (for instance when
* {@linkplain Mount#openForRead(String) opening for reading}).
*/
public static final String NOT_A_FILE = "Not a file";

/**
* The error message used when attempting to modify a read-only file or mount.
*/
public static final String ACCESS_DENIED = "Access denied";

/**
* The error message used when trying to overwrite a file (for instance when
* {@linkplain WritableMount#rename(String, String) renaming files} or {@linkplain WritableMount#makeDirectory(String)
* creating directories}).
*/
public static final String FILE_EXISTS = "File exists";

/**
* The error message used when trying to {@linkplain WritableMount#openForWrite(String) opening a directory to read}.
*/
public static final String CANNOT_WRITE_TO_DIRECTORY = "Cannot write to directory";

/**
* The error message used when the mount runs out of space.
*/
public static final String OUT_OF_SPACE = "Out of space";

private MountHelpers() {
}

/**
* Get the user-friendly reason for a {@link java.nio.file.FileSystemException}.
*
* @param exn The exception that occurred.
* @return The friendly reason for this exception.
*/
public static String getReason(FileSystemException exn) {
if (exn instanceof FileAlreadyExistsException) return FILE_EXISTS;
if (exn instanceof NoSuchFileException) return NO_SUCH_FILE;
if (exn instanceof NotDirectoryException) return NOT_A_DIRECTORY;
if (exn instanceof AccessDeniedException) return ACCESS_DENIED;

var reason = exn.getReason();
return reason != null ? reason.trim() : "Operation failed";
}
}
Loading

0 comments on commit 09e5217

Please sign in to comment.