Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

Commit d01dba0

Browse files
sunkuprfc2822
andauthored
FixInvalidDayOffsetPreprocessor: Allow DURATION as value (#182)
* Add test * Allow duration as value * Restrict the regex to properties that can have DURATION as a value * Tighten regex further * Convert negative asserts (not equals) to positive asserts (test for what is expected) * Test KDoc * Use capturing group; Add comments * Use replaceRange --------- Co-authored-by: Ricki Hirner <[email protected]>
1 parent 59d2728 commit d01dba0

File tree

3 files changed

+65
-29
lines changed

3 files changed

+65
-29
lines changed

lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,27 @@ object FixInvalidDayOffsetPreprocessor : StreamPreprocessor() {
1414
// Examples:
1515
// TRIGGER:-P2DT
1616
// TRIGGER:-PT2D
17-
"^(DURATION|TRIGGER):-?P((T-?\\d+D)|(-?\\d+DT))\$",
17+
// REFRESH-INTERVAL;VALUE=DURATION:-PT1D
18+
"(?:^|^(?:DURATION|REFRESH-INTERVAL|RELATED-TO|TRIGGER);VALUE=)(?:DURATION|TRIGGER):(-?P((T-?\\d+D)|(-?\\d+DT)))$",
1819
setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
1920
)
2021

2122
override fun fixString(original: String): String {
22-
var s: String = original
23+
var iCal: String = original
2324

2425
// Find all instances matching the defined expression
25-
val found = regexpForProblem().findAll(s)
26+
val found = regexpForProblem().findAll(iCal).toList()
2627

27-
// ..and repair them
28-
for (match in found) {
29-
val matchStr = match.value
30-
val fixed = matchStr
31-
.replace("PT", "P")
32-
.replace("DT", "D")
33-
s = s.replace(matchStr, fixed)
28+
// ... and repair them. Use reversed order so that already replaced occurrences don't interfere with the following matches.
29+
for (match in found.reversed()) {
30+
match.groups[1]?.let { duration -> // first capturing group is the duration value, for instance: "-PT1D"
31+
val fixed = duration.value // fixed is then for instance: "-P1D"
32+
.replace("PT", "P")
33+
.replace("DT", "D")
34+
iCal = iCal.replaceRange(duration.range, fixed)
35+
}
3436
}
35-
return s
37+
return iCal
3638
}
3739

3840
}

lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ abstract class StreamPreprocessor {
1313

1414
abstract fun regexpForProblem(): Regex?
1515

16+
/**
17+
* Fixes an iCalendar string.
18+
*
19+
* @param original The complete iCalendar string
20+
* @return The complete iCalendar string, but fixed
21+
*/
1622
abstract fun fixString(original: String): String
1723

1824
fun preprocess(reader: Reader): Reader {
@@ -21,7 +27,7 @@ abstract class StreamPreprocessor {
2127
val resetSupported = try {
2228
reader.reset()
2329
true
24-
} catch(e: IOException) {
30+
} catch(_: IOException) {
2531
false
2632
}
2733

lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,23 @@ import java.time.Duration
1212

1313
class FixInvalidDayOffsetPreprocessorTest {
1414

15-
private fun fixAndAssert(expected: String, testValue: String) {
16-
15+
/**
16+
* Calls [FixInvalidDayOffsetPreprocessor.fixString] and asserts the result is equal to [expected].
17+
*
18+
* @param expected The expected result
19+
* @param testValue The value to test
20+
* @param parseDuration If `true`, [Duration.parse] is called on the fixed value to make sure it's a valid duration
21+
*/
22+
private fun assertFixedEquals(expected: String, testValue: String, parseDuration: Boolean = true) {
1723
// Fix the duration string
1824
val fixed = FixInvalidDayOffsetPreprocessor.fixString(testValue)
1925

2026
// Test the duration can now be parsed
21-
for (line in fixed.split('\n')) {
22-
val duration = line.substring(line.indexOf(':') + 1)
23-
Duration.parse(duration)
24-
}
27+
if (parseDuration)
28+
for (line in fixed.split('\n')) {
29+
val duration = line.substring(line.indexOf(':') + 1)
30+
Duration.parse(duration)
31+
}
2532

2633
// Assert
2734
assertEquals(expected, fixed)
@@ -35,36 +42,57 @@ class FixInvalidDayOffsetPreprocessorTest {
3542
)
3643
}
3744

45+
@Test
46+
fun test_FixString_SucceedsAsValueOnCorrectProperties() {
47+
// By RFC 5545 the only properties allowed to hold DURATION as a VALUE are:
48+
// DURATION, REFRESH, RELATED, TRIGGER
49+
assertFixedEquals("DURATION;VALUE=DURATION:P1D", "DURATION;VALUE=DURATION:PT1D")
50+
assertFixedEquals("REFRESH-INTERVAL;VALUE=DURATION:P1D", "REFRESH-INTERVAL;VALUE=DURATION:PT1D")
51+
assertFixedEquals("RELATED-TO;VALUE=DURATION:P1D", "RELATED-TO;VALUE=DURATION:PT1D")
52+
assertFixedEquals("TRIGGER;VALUE=DURATION:P1D", "TRIGGER;VALUE=DURATION:PT1D")
53+
}
54+
55+
@Test
56+
fun test_FixString_FailsAsValueOnWrongProperty() {
57+
// The update from RFC 2445 to RFC 5545 disallows using DURATION as a VALUE in FREEBUSY
58+
assertFixedEquals("FREEBUSY;VALUE=DURATION:PT1D", "FREEBUSY;VALUE=DURATION:PT1D", parseDuration = false)
59+
}
60+
61+
@Test
62+
fun test_FixString_FailsIfNotAtStartOfLine() {
63+
assertFixedEquals("xxDURATION;VALUE=DURATION:PT1D", "xxDURATION;VALUE=DURATION:PT1D", parseDuration = false)
64+
}
65+
3866
@Test
3967
fun test_FixString_DayOffsetFrom_Invalid() {
40-
fixAndAssert("DURATION:-P1D", "DURATION:-PT1D")
41-
fixAndAssert("TRIGGER:-P2D", "TRIGGER:-PT2D")
68+
assertFixedEquals("DURATION:-P1D", "DURATION:-PT1D")
69+
assertFixedEquals("TRIGGER:-P2D", "TRIGGER:-PT2D")
4270

43-
fixAndAssert("DURATION:-P1D", "DURATION:-P1DT")
44-
fixAndAssert("TRIGGER:-P2D", "TRIGGER:-P2DT")
71+
assertFixedEquals("DURATION:-P1D", "DURATION:-P1DT")
72+
assertFixedEquals("TRIGGER:-P2D", "TRIGGER:-P2DT")
4573
}
4674

4775
@Test
4876
fun test_FixString_DayOffsetFrom_Valid() {
49-
fixAndAssert("DURATION:-PT12H", "DURATION:-PT12H")
50-
fixAndAssert("TRIGGER:-PT12H", "TRIGGER:-PT12H")
77+
assertFixedEquals("DURATION:-PT12H", "DURATION:-PT12H")
78+
assertFixedEquals("TRIGGER:-PT12H", "TRIGGER:-PT12H")
5179
}
5280

5381
@Test
5482
fun test_FixString_DayOffsetFromMultiple_Invalid() {
55-
fixAndAssert("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-PT1D\nTRIGGER:-PT2D")
56-
fixAndAssert("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-P1DT\nTRIGGER:-P2DT")
83+
assertFixedEquals("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-PT1D\nTRIGGER:-PT2D")
84+
assertFixedEquals("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-P1DT\nTRIGGER:-P2DT")
5785
}
5886

5987
@Test
6088
fun test_FixString_DayOffsetFromMultiple_Valid() {
61-
fixAndAssert("DURATION:-PT12H\nTRIGGER:-PT12H", "DURATION:-PT12H\nTRIGGER:-PT12H")
89+
assertFixedEquals("DURATION:-PT12H\nTRIGGER:-PT12H", "DURATION:-PT12H\nTRIGGER:-PT12H")
6290
}
6391

6492
@Test
6593
fun test_FixString_DayOffsetFromMultiple_Mixed() {
66-
fixAndAssert("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-PT1D\nDURATION:-PT12H\nTRIGGER:-PT2D")
67-
fixAndAssert("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-P1DT\nDURATION:-PT12H\nTRIGGER:-P2DT")
94+
assertFixedEquals("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-PT1D\nDURATION:-PT12H\nTRIGGER:-PT2D")
95+
assertFixedEquals("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-P1DT\nDURATION:-PT12H\nTRIGGER:-P2DT")
6896
}
6997

7098
@Test

0 commit comments

Comments
 (0)