Search code examples
kotlinkotlin-nativeokio

How to check if a path is inside another path using Okio?


I am using Okio in Kotlin/Native and I would like to check if one path is inside another path.

Although there is a equal/greater than/less than operator, it looks like it only compares the length.

Example:

"/a/b/c/d".toPath().startsWith("/a/b/c".toPath()) // should return true
"/a/b/d/d".toPath().startsWith("/a/b/c".toPath()) // should return false

But startsWith does not exist.

Kotlin/JVM supports this via Java: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/starts-with.html


Solution

  • I have created this extension function which implements startsWith as described in the question:

    fun Path.startsWith(other: Path) = normalized().run {
        other.normalized().let { normalizedOther ->
            normalizedOther.segments.size <= segments.size &&
                    segments
                        .slice(0 until normalizedOther.segments.size)
                        .filterIndexed { index, s -> normalizedOther.segments[index] != s }
                        .isEmpty()
        }
    }
    

    It first checks if the other path has more segments (or components) than this path which would already mean they don't match since /a/b/c can never start with /a/b/c/d (or even /1/2/3/4).

    If the segment count of other is the same or less, it proceeds with slicing this into as many segments as other has so that any sub-entries are ignored.

    Then, it filters the sliced segments of this that don't match by using the same index for accessing the segments of other.

    Now we have a list of segments that don't match on the same index. By checking if the list isEmpty(), we now have the conclusion of whether this startsWith other (you can turn this into an infix if you want.).

    Passing test:

    import okio.Path.Companion.toPath
    import kotlin.test.Test
    import kotlin.test.assertFalse
    import kotlin.test.assertTrue
    
    class PathsTests {
    
        @Test
        fun testStartsWith() {
            listOf(
                "/a/b/c/d" to "/a/b/c",
                "/a/b/c/d" to "/a/b/c/",
                "/A/B/C/D" to "/A/B/C",
                "/a/b/c/d" to "/a/b//c/",
                "/a/b/c/d" to "/a/b/../b/c",
                "/a/b/c/d" to "/a/../a/./b/../b///c",
                "\\a\\b\\c\\d" to "/a/../a/./b/../b///c",
                "/home/user/.config/test" to "/home/user",
                "/var/www/html/app" to "/var/www/html",
                "/home/user" to "/",
                "/" to "/",
                "////////////////////////" to "/",
                "/" to "////////////////////////",
                "/home/user" to "/home/user",
                "/home/user/./" to "/home/user",
                "/home/user" to "/home/user/./",
                "/./var" to "/var",
                "." to ".",
                "./" to ".",
                ".." to "..",
                "../.." to "../..",
                "./.." to "../.",
                "../." to "./..",
                "./../." to ".././.",
                "/." to "/.",
                "./" to ".",
                "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" to "/a/b/c",
                "/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a" to "/a/a/a"
            ).forEach { (pathString, otherPathString) ->
                assertTrue(
                    pathString.toPath().startsWith(otherPathString.toPath()),
                    "$pathString should start with $otherPathString"
                )
            }
    
            listOf(
                "/a/b/c" to "/a/b/c/d/",
                "/a/b/c/" to "/a/b/c/d",
                "/a/b/d/d" to "/a/b/c",
                "/a/b/d/d" to "/a/b/ce",
                "/a/b/ce" to "/a/b/c",
                "/a/b/c" to "/a/b/ce",
                "/abcd" to "/a/b/c/d",
                "/a/../b/c" to "/a/b/c",
                "/a/b/" to "/a/b//c",
                "/a/b/c/d" to "/a/b/../c",
                "/a/b/c/d" to "/a/./a/../b/./b///c",
                "/a/b/c" to "/c/b/a",
                "/a/a/a/a" to "/a/a/a/a/a",
                "\\a\\b\\d\\d" to "\\a\\b\\c",
                "\\a\\b\\d\\d" to "/a/b/c",
                "/home/user/.config/test" to "/home/user2",
                "/var/www/html/app" to "/var/local/www/html/app",
                "/home/user" to ".",
                "/" to "./",
                "/home/user" to "/home/user2",
                "/home/user/./" to "/home/user2",
                "/home/user2" to "/home/user/./",
                "../var" to "/var",
                "." to "..",
                "./" to "..",
                ".." to ".",
                "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" to "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/z",
                "/a/a/a" to "/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a",
                "/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a" to "/A",
            ).forEach { (pathString, otherPathString) ->
                assertFalse(
                    pathString.toPath().startsWith(otherPathString.toPath()),
                    "$pathString should not start with $otherPathString"
                )
            }
        }
    
    }