diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java index 7fb0d0538..1bebaf3e8 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java @@ -78,4 +78,15 @@ public interface IWritableMount extends IMount { default OptionalLong getCapacity() { return OptionalLong.empty(); } + + /** + * Returns whether a file with a given path is read-only or not. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprograms". + * @return If the file exists and is read-only. + * @throws IOException If an error occurs when checking whether the file is read-only. + */ + default boolean isReadOnly(String path) throws IOException { + return false; + } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java index 371c77f1b..c5d393ad7 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java @@ -143,6 +143,16 @@ public class FileMount implements IWritableMount { return file.exists() && file.isDirectory(); } + @Override + public boolean isReadOnly(String path) throws IOException { + var file = getRealPath(path); + while (true) { + if (file.exists()) return !file.canWrite(); + if (file.equals(rootPath)) return false; + file = file.getParentFile(); + } + } + @Override public void list(String path, List contents) throws IOException { if (!created()) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java index 7e8f7c60c..0ec038aa9 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapperMount.java @@ -94,6 +94,15 @@ public class FileSystemWrapperMount implements IFileSystem { } } + @Override + public boolean isReadOnly(String path) throws IOException { + try { + return filesystem.isReadOnly(path); + } catch (FileSystemException e) { + throw new IOException(e.getMessage()); + } + } + @Override public void list(String path, List contents) throws IOException { try { diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java index aa7b1c899..ed9a818ed 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java @@ -61,8 +61,12 @@ class MountWrapper { return writableMount == null ? OptionalLong.empty() : writableMount.getCapacity(); } - public boolean isReadOnly(String path) { - return writableMount == null; + public boolean isReadOnly(String path) throws FileSystemException { + try { + return writableMount == null || writableMount.isReadOnly(path); + } catch (IOException e) { + throw localExceptionOf(path, e); + } } public boolean exists(String path) throws FileSystemException { diff --git a/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileMountTest.java b/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileMountTest.java new file mode 100644 index 000000000..7e2a03093 --- /dev/null +++ b/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileMountTest.java @@ -0,0 +1,75 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.filesystem; + +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import dan200.computercraft.api.filesystem.IWritableMount; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FileMountTest { + private static final long CAPACITY = 1_000_000; + private final List cleanup = new ArrayList<>(); + + @AfterEach + public void cleanup() throws IOException { + for (var mount : cleanup) MoreFiles.deleteRecursively(mount, RecursiveDeleteOption.ALLOW_INSECURE); + } + + private Path createRoot() throws IOException { + var path = Files.createTempDirectory("cctweaked-test"); + cleanup.add(path); + return path; + } + + private IWritableMount getExisting(long capacity) throws IOException { + return new FileMount(createRoot().toFile(), capacity); + } + + private IWritableMount getNotExisting(long capacity) throws IOException { + return new FileMount(createRoot().resolve("mount").toFile(), capacity); + } + + @Test + public void testRootWritable() throws IOException { + assertFalse(getExisting(CAPACITY).isReadOnly("/")); + assertFalse(getNotExisting(CAPACITY).isReadOnly("/")); + } + + @Test + public void testMissingDirWritable() throws IOException { + assertFalse(getExisting(CAPACITY).isReadOnly("/foo/bar/baz/qux")); + } + + @Test + public void testDirReadOnly() throws IOException { + var root = createRoot(); + var mount = new FileMount(root.toFile(), CAPACITY); + mount.makeDirectory("read-only"); + + var attributes = Files.getFileAttributeView(root.resolve("read-only"), PosixFileAttributeView.class); + Assumptions.assumeTrue(attributes != null, "POSIX attributes are not available."); + + assertFalse(mount.isReadOnly("read-only"), "Directory should not be read-only yet"); + attributes.setPermissions(Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_EXECUTE)); + assertTrue(mount.isReadOnly("read-only"), "Directory should not be read-only yet"); + assertTrue(mount.isReadOnly("read-only/child"), "Child should be read-only"); + } +}