diff --git a/core/src/main/java/io/roastedroot/zerofs/ZeroFsSecureDirectoryStream.java b/core/src/main/java/io/roastedroot/zerofs/ZeroFsSecureDirectoryStream.java index 5f059cf1..41dacb97 100644 --- a/core/src/main/java/io/roastedroot/zerofs/ZeroFsSecureDirectoryStream.java +++ b/core/src/main/java/io/roastedroot/zerofs/ZeroFsSecureDirectoryStream.java @@ -1,5 +1,7 @@ package io.roastedroot.zerofs; +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; + import java.io.IOException; import java.nio.channels.SeekableByteChannel; import java.nio.file.ClosedDirectoryStreamException; @@ -177,21 +179,14 @@ public void move(Path srcPath, SecureDirectoryStream targetDir, Path targe throws IOException { checkOpen(); ZeroFsPath checkedSrcPath = checkPath(srcPath); - ZeroFsPath checkedTargetPath = checkPath(targetPath); - - if (!(targetDir instanceof ZeroFsSecureDirectoryStream)) { - throw new ProviderMismatchException( - "targetDir isn't a secure directory stream associated with this file system"); - } - ZeroFsSecureDirectoryStream checkedTargetDir = (ZeroFsSecureDirectoryStream) targetDir; + ZeroFsSecureDirectoryStream checkedTargetDir = checkDirectoryStream(targetDir); + ZeroFsPath checkedTargetPath = checkedTargetDir.checkPath(targetPath); - view.copy( - checkedSrcPath, - checkedTargetDir.view, - checkedTargetPath, - Set.of(), - true); + // like Files.move with ATOMIC_MOVE: same file system only, and links are not followed + Set options = + Options.getMoveOptions(checkedSrcPath, checkedTargetPath, ATOMIC_MOVE); + view.copy(checkedSrcPath, checkedTargetDir.view, checkedTargetPath, options, true); } @Override @@ -219,11 +214,28 @@ public File lookup() throws IOException { type); } - private static ZeroFsPath checkPath(Path path) { - if (path instanceof ZeroFsPath) { - return (ZeroFsPath) path; + private ZeroFsPath checkPath(Path path) { + if (!(path instanceof ZeroFsPath)) { + throw new ProviderMismatchException( + "path " + path + " is not associated with a ZeroFs file system"); + } + if (!path().getFileSystem().equals(path.getFileSystem())) { + throw new ProviderMismatchException( + "path " + + path + + " is associated with a different ZeroFs file system than this " + + "SecureDirectoryStream"); + } + return (ZeroFsPath) path; + } + + // the target stream's file system is checked by the atomic move requirement + private static ZeroFsSecureDirectoryStream checkDirectoryStream( + SecureDirectoryStream stream) { + if (!(stream instanceof ZeroFsSecureDirectoryStream)) { + throw new ProviderMismatchException( + stream + " isn't a secure directory stream associated with this file system"); } - throw new ProviderMismatchException( - "path " + path + " is not associated with a ZeroFs file system"); + return (ZeroFsSecureDirectoryStream) stream; } } diff --git a/core/src/test/java/io/roastedroot/zerofs/ZeroFsUnixLikeFileSystemTest.java b/core/src/test/java/io/roastedroot/zerofs/ZeroFsUnixLikeFileSystemTest.java index 172cb535..d4c72ccf 100644 --- a/core/src/test/java/io/roastedroot/zerofs/ZeroFsUnixLikeFileSystemTest.java +++ b/core/src/test/java/io/roastedroot/zerofs/ZeroFsUnixLikeFileSystemTest.java @@ -57,6 +57,7 @@ import java.nio.file.NotLinkException; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.ProviderMismatchException; import java.nio.file.SecureDirectoryStream; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; @@ -2153,6 +2154,114 @@ public void testSecureDirectoryStream() throws IOException { } } + @Test + public void testSecureDirectoryStream_differentFileSystems() throws IOException { + Path fs1DirPath = path("foo"); + Files.createDirectories(fs1DirPath); + Path fs1File = path("a"); + Files.createFile(fs1DirPath.resolve(fs1File)); + + try (FileSystem fs2 = ZeroFs.newFileSystem("fs2", UNIX_CONFIGURATION)) { + Path fs2DirPath = fs2.getPath("foo"); + Files.createDirectories(fs2DirPath); + Path fs2File = fs2.getPath("a"); + Files.createFile(fs2DirPath.resolve(fs2File)); + + try (SecureDirectoryStream fs1Dir = newDirStream(fs1DirPath)) { + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.newDirectoryStream(fs2DirPath)); + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.newByteChannel(fs2File, Set.of(READ))); + assertThrows( + ProviderMismatchException.class, () -> fs1Dir.deleteDirectory(fs2DirPath)); + assertThrows(ProviderMismatchException.class, () -> fs1Dir.deleteFile(fs2File)); + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.getFileAttributeView(fs2File, BasicFileAttributeView.class)); + Path otherFs1File = path("b"); + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.move(fs2File, fs1Dir, otherFs1File)); + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.move(fs1File, fs1Dir, fs2File)); + + try (SecureDirectoryStream fs2Dir = newDirStream(fs2DirPath)) { + assertThrows( + AtomicMoveNotSupportedException.class, + () -> fs1Dir.move(fs1File, fs2Dir, fs2File)); + } + } + } + } + + @Test + public void testSecureDirectoryStream_differentFileSystemProviders() throws IOException { + Path fs1DirPath = path("foo"); + Files.createDirectories(fs1DirPath); + Path fs1File = path("a"); + Files.createFile(fs1DirPath.resolve(fs1File)); + + Path fs2Path = Paths.get(System.getProperty("java.io.tmpdir")); + + try (SecureDirectoryStream fs1Dir = newDirStream(fs1DirPath)) { + assertThrows(ProviderMismatchException.class, () -> fs1Dir.newDirectoryStream(fs2Path)); + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.newByteChannel(fs2Path, Set.of(READ))); + assertThrows(ProviderMismatchException.class, () -> fs1Dir.deleteDirectory(fs2Path)); + assertThrows(ProviderMismatchException.class, () -> fs1Dir.deleteFile(fs2Path)); + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.getFileAttributeView(fs2Path, BasicFileAttributeView.class)); + Path otherFs1File = path("b"); + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.move(fs2Path, fs1Dir, otherFs1File)); + assertThrows( + ProviderMismatchException.class, () -> fs1Dir.move(fs1File, fs1Dir, fs2Path)); + + try (DirectoryStream fs2DirStream = Files.newDirectoryStream(fs2Path)) { + // the default file system only offers SecureDirectoryStream on some platforms + if (fs2DirStream instanceof SecureDirectoryStream) { + SecureDirectoryStream fs2Dir = (SecureDirectoryStream) fs2DirStream; + assertThrows( + ProviderMismatchException.class, + () -> fs1Dir.move(fs1File, fs2Dir, fs2Path)); + } + } + } + } + + @Test + public void testSecureDirectoryStream_move_doesNotFollowLinks() throws IOException { + Path dir = path("dir"); + Path fileName = path("file"); + Path file = dir.resolve(fileName); + Path linkName = path("link"); + + Files.createDirectories(dir); + Files.createFile(file); + Files.createSymbolicLink(dir.resolve(linkName), fileName); + + try (SecureDirectoryStream stream = newDirStream(dir)) { + Path movedLinkName = path("link2"); + stream.move(linkName, stream, movedLinkName); + + Path dirRelativeLink = dir.resolve(movedLinkName); + assertThatPath(dirRelativeLink).noFollowLinks().isSymbolicLink(); + assertThatPath(dirRelativeLink).isSameFileAs(file); + } + } + + private static SecureDirectoryStream newDirStream(Path path) throws IOException { + DirectoryStream stream = Files.newDirectoryStream(path); + assertInstanceOf(SecureDirectoryStream.class, stream); + return (SecureDirectoryStream) stream; + } + @Test public void testSecureDirectoryStreamBasedOnRelativePath() throws IOException { Files.createDirectories(path("foo"));