0

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

xdevs23
  • 3,824
  • 3
  • 20
  • 33

2 Answers2

0

You could .toString() the Paths to do this operation.

You could otherwise use relativeTo and check the result. That's not exhaustively correct but something like the following could be a start:

  @Test fun testingInclusion() {
    println("/a/b/c/d".toPath().contains("/a/b/c".toPath()))
    println("/a/b/d/d".toPath().contains("/a/b/c".toPath()))
  }

  private fun Path.contains(other: Path): Boolean {
    return !this.relativeTo(other).toString().contains("..")
  }
oldergod
  • 15,033
  • 7
  • 62
  • 88
  • Thank you for your suggestion. I was thinking about doing it using `relativeTo` but I thought this is a bit hacky. Using direct string comparison on the full path is hard to get exactly right and handle all edge cases (a naive implementation could fail with "/a/b/cc".startsWith("/a/b/c")). It would probably be most correct to check if every path component of `/a/b/c` is the same as `/a/b/c/d` up to the length of `/a/b/c`. This is what I have implemented right now and I'll post it as a separate answer. – xdevs23 Apr 03 '23 at 18:35
0

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"
            )
        }
    }

}
xdevs23
  • 3,824
  • 3
  • 20
  • 33