Skip to content

Commit c11ebb6

Browse files
committed
Require <getetag> eTag values
- introduce XmlUtils.requireReadText() - Property.parse(): ignore invalid properties - GetETag: require input text so that the value fields don't have to be nullable
1 parent f434c9d commit c11ebb6

File tree

8 files changed

+140
-47
lines changed

8 files changed

+140
-47
lines changed

src/main/kotlin/at/bitfire/dav4jvm/Property.kt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
package at.bitfire.dav4jvm
88

99
import at.bitfire.dav4jvm.Dav4jvm.log
10+
import at.bitfire.dav4jvm.exception.InvalidPropertyException
1011
import org.xmlpull.v1.XmlPullParser
1112
import java.io.Serializable
1213
import java.util.*
14+
import java.util.logging.Level
1315

1416
/**
1517
* Represents a WebDAV property.
@@ -20,8 +22,8 @@ import java.util.*
2022
interface Property {
2123

2224
data class Name(
23-
val namespace: String,
24-
val name: String
25+
val namespace: String,
26+
val name: String
2527
): Serializable {
2628

2729
override fun toString() = "$namespace:$name"
@@ -39,14 +41,19 @@ interface Property {
3941
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
4042
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1) {
4143
val depthBeforeParsing = parser.depth
42-
val name = Property.Name(parser.namespace, parser.name)
43-
val property = PropertyRegistry.create(name, parser)
44-
assert(parser.depth == depthBeforeParsing)
45-
46-
if (property != null) {
47-
properties.add(property)
48-
} else
49-
log.fine("Ignoring unknown property $name")
44+
val name = Name(parser.namespace, parser.name)
45+
46+
try {
47+
val property = PropertyRegistry.create(name, parser)
48+
assert(parser.depth == depthBeforeParsing)
49+
50+
if (property != null) {
51+
properties.add(property)
52+
} else
53+
log.fine("Ignoring unknown property $name")
54+
} catch (e: InvalidPropertyException) {
55+
log.log(Level.WARNING, "Ignoring invalid property", e)
56+
}
5057
}
5158
eventType = parser.next()
5259
}

src/main/kotlin/at/bitfire/dav4jvm/XmlUtils.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package at.bitfire.dav4jvm
88

9+
import at.bitfire.dav4jvm.exception.InvalidPropertyException
910
import org.xmlpull.v1.XmlPullParser
1011
import org.xmlpull.v1.XmlPullParserException
1112
import org.xmlpull.v1.XmlPullParserFactory
@@ -60,6 +61,16 @@ object XmlUtils {
6061
return text
6162
}
6263

64+
/**
65+
* Same as [readText], but requires a [XmlPullParser.TEXT] value.
66+
*
67+
* @throws InvalidPropertyException when no text could be read
68+
*/
69+
@Throws(InvalidPropertyException::class, IOException::class, XmlPullParserException::class)
70+
fun requireReadText(parser: XmlPullParser): String =
71+
readText(parser) ?:
72+
throw InvalidPropertyException("XML text for ${parser.namespace}:${parser.name} must not be empty")
73+
6374
@Throws(IOException::class, XmlPullParserException::class)
6475
fun readTextProperty(parser: XmlPullParser, name: Property.Name): String? {
6576
val depth = parser.depth
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package at.bitfire.dav4jvm.exception
2+
3+
/**
4+
* Represents an invalid XML (WebDAV) property. This is for instance thrown
5+
* when parsing something like `<multistatus>...<getetag><novalue/></getetag>`
6+
* because a text value would be expected.
7+
*/
8+
class InvalidPropertyException(message: String): Exception(message)

src/main/kotlin/at/bitfire/dav4jvm/property/GetETag.kt

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import org.xmlpull.v1.XmlPullParser
2020
* header value to the constructor and then use [eTag] and [weak].
2121
*/
2222
class GetETag(
23-
rawETag: String?
23+
rawETag: String
2424
): Property {
2525

2626
companion object {
@@ -32,46 +32,52 @@ class GetETag(
3232
}
3333

3434
/**
35-
* The parsed eTag value. May be null if the tag is weak.
35+
* The parsed ETag value, excluding the weakness indicator and the quotes.
3636
*/
37-
val eTag: String?
37+
val eTag: String
3838

3939
/**
40-
* If the tag is weak. May be null if the tag passed is null.
40+
* Whether the ETag is weak.
4141
*/
42-
val weak: Boolean?
42+
var weak: Boolean
4343

4444
init {
4545
/* entity-tag = [ weak ] opaque-tag
4646
weak = "W/"
4747
opaque-tag = quoted-string
4848
*/
49-
var tag: String? = rawETag
50-
if (tag != null) {
51-
// remove trailing "W/"
52-
if (tag.startsWith("W/") && tag.length >= 3) {
53-
// entity tag is weak
54-
tag = tag.substring(2)
55-
weak = true
56-
} else
57-
weak = false
58-
59-
tag = QuotedStringUtils.decodeQuotedString(tag)
49+
var tag = rawETag
50+
51+
// remove trailing "W/"
52+
if (tag.startsWith("W/") && tag.length >= 3) {
53+
// entity tag is weak
54+
tag = tag.substring(2)
55+
weak = true
6056
} else
61-
weak = null
57+
weak = false
6258

63-
eTag = tag
59+
eTag = QuotedStringUtils.decodeQuotedString(tag)
6460
}
6561

66-
override fun toString() = eTag ?: "(null)"
62+
override fun toString() = "ETag(weak=${weak}, tag=$eTag)"
63+
64+
override fun equals(other: Any?): Boolean {
65+
if (other !is GetETag)
66+
return false
67+
return eTag == other.eTag && weak == other.weak
68+
}
69+
70+
override fun hashCode(): Int {
71+
return eTag.hashCode() xor weak.hashCode()
72+
}
6773

6874

6975
object Factory: PropertyFactory {
7076

7177
override fun getName() = NAME
7278

7379
override fun create(parser: XmlPullParser) =
74-
GetETag(XmlUtils.readText(parser))
80+
GetETag(XmlUtils.requireReadText(parser))
7581

7682
}
7783

src/test/kotlin/at/bitfire/dav4jvm/DavCollectionTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,24 @@ class DavCollectionTest {
105105
assertTrue(response.isSuccess())
106106
assertEquals(Response.HrefRelation.MEMBER, relation)
107107
val eTag = response[GetETag::class.java]
108-
assertEquals("00001-abcd1", eTag?.eTag)
109-
assertTrue(eTag?.weak == false)
108+
assertEquals("00001-abcd1", eTag!!.eTag)
109+
assertFalse(eTag.weak)
110110
nrCalled++
111111
}
112112
url.resolve("/dav/vcard.vcf") -> {
113113
assertTrue(response.isSuccess())
114114
assertEquals(Response.HrefRelation.MEMBER, relation)
115115
val eTag = response[GetETag::class.java]
116-
assertEquals("00002-abcd1", eTag?.eTag)
117-
assertTrue(eTag?.weak == false)
116+
assertEquals("00002-abcd1", eTag!!.eTag)
117+
assertFalse(eTag.weak)
118118
nrCalled++
119119
}
120120
url.resolve("/dav/calendar.ics") -> {
121121
assertTrue(response.isSuccess())
122122
assertEquals(Response.HrefRelation.MEMBER, relation)
123123
val eTag = response[GetETag::class.java]
124-
assertEquals("00003-abcd1", eTag?.eTag)
125-
assertTrue(eTag?.weak == false)
124+
assertEquals("00003-abcd1", eTag!!.eTag)
125+
assertFalse(eTag.weak)
126126
nrCalled++
127127
}
128128
}

src/test/kotlin/at/bitfire/dav4jvm/DavResourceTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ class DavResourceTest {
245245

246246
val eTag = GetETag.fromResponse(response)
247247
assertEquals("My Weak ETag", eTag!!.eTag)
248-
assertTrue(eTag.weak!!)
248+
assertTrue(eTag.weak)
249249
assertEquals("application/x-test-result".toMediaType(), GetContentType(response.body!!.contentType()!!).type)
250250
}
251251
assertTrue(called)
@@ -268,9 +268,9 @@ class DavResourceTest {
268268
dav.get("*/*", null) { response ->
269269
called = true
270270
assertEquals(sampleText, response.body!!.string())
271-
val eTag = GetETag(response.header("ETag"))
271+
val eTag = GetETag(response.header("ETag")!!)
272272
assertEquals("StrongETag", eTag.eTag)
273-
assertFalse(eTag.weak!!)
273+
assertFalse(eTag.weak)
274274
}
275275
assertTrue(called)
276276

@@ -768,7 +768,7 @@ class DavResourceTest {
768768
dav.proppatch(
769769
setProperties = mapOf(Pair(Property.Name("sample", "setThis"), "Some Value")),
770770
removeProperties = listOf(Property.Name("sample", "removeThis"))
771-
) { response, hrefRelation ->
771+
) { _, hrefRelation ->
772772
called = true
773773
assertEquals(Response.HrefRelation.SELF, hrefRelation)
774774
}
@@ -804,7 +804,7 @@ class DavResourceTest {
804804
called = true
805805
val eTag = GetETag.fromResponse(response)!!
806806
assertEquals("Weak PUT ETag", eTag.eTag)
807-
assertTrue(eTag.weak!!)
807+
assertTrue(eTag.weak)
808808
assertEquals(response.request.url, dav.location)
809809
}
810810
assertTrue(called)
@@ -871,7 +871,7 @@ class DavResourceTest {
871871
" </response>" +
872872
"</multistatus>"))
873873
var called = false
874-
dav.search("<TEST/>") { response, hrefRelation, ->
874+
dav.search("<TEST/>") { response, hrefRelation ->
875875
assertEquals(Response.HrefRelation.SELF, hrefRelation)
876876
assertEquals("Found something", response[DisplayName::class.java]?.displayName)
877877
called = true
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package at.bitfire.dav4jvm
8+
9+
import at.bitfire.dav4jvm.property.GetETag
10+
import org.junit.Assert.assertEquals
11+
import org.junit.Assert.assertTrue
12+
import org.junit.Test
13+
import org.xmlpull.v1.XmlPullParser
14+
import org.xmlpull.v1.XmlPullParserFactory
15+
import java.io.StringReader
16+
17+
class PropertyTest {
18+
19+
@Test
20+
fun testParse_InvalidProperty() {
21+
val parser = XmlPullParserFactory.newInstance().apply {
22+
isNamespaceAware = true
23+
}.newPullParser()
24+
parser.setInput(StringReader("<multistatus xmlns='DAV:'><getetag/></multistatus>"))
25+
do {
26+
parser.next()
27+
} while (parser.eventType != XmlPullParser.START_TAG && parser.name != "multistatus")
28+
29+
// we're now at the start of <multistatus>
30+
assertEquals(XmlPullParser.START_TAG, parser.eventType)
31+
assertEquals("multistatus", parser.name)
32+
33+
// parse invalid DAV:getetag
34+
assertTrue(Property.parse(parser).isEmpty())
35+
36+
// we're now at the end of <multistatus>
37+
assertEquals(XmlPullParser.END_TAG, parser.eventType)
38+
assertEquals("multistatus", parser.name)
39+
}
40+
41+
@Test
42+
fun testParse_ValidProperty() {
43+
val parser = XmlPullParserFactory.newInstance().apply {
44+
isNamespaceAware = true
45+
}.newPullParser()
46+
parser.setInput(StringReader("<multistatus xmlns='DAV:'><getetag>12345</getetag></multistatus>"))
47+
do {
48+
parser.next()
49+
} while (parser.eventType != XmlPullParser.START_TAG && parser.name != "multistatus")
50+
51+
// we're now at the start of <multistatus>
52+
assertEquals(XmlPullParser.START_TAG, parser.eventType)
53+
assertEquals("multistatus", parser.name)
54+
55+
val etag = Property.parse(parser).first()
56+
assertEquals(GetETag("12345"), etag)
57+
58+
// we're now at the end of <multistatus>
59+
assertEquals(XmlPullParser.END_TAG, parser.eventType)
60+
assertEquals("multistatus", parser.name)
61+
}
62+
63+
}

src/test/kotlin/at/bitfire/dav4jvm/property/GetETagTest.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
package at.bitfire.dav4jvm.property
22

3+
import at.bitfire.dav4jvm.exception.InvalidPropertyException
34
import org.junit.Assert.*
45
import org.junit.Test
56

67
class GetETagTest: PropertyTest() {
78

8-
@Test
9+
@Test()
910
fun testGetETag_NoText() {
10-
val results = parseProperty("<getetag xmlns=\"DAV:\"><invalid/></getetag>")
11-
val getETag = results.first() as GetETag
12-
assertNull(getETag.eTag)
13-
assertNull(getETag.weak)
11+
assertTrue(parseProperty("<getetag xmlns=\"DAV:\"><invalid/></getetag>").isEmpty())
1412
}
1513

1614
@Test

0 commit comments

Comments
 (0)