Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit c39b4544 authored by Arnau Mora's avatar Arnau Mora Committed by GitHub
Browse files

Handling period durations of days with `T` prefix (#74)



* Added `fixInvalidDayOffset`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Added tests for `fixInvalidDayOffset`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Implemented `fixInvalidDayOffset`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Added `testFixInvalidDuration`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Comments

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Generalize stream preprocessors

* Using `regexpForProblem` instead of `INVALID_DAY_PERIOD_REGEX`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Clean fixme

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Added `FixInvalidDayOffsetPreprocessorTest`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Typo

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Fixed tests

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Moved test to `FixInvalidDayOffsetPreprocessorTest`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Changed conversion to `PT`-`P`

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Updated tests

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>

* Minor changes

Signed-off-by: default avatarArnau Mora <arnyminer.z@gmail.com>
Co-authored-by: default avatarRicki Hirner <hirner@bitfire.at>
parent 9c970b59
Loading
Loading
Loading
Loading
+54 −3
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ import org.junit.Assert.assertEquals
import org.junit.Test
import java.io.InputStreamReader
import java.io.StringReader
import java.time.Duration

class ICalPreprocessorTest {

@@ -56,10 +57,60 @@ class ICalPreprocessorTest {
                "TZNAME:EST" +
                "END:STANDARD\n" +
                "END:VTIMEZONE"
        ICalPreprocessor.fixInvalidUtcOffset(StringReader(invalid)).let { result ->
        ICalPreprocessor.preprocessStream(StringReader(invalid)).let { result ->
            assertEquals(valid, IOUtils.toString(result))
        }
        ICalPreprocessor.fixInvalidUtcOffset(StringReader(valid)).let { result ->
        ICalPreprocessor.preprocessStream(StringReader(valid)).let { result ->
            assertEquals(valid, IOUtils.toString(result))
        }
    }

    @Test
    fun testFixInvalidDuration() {
        val invalid = "BEGIN:VEVENT\n" +
                "LAST-MODIFIED:20230108T011226Z\n" +
                "DTSTAMP:20230108T011226Z\n" +
                "X-ECAL-SCHEDULE:63b0e38979739f000d5c1724\n" +
                "DTSTART:20230101T015100Z\n" +
                "DTEND:20230101T020600Z\n" +
                "SUMMARY:This is a test event\n" +
                "TRANSP:TRANSPARENT\n" +
                "SEQUENCE:0\n" +
                "UID:63b0e389453c5d000e1161ae\n" +
                "PRIORITY:5\n" +
                "X-MICROSOFT-CDO-IMPORTANCE:1\n" +
                "CLASS:PUBLIC\n" +
                "DESCRIPTION:Example description\n" +
                "BEGIN:VALARM\n" +
                "TRIGGER:-PT2D\n" +
                "ACTION:DISPLAY\n" +
                "DESCRIPTION:Reminder\n" +
                "END:VALARM\n" +
                "END:VEVENT"
        val valid = "BEGIN:VEVENT\n" +
                "LAST-MODIFIED:20230108T011226Z\n" +
                "DTSTAMP:20230108T011226Z\n" +
                "X-ECAL-SCHEDULE:63b0e38979739f000d5c1724\n" +
                "DTSTART:20230101T015100Z\n" +
                "DTEND:20230101T020600Z\n" +
                "SUMMARY:This is a test event\n" +
                "TRANSP:TRANSPARENT\n" +
                "SEQUENCE:0\n" +
                "UID:63b0e389453c5d000e1161ae\n" +
                "PRIORITY:5\n" +
                "X-MICROSOFT-CDO-IMPORTANCE:1\n" +
                "CLASS:PUBLIC\n" +
                "DESCRIPTION:Example description\n" +
                "BEGIN:VALARM\n" +
                "TRIGGER:-P2D\n" +
                "ACTION:DISPLAY\n" +
                "DESCRIPTION:Reminder\n" +
                "END:VALARM\n" +
                "END:VEVENT"
        ICalPreprocessor.preprocessStream(StringReader(invalid)).let { result ->
            assertEquals(valid, IOUtils.toString(result))
        }
        ICalPreprocessor.preprocessStream(StringReader(valid)).let { result ->
            assertEquals(valid, IOUtils.toString(result))
        }
    }
@@ -72,7 +123,7 @@ class ICalPreprocessorTest {
            val vEvent = calendar.getComponent(Component.VEVENT) as VEvent

            assertEquals("W. Europe Standard Time", vEvent.startDate.timeZone.id)
            ICalPreprocessor.preProcess(calendar)
            ICalPreprocessor.preprocessCalendar(calendar)
            assertEquals("Europe/Vienna", vEvent.startDate.timeZone.id)
        }
    }
+4 −4
Original line number Diff line number Diff line
@@ -80,13 +80,13 @@ open class ICalendar {
            Ical4Android.log.fine("Parsing iCalendar stream")
            Ical4Android.checkThreadContextClassLoader()

            // apply hacks and workarounds that operate on plain text level
            val reader2 = ICalPreprocessor.fixInvalidUtcOffset(reader)
            // preprocess stream to work around some problems that can't be fixed later
            val preprocessed = ICalPreprocessor.preprocessStream(reader)

            // parse stream
            val calendar: Calendar
            try {
                calendar = CalendarBuilder().build(reader2)
                calendar = CalendarBuilder().build(preprocessed)
            } catch(e: ParserException) {
                throw InvalidCalendarException("Couldn't parse iCalendar", e)
            } catch(e: IllegalArgumentException) {
@@ -95,7 +95,7 @@ open class ICalendar {

            // apply ICalPreprocessor for increased compatibility
            try {
                ICalPreprocessor.preProcess(calendar)
                ICalPreprocessor.preprocessCalendar(calendar)
            } catch (e: Exception) {
                Ical4Android.log.log(Level.WARNING, "Couldn't pre-process iCalendar", e)
            }
+30 −0
Original line number Diff line number Diff line
/***************************************************************************************************
 * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
 **************************************************************************************************/

package at.bitfire.ical4android.validation

/**
 * Fixes durations with day offsets with the 'T' prefix.
 * See also https://github.com/bitfireAT/icsx5/issues/100
 */
object FixInvalidDayOffsetPreprocessor : StreamPreprocessor() {

    override fun regexpForProblem() = Regex(
        "^(DURATION|TRIGGER):-?PT-?\\d+D$",
        setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
    )

    override fun fixString(original: String): String {
        var s: String = original

        // Find all matches for the expression
        val found = regexpForProblem().find(s) ?: return s
        for (match in found.groupValues) {
            val fixed = match.replace("PT", "P")
            s = s.replace(match, fixed)
        }
        return s
    }

}
 No newline at end of file
+31 −0
Original line number Diff line number Diff line
/***************************************************************************************************
 * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
 **************************************************************************************************/

package at.bitfire.ical4android.validation

import at.bitfire.ical4android.Ical4Android
import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor.TZOFFSET_REGEXP
import java.util.logging.Level


/**
 * Some servers modify UTC offsets in TZOFFSET(FROM,TO) like "+005730" to an invalid "+5730".
 *
 * Rewrites values of all TZOFFSETFROM and TZOFFSETTO properties which match [TZOFFSET_REGEXP]
 * so that an hour value of 00 is inserted.
 */
object FixInvalidUtcOffsetPreprocessor: StreamPreprocessor() {

    private val TZOFFSET_REGEXP = Regex("^(TZOFFSET(FROM|TO):[+\\-]?)((18|19|[2-6]\\d)\\d\\d)$",
        setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))

    override fun regexpForProblem() = TZOFFSET_REGEXP

    override fun fixString(original: String) =
        original.replace(TZOFFSET_REGEXP) {
            Ical4Android.log.log(Level.FINE, "Applying Synology WebDAV fix to invalid utc-offset", it.value)
            "${it.groupValues[1]}00${it.groupValues[3]}"
        }

}
 No newline at end of file
+22 −52
Original line number Diff line number Diff line
@@ -11,10 +11,7 @@ import net.fortuna.ical4j.transform.rfc5545.CreatedPropertyRule
import net.fortuna.ical4j.transform.rfc5545.DateListPropertyRule
import net.fortuna.ical4j.transform.rfc5545.DatePropertyRule
import net.fortuna.ical4j.transform.rfc5545.Rfc5545PropertyRule
import org.apache.commons.io.IOUtils
import java.io.IOException
import java.io.Reader
import java.io.StringReader
import java.util.*
import java.util.logging.Level

@@ -28,8 +25,6 @@ import java.util.logging.Level
 */
object ICalPreprocessor {

    private val TZOFFSET_REGEXP = Regex("^(TZOFFSET(FROM|TO):[+\\-]?)((18|19|[2-6]\\d)\\d\\d)$", RegexOption.MULTILINE)

    private val propertyRules = arrayOf(
        CreatedPropertyRule(),      // make sure CREATED is UTC

@@ -37,46 +32,22 @@ object ICalPreprocessor {
        DateListPropertyRule(),     // ... by the ical4j VTIMEZONE with the same TZID!
    )

    val streamPreprocessors = arrayOf(
        FixInvalidUtcOffsetPreprocessor,    // fix things like TZOFFSET(FROM,TO):+5730
        FixInvalidDayOffsetPreprocessor     // fix things like DURATION:PT2D
    )

    /**
     * Some servers modify UTC offsets in TZOFFSET(FROM,TO) like "+005730" to an invalid "+5730".
     *
     * Rewrites values of all TZOFFSETFROM and TZOFFSETTO properties which match [TZOFFSET_REGEXP]
     * so that an hour value of 00 is inserted.
     * Applies [streamPreprocessors] to a given [Reader] that reads an iCalendar object
     * in order to repair some things that must be fixed before parsing.
     *
     * @param reader Reader that reads the potentially broken iCalendar (which for instance contains `TZOFFSETFROM:+5730`)
     * @return Reader that reads the fixed iCalendar (for instance `TZOFFSETFROM:+005730`)
     * @param original    original iCalendar object
     * @return            the potentially repaired iCalendar object
     */
    fun fixInvalidUtcOffset(reader: Reader): Reader {
        fun fixStringFromReader() =
                IOUtils.toString(reader).replace(TZOFFSET_REGEXP) {
                    Ical4Android.log.log(Level.FINE, "Applying Synology WebDAV fix to invalid utc-offset", it.value)
                    "${it.groupValues[1]}00${it.groupValues[3]}"
                }

        var result: String? = null

        val resetSupported = try {
            reader.reset()
            true
        } catch(e: IOException) {
            false
        }

        if (resetSupported) {
            // reset is supported, no need to copy the whole stream to another String (unless we have to fix the TZOFFSET)
            if (Scanner(reader).findWithinHorizon(TZOFFSET_REGEXP.toPattern(), 0) != null) {
                reader.reset()
                result = fixStringFromReader()
            }
        } else
            result = fixStringFromReader()

        if (result != null)
            return StringReader(result)

        // not modified, return original iCalendar
        reader.reset()
    fun preprocessStream(original: Reader): Reader {
        var reader = original
        for (preprocessor in streamPreprocessors)
            reader = preprocessor.preprocess(reader)
        return reader
    }

@@ -86,12 +57,11 @@ object ICalPreprocessor {
     *
     * @param calendar the calendar object that is going to be modified
     */
    fun preProcess(calendar: Calendar) {
        for (component in calendar.components) {
    fun preprocessCalendar(calendar: Calendar) {
        for (component in calendar.components)
            for (property in component.properties)
                applyRules(property)
    }
    }

    @Suppress("UNCHECKED_CAST")
    private fun applyRules(property: Property) {
Loading