Skip to content

Commit d1ba5b3

Browse files
authored
Extract Href Resolving Logic (#134)
- Move href resolving logic to a separate method `resolveHref` - Add unit tests for `resolveHref` in `ResponseParserTest`
1 parent 23285c7 commit d1ba5b3

File tree

2 files changed

+99
-32
lines changed

2 files changed

+99
-32
lines changed

src/main/kotlin/at/bitfire/dav4jvm/ktor/ResponseParser.kt

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ import at.bitfire.dav4jvm.property.webdav.ResourceType
1717
import at.bitfire.dav4jvm.property.webdav.WebDAV
1818
import io.ktor.http.HttpStatusCode
1919
import io.ktor.http.URLBuilder
20+
import io.ktor.http.URLParserException
2021
import io.ktor.http.Url
2122
import io.ktor.http.isSuccess
2223
import io.ktor.http.takeFrom
24+
import org.jetbrains.annotations.VisibleForTesting
2325
import org.xmlpull.v1.XmlPullParser
26+
import java.util.logging.Level
2427
import java.util.logging.Logger
2528

2629
/**
@@ -60,38 +63,8 @@ class ResponseParser(
6063
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
6164
if (eventType == XmlPullParser.START_TAG && parser.depth == depth+1)
6265
when (parser.propertyName()) {
63-
WebDAV.Href -> {
64-
var sHref = parser.nextText()
65-
var hierarchical = false
66-
if (!sHref.startsWith("/")) {
67-
/* According to RFC 4918 8.3 URL Handling, only absolute paths are allowed as relative
68-
URLs. However, some servers reply with relative paths. */
69-
val firstColon = sHref.indexOf(':')
70-
if (firstColon != -1) {
71-
/* There are some servers which return not only relative paths, but relative paths like "a:b.vcf",
72-
which would be interpreted as scheme: "a", scheme-specific part: "b.vcf" normally.
73-
For maximum compatibility, we prefix all relative paths which contain ":" (but not "://"),
74-
with "./" to allow resolving by HttpUrl. */
75-
try {
76-
if (sHref.substring(firstColon, firstColon + 3) == "://")
77-
hierarchical = true
78-
} catch (e: IndexOutOfBoundsException) {
79-
// no "://"
80-
}
81-
if (!hierarchical)
82-
sHref = "./$sHref"
83-
84-
}
85-
}
86-
87-
if (!hierarchical) {
88-
val urlBuilder = URLBuilder(location).takeFrom(sHref)
89-
urlBuilder.pathSegments = urlBuilder.pathSegments.filterNot { it == "." } // Drop segments that are "./"
90-
hrefOrNull = urlBuilder.build()
91-
} else {
92-
hrefOrNull = URLBuilder(location).takeFrom(sHref).build()
93-
}
94-
}
66+
WebDAV.Href ->
67+
hrefOrNull = resolveHref(parser.nextText())
9568
WebDAV.Status ->
9669
status = KtorHttpUtils.parseStatusLine(parser.nextText())
9770
WebDAV.PropStat ->
@@ -168,4 +141,42 @@ class ResponseParser(
168141
)
169142
}
170143

144+
@VisibleForTesting
145+
internal fun resolveHref(hrefString: String): Url? {
146+
var sHref = hrefString
147+
148+
var preserve = false
149+
if (!sHref.startsWith("/")) {
150+
/* According to RFC 4918 8.3 URL Handling, only absolute paths are allowed as relative
151+
URLs. However, some servers reply with relative paths. */
152+
val firstColon = sHref.indexOf(':')
153+
if (firstColon != -1) {
154+
/* There are some servers which return not only relative paths, but relative paths like "a:b.vcf",
155+
which would be interpreted as scheme: "a", scheme-specific part: "b.vcf" normally.
156+
For maximum compatibility, we prefix all relative paths which contain ":" (but not "://"),
157+
with "./" to allow resolving by HttpUrl. */
158+
try {
159+
if (sHref.substring(firstColon, firstColon + 3) == "://")
160+
preserve = true
161+
} catch (_: IndexOutOfBoundsException) {
162+
// no "://"
163+
}
164+
if (!preserve)
165+
sHref = "./$sHref"
166+
}
167+
}
168+
169+
val urlBuilder = try {
170+
URLBuilder(location).takeFrom(sHref)
171+
} catch (e: URLParserException) {
172+
logger.log(Level.WARNING, "Unresolvable <href> in <response>: $hrefString", e)
173+
return null
174+
}
175+
176+
if (!preserve) // drop segments that are "./"
177+
urlBuilder.pathSegments = urlBuilder.pathSegments.filterNot { it == "." }
178+
179+
return urlBuilder.build()
180+
}
181+
171182
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* SPDX-License-Identifier: MPL-2.0
9+
*/
10+
11+
package at.bitfire.dav4jvm.ktor
12+
13+
import io.ktor.http.Url
14+
import org.junit.Assert.assertEquals
15+
import org.junit.Test
16+
17+
class ResponseParserTest {
18+
19+
val baseUrl = Url("https://example.com/collection/")
20+
val parser = ResponseParser(baseUrl, callback = { _, _ ->
21+
// no-op
22+
})
23+
24+
@Test
25+
fun `resolveHref with absolute URL`() {
26+
assertEquals(
27+
Url("https://example.com/collection/member"),
28+
parser.resolveHref("https://example.com/collection/member")
29+
)
30+
}
31+
32+
@Test
33+
fun `resolveHref with absolute path`() {
34+
assertEquals(
35+
Url("https://example.com/collection/member"),
36+
parser.resolveHref("/collection/member")
37+
)
38+
}
39+
40+
@Test
41+
fun `resolveHref with relative path`() {
42+
assertEquals(
43+
Url("https://example.com/collection/member"),
44+
parser.resolveHref("member")
45+
)
46+
}
47+
48+
@Test
49+
fun `resolveHref with relative path with colon`() {
50+
assertEquals(
51+
Url("https://example.com/collection/mem:ber"),
52+
parser.resolveHref("mem:ber")
53+
)
54+
}
55+
56+
}

0 commit comments

Comments
 (0)