Improve dummy-lock handling
authorLanius Trolling <lanius@laniustrolling.dev>
Sun, 22 Dec 2024 20:46:24 +0000 (15:46 -0500)
committerLanius Trolling <lanius@laniustrolling.dev>
Sun, 22 Dec 2024 20:46:24 +0000 (15:46 -0500)
src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt

index c33a6dff505643e04cc559939b1380b17cbcfb3a..46c57957035660fee60bc34f1c9207b250a50fad 100644 (file)
@@ -34,7 +34,13 @@ import io.ktor.server.routing.Route
 import io.ktor.server.routing.method
 import io.ktor.server.routing.route
 import io.ktor.util.AttributeKey
-import kotlinx.html.*
+import kotlinx.html.a
+import kotlinx.html.body
+import kotlinx.html.h1
+import kotlinx.html.head
+import kotlinx.html.li
+import kotlinx.html.title
+import kotlinx.html.ul
 import java.net.URI
 import java.time.Instant
 import java.time.ZoneOffset
@@ -69,7 +75,10 @@ sealed class WebDavProperties : XmlInsertable {
                                        resourceProps()
                                        "supportedlock" {
                                                "lockentry" {
-                                                       "lockscope" { "shared"() }
+                                                       "lockscope" {
+                                                               "shared"()
+                                                               "exclusive"()
+                                                       }
                                                        "locktype" { "write"() }
                                                }
                                        }
@@ -356,28 +365,139 @@ suspend fun ApplicationCall.webDavDelete(path: StoragePath) {
                respond(HttpStatusCode.NotFound)
 }
 
-suspend fun ApplicationCall.webDavLock(path: StoragePath) {
-       beforeWebDav()
+private sealed interface WebDavLockInfo {
+       @JvmInline
+       value class Scalar(val value: String?) : WebDavLockInfo
        
-       if (request.header(HttpHeaders.ContentType) != null)
-               receiveText()
+       @JvmInline
+       value class Complex(val subTags: Map<String, WebDavLockInfo>) : WebDavLockInfo
+}
+
+private fun XmlTag.webDavLockInfo(info: WebDavLockInfo, tag: String) {
+       when (info) {
+               is WebDavLockInfo.Scalar -> {
+                       info.value?.let {
+                               tag { +it }
+                       } ?: tag()
+               }
+               
+               is WebDavLockInfo.Complex -> {
+                       tag {
+                               for ((k, v) in info.subTags)
+                                       webDavLockInfo(v, k)
+                       }
+               }
+       }
+}
+
+private fun getXmlTagPrefix(line: String): String {
+       val xmlnsIndex = line.indexOf("xmlns:")
+       if (xmlnsIndex < 0)
+               return ""
+       
+       val namespace = line.substring(xmlnsIndex + 6).substringBefore('=')
+       return "$namespace:"
+}
+
+private enum class XmlTagType {
+       SCALAR, OPEN, CLOSE
+}
+
+private fun getXmlTagType(line: String, prefix: String): Triple<String, XmlTagType, String?>? {
+       val (isClose, lineMinusAngleBracket) = if (line.startsWith("</"))
+               Pair(true, line.substring(2))
+       else if (line.startsWith("<"))
+               Pair(false, line.substring(1))
+       else
+               return null
+       
+       val lineMinusPrefix = lineMinusAngleBracket.removePrefix(prefix)
+       val tag = lineMinusPrefix.substringBefore('>')
+       if (isClose)
+               return Triple(tag, XmlTagType.CLOSE, null)
+       
+       if (tag.endsWith("/"))
+               return Triple(tag.substring(0, tag.length - 1), XmlTagType.SCALAR, null)
+       
+       if (lineMinusPrefix.endsWith("</$prefix$tag>"))
+               return Triple(tag, XmlTagType.SCALAR, lineMinusPrefix.substring(tag.length + 1, tag.length + prefix.length + 3))
        
+       return Triple(tag, XmlTagType.OPEN, null)
+}
+
+private fun <T : Any> Iterator<T>.nextOrNull(): T? = if (hasNext()) next() else null
+
+private fun Iterator<String>.buildLockInfoTree(xmlTagPrefix: String? = null): Pair<Pair<String, WebDavLockInfo>?, Boolean> {
+       val line = nextOrNull()?.trim() ?: return Pair(null, false)
+       if (line.isBlank() || line.startsWith("<?"))
+               return Pair(null, true)
+       
+       val prefix = xmlTagPrefix ?: getXmlTagPrefix(line)
+       val (tagName, tagType, tagValue) = getXmlTagType(line, prefix) ?: return Pair(null, true)
+       
+       when (tagType) {
+               XmlTagType.SCALAR -> return Pair(Pair(tagName, WebDavLockInfo.Scalar(tagValue)), true)
+               XmlTagType.OPEN -> {
+                       val subTrees = buildMap {
+                               do {
+                                       val (subTree, hasNext) = buildLockInfoTree(prefix)
+                                       subTree?.let { (k, v) -> put(k, v) }
+                               } while (hasNext)
+                       }
+                       
+                       return Pair(Pair(tagName, WebDavLockInfo.Complex(subTrees)), true)
+               }
+               
+               XmlTagType.CLOSE -> return Pair(null, false)
+       }
+}
+
+private val defaultLockInfo: WebDavLockInfo.Complex = WebDavLockInfo.Complex(
+       mapOf(
+               "lockscope" to WebDavLockInfo.Complex(
+                       mapOf(
+                               "shared" to WebDavLockInfo.Scalar(null)
+                       )
+               ),
+               "locktype" to WebDavLockInfo.Complex(
+                       mapOf(
+                               "write" to WebDavLockInfo.Scalar(null)
+                       )
+               ),
+               "owner" to WebDavLockInfo.Scalar(null),
+       )
+)
+
+private const val InfiniteTimeout: String = "Second-31556925000"
+
+private suspend fun ApplicationCall.parseRequestLockInfo(): WebDavLockInfo.Complex {
        val depth = request.header(HttpHeaders.Depth) ?: "Infinity"
+       val (requestLockInfoTag, _) = receiveText().lineSequence().iterator().buildLockInfoTree()
+       val requestLockInfo = requestLockInfoTag?.takeIf { it.first == "lockinfo" }?.second as? WebDavLockInfo.Complex ?: defaultLockInfo
+       
+       return WebDavLockInfo.Complex(
+               requestLockInfo.subTags + mapOf(
+                       "depth" to WebDavLockInfo.Scalar(depth),
+                       "timeout" to WebDavLockInfo.Scalar(InfiniteTimeout),
+                       "locktoken" to WebDavLockInfo.Complex(
+                               mapOf(
+                                       "href" to WebDavLockInfo.Scalar("opaquelocktoken:${UUID.randomUUID()}")
+                               )
+                       )
+               )
+       )
+}
+
+suspend fun ApplicationCall.webDavLock(path: StoragePath) {
+       beforeWebDav()
+       
+       val info = parseRequestLockInfo()
        
        respondXml {
                declaration()
                        .root("prop", namespace = "DAV:") {
                                "lockdiscovery" {
-                                       "activelock" {
-                                               "lockscope" { "shared"() }
-                                               "locktype" { "write"() }
-                                               "depth" { +depth }
-                                               "owner"()
-                                               "timeout" { +"Second-86400" }
-                                               "locktoken" {
-                                                       "href" { +"opaquelocktoken:${UUID.randomUUID()}" }
-                                               }
-                                       }
+                                       webDavLockInfo(info, "activeLock")
                                }
                        }
        }