From 8aa21dff54eacb5183fc77d4aa0cdf3d2864a2af Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Sun, 7 Jan 2024 14:56:16 +0000 Subject: [PATCH] Deny cyclic FSFolders (#3) Contributes to #2 . Handle cyclic folders explicitly, instead of relying on the filesystem. Reviewed-on: https://git.koval.net/cyclane/teamcity-build-step-extension-test-task/pulls/3 --- src/main/kotlin/filesystem/FSCreator.kt | 21 ++++++++--- src/main/kotlin/filesystem/FSEntry.kt | 20 +++++++++- src/test/kotlin/filesystem/FSCreatorTest.kt | 27 +++++++++---- src/test/kotlin/filesystem/FSEntryTest.kt | 42 +++++++++++++++++++++ 4 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/test/kotlin/filesystem/FSEntryTest.kt diff --git a/src/main/kotlin/filesystem/FSCreator.kt b/src/main/kotlin/filesystem/FSCreator.kt index a1794ae..4b8d4ca 100644 --- a/src/main/kotlin/filesystem/FSCreator.kt +++ b/src/main/kotlin/filesystem/FSCreator.kt @@ -1,21 +1,30 @@ package filesystem import java.nio.file.FileAlreadyExistsException -import java.nio.file.FileSystemException import java.nio.file.Files import java.nio.file.Path class FSCreator { /** * Create entry, leaving existing folders' contents, but overwriting existing files. + * @throws CyclicFolderException Cyclic folders cannot be created. */ - @Throws(FileSystemException::class) + @Throws(CyclicFolderException::class) fun create( entryToCreate: FSEntry, destination: String, ) { - val queue = ArrayDeque>() - queue.add(entryToCreate to Path.of(destination)) + // No point in running anything if we know the input is invalid. + if (entryToCreate is FSFolder && entryToCreate.isCyclic()) { + throw CyclicFolderException() + } + + val queue = + ArrayDeque( + listOf( + entryToCreate to Path.of(destination), + ), + ) while (queue.isNotEmpty()) { val (entry, dest) = queue.removeFirst() @@ -33,4 +42,6 @@ class FSCreator { } } } -} \ No newline at end of file +} + +class CyclicFolderException : Exception("Cyclic FSFolders are not supported") \ No newline at end of file diff --git a/src/main/kotlin/filesystem/FSEntry.kt b/src/main/kotlin/filesystem/FSEntry.kt index 3856abd..6cad9ad 100644 --- a/src/main/kotlin/filesystem/FSEntry.kt +++ b/src/main/kotlin/filesystem/FSEntry.kt @@ -6,4 +6,22 @@ sealed class FSEntry(val name: String) class FSFile(name: String, val content: String) : FSEntry(name) -class FSFolder(name: String, val entries: List) : FSEntry(name) \ No newline at end of file +class FSFolder(name: String, val entries: List) : FSEntry(name) { + /** + * Check whether a folder is cyclic. + */ + fun isCyclic(): Boolean { + val seen = listOf(this).toHashSet() + val queue = ArrayDeque(entries) + while (queue.isNotEmpty()) { + val entry = queue.removeFirst() + if (!seen.add(entry)) { + return true + } + if (entry is FSFolder) { + queue.addAll(entry.entries) + } + } + return false + } +} \ No newline at end of file diff --git a/src/test/kotlin/filesystem/FSCreatorTest.kt b/src/test/kotlin/filesystem/FSCreatorTest.kt index 7cc3fe1..ab7c2df 100644 --- a/src/test/kotlin/filesystem/FSCreatorTest.kt +++ b/src/test/kotlin/filesystem/FSCreatorTest.kt @@ -1,7 +1,6 @@ package filesystem import org.junit.jupiter.api.* -import java.nio.file.FileSystemException import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.TimeUnit @@ -81,7 +80,7 @@ class FSCreatorTest { FSFile("hi", "hi"), ), ), - FSFolder("another-folder", listOf()), + FSFolder("folder", listOf()), FSFile("1.txt", "One!"), FSFile("2.txt", "Two!"), ), @@ -95,7 +94,7 @@ class FSCreatorTest { "folder", listOf( FSFolder( - "another-folder", + "folder", listOf( FSFolder( "secrets", @@ -113,22 +112,36 @@ class FSCreatorTest { ) } assertEquals("hi", Files.readString(Path.of("_tmp/folder/sub-folder/hi"))) - assertEquals("P4ssW0rd", Files.readString(Path.of("_tmp/folder/another-folder/secrets/secret"))) + assertEquals("P4ssW0rd", Files.readString(Path.of("_tmp/folder/folder/secrets/secret"))) assertEquals("One is a good number", Files.readString(Path.of("_tmp/folder/1.txt"))) assertEquals("Two!", Files.readString(Path.of("_tmp/folder/2.txt"))) assertEquals("Three!", Files.readString(Path.of("_tmp/folder/3.txt"))) } @Test - @Timeout(500, unit = TimeUnit.MILLISECONDS) // in case implementation starts trying to handle recursion - fun `create throws on recursive folder`() { + @Timeout(500, unit = TimeUnit.MILLISECONDS) // in case implementation starts trying to handle cyclic folders + fun `create throws on cyclic folder`() { val files = mutableListOf() val folder = FSFolder("folder", files) files.add(folder) - assertThrows { + assertThrows { creator.create(folder, "_tmp") } } + + @Test + @Timeout(500, unit = TimeUnit.MILLISECONDS) + fun `create throws on long cyclic folder`() { + val files = mutableListOf() + val folder1 = FSFolder("folder", files) + val folder2 = FSFolder("folder2", listOf(folder1)) + val folder3 = FSFolder("folder3", listOf(folder2)) + val folder4 = FSFolder("folder4", listOf(folder3)) + files.add(folder4) + assertThrows { + creator.create(folder4, "_tmp") + } + } } fun deleteRecursive(path: Path) { diff --git a/src/test/kotlin/filesystem/FSEntryTest.kt b/src/test/kotlin/filesystem/FSEntryTest.kt new file mode 100644 index 0000000..b474ac2 --- /dev/null +++ b/src/test/kotlin/filesystem/FSEntryTest.kt @@ -0,0 +1,42 @@ +package filesystem + +import org.junit.jupiter.api.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class FSEntryTest { + @Test + fun `non-cyclic folder`() { + val folder = + FSFolder( + "folder", + listOf( + FSFolder("folder", listOf()), + FSFile("text.txt", "Hello!"), + ), + ) + assertFalse(folder.isCyclic()) + } + + @Test + fun `cyclic folder`() { + val files = mutableListOf() + val folder = FSFolder("folder", files) + files.add(folder) + assertTrue(folder.isCyclic()) + } + + @Test + fun `long cyclic folder`() { + val files = mutableListOf() + val folder1 = FSFolder("folder", files) + val folder2 = FSFolder("folder2", listOf(folder1)) + val folder3 = FSFolder("folder3", listOf(folder2)) + val folder4 = FSFolder("folder4", listOf(folder3)) + files.add(folder4) + assertTrue(folder1.isCyclic()) + assertTrue(folder2.isCyclic()) + assertTrue(folder3.isCyclic()) + assertTrue(folder4.isCyclic()) + } +} \ No newline at end of file