Deny cyclic FSFolders (#3)
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Test Workflow / Lint and test library (push) Successful in 3m56s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Test Workflow / Lint and test library (push) Successful in 3m56s
				
			Contributes to #2 . Handle cyclic folders explicitly, instead of relying on the filesystem. Reviewed-on: #3
This commit is contained in:
		@@ -1,21 +1,30 @@
 | 
				
			|||||||
package filesystem
 | 
					package filesystem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.nio.file.FileAlreadyExistsException
 | 
					import java.nio.file.FileAlreadyExistsException
 | 
				
			||||||
import java.nio.file.FileSystemException
 | 
					 | 
				
			||||||
import java.nio.file.Files
 | 
					import java.nio.file.Files
 | 
				
			||||||
import java.nio.file.Path
 | 
					import java.nio.file.Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FSCreator {
 | 
					class FSCreator {
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Create entry, leaving existing folders' contents, but overwriting existing files.
 | 
					     * 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(
 | 
					    fun create(
 | 
				
			||||||
        entryToCreate: FSEntry,
 | 
					        entryToCreate: FSEntry,
 | 
				
			||||||
        destination: String,
 | 
					        destination: String,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        val queue = ArrayDeque<Pair<FSEntry, Path>>()
 | 
					        // No point in running anything if we know the input is invalid.
 | 
				
			||||||
        queue.add(entryToCreate to Path.of(destination))
 | 
					        if (entryToCreate is FSFolder && entryToCreate.isCyclic()) {
 | 
				
			||||||
 | 
					            throw CyclicFolderException()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val queue =
 | 
				
			||||||
 | 
					            ArrayDeque(
 | 
				
			||||||
 | 
					                listOf(
 | 
				
			||||||
 | 
					                    entryToCreate to Path.of(destination),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        while (queue.isNotEmpty()) {
 | 
					        while (queue.isNotEmpty()) {
 | 
				
			||||||
            val (entry, dest) = queue.removeFirst()
 | 
					            val (entry, dest) = queue.removeFirst()
 | 
				
			||||||
@@ -34,3 +43,5 @@ class FSCreator {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CyclicFolderException : Exception("Cyclic FSFolders are not supported")
 | 
				
			||||||
@@ -6,4 +6,22 @@ sealed class FSEntry(val name: String)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class FSFile(name: String, val content: String) : FSEntry(name)
 | 
					class FSFile(name: String, val content: String) : FSEntry(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FSFolder(name: String, val entries: List<FSEntry>) : FSEntry(name)
 | 
					class FSFolder(name: String, val entries: List<FSEntry>) : FSEntry(name) {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check whether a folder is cyclic.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    fun isCyclic(): Boolean {
 | 
				
			||||||
 | 
					        val seen = listOf(this).toHashSet<FSEntry>()
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
package filesystem
 | 
					package filesystem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.junit.jupiter.api.*
 | 
					import org.junit.jupiter.api.*
 | 
				
			||||||
import java.nio.file.FileSystemException
 | 
					 | 
				
			||||||
import java.nio.file.Files
 | 
					import java.nio.file.Files
 | 
				
			||||||
import java.nio.file.Path
 | 
					import java.nio.file.Path
 | 
				
			||||||
import java.util.concurrent.TimeUnit
 | 
					import java.util.concurrent.TimeUnit
 | 
				
			||||||
@@ -81,7 +80,7 @@ class FSCreatorTest {
 | 
				
			|||||||
                                FSFile("hi", "hi"),
 | 
					                                FSFile("hi", "hi"),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        FSFolder("another-folder", listOf()),
 | 
					                        FSFolder("folder", listOf()),
 | 
				
			||||||
                        FSFile("1.txt", "One!"),
 | 
					                        FSFile("1.txt", "One!"),
 | 
				
			||||||
                        FSFile("2.txt", "Two!"),
 | 
					                        FSFile("2.txt", "Two!"),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
@@ -95,7 +94,7 @@ class FSCreatorTest {
 | 
				
			|||||||
                    "folder",
 | 
					                    "folder",
 | 
				
			||||||
                    listOf(
 | 
					                    listOf(
 | 
				
			||||||
                        FSFolder(
 | 
					                        FSFolder(
 | 
				
			||||||
                            "another-folder",
 | 
					                            "folder",
 | 
				
			||||||
                            listOf(
 | 
					                            listOf(
 | 
				
			||||||
                                FSFolder(
 | 
					                                FSFolder(
 | 
				
			||||||
                                    "secrets",
 | 
					                                    "secrets",
 | 
				
			||||||
@@ -113,22 +112,36 @@ class FSCreatorTest {
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        assertEquals("hi", Files.readString(Path.of("_tmp/folder/sub-folder/hi")))
 | 
					        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("One is a good number", Files.readString(Path.of("_tmp/folder/1.txt")))
 | 
				
			||||||
        assertEquals("Two!", Files.readString(Path.of("_tmp/folder/2.txt")))
 | 
					        assertEquals("Two!", Files.readString(Path.of("_tmp/folder/2.txt")))
 | 
				
			||||||
        assertEquals("Three!", Files.readString(Path.of("_tmp/folder/3.txt")))
 | 
					        assertEquals("Three!", Files.readString(Path.of("_tmp/folder/3.txt")))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    @Timeout(500, unit = TimeUnit.MILLISECONDS) // in case implementation starts trying to handle recursion
 | 
					    @Timeout(500, unit = TimeUnit.MILLISECONDS) // in case implementation starts trying to handle cyclic folders
 | 
				
			||||||
    fun `create throws on recursive folder`() {
 | 
					    fun `create throws on cyclic folder`() {
 | 
				
			||||||
        val files = mutableListOf<FSEntry>()
 | 
					        val files = mutableListOf<FSEntry>()
 | 
				
			||||||
        val folder = FSFolder("folder", files)
 | 
					        val folder = FSFolder("folder", files)
 | 
				
			||||||
        files.add(folder)
 | 
					        files.add(folder)
 | 
				
			||||||
        assertThrows<FileSystemException> {
 | 
					        assertThrows<CyclicFolderException> {
 | 
				
			||||||
            creator.create(folder, "_tmp")
 | 
					            creator.create(folder, "_tmp")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    @Timeout(500, unit = TimeUnit.MILLISECONDS)
 | 
				
			||||||
 | 
					    fun `create throws on long cyclic folder`() {
 | 
				
			||||||
 | 
					        val files = mutableListOf<FSEntry>()
 | 
				
			||||||
 | 
					        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<CyclicFolderException> {
 | 
				
			||||||
 | 
					            creator.create(folder4, "_tmp")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun deleteRecursive(path: Path) {
 | 
					fun deleteRecursive(path: Path) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										42
									
								
								src/test/kotlin/filesystem/FSEntryTest.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/test/kotlin/filesystem/FSEntryTest.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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<FSEntry>()
 | 
				
			||||||
 | 
					        val folder = FSFolder("folder", files)
 | 
				
			||||||
 | 
					        files.add(folder)
 | 
				
			||||||
 | 
					        assertTrue(folder.isCyclic())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    fun `long cyclic folder`() {
 | 
				
			||||||
 | 
					        val files = mutableListOf<FSEntry>()
 | 
				
			||||||
 | 
					        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())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user