From 2f357dc01b37889dc7f0c88a464522c605cc3af7 Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Wed, 23 Oct 2024 08:08:02 +0530 Subject: [PATCH 1/9] Instrumented path based operations using hooks defined in `Checker` --- os/src-jvm/package.scala | 2 + os/src-native/package.scala | 2 + os/src/FileOps.scala | 27 +- os/src/Model.scala | 25 ++ os/src/PermsOps.scala | 7 +- os/src/ReadWriteOps.scala | 34 ++- os/src/StatOps.scala | 1 + os/src/TempOps.scala | 8 +- os/src/ZipOps.scala | 4 + os/test/resources/restricted/File.txt | 1 + os/test/resources/restricted/Multi Line.txt | 4 + os/test/resources/restricted/folder1/one.txt | 1 + .../restricted/folder2/nestedA/a.txt | 1 + .../restricted/folder2/nestedB/b.txt | 1 + .../resources/restricted/misc/broken-symlink | 0 .../resources/restricted/misc/file-symlink | 1 + .../resources/restricted/misc/folder-symlink | 1 + os/test/src/FilesystemMetadataTests.scala | 23 +- os/test/src/FilesystemPermissionsTests.scala | 64 +++- .../src/ManipulatingFilesFoldersTests.scala | 273 +++++++++++++++++- os/test/src/ReadingWritingTests.scala | 62 +++- os/test/src/TestUtil.scala | 53 ++++ os/test/src/ZipOpTests.scala | 67 ++++- 23 files changed, 636 insertions(+), 26 deletions(-) create mode 100644 os/test/resources/restricted/File.txt create mode 100644 os/test/resources/restricted/Multi Line.txt create mode 100644 os/test/resources/restricted/folder1/one.txt create mode 100644 os/test/resources/restricted/folder2/nestedA/a.txt create mode 100644 os/test/resources/restricted/folder2/nestedB/b.txt create mode 100644 os/test/resources/restricted/misc/broken-symlink create mode 100644 os/test/resources/restricted/misc/file-symlink create mode 120000 os/test/resources/restricted/misc/folder-symlink diff --git a/os/src-jvm/package.scala b/os/src-jvm/package.scala index 417aa673..48c2fd39 100644 --- a/os/src-jvm/package.scala +++ b/os/src-jvm/package.scala @@ -59,6 +59,8 @@ package object os { val sub: SubPath = SubPath.sub + val checker: DynamicVariable[Checker] = new DynamicVariable[Checker](Checker.Nop) + /** * Extractor to let you easily pattern match on [[os.Path]]s. Lets you do * diff --git a/os/src-native/package.scala b/os/src-native/package.scala index 0dbef162..255dd347 100644 --- a/os/src-native/package.scala +++ b/os/src-native/package.scala @@ -52,6 +52,8 @@ package object os { val sub: SubPath = SubPath.sub + val checker: DynamicVariable[Checker] = new DynamicVariable[Checker](Checker.Nop) + /** * Extractor to let you easily pattern match on [[os.Path]]s. Lets you do * diff --git a/os/src/FileOps.scala b/os/src/FileOps.scala index d0276f0f..c276c64d 100644 --- a/os/src/FileOps.scala +++ b/os/src/FileOps.scala @@ -22,8 +22,12 @@ import scala.util.Try * ignore the destination if it already exists, using [[os.makeDir.all]] */ object makeDir extends Function1[Path, Unit] { - def apply(path: Path): Unit = Files.createDirectory(path.wrapped) + def apply(path: Path): Unit = { + checker.value.onWrite(path) + Files.createDirectory(path.wrapped) + } def apply(path: Path, perms: PermSet): Unit = { + checker.value.onWrite(path) Files.createDirectory( path.wrapped, PosixFilePermissions.asFileAttribute(perms.toSet()) @@ -38,6 +42,7 @@ object makeDir extends Function1[Path, Unit] { object all extends Function1[Path, Unit] { def apply(path: Path): Unit = apply(path, null, true) def apply(path: Path, perms: PermSet = null, acceptLinkedDirectory: Boolean = true): Unit = { + checker.value.onWrite(path) // We special case calling makeDir.all on a symlink to a directory; // normally createDirectories blows up noisily, when really what most // people would want is for it to succeed since there is a (linked) @@ -84,6 +89,9 @@ object move { atomicMove: Boolean = false, createFolders: Boolean = false ): Unit = { + checker.value.onRead(from) + checker.value.onWrite(from / RelPath.up) + checker.value.onWrite(to) if (createFolders && to.segmentCount != 0) makeDir.all(to / up) val opts1 = if (replaceExisting) Array[CopyOption](StandardCopyOption.REPLACE_EXISTING) @@ -176,6 +184,8 @@ object copy { createFolders: Boolean = false, mergeFolders: Boolean = false ): Unit = { + checker.value.onRead(from) + checker.value.onWrite(to) if (createFolders && to.segmentCount != 0) makeDir.all(to / up) val opts1 = if (followLinks) Array[CopyOption]() @@ -191,18 +201,17 @@ object copy { s"Can't copy a directory into itself: $to is inside $from" ) - def copyOne(p: Path): file.Path = { + def copyOne(p: Path): Unit = { val target = to / p.relativeTo(from) if (mergeFolders && isDir(p, followLinks) && isDir(target, followLinks)) { // nothing to do - target.wrapped } else { Files.copy(p.wrapped, target.wrapped, opts1 ++ opts2 ++ opts3: _*) } } copyOne(from) - if (stat(from, followLinks = followLinks).isDir) walk(from).map(copyOne) + if (stat(from, followLinks = followLinks).isDir) for (p <- walk(from)) copyOne(p) } /** This overload is only to keep binary compatibility with older os-lib versions. */ @@ -311,6 +320,7 @@ object copy { object remove extends Function1[Path, Boolean] { def apply(target: Path): Boolean = apply(target, false) def apply(target: Path, checkExists: Boolean = false): Boolean = { + checker.value.onWrite(target) if (checkExists) { Files.delete(target.wrapped) true @@ -322,6 +332,7 @@ object remove extends Function1[Path, Boolean] { object all extends Function1[Path, Unit] { def apply(target: Path) = { require(target.segmentCount != 0, s"Cannot remove a root directory: $target") + checker.value.onWrite(target) val nioTarget = target.wrapped if (Files.exists(nioTarget, LinkOption.NOFOLLOW_LINKS)) { @@ -350,6 +361,8 @@ object exists extends Function1[Path, Boolean] { */ object hardlink { def apply(link: Path, dest: Path) = { + checker.value.onWrite(link) + checker.value.onRead(dest) Files.createLink(link.wrapped, dest.wrapped) } } @@ -359,6 +372,12 @@ object hardlink { */ object symlink { def apply(link: Path, dest: FilePath, perms: PermSet = null): Unit = { + checker.value.onWrite(link) + checker.value.onRead(dest match { + case p: RelPath => link / RelPath.up / p + case p: SubPath => link / RelPath.up / p + case p: Path => p + }) val permArray: Array[FileAttribute[_]] = if (perms == null) Array[FileAttribute[_]]() else Array(PosixFilePermissions.asFileAttribute(perms.toSet())) diff --git a/os/src/Model.scala b/os/src/Model.scala index f2389407..cbc2d6b6 100644 --- a/os/src/Model.scala +++ b/os/src/Model.scala @@ -283,3 +283,28 @@ object PosixStatInfo { ) } } + +/** + * Defines hooks for path based operations. + * + * This, in conjunction with [[checker]], can be used to implement custom checks like + * - restricting operations to some path(s) + * - logging operations + */ +trait Checker { + + /** A hook for a read operation on `path`. */ + def onRead(path: ReadablePath): Unit + + /** A hook for a write operation on `path`. */ + def onWrite(path: Path): Unit +} + +object Checker { + + /** A no-op [[Checker]]. */ + object Nop extends Checker { + def onRead(path: ReadablePath): Unit = () + def onWrite(path: Path): Unit = () + } +} diff --git a/os/src/PermsOps.scala b/os/src/PermsOps.scala index 95f6bb32..73ef6d19 100644 --- a/os/src/PermsOps.scala +++ b/os/src/PermsOps.scala @@ -24,6 +24,7 @@ object perms extends Function1[Path, PermSet] { */ object set { def apply(p: Path, arg2: PermSet): Unit = { + checker.value.onWrite(p) Files.setPosixFilePermissions(p.wrapped, arg2.toSet()) } } @@ -44,7 +45,10 @@ object owner extends Function1[Path, UserPrincipal] { * Set the owner of the file/folder at the given path */ object set { - def apply(arg1: Path, arg2: UserPrincipal): Unit = Files.setOwner(arg1.wrapped, arg2) + def apply(arg1: Path, arg2: UserPrincipal): Unit = { + checker.value.onWrite(arg1) + Files.setOwner(arg1.wrapped, arg2) + } def apply(arg1: Path, arg2: String): Unit = { apply( arg1, @@ -73,6 +77,7 @@ object group extends Function1[Path, GroupPrincipal] { */ object set { def apply(arg1: Path, arg2: GroupPrincipal): Unit = { + checker.value.onWrite(arg1) Files.getFileAttributeView( arg1.wrapped, classOf[PosixFileAttributeView], diff --git a/os/src/ReadWriteOps.scala b/os/src/ReadWriteOps.scala index 3ea0d83d..62b8c0be 100644 --- a/os/src/ReadWriteOps.scala +++ b/os/src/ReadWriteOps.scala @@ -27,6 +27,7 @@ object write { createFolders: Boolean = false, openOptions: Seq[OpenOption] = Seq(CREATE, WRITE) ) = { + checker.value.onWrite(target) if (createFolders) makeDir.all(target / RelPath.up, perms) if (perms != null && !exists(target)) { val permArray = @@ -53,6 +54,7 @@ object write { perms: PermSet, offset: Long ) = { + checker.value.onWrite(target) import collection.JavaConverters._ val permArray: Array[FileAttribute[_]] = @@ -166,6 +168,7 @@ object write { */ object channel extends Function1[Path, SeekableByteChannel] { def write(p: Path, options: Seq[StandardOpenOption]) = { + checker.value.onWrite(p) java.nio.file.Files.newByteChannel(p.toNIO, options.toArray: _*) } def apply(p: Path): SeekableByteChannel = { @@ -212,6 +215,7 @@ object write { */ object truncate { def apply(p: Path, size: Long): Unit = { + checker.value.onWrite(p) val channel = FileChannel.open(p.toNIO, StandardOpenOption.WRITE) try channel.truncate(size) finally channel.close() @@ -242,16 +246,21 @@ object read extends Function1[ReadablePath, String] { * Opens a [[java.io.InputStream]] to read from the given file */ object inputStream extends Function1[ReadablePath, java.io.InputStream] { - def apply(p: ReadablePath): java.io.InputStream = p.getInputStream + def apply(p: ReadablePath): java.io.InputStream = { + checker.value.onRead(p) + p.getInputStream + } } object stream extends Function1[ReadablePath, geny.Readable] { - def apply(p: ReadablePath): geny.Readable = new geny.Readable { - override def contentLength: Option[Long] = p.toSource.contentLength - def readBytesThrough[T](f: java.io.InputStream => T): T = { - val is = p.getInputStream - try f(is) - finally is.close() + def apply(p: ReadablePath): geny.Readable = { + new geny.Readable { + override def contentLength: Option[Long] = p.toSource.contentLength + def readBytesThrough[T](f: java.io.InputStream => T): T = { + val is = os.read.inputStream(p) + try f(is) + finally is.close() + } } } } @@ -260,7 +269,10 @@ object read extends Function1[ReadablePath, String] { * Opens a [[SeekableByteChannel]] to read from the given file. */ object channel extends Function1[Path, SeekableByteChannel] { - def apply(p: Path): SeekableByteChannel = p.toSource.getChannel() + def apply(p: Path): SeekableByteChannel = { + checker.value.onRead(p) + p.toSource.getChannel() + } } /** @@ -271,7 +283,7 @@ object read extends Function1[ReadablePath, String] { object bytes extends Function1[ReadablePath, Array[Byte]] { def apply(arg: ReadablePath): Array[Byte] = { val out = new java.io.ByteArrayOutputStream() - val stream = arg.getInputStream + val stream = os.read.inputStream(arg) try Internals.transfer(stream, out) finally stream.close() out.toByteArray @@ -279,7 +291,7 @@ object read extends Function1[ReadablePath, String] { def apply(arg: Path, offset: Long, count: Int): Array[Byte] = { val arr = new Array[Byte](count) val buf = ByteBuffer.wrap(arr) - val channel = arg.toSource.getChannel() + val channel = os.read.channel(arg) try { channel.position(offset) val finalCount = channel.read(buf) @@ -360,7 +372,7 @@ object read extends Function1[ReadablePath, String] { def apply(arg: ReadablePath, charSet: Codec) = { new geny.Generator[String] { def generate(handleItem: String => Generator.Action) = { - val is = arg.getInputStream + val is = os.read.inputStream(arg) val isr = new InputStreamReader(is, charSet.decoder) val buf = new BufferedReader(isr) var currentAction: Generator.Action = Generator.Continue diff --git a/os/src/StatOps.scala b/os/src/StatOps.scala index 958138a3..cf8695ef 100644 --- a/os/src/StatOps.scala +++ b/os/src/StatOps.scala @@ -74,6 +74,7 @@ object mtime extends Function1[Path, Long] { */ object set { def apply(p: Path, millis: Long) = { + checker.value.onWrite(p) Files.setLastModifiedTime(p.wrapped, FileTime.fromMillis(millis)) } } diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 900fe6b7..3c70c716 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -35,7 +35,9 @@ object temp { val nioPath = dir match { case null => java.nio.file.Files.createTempFile(prefix, suffix, permArray: _*) - case _ => java.nio.file.Files.createTempFile(dir.wrapped, prefix, suffix, permArray: _*) + case _ => + checker.value.onWrite(dir) + java.nio.file.Files.createTempFile(dir.wrapped, prefix, suffix, permArray: _*) } if (contents != null) write.over(Path(nioPath), contents) @@ -63,7 +65,9 @@ object temp { val nioPath = dir match { case null => java.nio.file.Files.createTempDirectory(prefix, permArray: _*) - case _ => java.nio.file.Files.createTempDirectory(dir.wrapped, prefix, permArray: _*) + case _ => + checker.value.onWrite(dir) + java.nio.file.Files.createTempDirectory(dir.wrapped, prefix, permArray: _*) } if (deleteOnExit) nioPath.toFile.deleteOnExit() diff --git a/os/src/ZipOps.scala b/os/src/ZipOps.scala index b4721e76..a62d7d49 100644 --- a/os/src/ZipOps.scala +++ b/os/src/ZipOps.scala @@ -45,6 +45,9 @@ object zip { deletePatterns: Seq[Regex] = List(), compressionLevel: Int = java.util.zip.Deflater.DEFAULT_COMPRESSION ): os.Path = { + checker.value.onWrite(dest) + // check read preemptively in case "dest" is created + for (source <- sources) checker.value.onRead(source.src) if (os.exists(dest)) { val opened = open(dest) @@ -268,6 +271,7 @@ object unzip { excludePatterns: Seq[Regex] = List(), includePatterns: Seq[Regex] = List() ): Unit = { + checker.value.onWrite(dest) for ((zipEntry, zipInputStream) <- streamRaw(source, excludePatterns, includePatterns)) { val newFile = dest / os.SubPath(zipEntry.getName) if (zipEntry.isDirectory) os.makeDir.all(newFile) diff --git a/os/test/resources/restricted/File.txt b/os/test/resources/restricted/File.txt new file mode 100644 index 00000000..c295cb70 --- /dev/null +++ b/os/test/resources/restricted/File.txt @@ -0,0 +1 @@ +I am a restricted cow \ No newline at end of file diff --git a/os/test/resources/restricted/Multi Line.txt b/os/test/resources/restricted/Multi Line.txt new file mode 100644 index 00000000..03a7b2c7 --- /dev/null +++ b/os/test/resources/restricted/Multi Line.txt @@ -0,0 +1,4 @@ +I am restricted cow +Hear me moo +I weigh twice as much as you +And I look good on the barbecue \ No newline at end of file diff --git a/os/test/resources/restricted/folder1/one.txt b/os/test/resources/restricted/folder1/one.txt new file mode 100644 index 00000000..7959e0c6 --- /dev/null +++ b/os/test/resources/restricted/folder1/one.txt @@ -0,0 +1 @@ +Contents of restricted folder one \ No newline at end of file diff --git a/os/test/resources/restricted/folder2/nestedA/a.txt b/os/test/resources/restricted/folder2/nestedA/a.txt new file mode 100644 index 00000000..27ce3da0 --- /dev/null +++ b/os/test/resources/restricted/folder2/nestedA/a.txt @@ -0,0 +1 @@ +Contents of restricted nested A \ No newline at end of file diff --git a/os/test/resources/restricted/folder2/nestedB/b.txt b/os/test/resources/restricted/folder2/nestedB/b.txt new file mode 100644 index 00000000..f7539d86 --- /dev/null +++ b/os/test/resources/restricted/folder2/nestedB/b.txt @@ -0,0 +1 @@ +Contents of restricted nested B \ No newline at end of file diff --git a/os/test/resources/restricted/misc/broken-symlink b/os/test/resources/restricted/misc/broken-symlink new file mode 100644 index 00000000..e69de29b diff --git a/os/test/resources/restricted/misc/file-symlink b/os/test/resources/restricted/misc/file-symlink new file mode 100644 index 00000000..c295cb70 --- /dev/null +++ b/os/test/resources/restricted/misc/file-symlink @@ -0,0 +1 @@ +I am a restricted cow \ No newline at end of file diff --git a/os/test/resources/restricted/misc/folder-symlink b/os/test/resources/restricted/misc/folder-symlink new file mode 120000 index 00000000..6ff69ba0 --- /dev/null +++ b/os/test/resources/restricted/misc/folder-symlink @@ -0,0 +1 @@ +../folder1 \ No newline at end of file diff --git a/os/test/src/FilesystemMetadataTests.scala b/os/test/src/FilesystemMetadataTests.scala index f5d654db..debe2ce4 100644 --- a/os/test/src/FilesystemMetadataTests.scala +++ b/os/test/src/FilesystemMetadataTests.scala @@ -1,6 +1,6 @@ package test.os -import test.os.TestUtil.prep +import test.os.TestUtil._ import utest._ object FilesystemMetadataTests extends TestSuite { @@ -9,6 +9,9 @@ object FilesystemMetadataTests extends TestSuite { private val multilineSizes = Set[Long](81, 84) def tests = Tests { + // restricted directory + val rd = os.pwd / "os/test/resources/restricted" + test("stat") { test - prep { wd => os.stat(wd / "File.txt").size ==> 8 @@ -69,7 +72,25 @@ object FilesystemMetadataTests extends TestSuite { os.mtime(wd / "File.txt") ==> 70000 os.mtime(wd / "misc/file-symlink") ==> 70000 assert(os.mtime(wd / "misc/file-symlink", followLinks = false) != 40000) + } + test("checker") - prepChecker { wd => + val before = os.mtime(rd / "File.txt") + intercept[WriteDenied] { + os.mtime.set(rd / "File.txt", 0) + } + os.mtime(rd / "File.txt") ==> before + os.mtime.set(wd / "File.txt", 0) + os.mtime(wd / "File.txt") ==> 0 + + os.mtime.set(wd / "File.txt", 90000) + os.mtime(wd / "File.txt") ==> 90000 + os.mtime(wd / "misc/file-symlink") ==> 90000 + + os.mtime.set(wd / "misc/file-symlink", 70000) + os.mtime(wd / "File.txt") ==> 70000 + os.mtime(wd / "misc/file-symlink") ==> 70000 + assert(os.mtime(wd / "misc/file-symlink", followLinks = false) != 40000) } } } diff --git a/os/test/src/FilesystemPermissionsTests.scala b/os/test/src/FilesystemPermissionsTests.scala index 3b6c8439..3ac2e3cb 100644 --- a/os/test/src/FilesystemPermissionsTests.scala +++ b/os/test/src/FilesystemPermissionsTests.scala @@ -1,10 +1,13 @@ package test.os -import test.os.TestUtil.prep +import test.os.TestUtil._ import utest._ object FilesystemPermissionsTests extends TestSuite { def tests = Tests { + // restricted directory + val rd = os.pwd / "os/test/resources/restricted" + test("perms") { test - prep { wd => if (Unix()) { @@ -15,6 +18,25 @@ object FilesystemPermissionsTests extends TestSuite { os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x" + os.perms.set(wd / "File.txt", "r-xr-xr-x") + os.perms.set(wd / "File.txt", Integer.parseInt("555", 8)) + } + } + test("checker") - prepChecker { wd => + if (Unix()) { + val before = os.perms(rd / "File.txt") + intercept[WriteDenied] { + os.perms.set(rd / "File.txt", "rwxrwxrwx") + } + os.perms(rd / "File.txt") ==> before + + os.perms.set(wd / "File.txt", "rwxrwxrwx") + os.perms(wd / "File.txt").toString() ==> "rwxrwxrwx" + os.perms(wd / "File.txt").toInt() ==> Integer.parseInt("777", 8) + + os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) + os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x" + os.perms.set(wd / "File.txt", "r-xr-xr-x") os.perms.set(wd / "File.txt", Integer.parseInt("555", 8)) } @@ -34,12 +56,14 @@ object FilesystemPermissionsTests extends TestSuite { } } } - } - test("group") { - test - prep { wd => + test("checker") - prepChecker { wd => if (Unix()) { // Only works as root :( if (false) { + intercept[WriteDenied] { + os.owner.set(rd / "File.txt", "nobody") + } + val originalOwner = os.owner(wd / "File.txt") os.owner.set(wd / "File.txt", "nobody") @@ -50,5 +74,37 @@ object FilesystemPermissionsTests extends TestSuite { } } } + test("group") { + test - prep { wd => + if (Unix()) { + // Only works as root :( + if (false) { + val originalGroup = os.group(wd / "File.txt") + + os.group.set(wd / "File.txt", "nobody") + os.group(wd / "File.txt").getName ==> "nobody" + + os.group.set(wd / "File.txt", originalGroup) + } + } + } + test("checker") - prepChecker { wd => + if (Unix()) { + // Only works as root :( + if (false) { + intercept[WriteDenied] { + os.group.set(rd / "File.txt", "nobody") + } + + val originalGroup = os.group(wd / "File.txt") + + os.group.set(wd / "File.txt", "nobody") + os.group(wd / "File.txt").getName ==> "nobody" + + os.group.set(wd / "File.txt", originalGroup) + } + } + } + } } } diff --git a/os/test/src/ManipulatingFilesFoldersTests.scala b/os/test/src/ManipulatingFilesFoldersTests.scala index 60627a2d..b378f92a 100644 --- a/os/test/src/ManipulatingFilesFoldersTests.scala +++ b/os/test/src/ManipulatingFilesFoldersTests.scala @@ -1,10 +1,13 @@ package test.os -import test.os.TestUtil.prep +import test.os.TestUtil._ import utest._ object ManipulatingFilesFoldersTests extends TestSuite { def tests = Tests { + // restricted directory + val rd = os.pwd / "os/test/resources/restricted" + test("exists") { test - prep { wd => os.exists(wd / "File.txt") ==> true @@ -35,6 +38,45 @@ object ManipulatingFilesFoldersTests extends TestSuite { |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } + test("checker") - prepChecker { wd => + intercept[ReadDenied] { + os.move(rd / "folder1/one.txt", wd / "folder1/File.txt") + } + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + + os.checker.withValue(AccessChecker(Seq(rd / "folder1/one.txt"), Seq(wd))) { + intercept[WriteDenied] { // no write access on parent folder (rd / "folder1") + os.move(rd / "folder1/one.txt", wd / "folder1/File.txt") + } + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + intercept[WriteDenied] { + os.move(wd / "folder1/one.txt", rd / "folder1/File.txt") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + intercept[WriteDenied] { + os.move(wd / "folder2/nestedA", rd / "folder2/nestedC") + } + os.list(rd / "folder2") ==> Seq(rd / "folder2/nestedA", rd / "folder2/nestedB") + + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + os.move(wd / "folder1/one.txt", wd / "folder1/first.txt") + os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt") + + os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") + os.move(wd / "folder2/nestedA", wd / "folder2/nestedC") + os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedB", wd / "folder2/nestedC") + + os.read(wd / "File.txt") ==> "I am cow" + os.move(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) + os.read(wd / "File.txt") ==> + """I am cow + |Hear me moo + |I weigh twice as much as you + |And I look good on the barbecue""".stripMargin + } test("matching") { test - prep { wd => import os.{GlobSyntax, /} @@ -92,6 +134,42 @@ object ManipulatingFilesFoldersTests extends TestSuite { |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } + test("checker") - prepChecker { wd => + intercept[ReadDenied] { + os.copy(rd / "folder1/one.txt", wd / "folder1/File.txt") + } + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + + intercept[WriteDenied] { + os.copy(wd / "folder1/one.txt", rd / "folder1/File.txt") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + intercept[WriteDenied] { + os.copy(wd / "folder2/nestedA", rd / "folder2/nestedC") + } + os.list(rd / "folder2") ==> Seq(rd / "folder2/nestedA", rd / "folder2/nestedB") + + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + os.copy(wd / "folder1/one.txt", wd / "folder1/first.txt") + os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt", wd / "folder1/one.txt") + + os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") + os.copy(wd / "folder2/nestedA", wd / "folder2/nestedC") + os.list(wd / "folder2") ==> Seq( + wd / "folder2/nestedA", + wd / "folder2/nestedB", + wd / "folder2/nestedC" + ) + + os.read(wd / "File.txt") ==> "I am cow" + os.copy(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) + os.read(wd / "File.txt") ==> + """I am cow + |Hear me moo + |I weigh twice as much as you + |And I look good on the barbecue""".stripMargin + } test("into") { test - prep { wd => os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") @@ -155,12 +233,32 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.makeDir(wd / "new_folder") os.exists(wd / "new_folder") ==> true } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.makeDir(rd / "new_folder") + } + os.exists(rd / "new_folder") ==> false + + os.exists(wd / "new_folder") ==> false + os.makeDir(wd / "new_folder") + os.exists(wd / "new_folder") ==> true + } test("all") { test - prep { wd => os.exists(wd / "new_folder") ==> false os.makeDir.all(wd / "new_folder/inner/deep") os.exists(wd / "new_folder/inner/deep") ==> true } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.makeDir.all(rd / "new_folder/inner/deep") + } + os.exists(rd / "new_folder") ==> false + + os.exists(wd / "new_folder") ==> false + os.makeDir.all(wd / "new_folder/inner/deep") + os.exists(wd / "new_folder/inner/deep") ==> true + } } } test("remove") { @@ -175,6 +273,35 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1/one.txt") ==> false os.exists(wd / "folder1") ==> false } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.remove(rd / "File.txt") + } + os.exists(rd / "File.txt") ==> true + + intercept[WriteDenied] { + os.remove(rd / "folder1") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + Unchecked.scope(os.makeDir(rd / "folder"), os.remove(rd / "folder")) { + intercept[WriteDenied] { + os.remove(rd / "folder") + } + os.exists(rd / "folder") ==> true + } + os.exists(rd / "folder") ==> false + + os.exists(wd / "File.txt") ==> true + os.remove(wd / "File.txt") + os.exists(wd / "File.txt") ==> false + + os.exists(wd / "folder1/one.txt") ==> true + os.remove(wd / "folder1/one.txt") + os.remove(wd / "folder1") + os.exists(wd / "folder1/one.txt") ==> false + os.exists(wd / "folder1") ==> false + } test("link") { test - prep { wd => os.remove(wd / "misc/file-symlink") @@ -186,6 +313,35 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1", followLinks = false) ==> true os.exists(wd / "folder1/one.txt", followLinks = false) ==> true + os.remove(wd / "misc/broken-symlink") + os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false + } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.remove(rd / "misc/file-symlink") + } + os.exists(rd / "misc/file-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove(rd / "misc/folder-symlink") + } + os.exists(rd / "misc/folder-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove(rd / "misc/broken-symlink") + } + os.exists(rd / "misc/broken-symlink", followLinks = false) ==> true + os.exists(rd / "misc/broken-symlink") ==> true + + os.remove(wd / "misc/file-symlink") + os.exists(wd / "misc/file-symlink", followLinks = false) ==> false + os.exists(wd / "File.txt", followLinks = false) ==> true + + os.remove(wd / "misc/folder-symlink") + os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false + os.exists(wd / "folder1", followLinks = false) ==> true + os.exists(wd / "folder1/one.txt", followLinks = false) ==> true + os.remove(wd / "misc/broken-symlink") os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false } @@ -197,6 +353,17 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1/one.txt") ==> false os.exists(wd / "folder1") ==> false } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.remove.all(rd / "folder1") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + os.exists(wd / "folder1/one.txt") ==> true + os.remove.all(wd / "folder1") + os.exists(wd / "folder1/one.txt") ==> false + os.exists(wd / "folder1") ==> false + } test("link") { test - prep { wd => os.remove.all(wd / "misc/file-symlink") @@ -208,6 +375,33 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1", followLinks = false) ==> true os.exists(wd / "folder1/one.txt", followLinks = false) ==> true + os.remove.all(wd / "misc/broken-symlink") + os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false + } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.remove.all(rd / "misc/file-symlink") + } + os.exists(rd / "misc/file-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove.all(rd / "misc/folder-symlink") + } + os.exists(rd / "misc/folder-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove.all(rd / "misc/broken-symlink") + } + os.exists(rd / "misc/broken-symlink", followLinks = false) ==> true + + os.remove.all(wd / "misc/file-symlink") + os.exists(wd / "misc/file-symlink", followLinks = false) ==> false + + os.remove.all(wd / "misc/folder-symlink") + os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false + os.exists(wd / "folder1", followLinks = false) ==> true + os.exists(wd / "folder1/one.txt", followLinks = false) ==> true + os.remove.all(wd / "misc/broken-symlink") os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false } @@ -221,6 +415,22 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.read(wd / "Linked.txt") ==> "I am cow" os.isLink(wd / "Linked.txt") ==> false } + test("checker") - prepChecker { wd => + intercept[ReadDenied] { + os.hardlink(wd / "Linked.txt", rd / "File.txt") + } + os.exists(wd / "Linked.txt") ==> false + + intercept[WriteDenied] { + os.hardlink(rd / "Linked.txt", wd / "File.txt") + } + os.exists(rd / "Linked.txt") ==> false + + os.hardlink(wd / "Linked.txt", wd / "File.txt") + os.exists(wd / "Linked.txt") + os.read(wd / "Linked.txt") ==> "I am cow" + os.isLink(wd / "Linked.txt") ==> false + } } test("symlink") { test - prep { wd => @@ -234,6 +444,43 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.read(wd / "Linked2.txt") ==> "I am cow" os.isLink(wd / "Linked2.txt") ==> true } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.symlink(rd / "Linked.txt", wd / "File.txt") + } + os.exists(rd / "Linked.txt") ==> false + + intercept[WriteDenied] { + os.symlink(rd / "Linked.txt", os.rel / "File.txt") + } + os.exists(rd / "Linked.txt") ==> false + + intercept[WriteDenied] { + os.symlink(rd / "LinkedFolder1", wd / "folder1") + } + os.exists(rd / "LinkedFolder1") ==> false + + intercept[WriteDenied] { + os.symlink(rd / "LinkedFolder2", os.rel / "folder1") + } + os.exists(rd / "LinkedFolder2") ==> false + + os.symlink(wd / "Linked.txt", wd / "File.txt") + os.read(wd / "Linked.txt") ==> "I am cow" + os.isLink(wd / "Linked.txt") ==> true + + os.symlink(wd / "Linked2.txt", os.rel / "File.txt") + os.read(wd / "Linked2.txt") ==> "I am cow" + os.isLink(wd / "Linked2.txt") ==> true + + os.symlink(wd / "LinkedFolder1", wd / "folder1") + os.walk(wd / "LinkedFolder1", followLinks = true) ==> Seq(wd / "LinkedFolder1/one.txt") + os.isLink(wd / "LinkedFolder1") ==> true + + os.symlink(wd / "LinkedFolder2", os.rel / "folder1") + os.walk(wd / "LinkedFolder2", followLinks = true) ==> Seq(wd / "LinkedFolder2/one.txt") + os.isLink(wd / "LinkedFolder2") ==> true + } } test("followLink") { test - prep { wd => @@ -264,6 +511,18 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.write.over(tempOne, "Hello") os.read(tempOne) ==> "Hello" } + test("checker") - prepChecker { wd => + val before = os.walk(rd) + intercept[WriteDenied] { + os.temp("default content", dir = rd) + } + os.walk(rd) ==> before + + val tempOne = os.temp("default content", dir = wd) + os.read(tempOne) ==> "default content" + os.write.over(tempOne, "Hello") + os.read(tempOne) ==> "Hello" + } test("dir") { test - prep { wd => val tempDir = os.temp.dir() @@ -271,6 +530,18 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.write(tempDir / "file", "Hello") os.list(tempDir) ==> Seq(tempDir / "file") } + test("checker") - prepChecker { wd => + val before = os.walk(rd) + intercept[WriteDenied] { + os.temp.dir(dir = rd) + } + os.walk(rd) ==> before + + val tempDir = os.temp.dir(dir = wd) + os.list(tempDir) ==> Nil + os.write(tempDir / "file", "Hello") + os.list(tempDir) ==> Seq(tempDir / "file") + } } } } diff --git a/os/test/src/ReadingWritingTests.scala b/os/test/src/ReadingWritingTests.scala index 69910b98..53acfff4 100644 --- a/os/test/src/ReadingWritingTests.scala +++ b/os/test/src/ReadingWritingTests.scala @@ -3,6 +3,9 @@ import utest._ import TestUtil._ object ReadingWritingTests extends TestSuite { def tests = Tests { + // restricted directory + val rd = os.pwd / "os/test/resources/restricted" + test("read") { test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" @@ -28,6 +31,24 @@ object ReadingWritingTests extends TestSuite { is.close() } } + test("checker") - prepChecker { wd => + os.exists(rd / "File.txt") ==> true + intercept[ReadDenied] { + os.read.inputStream(rd / "File.txt") + } + + val is = os.read.inputStream(wd / "File.txt") // ==> "I am cow" + is.read() ==> 'I' + is.read() ==> ' ' + is.read() ==> 'a' + is.read() ==> 'm' + is.read() ==> ' ' + is.read() ==> 'c' + is.read() ==> 'o' + is.read() ==> 'w' + is.read() ==> -1 + is.close() + } test("bytes") { test - prep { wd => os.read.bytes(wd / "File.txt") ==> "I am cow".getBytes @@ -81,6 +102,18 @@ object ReadingWritingTests extends TestSuite { os.write(wd / "NewBinary.bin", Array[Byte](0, 1, 2, 3)) os.read.bytes(wd / "NewBinary.bin") ==> Array[Byte](0, 1, 2, 3) } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.write(rd / "New File.txt", "New File Contents") + } + os.exists(rd / "New File.txt") ==> false + + os.write(wd / "New File.txt", "New File Contents") + os.read(wd / "New File.txt") ==> "New File Contents" + + os.write(wd / "NewBinary.bin", Array[Byte](0, 1, 2, 3)) + os.read.bytes(wd / "NewBinary.bin") ==> Array[Byte](0, 1, 2, 3) + } test("append") { test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" @@ -111,7 +144,7 @@ object ReadingWritingTests extends TestSuite { os.read(wd / "File.txt") ==> "We are sow" } } - test("inputStream") { + test("outputStream") { test - prep { wd => val out = os.write.outputStream(wd / "New File.txt") out.write('H') @@ -121,6 +154,22 @@ object ReadingWritingTests extends TestSuite { out.write('o') out.close() + os.read(wd / "New File.txt") ==> "Hello" + } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.write.outputStream(rd / "New File.txt") + } + os.exists(rd / "New File.txt") ==> false + + val out = os.write.outputStream(wd / "New File.txt") + out.write('H') + out.write('e') + out.write('l') + out.write('l') + out.write('o') + out.close() + os.read(wd / "New File.txt") ==> "Hello" } } @@ -129,6 +178,17 @@ object ReadingWritingTests extends TestSuite { test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" + os.truncate(wd / "File.txt", 4) + os.read(wd / "File.txt") ==> "I am" + } + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.truncate(rd / "File.txt", 4) + } + Unchecked(os.read(rd / "File.txt")) ==> "I am a restricted cow" + + os.read(wd / "File.txt") ==> "I am cow" + os.truncate(wd / "File.txt", 4) os.read(wd / "File.txt") ==> "I am" } diff --git a/os/test/src/TestUtil.scala b/os/test/src/TestUtil.scala index 49eec2f7..bee1aaa6 100644 --- a/os/test/src/TestUtil.scala +++ b/os/test/src/TestUtil.scala @@ -76,6 +76,59 @@ object TestUtil { f(os.Path(directory.toAbsolutePath)) } + def prepChecker[T](f: os.Path => T)(implicit tp: TestPath, fn: sourcecode.FullName): T = + prep(wd => os.checker.withValue(AccessChecker(wd))(f(wd))) + + object AccessChecker { + def apply(roots: os.Path*): AccessChecker = AccessChecker(roots, roots) + } + + case class AccessChecker(readRoots: Seq[os.Path], writeRoots: Seq[os.Path]) extends os.Checker { + + def onRead(path: os.ReadablePath): Unit = { + path match { + case path: os.Path => + if (!readRoots.exists(path.startsWith)) throw ReadDenied(path, readRoots) + case _ => + } + } + + def onWrite(path: os.Path): Unit = { + // skip check when not writing to filesystem (like when writing to a zip file) + if (path.wrapped.getFileSystem.provider().getScheme == "file") { + if (!writeRoots.exists(path.startsWith)) throw WriteDenied(path, writeRoots) + } + } + } + + object Unchecked { + + def apply[T](thunk: => T): T = + os.checker.withValue(os.Checker.Nop)(thunk) + + def scope[T](acquire: => Unit, release: => Unit)(thunk: => T): T = { + apply(acquire) + try thunk + finally apply(release) + } + } + + case class ReadDenied(requested: os.Path, allowed: Seq[os.Path]) + extends Exception( + s"Cannot read from $requested. Read is ${ + if (allowed.isEmpty) "not permitted" + else s"restricted to ${allowed.mkString(", ")}" + }." + ) + + case class WriteDenied(requested: os.Path, allowed: Seq[os.Path]) + extends Exception( + s"Cannot write to $requested. Write is ${ + if (allowed.isEmpty) "not permitted" + else s"restricted to ${allowed.mkString(", ")}" + }." + ) + lazy val isDotty = { val cl: ClassLoader = Thread.currentThread().getContextClassLoader try { diff --git a/os/test/src/ZipOpTests.scala b/os/test/src/ZipOpTests.scala index 3d41bdb6..5ce6594d 100644 --- a/os/test/src/ZipOpTests.scala +++ b/os/test/src/ZipOpTests.scala @@ -1,7 +1,7 @@ package test.os import os.zip -import test.os.TestUtil.prep +import test.os.TestUtil._ import utest._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} @@ -10,6 +10,47 @@ import java.util.zip.{ZipEntry, ZipOutputStream} object ZipOpTests extends TestSuite { def tests = Tests { + // restricted directory + val rd = os.pwd / "os/test/resources/restricted" + + test("checker") - prepChecker { wd => + intercept[WriteDenied] { + os.zip( + dest = rd / "zipped.zip", + sources = Seq( + wd / "File.txt", + wd / "folder1" + ) + ) + } + os.exists(rd / "zipped.zip") ==> false + + intercept[ReadDenied] { + os.zip( + dest = wd / "zipped.zip", + sources = Seq( + wd / "File.txt", + rd / "folder1" + ) + ) + } + os.exists(wd / "zipped.zip") ==> false + + val zipFile = os.zip( + wd / "zipped.zip", + Seq( + wd / "File.txt", + wd / "folder1" + ) + ) + + val unzipDir = os.unzip(zipFile, wd / "unzipped") + os.walk(unzipDir).sorted ==> Seq( + unzipDir / "File.txt", + unzipDir / "one.txt" + ) + } + test("level") - prep { wd => val zipsForLevel = for (i <- Range.inclusive(0, 9)) yield { os.write.over(wd / "File.txt", Range(0, 1000).map(x => x.toString * x)) @@ -127,6 +168,30 @@ object ZipOpTests extends TestSuite { assert(paths == Seq(unzippedFolder / "File.txt")) } + test("unzipChecker") - prepChecker { wd => + val zipFileName = "zipped.zip" + val zipFile: os.Path = os.zip( + dest = wd / zipFileName, + sources = Seq( + wd / "File.txt", + wd / "folder1" + ) + ) + + intercept[WriteDenied] { + os.unzip( + source = zipFile, + dest = rd / "unzipped" + ) + } + os.exists(rd / "unzipped") ==> false + + val unzipDir = os.unzip( + source = zipFile, + dest = wd / "unzipped" + ) + os.walk(unzipDir).length ==> 2 + } test("list") - prep { wd => // Zipping files and folders in a new zip file val zipFileName = "listContentsOfZipFileWithoutExtracting.zip" From 8c020445a67dc02e908a2bf5f0ad3aeae4e5b907 Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Fri, 25 Oct 2024 16:50:15 +0530 Subject: [PATCH 2/9] Added experimental annotation and updated checks in move --- build.sc | 3 +++ os/src/FileOps.scala | 12 ++++++++++-- os/src/Model.scala | 6 ++++-- os/src/PermsOps.scala | 3 +++ os/src/ReadWriteOps.scala | 6 ++++++ os/src/StatOps.scala | 1 + os/src/TempOps.scala | 3 ++- os/src/ZipOps.scala | 2 ++ os/src/experimental.scala | 8 ++++++++ os/test/src/ManipulatingFilesFoldersTests.scala | 2 +- 10 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 os/src/experimental.scala diff --git a/build.sc b/build.sc index 734c2416..08def76e 100644 --- a/build.sc +++ b/build.sc @@ -72,6 +72,9 @@ trait MiMaChecks extends Mima { // this is fine, because ProcessLike is sealed (and its subclasses should be final) ProblemFilter.exclude[ReversedMissingMethodProblem]("os.ProcessLike.joinPumperThreadsHook") ) + override def mimaExcludeAnnotations: T[Seq[String]] = Seq( + "os.experimental" + ) } trait OsLibModule diff --git a/os/src/FileOps.scala b/os/src/FileOps.scala index c276c64d..40f41f77 100644 --- a/os/src/FileOps.scala +++ b/os/src/FileOps.scala @@ -22,10 +22,12 @@ import scala.util.Try * ignore the destination if it already exists, using [[os.makeDir.all]] */ object makeDir extends Function1[Path, Unit] { + @experimental def apply(path: Path): Unit = { checker.value.onWrite(path) Files.createDirectory(path.wrapped) } + @experimental def apply(path: Path, perms: PermSet): Unit = { checker.value.onWrite(path) Files.createDirectory( @@ -41,6 +43,7 @@ object makeDir extends Function1[Path, Unit] { */ object all extends Function1[Path, Unit] { def apply(path: Path): Unit = apply(path, null, true) + @experimental def apply(path: Path, perms: PermSet = null, acceptLinkedDirectory: Boolean = true): Unit = { checker.value.onWrite(path) // We special case calling makeDir.all on a symlink to a directory; @@ -82,6 +85,7 @@ object move { def matching(partialFunction: PartialFunction[Path, Path]): PartialFunction[Path, Unit] = { matching()(partialFunction) } + @experimental def apply( from: Path, to: Path, @@ -89,8 +93,7 @@ object move { atomicMove: Boolean = false, createFolders: Boolean = false ): Unit = { - checker.value.onRead(from) - checker.value.onWrite(from / RelPath.up) + checker.value.onWrite(from) checker.value.onWrite(to) if (createFolders && to.segmentCount != 0) makeDir.all(to / up) val opts1 = @@ -175,6 +178,7 @@ object copy { matching()(partialFunction) } + @experimental def apply( from: Path, to: Path, @@ -319,6 +323,7 @@ object copy { */ object remove extends Function1[Path, Boolean] { def apply(target: Path): Boolean = apply(target, false) + @experimental def apply(target: Path, checkExists: Boolean = false): Boolean = { checker.value.onWrite(target) if (checkExists) { @@ -330,6 +335,7 @@ object remove extends Function1[Path, Boolean] { } object all extends Function1[Path, Unit] { + @experimental def apply(target: Path) = { require(target.segmentCount != 0, s"Cannot remove a root directory: $target") checker.value.onWrite(target) @@ -360,6 +366,7 @@ object exists extends Function1[Path, Boolean] { * Creates a hardlink between two paths */ object hardlink { + @experimental def apply(link: Path, dest: Path) = { checker.value.onWrite(link) checker.value.onRead(dest) @@ -371,6 +378,7 @@ object hardlink { * Creates a symbolic link between two paths */ object symlink { + @experimental def apply(link: Path, dest: FilePath, perms: PermSet = null): Unit = { checker.value.onWrite(link) checker.value.onRead(dest match { diff --git a/os/src/Model.scala b/os/src/Model.scala index cbc2d6b6..f5095f5a 100644 --- a/os/src/Model.scala +++ b/os/src/Model.scala @@ -288,9 +288,10 @@ object PosixStatInfo { * Defines hooks for path based operations. * * This, in conjunction with [[checker]], can be used to implement custom checks like - * - restricting operations to some path(s) - * - logging operations + * - restricting an operation to some path(s) + * - logging an operation */ +@experimental trait Checker { /** A hook for a read operation on `path`. */ @@ -300,6 +301,7 @@ trait Checker { def onWrite(path: Path): Unit } +@experimental object Checker { /** A no-op [[Checker]]. */ diff --git a/os/src/PermsOps.scala b/os/src/PermsOps.scala index 73ef6d19..df99fa29 100644 --- a/os/src/PermsOps.scala +++ b/os/src/PermsOps.scala @@ -23,6 +23,7 @@ object perms extends Function1[Path, PermSet] { * default set of permissions and having `os.perms.set` over-write them later */ object set { + @experimental def apply(p: Path, arg2: PermSet): Unit = { checker.value.onWrite(p) Files.setPosixFilePermissions(p.wrapped, arg2.toSet()) @@ -45,6 +46,7 @@ object owner extends Function1[Path, UserPrincipal] { * Set the owner of the file/folder at the given path */ object set { + @experimental def apply(arg1: Path, arg2: UserPrincipal): Unit = { checker.value.onWrite(arg1) Files.setOwner(arg1.wrapped, arg2) @@ -76,6 +78,7 @@ object group extends Function1[Path, GroupPrincipal] { * Set the owning group of the file/folder at the given path */ object set { + @experimental def apply(arg1: Path, arg2: GroupPrincipal): Unit = { checker.value.onWrite(arg1) Files.getFileAttributeView( diff --git a/os/src/ReadWriteOps.scala b/os/src/ReadWriteOps.scala index 62b8c0be..b8584723 100644 --- a/os/src/ReadWriteOps.scala +++ b/os/src/ReadWriteOps.scala @@ -21,6 +21,7 @@ object write { /** * Open a [[java.io.OutputStream]] to write to the given file */ + @experimental def outputStream( target: Path, perms: PermSet = null, @@ -47,6 +48,7 @@ object write { * from `java.nio.file.Files.write` so we could re-use it properly for * different combinations of flags and all sorts of [[Source]]s */ + @experimental def write( target: Path, data: Source, @@ -167,6 +169,7 @@ object write { * Opens a [[SeekableByteChannel]] to write to the given file. */ object channel extends Function1[Path, SeekableByteChannel] { + @experimental def write(p: Path, options: Seq[StandardOpenOption]) = { checker.value.onWrite(p) java.nio.file.Files.newByteChannel(p.toNIO, options.toArray: _*) @@ -214,6 +217,7 @@ object write { * given size, does nothing. */ object truncate { + @experimental def apply(p: Path, size: Long): Unit = { checker.value.onWrite(p) val channel = FileChannel.open(p.toNIO, StandardOpenOption.WRITE) @@ -246,6 +250,7 @@ object read extends Function1[ReadablePath, String] { * Opens a [[java.io.InputStream]] to read from the given file */ object inputStream extends Function1[ReadablePath, java.io.InputStream] { + @experimental def apply(p: ReadablePath): java.io.InputStream = { checker.value.onRead(p) p.getInputStream @@ -269,6 +274,7 @@ object read extends Function1[ReadablePath, String] { * Opens a [[SeekableByteChannel]] to read from the given file. */ object channel extends Function1[Path, SeekableByteChannel] { + @experimental def apply(p: Path): SeekableByteChannel = { checker.value.onRead(p) p.toSource.getChannel() diff --git a/os/src/StatOps.scala b/os/src/StatOps.scala index cf8695ef..856051f0 100644 --- a/os/src/StatOps.scala +++ b/os/src/StatOps.scala @@ -73,6 +73,7 @@ object mtime extends Function1[Path, Long] { * https://stackoverflow.com/questions/17308363/symlink-lastmodifiedtime-in-java-1-7 */ object set { + @experimental def apply(p: Path, millis: Long) = { checker.value.onWrite(p) Files.setLastModifiedTime(p.wrapped, FileTime.fromMillis(millis)) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 3c70c716..1d7cba43 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -20,6 +20,7 @@ object temp { * By default, temporary files are deleted on JVM exit. You can disable that * behavior by setting `deleteOnExit = false` */ + @experimental def apply( contents: Source = null, dir: Path = null, @@ -28,7 +29,6 @@ object temp { deleteOnExit: Boolean = true, perms: PermSet = null ): Path = { - import collection.JavaConverters._ val permArray: Array[FileAttribute[_]] = if (perms == null) Array.empty else Array(PosixFilePermissions.asFileAttribute(perms.toSet())) @@ -53,6 +53,7 @@ object temp { * By default, temporary directories are deleted on JVM exit. You can disable that * behavior by setting `deleteOnExit = false` */ + @experimental def dir( dir: Path = null, prefix: String = null, diff --git a/os/src/ZipOps.scala b/os/src/ZipOps.scala index a62d7d49..da15e909 100644 --- a/os/src/ZipOps.scala +++ b/os/src/ZipOps.scala @@ -36,6 +36,7 @@ object zip { * @param compressionLevel number from 0-9, where 0 is no compression and 9 is best compression. Defaults to -1 (default compression) * @return The path to the created ZIP archive. */ + @experimental def apply( dest: os.Path, sources: Seq[ZipSource] = List(), @@ -265,6 +266,7 @@ object unzip { * @param dest The path to the destination directory for extracted files. * @param excludePatterns A list of regular expression patterns to exclude files during extraction. (Optional) */ + @experimental def stream( source: geny.Readable, dest: os.Path, diff --git a/os/src/experimental.scala b/os/src/experimental.scala new file mode 100644 index 00000000..3294cfd2 --- /dev/null +++ b/os/src/experimental.scala @@ -0,0 +1,8 @@ +package os + +import scala.annotation.StaticAnnotation + +/** + * Annotation to mark experimental API, which is not guaranteed to stay. + */ +class experimental extends StaticAnnotation {} diff --git a/os/test/src/ManipulatingFilesFoldersTests.scala b/os/test/src/ManipulatingFilesFoldersTests.scala index b378f92a..fc664fae 100644 --- a/os/test/src/ManipulatingFilesFoldersTests.scala +++ b/os/test/src/ManipulatingFilesFoldersTests.scala @@ -39,7 +39,7 @@ object ManipulatingFilesFoldersTests extends TestSuite { |And I look good on the barbecue""".stripMargin } test("checker") - prepChecker { wd => - intercept[ReadDenied] { + intercept[WriteDenied] { os.move(rd / "folder1/one.txt", wd / "folder1/File.txt") } os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") From 169f66af484bd4314edce34a7a07ca5b790789dc Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Fri, 25 Oct 2024 17:08:15 +0530 Subject: [PATCH 3/9] Updated path to resources in tests --- os/test/src/FilesystemMetadataTests.scala | 2 +- os/test/src/FilesystemPermissionsTests.scala | 2 +- os/test/src/ManipulatingFilesFoldersTests.scala | 2 +- os/test/src/ReadingWritingTests.scala | 2 +- os/test/src/ZipOpTests.scala | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/os/test/src/FilesystemMetadataTests.scala b/os/test/src/FilesystemMetadataTests.scala index debe2ce4..c5b3af61 100644 --- a/os/test/src/FilesystemMetadataTests.scala +++ b/os/test/src/FilesystemMetadataTests.scala @@ -10,7 +10,7 @@ object FilesystemMetadataTests extends TestSuite { def tests = Tests { // restricted directory - val rd = os.pwd / "os/test/resources/restricted" + val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" test("stat") { test - prep { wd => diff --git a/os/test/src/FilesystemPermissionsTests.scala b/os/test/src/FilesystemPermissionsTests.scala index 3ac2e3cb..7e77cc7d 100644 --- a/os/test/src/FilesystemPermissionsTests.scala +++ b/os/test/src/FilesystemPermissionsTests.scala @@ -6,7 +6,7 @@ import utest._ object FilesystemPermissionsTests extends TestSuite { def tests = Tests { // restricted directory - val rd = os.pwd / "os/test/resources/restricted" + val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" test("perms") { test - prep { wd => diff --git a/os/test/src/ManipulatingFilesFoldersTests.scala b/os/test/src/ManipulatingFilesFoldersTests.scala index fc664fae..b2dd418c 100644 --- a/os/test/src/ManipulatingFilesFoldersTests.scala +++ b/os/test/src/ManipulatingFilesFoldersTests.scala @@ -6,7 +6,7 @@ import utest._ object ManipulatingFilesFoldersTests extends TestSuite { def tests = Tests { // restricted directory - val rd = os.pwd / "os/test/resources/restricted" + val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" test("exists") { test - prep { wd => diff --git a/os/test/src/ReadingWritingTests.scala b/os/test/src/ReadingWritingTests.scala index 53acfff4..5bf46bc8 100644 --- a/os/test/src/ReadingWritingTests.scala +++ b/os/test/src/ReadingWritingTests.scala @@ -4,7 +4,7 @@ import TestUtil._ object ReadingWritingTests extends TestSuite { def tests = Tests { // restricted directory - val rd = os.pwd / "os/test/resources/restricted" + val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" test("read") { test - prep { wd => diff --git a/os/test/src/ZipOpTests.scala b/os/test/src/ZipOpTests.scala index 5ce6594d..f5e1a3ae 100644 --- a/os/test/src/ZipOpTests.scala +++ b/os/test/src/ZipOpTests.scala @@ -11,7 +11,7 @@ object ZipOpTests extends TestSuite { def tests = Tests { // restricted directory - val rd = os.pwd / "os/test/resources/restricted" + val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" test("checker") - prepChecker { wd => intercept[WriteDenied] { From 95fe56a4694fc910976d91c9ed080ba8f7986fa1 Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Fri, 25 Oct 2024 17:18:58 +0530 Subject: [PATCH 4/9] Removed experimental annotation from operations --- os/src/FileOps.scala | 9 --------- os/src/PermsOps.scala | 3 --- os/src/ReadWriteOps.scala | 6 ------ os/src/StatOps.scala | 1 - os/src/TempOps.scala | 2 -- os/src/ZipOps.scala | 2 -- 6 files changed, 23 deletions(-) diff --git a/os/src/FileOps.scala b/os/src/FileOps.scala index 40f41f77..4fd9becb 100644 --- a/os/src/FileOps.scala +++ b/os/src/FileOps.scala @@ -22,12 +22,10 @@ import scala.util.Try * ignore the destination if it already exists, using [[os.makeDir.all]] */ object makeDir extends Function1[Path, Unit] { - @experimental def apply(path: Path): Unit = { checker.value.onWrite(path) Files.createDirectory(path.wrapped) } - @experimental def apply(path: Path, perms: PermSet): Unit = { checker.value.onWrite(path) Files.createDirectory( @@ -43,7 +41,6 @@ object makeDir extends Function1[Path, Unit] { */ object all extends Function1[Path, Unit] { def apply(path: Path): Unit = apply(path, null, true) - @experimental def apply(path: Path, perms: PermSet = null, acceptLinkedDirectory: Boolean = true): Unit = { checker.value.onWrite(path) // We special case calling makeDir.all on a symlink to a directory; @@ -85,7 +82,6 @@ object move { def matching(partialFunction: PartialFunction[Path, Path]): PartialFunction[Path, Unit] = { matching()(partialFunction) } - @experimental def apply( from: Path, to: Path, @@ -178,7 +174,6 @@ object copy { matching()(partialFunction) } - @experimental def apply( from: Path, to: Path, @@ -323,7 +318,6 @@ object copy { */ object remove extends Function1[Path, Boolean] { def apply(target: Path): Boolean = apply(target, false) - @experimental def apply(target: Path, checkExists: Boolean = false): Boolean = { checker.value.onWrite(target) if (checkExists) { @@ -335,7 +329,6 @@ object remove extends Function1[Path, Boolean] { } object all extends Function1[Path, Unit] { - @experimental def apply(target: Path) = { require(target.segmentCount != 0, s"Cannot remove a root directory: $target") checker.value.onWrite(target) @@ -366,7 +359,6 @@ object exists extends Function1[Path, Boolean] { * Creates a hardlink between two paths */ object hardlink { - @experimental def apply(link: Path, dest: Path) = { checker.value.onWrite(link) checker.value.onRead(dest) @@ -378,7 +370,6 @@ object hardlink { * Creates a symbolic link between two paths */ object symlink { - @experimental def apply(link: Path, dest: FilePath, perms: PermSet = null): Unit = { checker.value.onWrite(link) checker.value.onRead(dest match { diff --git a/os/src/PermsOps.scala b/os/src/PermsOps.scala index df99fa29..73ef6d19 100644 --- a/os/src/PermsOps.scala +++ b/os/src/PermsOps.scala @@ -23,7 +23,6 @@ object perms extends Function1[Path, PermSet] { * default set of permissions and having `os.perms.set` over-write them later */ object set { - @experimental def apply(p: Path, arg2: PermSet): Unit = { checker.value.onWrite(p) Files.setPosixFilePermissions(p.wrapped, arg2.toSet()) @@ -46,7 +45,6 @@ object owner extends Function1[Path, UserPrincipal] { * Set the owner of the file/folder at the given path */ object set { - @experimental def apply(arg1: Path, arg2: UserPrincipal): Unit = { checker.value.onWrite(arg1) Files.setOwner(arg1.wrapped, arg2) @@ -78,7 +76,6 @@ object group extends Function1[Path, GroupPrincipal] { * Set the owning group of the file/folder at the given path */ object set { - @experimental def apply(arg1: Path, arg2: GroupPrincipal): Unit = { checker.value.onWrite(arg1) Files.getFileAttributeView( diff --git a/os/src/ReadWriteOps.scala b/os/src/ReadWriteOps.scala index b8584723..62b8c0be 100644 --- a/os/src/ReadWriteOps.scala +++ b/os/src/ReadWriteOps.scala @@ -21,7 +21,6 @@ object write { /** * Open a [[java.io.OutputStream]] to write to the given file */ - @experimental def outputStream( target: Path, perms: PermSet = null, @@ -48,7 +47,6 @@ object write { * from `java.nio.file.Files.write` so we could re-use it properly for * different combinations of flags and all sorts of [[Source]]s */ - @experimental def write( target: Path, data: Source, @@ -169,7 +167,6 @@ object write { * Opens a [[SeekableByteChannel]] to write to the given file. */ object channel extends Function1[Path, SeekableByteChannel] { - @experimental def write(p: Path, options: Seq[StandardOpenOption]) = { checker.value.onWrite(p) java.nio.file.Files.newByteChannel(p.toNIO, options.toArray: _*) @@ -217,7 +214,6 @@ object write { * given size, does nothing. */ object truncate { - @experimental def apply(p: Path, size: Long): Unit = { checker.value.onWrite(p) val channel = FileChannel.open(p.toNIO, StandardOpenOption.WRITE) @@ -250,7 +246,6 @@ object read extends Function1[ReadablePath, String] { * Opens a [[java.io.InputStream]] to read from the given file */ object inputStream extends Function1[ReadablePath, java.io.InputStream] { - @experimental def apply(p: ReadablePath): java.io.InputStream = { checker.value.onRead(p) p.getInputStream @@ -274,7 +269,6 @@ object read extends Function1[ReadablePath, String] { * Opens a [[SeekableByteChannel]] to read from the given file. */ object channel extends Function1[Path, SeekableByteChannel] { - @experimental def apply(p: Path): SeekableByteChannel = { checker.value.onRead(p) p.toSource.getChannel() diff --git a/os/src/StatOps.scala b/os/src/StatOps.scala index 856051f0..cf8695ef 100644 --- a/os/src/StatOps.scala +++ b/os/src/StatOps.scala @@ -73,7 +73,6 @@ object mtime extends Function1[Path, Long] { * https://stackoverflow.com/questions/17308363/symlink-lastmodifiedtime-in-java-1-7 */ object set { - @experimental def apply(p: Path, millis: Long) = { checker.value.onWrite(p) Files.setLastModifiedTime(p.wrapped, FileTime.fromMillis(millis)) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 1d7cba43..f31f16d7 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -20,7 +20,6 @@ object temp { * By default, temporary files are deleted on JVM exit. You can disable that * behavior by setting `deleteOnExit = false` */ - @experimental def apply( contents: Source = null, dir: Path = null, @@ -53,7 +52,6 @@ object temp { * By default, temporary directories are deleted on JVM exit. You can disable that * behavior by setting `deleteOnExit = false` */ - @experimental def dir( dir: Path = null, prefix: String = null, diff --git a/os/src/ZipOps.scala b/os/src/ZipOps.scala index da15e909..a62d7d49 100644 --- a/os/src/ZipOps.scala +++ b/os/src/ZipOps.scala @@ -36,7 +36,6 @@ object zip { * @param compressionLevel number from 0-9, where 0 is no compression and 9 is best compression. Defaults to -1 (default compression) * @return The path to the created ZIP archive. */ - @experimental def apply( dest: os.Path, sources: Seq[ZipSource] = List(), @@ -266,7 +265,6 @@ object unzip { * @param dest The path to the destination directory for extracted files. * @param excludePatterns A list of regular expression patterns to exclude files during extraction. (Optional) */ - @experimental def stream( source: geny.Readable, dest: os.Path, From b06e184f676cf2db75b252ec14c0af1c1bfb9356 Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Fri, 25 Oct 2024 17:22:48 +0530 Subject: [PATCH 5/9] Check write for destination --- os/src/FileOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os/src/FileOps.scala b/os/src/FileOps.scala index 4fd9becb..9f85a217 100644 --- a/os/src/FileOps.scala +++ b/os/src/FileOps.scala @@ -372,7 +372,7 @@ object hardlink { object symlink { def apply(link: Path, dest: FilePath, perms: PermSet = null): Unit = { checker.value.onWrite(link) - checker.value.onRead(dest match { + checker.value.onWrite(dest match { case p: RelPath => link / RelPath.up / p case p: SubPath => link / RelPath.up / p case p: Path => p From bfb23195b47a47ec954fd2c00f0d7a52b45dd9bb Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Fri, 25 Oct 2024 17:29:40 +0530 Subject: [PATCH 6/9] Check write for destination --- os/src/FileOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os/src/FileOps.scala b/os/src/FileOps.scala index 9f85a217..207aec12 100644 --- a/os/src/FileOps.scala +++ b/os/src/FileOps.scala @@ -361,7 +361,7 @@ object exists extends Function1[Path, Boolean] { object hardlink { def apply(link: Path, dest: Path) = { checker.value.onWrite(link) - checker.value.onRead(dest) + checker.value.onWrite(dest) Files.createLink(link.wrapped, dest.wrapped) } } From 3b4d5047259877d1c852f5dacc22ad8e7fbdbfa8 Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Fri, 25 Oct 2024 18:30:25 +0530 Subject: [PATCH 7/9] Fix hardlink test --- os/test/src/ManipulatingFilesFoldersTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os/test/src/ManipulatingFilesFoldersTests.scala b/os/test/src/ManipulatingFilesFoldersTests.scala index b2dd418c..3176fc2c 100644 --- a/os/test/src/ManipulatingFilesFoldersTests.scala +++ b/os/test/src/ManipulatingFilesFoldersTests.scala @@ -416,7 +416,7 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.isLink(wd / "Linked.txt") ==> false } test("checker") - prepChecker { wd => - intercept[ReadDenied] { + intercept[WriteDenied] { os.hardlink(wd / "Linked.txt", rd / "File.txt") } os.exists(wd / "Linked.txt") ==> false From 9e32e77caea8af5141f008007178ca64d5f87d84 Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Fri, 25 Oct 2024 18:31:03 +0530 Subject: [PATCH 8/9] Added experimental annotation to variable --- os/src-jvm/package.scala | 1 + os/src-native/package.scala | 1 + 2 files changed, 2 insertions(+) diff --git a/os/src-jvm/package.scala b/os/src-jvm/package.scala index 48c2fd39..caa08009 100644 --- a/os/src-jvm/package.scala +++ b/os/src-jvm/package.scala @@ -59,6 +59,7 @@ package object os { val sub: SubPath = SubPath.sub + @experimental val checker: DynamicVariable[Checker] = new DynamicVariable[Checker](Checker.Nop) /** diff --git a/os/src-native/package.scala b/os/src-native/package.scala index 255dd347..f0afa876 100644 --- a/os/src-native/package.scala +++ b/os/src-native/package.scala @@ -52,6 +52,7 @@ package object os { val sub: SubPath = SubPath.sub + @experimental val checker: DynamicVariable[Checker] = new DynamicVariable[Checker](Checker.Nop) /** From 5bb76cc44204866f7507b32acdf07745b696f072 Mon Sep 17 00:00:00 2001 From: Ajay Chandran Date: Sat, 26 Oct 2024 11:48:36 +0530 Subject: [PATCH 9/9] Segregated checker tests --- os/test/src/CheckerTests.scala | 484 ++++++++++++++++++ os/test/src/FilesystemMetadataTests.scala | 23 +- os/test/src/FilesystemPermissionsTests.scala | 58 +-- .../src/ManipulatingFilesFoldersTests.scala | 273 +--------- os/test/src/ReadingWritingTests.scala | 60 --- os/test/src/TestUtil.scala | 1 + os/test/src/ZipOpTests.scala | 67 +-- 7 files changed, 489 insertions(+), 477 deletions(-) create mode 100644 os/test/src/CheckerTests.scala diff --git a/os/test/src/CheckerTests.scala b/os/test/src/CheckerTests.scala new file mode 100644 index 00000000..e992d3bf --- /dev/null +++ b/os/test/src/CheckerTests.scala @@ -0,0 +1,484 @@ +package test.os + +import test.os.TestUtil._ +import utest._ + +object CheckerTests extends TestSuite { + + def tests: Tests = Tests { + // restricted directory + val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" + + test("stat") { + test("mtime") - prepChecker { wd => + val before = os.mtime(rd / "File.txt") + intercept[WriteDenied] { + os.mtime.set(rd / "File.txt", 0) + } + os.mtime(rd / "File.txt") ==> before + + os.mtime.set(wd / "File.txt", 0) + os.mtime(wd / "File.txt") ==> 0 + + os.mtime.set(wd / "File.txt", 90000) + os.mtime(wd / "File.txt") ==> 90000 + os.mtime(wd / "misc/file-symlink") ==> 90000 + + os.mtime.set(wd / "misc/file-symlink", 70000) + os.mtime(wd / "File.txt") ==> 70000 + os.mtime(wd / "misc/file-symlink") ==> 70000 + assert(os.mtime(wd / "misc/file-symlink", followLinks = false) != 40000) + } + } + + test("perms") { + test - prepChecker { wd => + if (Unix()) { + val before = os.perms(rd / "File.txt") + intercept[WriteDenied] { + os.perms.set(rd / "File.txt", "rwxrwxrwx") + } + os.perms(rd / "File.txt") ==> before + + os.perms.set(wd / "File.txt", "rwxrwxrwx") + os.perms(wd / "File.txt").toString() ==> "rwxrwxrwx" + os.perms(wd / "File.txt").toInt() ==> Integer.parseInt("777", 8) + + os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) + os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x" + + os.perms.set(wd / "File.txt", "r-xr-xr-x") + os.perms.set(wd / "File.txt", Integer.parseInt("555", 8)) + } + } + test("owner") - prepChecker { wd => + if (Unix()) { + // Only works as root :( + if (false) { + intercept[WriteDenied] { + os.owner.set(rd / "File.txt", "nobody") + } + + val originalOwner = os.owner(wd / "File.txt") + + os.owner.set(wd / "File.txt", "nobody") + os.owner(wd / "File.txt").getName ==> "nobody" + + os.owner.set(wd / "File.txt", originalOwner) + } + } + } + test("group") - prepChecker { wd => + if (Unix()) { + // Only works as root :( + if (false) { + intercept[WriteDenied] { + os.group.set(rd / "File.txt", "nobody") + } + + val originalGroup = os.group(wd / "File.txt") + + os.group.set(wd / "File.txt", "nobody") + os.group(wd / "File.txt").getName ==> "nobody" + + os.group.set(wd / "File.txt", originalGroup) + } + } + } + } + + test("move") - prepChecker { wd => + intercept[WriteDenied] { + os.move(rd / "folder1/one.txt", wd / "folder1/File.txt") + } + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + + intercept[WriteDenied] { + os.move(wd / "folder1/one.txt", rd / "folder1/File.txt") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + intercept[WriteDenied] { + os.move(wd / "folder2/nestedA", rd / "folder2/nestedC") + } + os.list(rd / "folder2") ==> Seq(rd / "folder2/nestedA", rd / "folder2/nestedB") + + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + os.move(wd / "folder1/one.txt", wd / "folder1/first.txt") + os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt") + + os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") + os.move(wd / "folder2/nestedA", wd / "folder2/nestedC") + os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedB", wd / "folder2/nestedC") + + os.read(wd / "File.txt") ==> "I am cow" + os.move(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) + os.read(wd / "File.txt") ==> + """I am cow + |Hear me moo + |I weigh twice as much as you + |And I look good on the barbecue""".stripMargin + } + test("copy") - prepChecker { wd => + intercept[ReadDenied] { + os.copy(rd / "folder1/one.txt", wd / "folder1/File.txt") + } + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + + intercept[WriteDenied] { + os.copy(wd / "folder1/one.txt", rd / "folder1/File.txt") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + intercept[WriteDenied] { + os.copy(wd / "folder2/nestedA", rd / "folder2/nestedC") + } + os.list(rd / "folder2") ==> Seq(rd / "folder2/nestedA", rd / "folder2/nestedB") + + os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") + os.copy(wd / "folder1/one.txt", wd / "folder1/first.txt") + os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt", wd / "folder1/one.txt") + + os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") + os.copy(wd / "folder2/nestedA", wd / "folder2/nestedC") + os.list(wd / "folder2") ==> Seq( + wd / "folder2/nestedA", + wd / "folder2/nestedB", + wd / "folder2/nestedC" + ) + + os.read(wd / "File.txt") ==> "I am cow" + os.copy(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) + os.read(wd / "File.txt") ==> + """I am cow + |Hear me moo + |I weigh twice as much as you + |And I look good on the barbecue""".stripMargin + } + test("makeDir") { + test - prepChecker { wd => + intercept[WriteDenied] { + os.makeDir(rd / "new_folder") + } + os.exists(rd / "new_folder") ==> false + + os.exists(wd / "new_folder") ==> false + os.makeDir(wd / "new_folder") + os.exists(wd / "new_folder") ==> true + } + test("all") - prepChecker { wd => + intercept[WriteDenied] { + os.makeDir.all(rd / "new_folder/inner/deep") + } + os.exists(rd / "new_folder") ==> false + + os.exists(wd / "new_folder") ==> false + os.makeDir.all(wd / "new_folder/inner/deep") + os.exists(wd / "new_folder/inner/deep") ==> true + } + } + test("remove") { + test - prepChecker { wd => + intercept[WriteDenied] { + os.remove(rd / "File.txt") + } + os.exists(rd / "File.txt") ==> true + + intercept[WriteDenied] { + os.remove(rd / "folder1") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + Unchecked.scope(os.makeDir(rd / "folder"), os.remove(rd / "folder")) { + intercept[WriteDenied] { + os.remove(rd / "folder") + } + os.exists(rd / "folder") ==> true + } + os.exists(rd / "folder") ==> false + + os.exists(wd / "File.txt") ==> true + os.remove(wd / "File.txt") + os.exists(wd / "File.txt") ==> false + + os.exists(wd / "folder1/one.txt") ==> true + os.remove(wd / "folder1/one.txt") + os.remove(wd / "folder1") + os.exists(wd / "folder1/one.txt") ==> false + os.exists(wd / "folder1") ==> false + } + test("link") - prepChecker { wd => + intercept[WriteDenied] { + os.remove(rd / "misc/file-symlink") + } + os.exists(rd / "misc/file-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove(rd / "misc/folder-symlink") + } + os.exists(rd / "misc/folder-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove(rd / "misc/broken-symlink") + } + os.exists(rd / "misc/broken-symlink", followLinks = false) ==> true + os.exists(rd / "misc/broken-symlink") ==> true + + os.remove(wd / "misc/file-symlink") + os.exists(wd / "misc/file-symlink", followLinks = false) ==> false + os.exists(wd / "File.txt", followLinks = false) ==> true + + os.remove(wd / "misc/folder-symlink") + os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false + os.exists(wd / "folder1", followLinks = false) ==> true + os.exists(wd / "folder1/one.txt", followLinks = false) ==> true + + os.remove(wd / "misc/broken-symlink") + os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false + } + test("all") { + test - prepChecker { wd => + intercept[WriteDenied] { + os.remove.all(rd / "folder1") + } + os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") + + os.exists(wd / "folder1/one.txt") ==> true + os.remove.all(wd / "folder1") + os.exists(wd / "folder1/one.txt") ==> false + os.exists(wd / "folder1") ==> false + } + test("link") - prepChecker { wd => + intercept[WriteDenied] { + os.remove.all(rd / "misc/file-symlink") + } + os.exists(rd / "misc/file-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove.all(rd / "misc/folder-symlink") + } + os.exists(rd / "misc/folder-symlink", followLinks = false) ==> true + + intercept[WriteDenied] { + os.remove.all(rd / "misc/broken-symlink") + } + os.exists(rd / "misc/broken-symlink", followLinks = false) ==> true + + os.remove.all(wd / "misc/file-symlink") + os.exists(wd / "misc/file-symlink", followLinks = false) ==> false + + os.remove.all(wd / "misc/folder-symlink") + os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false + os.exists(wd / "folder1", followLinks = false) ==> true + os.exists(wd / "folder1/one.txt", followLinks = false) ==> true + + os.remove.all(wd / "misc/broken-symlink") + os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false + } + } + } + test("hardlink") - prepChecker { wd => + intercept[WriteDenied] { + os.hardlink(wd / "Linked.txt", rd / "File.txt") + } + os.exists(wd / "Linked.txt") ==> false + + intercept[WriteDenied] { + os.hardlink(rd / "Linked.txt", wd / "File.txt") + } + os.exists(rd / "Linked.txt") ==> false + + os.hardlink(wd / "Linked.txt", wd / "File.txt") + os.exists(wd / "Linked.txt") + os.read(wd / "Linked.txt") ==> "I am cow" + os.isLink(wd / "Linked.txt") ==> false + } + test("symlink") - prepChecker { wd => + intercept[WriteDenied] { + os.symlink(rd / "Linked.txt", wd / "File.txt") + } + os.exists(rd / "Linked.txt") ==> false + + intercept[WriteDenied] { + os.symlink(rd / "Linked.txt", os.rel / "File.txt") + } + os.exists(rd / "Linked.txt") ==> false + + intercept[WriteDenied] { + os.symlink(rd / "LinkedFolder1", wd / "folder1") + } + os.exists(rd / "LinkedFolder1") ==> false + + intercept[WriteDenied] { + os.symlink(rd / "LinkedFolder2", os.rel / "folder1") + } + os.exists(rd / "LinkedFolder2") ==> false + + os.symlink(wd / "Linked.txt", wd / "File.txt") + os.read(wd / "Linked.txt") ==> "I am cow" + os.isLink(wd / "Linked.txt") ==> true + + os.symlink(wd / "Linked2.txt", os.rel / "File.txt") + os.read(wd / "Linked2.txt") ==> "I am cow" + os.isLink(wd / "Linked2.txt") ==> true + + os.symlink(wd / "LinkedFolder1", wd / "folder1") + os.walk(wd / "LinkedFolder1", followLinks = true) ==> Seq(wd / "LinkedFolder1/one.txt") + os.isLink(wd / "LinkedFolder1") ==> true + + os.symlink(wd / "LinkedFolder2", os.rel / "folder1") + os.walk(wd / "LinkedFolder2", followLinks = true) ==> Seq(wd / "LinkedFolder2/one.txt") + os.isLink(wd / "LinkedFolder2") ==> true + } + test("temp") { + test - prepChecker { wd => + val before = os.walk(rd) + intercept[WriteDenied] { + os.temp("default content", dir = rd) + } + os.walk(rd) ==> before + + val tempOne = os.temp("default content", dir = wd) + os.read(tempOne) ==> "default content" + os.write.over(tempOne, "Hello") + os.read(tempOne) ==> "Hello" + } + test("dir") - prepChecker { wd => + val before = os.walk(rd) + intercept[WriteDenied] { + os.temp.dir(dir = rd) + } + os.walk(rd) ==> before + + val tempDir = os.temp.dir(dir = wd) + os.list(tempDir) ==> Nil + os.write(tempDir / "file", "Hello") + os.list(tempDir) ==> Seq(tempDir / "file") + } + } + + test("read") { + test("inputStream") - prepChecker { wd => + os.exists(rd / "File.txt") ==> true + intercept[ReadDenied] { + os.read.inputStream(rd / "File.txt") + } + + val is = os.read.inputStream(wd / "File.txt") // ==> "I am cow" + is.read() ==> 'I' + is.read() ==> ' ' + is.read() ==> 'a' + is.read() ==> 'm' + is.read() ==> ' ' + is.read() ==> 'c' + is.read() ==> 'o' + is.read() ==> 'w' + is.read() ==> -1 + is.close() + } + } + test("write") { + test - prepChecker { wd => + intercept[WriteDenied] { + os.write(rd / "New File.txt", "New File Contents") + } + os.exists(rd / "New File.txt") ==> false + + os.write(wd / "New File.txt", "New File Contents") + os.read(wd / "New File.txt") ==> "New File Contents" + + os.write(wd / "NewBinary.bin", Array[Byte](0, 1, 2, 3)) + os.read.bytes(wd / "NewBinary.bin") ==> Array[Byte](0, 1, 2, 3) + } + test("outputStream") - prepChecker { wd => + intercept[WriteDenied] { + os.write.outputStream(rd / "New File.txt") + } + os.exists(rd / "New File.txt") ==> false + + val out = os.write.outputStream(wd / "New File.txt") + out.write('H') + out.write('e') + out.write('l') + out.write('l') + out.write('o') + out.close() + + os.read(wd / "New File.txt") ==> "Hello" + } + } + test("truncate") - prepChecker { wd => + intercept[WriteDenied] { + os.truncate(rd / "File.txt", 4) + } + Unchecked(os.read(rd / "File.txt")) ==> "I am a restricted cow" + + os.read(wd / "File.txt") ==> "I am cow" + + os.truncate(wd / "File.txt", 4) + os.read(wd / "File.txt") ==> "I am" + } + + test("zip") - prepChecker { wd => + intercept[WriteDenied] { + os.zip( + dest = rd / "zipped.zip", + sources = Seq( + wd / "File.txt", + wd / "folder1" + ) + ) + } + os.exists(rd / "zipped.zip") ==> false + + intercept[ReadDenied] { + os.zip( + dest = wd / "zipped.zip", + sources = Seq( + wd / "File.txt", + rd / "folder1" + ) + ) + } + os.exists(wd / "zipped.zip") ==> false + + val zipFile = os.zip( + wd / "zipped.zip", + Seq( + wd / "File.txt", + wd / "folder1" + ) + ) + + val unzipDir = os.unzip(zipFile, wd / "unzipped") + os.walk(unzipDir).sorted ==> Seq( + unzipDir / "File.txt", + unzipDir / "one.txt" + ) + } + test("unzip") - prepChecker { wd => + val zipFileName = "zipped.zip" + val zipFile: os.Path = os.zip( + dest = wd / zipFileName, + sources = Seq( + wd / "File.txt", + wd / "folder1" + ) + ) + + intercept[WriteDenied] { + os.unzip( + source = zipFile, + dest = rd / "unzipped" + ) + } + os.exists(rd / "unzipped") ==> false + + val unzipDir = os.unzip( + source = zipFile, + dest = wd / "unzipped" + ) + os.walk(unzipDir).length ==> 2 + } + } +} diff --git a/os/test/src/FilesystemMetadataTests.scala b/os/test/src/FilesystemMetadataTests.scala index c5b3af61..f5d654db 100644 --- a/os/test/src/FilesystemMetadataTests.scala +++ b/os/test/src/FilesystemMetadataTests.scala @@ -1,6 +1,6 @@ package test.os -import test.os.TestUtil._ +import test.os.TestUtil.prep import utest._ object FilesystemMetadataTests extends TestSuite { @@ -9,9 +9,6 @@ object FilesystemMetadataTests extends TestSuite { private val multilineSizes = Set[Long](81, 84) def tests = Tests { - // restricted directory - val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" - test("stat") { test - prep { wd => os.stat(wd / "File.txt").size ==> 8 @@ -72,25 +69,7 @@ object FilesystemMetadataTests extends TestSuite { os.mtime(wd / "File.txt") ==> 70000 os.mtime(wd / "misc/file-symlink") ==> 70000 assert(os.mtime(wd / "misc/file-symlink", followLinks = false) != 40000) - } - test("checker") - prepChecker { wd => - val before = os.mtime(rd / "File.txt") - intercept[WriteDenied] { - os.mtime.set(rd / "File.txt", 0) - } - os.mtime(rd / "File.txt") ==> before - os.mtime.set(wd / "File.txt", 0) - os.mtime(wd / "File.txt") ==> 0 - - os.mtime.set(wd / "File.txt", 90000) - os.mtime(wd / "File.txt") ==> 90000 - os.mtime(wd / "misc/file-symlink") ==> 90000 - - os.mtime.set(wd / "misc/file-symlink", 70000) - os.mtime(wd / "File.txt") ==> 70000 - os.mtime(wd / "misc/file-symlink") ==> 70000 - assert(os.mtime(wd / "misc/file-symlink", followLinks = false) != 40000) } } } diff --git a/os/test/src/FilesystemPermissionsTests.scala b/os/test/src/FilesystemPermissionsTests.scala index 7e77cc7d..32580dc8 100644 --- a/os/test/src/FilesystemPermissionsTests.scala +++ b/os/test/src/FilesystemPermissionsTests.scala @@ -1,13 +1,10 @@ package test.os -import test.os.TestUtil._ +import test.os.TestUtil.prep import utest._ object FilesystemPermissionsTests extends TestSuite { def tests = Tests { - // restricted directory - val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" - test("perms") { test - prep { wd => if (Unix()) { @@ -18,25 +15,6 @@ object FilesystemPermissionsTests extends TestSuite { os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x" - os.perms.set(wd / "File.txt", "r-xr-xr-x") - os.perms.set(wd / "File.txt", Integer.parseInt("555", 8)) - } - } - test("checker") - prepChecker { wd => - if (Unix()) { - val before = os.perms(rd / "File.txt") - intercept[WriteDenied] { - os.perms.set(rd / "File.txt", "rwxrwxrwx") - } - os.perms(rd / "File.txt") ==> before - - os.perms.set(wd / "File.txt", "rwxrwxrwx") - os.perms(wd / "File.txt").toString() ==> "rwxrwxrwx" - os.perms(wd / "File.txt").toInt() ==> Integer.parseInt("777", 8) - - os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) - os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x" - os.perms.set(wd / "File.txt", "r-xr-xr-x") os.perms.set(wd / "File.txt", Integer.parseInt("555", 8)) } @@ -52,23 +30,6 @@ object FilesystemPermissionsTests extends TestSuite { os.owner.set(wd / "File.txt", "nobody") os.owner(wd / "File.txt").getName ==> "nobody" - os.owner.set(wd / "File.txt", originalOwner) - } - } - } - test("checker") - prepChecker { wd => - if (Unix()) { - // Only works as root :( - if (false) { - intercept[WriteDenied] { - os.owner.set(rd / "File.txt", "nobody") - } - - val originalOwner = os.owner(wd / "File.txt") - - os.owner.set(wd / "File.txt", "nobody") - os.owner(wd / "File.txt").getName ==> "nobody" - os.owner.set(wd / "File.txt", originalOwner) } } @@ -84,23 +45,6 @@ object FilesystemPermissionsTests extends TestSuite { os.group.set(wd / "File.txt", "nobody") os.group(wd / "File.txt").getName ==> "nobody" - os.group.set(wd / "File.txt", originalGroup) - } - } - } - test("checker") - prepChecker { wd => - if (Unix()) { - // Only works as root :( - if (false) { - intercept[WriteDenied] { - os.group.set(rd / "File.txt", "nobody") - } - - val originalGroup = os.group(wd / "File.txt") - - os.group.set(wd / "File.txt", "nobody") - os.group(wd / "File.txt").getName ==> "nobody" - os.group.set(wd / "File.txt", originalGroup) } } diff --git a/os/test/src/ManipulatingFilesFoldersTests.scala b/os/test/src/ManipulatingFilesFoldersTests.scala index 3176fc2c..60627a2d 100644 --- a/os/test/src/ManipulatingFilesFoldersTests.scala +++ b/os/test/src/ManipulatingFilesFoldersTests.scala @@ -1,13 +1,10 @@ package test.os -import test.os.TestUtil._ +import test.os.TestUtil.prep import utest._ object ManipulatingFilesFoldersTests extends TestSuite { def tests = Tests { - // restricted directory - val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" - test("exists") { test - prep { wd => os.exists(wd / "File.txt") ==> true @@ -38,45 +35,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.move(rd / "folder1/one.txt", wd / "folder1/File.txt") - } - os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") - - os.checker.withValue(AccessChecker(Seq(rd / "folder1/one.txt"), Seq(wd))) { - intercept[WriteDenied] { // no write access on parent folder (rd / "folder1") - os.move(rd / "folder1/one.txt", wd / "folder1/File.txt") - } - } - os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") - - intercept[WriteDenied] { - os.move(wd / "folder1/one.txt", rd / "folder1/File.txt") - } - os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") - - intercept[WriteDenied] { - os.move(wd / "folder2/nestedA", rd / "folder2/nestedC") - } - os.list(rd / "folder2") ==> Seq(rd / "folder2/nestedA", rd / "folder2/nestedB") - - os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") - os.move(wd / "folder1/one.txt", wd / "folder1/first.txt") - os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt") - - os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") - os.move(wd / "folder2/nestedA", wd / "folder2/nestedC") - os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedB", wd / "folder2/nestedC") - - os.read(wd / "File.txt") ==> "I am cow" - os.move(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) - os.read(wd / "File.txt") ==> - """I am cow - |Hear me moo - |I weigh twice as much as you - |And I look good on the barbecue""".stripMargin - } test("matching") { test - prep { wd => import os.{GlobSyntax, /} @@ -134,42 +92,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } - test("checker") - prepChecker { wd => - intercept[ReadDenied] { - os.copy(rd / "folder1/one.txt", wd / "folder1/File.txt") - } - os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") - - intercept[WriteDenied] { - os.copy(wd / "folder1/one.txt", rd / "folder1/File.txt") - } - os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") - - intercept[WriteDenied] { - os.copy(wd / "folder2/nestedA", rd / "folder2/nestedC") - } - os.list(rd / "folder2") ==> Seq(rd / "folder2/nestedA", rd / "folder2/nestedB") - - os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") - os.copy(wd / "folder1/one.txt", wd / "folder1/first.txt") - os.list(wd / "folder1") ==> Seq(wd / "folder1/first.txt", wd / "folder1/one.txt") - - os.list(wd / "folder2") ==> Seq(wd / "folder2/nestedA", wd / "folder2/nestedB") - os.copy(wd / "folder2/nestedA", wd / "folder2/nestedC") - os.list(wd / "folder2") ==> Seq( - wd / "folder2/nestedA", - wd / "folder2/nestedB", - wd / "folder2/nestedC" - ) - - os.read(wd / "File.txt") ==> "I am cow" - os.copy(wd / "Multi Line.txt", wd / "File.txt", replaceExisting = true) - os.read(wd / "File.txt") ==> - """I am cow - |Hear me moo - |I weigh twice as much as you - |And I look good on the barbecue""".stripMargin - } test("into") { test - prep { wd => os.list(wd / "folder1") ==> Seq(wd / "folder1/one.txt") @@ -233,32 +155,12 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.makeDir(wd / "new_folder") os.exists(wd / "new_folder") ==> true } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.makeDir(rd / "new_folder") - } - os.exists(rd / "new_folder") ==> false - - os.exists(wd / "new_folder") ==> false - os.makeDir(wd / "new_folder") - os.exists(wd / "new_folder") ==> true - } test("all") { test - prep { wd => os.exists(wd / "new_folder") ==> false os.makeDir.all(wd / "new_folder/inner/deep") os.exists(wd / "new_folder/inner/deep") ==> true } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.makeDir.all(rd / "new_folder/inner/deep") - } - os.exists(rd / "new_folder") ==> false - - os.exists(wd / "new_folder") ==> false - os.makeDir.all(wd / "new_folder/inner/deep") - os.exists(wd / "new_folder/inner/deep") ==> true - } } } test("remove") { @@ -273,35 +175,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1/one.txt") ==> false os.exists(wd / "folder1") ==> false } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.remove(rd / "File.txt") - } - os.exists(rd / "File.txt") ==> true - - intercept[WriteDenied] { - os.remove(rd / "folder1") - } - os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") - - Unchecked.scope(os.makeDir(rd / "folder"), os.remove(rd / "folder")) { - intercept[WriteDenied] { - os.remove(rd / "folder") - } - os.exists(rd / "folder") ==> true - } - os.exists(rd / "folder") ==> false - - os.exists(wd / "File.txt") ==> true - os.remove(wd / "File.txt") - os.exists(wd / "File.txt") ==> false - - os.exists(wd / "folder1/one.txt") ==> true - os.remove(wd / "folder1/one.txt") - os.remove(wd / "folder1") - os.exists(wd / "folder1/one.txt") ==> false - os.exists(wd / "folder1") ==> false - } test("link") { test - prep { wd => os.remove(wd / "misc/file-symlink") @@ -313,35 +186,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1", followLinks = false) ==> true os.exists(wd / "folder1/one.txt", followLinks = false) ==> true - os.remove(wd / "misc/broken-symlink") - os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false - } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.remove(rd / "misc/file-symlink") - } - os.exists(rd / "misc/file-symlink", followLinks = false) ==> true - - intercept[WriteDenied] { - os.remove(rd / "misc/folder-symlink") - } - os.exists(rd / "misc/folder-symlink", followLinks = false) ==> true - - intercept[WriteDenied] { - os.remove(rd / "misc/broken-symlink") - } - os.exists(rd / "misc/broken-symlink", followLinks = false) ==> true - os.exists(rd / "misc/broken-symlink") ==> true - - os.remove(wd / "misc/file-symlink") - os.exists(wd / "misc/file-symlink", followLinks = false) ==> false - os.exists(wd / "File.txt", followLinks = false) ==> true - - os.remove(wd / "misc/folder-symlink") - os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false - os.exists(wd / "folder1", followLinks = false) ==> true - os.exists(wd / "folder1/one.txt", followLinks = false) ==> true - os.remove(wd / "misc/broken-symlink") os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false } @@ -353,17 +197,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1/one.txt") ==> false os.exists(wd / "folder1") ==> false } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.remove.all(rd / "folder1") - } - os.list(rd / "folder1") ==> Seq(rd / "folder1/one.txt") - - os.exists(wd / "folder1/one.txt") ==> true - os.remove.all(wd / "folder1") - os.exists(wd / "folder1/one.txt") ==> false - os.exists(wd / "folder1") ==> false - } test("link") { test - prep { wd => os.remove.all(wd / "misc/file-symlink") @@ -375,33 +208,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1", followLinks = false) ==> true os.exists(wd / "folder1/one.txt", followLinks = false) ==> true - os.remove.all(wd / "misc/broken-symlink") - os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false - } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.remove.all(rd / "misc/file-symlink") - } - os.exists(rd / "misc/file-symlink", followLinks = false) ==> true - - intercept[WriteDenied] { - os.remove.all(rd / "misc/folder-symlink") - } - os.exists(rd / "misc/folder-symlink", followLinks = false) ==> true - - intercept[WriteDenied] { - os.remove.all(rd / "misc/broken-symlink") - } - os.exists(rd / "misc/broken-symlink", followLinks = false) ==> true - - os.remove.all(wd / "misc/file-symlink") - os.exists(wd / "misc/file-symlink", followLinks = false) ==> false - - os.remove.all(wd / "misc/folder-symlink") - os.exists(wd / "misc/folder-symlink", followLinks = false) ==> false - os.exists(wd / "folder1", followLinks = false) ==> true - os.exists(wd / "folder1/one.txt", followLinks = false) ==> true - os.remove.all(wd / "misc/broken-symlink") os.exists(wd / "misc/broken-symlink", followLinks = false) ==> false } @@ -415,22 +221,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.read(wd / "Linked.txt") ==> "I am cow" os.isLink(wd / "Linked.txt") ==> false } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.hardlink(wd / "Linked.txt", rd / "File.txt") - } - os.exists(wd / "Linked.txt") ==> false - - intercept[WriteDenied] { - os.hardlink(rd / "Linked.txt", wd / "File.txt") - } - os.exists(rd / "Linked.txt") ==> false - - os.hardlink(wd / "Linked.txt", wd / "File.txt") - os.exists(wd / "Linked.txt") - os.read(wd / "Linked.txt") ==> "I am cow" - os.isLink(wd / "Linked.txt") ==> false - } } test("symlink") { test - prep { wd => @@ -444,43 +234,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.read(wd / "Linked2.txt") ==> "I am cow" os.isLink(wd / "Linked2.txt") ==> true } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.symlink(rd / "Linked.txt", wd / "File.txt") - } - os.exists(rd / "Linked.txt") ==> false - - intercept[WriteDenied] { - os.symlink(rd / "Linked.txt", os.rel / "File.txt") - } - os.exists(rd / "Linked.txt") ==> false - - intercept[WriteDenied] { - os.symlink(rd / "LinkedFolder1", wd / "folder1") - } - os.exists(rd / "LinkedFolder1") ==> false - - intercept[WriteDenied] { - os.symlink(rd / "LinkedFolder2", os.rel / "folder1") - } - os.exists(rd / "LinkedFolder2") ==> false - - os.symlink(wd / "Linked.txt", wd / "File.txt") - os.read(wd / "Linked.txt") ==> "I am cow" - os.isLink(wd / "Linked.txt") ==> true - - os.symlink(wd / "Linked2.txt", os.rel / "File.txt") - os.read(wd / "Linked2.txt") ==> "I am cow" - os.isLink(wd / "Linked2.txt") ==> true - - os.symlink(wd / "LinkedFolder1", wd / "folder1") - os.walk(wd / "LinkedFolder1", followLinks = true) ==> Seq(wd / "LinkedFolder1/one.txt") - os.isLink(wd / "LinkedFolder1") ==> true - - os.symlink(wd / "LinkedFolder2", os.rel / "folder1") - os.walk(wd / "LinkedFolder2", followLinks = true) ==> Seq(wd / "LinkedFolder2/one.txt") - os.isLink(wd / "LinkedFolder2") ==> true - } } test("followLink") { test - prep { wd => @@ -511,18 +264,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.write.over(tempOne, "Hello") os.read(tempOne) ==> "Hello" } - test("checker") - prepChecker { wd => - val before = os.walk(rd) - intercept[WriteDenied] { - os.temp("default content", dir = rd) - } - os.walk(rd) ==> before - - val tempOne = os.temp("default content", dir = wd) - os.read(tempOne) ==> "default content" - os.write.over(tempOne, "Hello") - os.read(tempOne) ==> "Hello" - } test("dir") { test - prep { wd => val tempDir = os.temp.dir() @@ -530,18 +271,6 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.write(tempDir / "file", "Hello") os.list(tempDir) ==> Seq(tempDir / "file") } - test("checker") - prepChecker { wd => - val before = os.walk(rd) - intercept[WriteDenied] { - os.temp.dir(dir = rd) - } - os.walk(rd) ==> before - - val tempDir = os.temp.dir(dir = wd) - os.list(tempDir) ==> Nil - os.write(tempDir / "file", "Hello") - os.list(tempDir) ==> Seq(tempDir / "file") - } } } } diff --git a/os/test/src/ReadingWritingTests.scala b/os/test/src/ReadingWritingTests.scala index 5bf46bc8..12d2b200 100644 --- a/os/test/src/ReadingWritingTests.scala +++ b/os/test/src/ReadingWritingTests.scala @@ -3,9 +3,6 @@ import utest._ import TestUtil._ object ReadingWritingTests extends TestSuite { def tests = Tests { - // restricted directory - val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" - test("read") { test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" @@ -31,24 +28,6 @@ object ReadingWritingTests extends TestSuite { is.close() } } - test("checker") - prepChecker { wd => - os.exists(rd / "File.txt") ==> true - intercept[ReadDenied] { - os.read.inputStream(rd / "File.txt") - } - - val is = os.read.inputStream(wd / "File.txt") // ==> "I am cow" - is.read() ==> 'I' - is.read() ==> ' ' - is.read() ==> 'a' - is.read() ==> 'm' - is.read() ==> ' ' - is.read() ==> 'c' - is.read() ==> 'o' - is.read() ==> 'w' - is.read() ==> -1 - is.close() - } test("bytes") { test - prep { wd => os.read.bytes(wd / "File.txt") ==> "I am cow".getBytes @@ -102,18 +81,6 @@ object ReadingWritingTests extends TestSuite { os.write(wd / "NewBinary.bin", Array[Byte](0, 1, 2, 3)) os.read.bytes(wd / "NewBinary.bin") ==> Array[Byte](0, 1, 2, 3) } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.write(rd / "New File.txt", "New File Contents") - } - os.exists(rd / "New File.txt") ==> false - - os.write(wd / "New File.txt", "New File Contents") - os.read(wd / "New File.txt") ==> "New File Contents" - - os.write(wd / "NewBinary.bin", Array[Byte](0, 1, 2, 3)) - os.read.bytes(wd / "NewBinary.bin") ==> Array[Byte](0, 1, 2, 3) - } test("append") { test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" @@ -154,22 +121,6 @@ object ReadingWritingTests extends TestSuite { out.write('o') out.close() - os.read(wd / "New File.txt") ==> "Hello" - } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.write.outputStream(rd / "New File.txt") - } - os.exists(rd / "New File.txt") ==> false - - val out = os.write.outputStream(wd / "New File.txt") - out.write('H') - out.write('e') - out.write('l') - out.write('l') - out.write('o') - out.close() - os.read(wd / "New File.txt") ==> "Hello" } } @@ -178,17 +129,6 @@ object ReadingWritingTests extends TestSuite { test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" - os.truncate(wd / "File.txt", 4) - os.read(wd / "File.txt") ==> "I am" - } - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.truncate(rd / "File.txt", 4) - } - Unchecked(os.read(rd / "File.txt")) ==> "I am a restricted cow" - - os.read(wd / "File.txt") ==> "I am cow" - os.truncate(wd / "File.txt", 4) os.read(wd / "File.txt") ==> "I am" } diff --git a/os/test/src/TestUtil.scala b/os/test/src/TestUtil.scala index dac4d974..fc82a7c6 100644 --- a/os/test/src/TestUtil.scala +++ b/os/test/src/TestUtil.scala @@ -1,6 +1,7 @@ package test.os import utest.framework.TestPath + import java.io.IOException import java.nio.file._ import java.nio.file.attribute.BasicFileAttributes diff --git a/os/test/src/ZipOpTests.scala b/os/test/src/ZipOpTests.scala index f5e1a3ae..3d41bdb6 100644 --- a/os/test/src/ZipOpTests.scala +++ b/os/test/src/ZipOpTests.scala @@ -1,7 +1,7 @@ package test.os import os.zip -import test.os.TestUtil._ +import test.os.TestUtil.prep import utest._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} @@ -10,47 +10,6 @@ import java.util.zip.{ZipEntry, ZipOutputStream} object ZipOpTests extends TestSuite { def tests = Tests { - // restricted directory - val rd = os.Path(sys.env("OS_TEST_RESOURCE_FOLDER")) / "restricted" - - test("checker") - prepChecker { wd => - intercept[WriteDenied] { - os.zip( - dest = rd / "zipped.zip", - sources = Seq( - wd / "File.txt", - wd / "folder1" - ) - ) - } - os.exists(rd / "zipped.zip") ==> false - - intercept[ReadDenied] { - os.zip( - dest = wd / "zipped.zip", - sources = Seq( - wd / "File.txt", - rd / "folder1" - ) - ) - } - os.exists(wd / "zipped.zip") ==> false - - val zipFile = os.zip( - wd / "zipped.zip", - Seq( - wd / "File.txt", - wd / "folder1" - ) - ) - - val unzipDir = os.unzip(zipFile, wd / "unzipped") - os.walk(unzipDir).sorted ==> Seq( - unzipDir / "File.txt", - unzipDir / "one.txt" - ) - } - test("level") - prep { wd => val zipsForLevel = for (i <- Range.inclusive(0, 9)) yield { os.write.over(wd / "File.txt", Range(0, 1000).map(x => x.toString * x)) @@ -168,30 +127,6 @@ object ZipOpTests extends TestSuite { assert(paths == Seq(unzippedFolder / "File.txt")) } - test("unzipChecker") - prepChecker { wd => - val zipFileName = "zipped.zip" - val zipFile: os.Path = os.zip( - dest = wd / zipFileName, - sources = Seq( - wd / "File.txt", - wd / "folder1" - ) - ) - - intercept[WriteDenied] { - os.unzip( - source = zipFile, - dest = rd / "unzipped" - ) - } - os.exists(rd / "unzipped") ==> false - - val unzipDir = os.unzip( - source = zipFile, - dest = wd / "unzipped" - ) - os.walk(unzipDir).length ==> 2 - } test("list") - prep { wd => // Zipping files and folders in a new zip file val zipFileName = "listContentsOfZipFileWithoutExtracting.zip"