bitfireAT/ical4android

"UnsupportedOperationException: TimeZone is not applicable to current value" when parsing iCalendar from iCloud

Closed this issue · 4 comments

We got this parsing error over a support request:

EXCEPTION
at.bitfire.ical4android.InvalidCalendarException: Couldn't parse iCalendar
	at at.bitfire.ical4android.ICalendar$Companion.fromReader(ICalendar.kt:161)
	at at.bitfire.ical4android.Event$Companion.eventsFromReader(Event.kt:8)
…
Caused by: net.fortuna.ical4j.data.ParserException: Error at line 22:TimeZone is not applicable to current value
	at net.fortuna.ical4j.data.CalendarParserImpl.parse(CalendarParserImpl.java:17)
	at net.fortuna.ical4j.data.CalendarBuilder.build(CalendarBuilder.java:3)
	at net.fortuna.ical4j.data.CalendarBuilder.build(CalendarBuilder.java:2)
	at at.bitfire.ical4android.ICalendar$Companion.fromReader(ICalendar.kt:59)
	... 37 more
Caused by: java.lang.UnsupportedOperationException: TimeZone is not applicable to current value
	at net.fortuna.ical4j.model.property.DateListProperty.setTimeZone(DateListProperty.java:64)
	at net.fortuna.ical4j.data.DefaultContentHandler.resolveTimezones(DefaultContentHandler.java:63)
	at net.fortuna.ical4j.data.DefaultContentHandler.endCalendar(DefaultContentHandler.java:1)
	at net.fortuna.ical4j.data.CalendarParserImpl.parseCalendar(CalendarParserImpl.java:50)
	at net.fortuna.ical4j.data.CalendarParserImpl.parseCalendarList(CalendarParserImpl.java:15)
	at net.fortuna.ical4j.data.CalendarParserImpl.parse(CalendarParserImpl.java:13)
	... 40 more

I think it's because a TZID was applied to a DateListProperty without dates, something like RDATE;TZID=Some/TZ:. Unfortunately we don't have the iCalendar (I have requested it, but little hope to actually get it) and I can't reproduce with this test:

class Ical4jTest {

    @Test
    fun testDateList_WithoutDates_WithTZ() {
        val cal = ICalendar.fromReader(StringReader("BEGIN:VCALENDAR\r\n" +
                "VERSION:2.0\r\n" +
                "BEGIN:VEVENT\r\n" +
                "SUMMARY:Test\r\n" +
                "DTSTART;TZID=Europe/Vienna:20230824T161334\r\n" +
                "RDATE;TZID=Europe/Vienna:\r\n" +
                "END:VEVENT\r\n" +
                "END:VCALENDAR"))
        val event = cal.getComponent<VEvent>(Component.VEVENT)
        assertEquals("Test", event.summary.value)

        val rDate = event.getProperty<RDate>(Property.RDATE)
        assertNull(rDate.dates)
    }

}

Here we see that dates is not null, but an empty array, and then the code that causes the exception doesn't make any problems:

https://github.com/ical4j/ical4j/blob/527c76a34456e5916ed2efc8f2c960ddba8e2790/src/main/java/net/fortuna/ical4j/model/property/DateListProperty.java#L130-L133

  • Manage to reproduce the problem
  • Provide a PR for ical4j so that this is ignored, at least in lenient mode – as far as I can see, there's no disadvantage in ignoring the timezone in this case.

Updating

TLDR; no way to reproduce yet

After some tests without success, I will keep here everything I find.

The only way that DateListProperty doesn't get any value for dates is for constructor

/**
 * @param name       the property name
 * @param parameters property parameters
 */
public DateListProperty(final String name, final ParameterList parameters, PropertyFactory factory) {
    super(name, parameters, factory);
}

at RDate, this constructor is called here:

/**
 * @param aList  a list of parameters for this component
 * @param aValue a value string for this component
 * @throws ParseException where the specified value string is not a valid date-time/date representation
 */
public RDate(final ParameterList aList, final String aValue)
        throws ParseException {
    super(RDATE, aList, new Factory());
    periods = new PeriodList(false, true);
    setValue(aValue);
}

However, this constructor calls setValue, so it's quite improbable that dates doesn't get initialized.

After looking at this, there's an empty constructor in RDate that doesn't initialize anything for dates:

/**
 * Default constructor.
 */
public RDate() {
    super(RDATE, new Factory());
    periods = new PeriodList(false, true);
}

Then, if we do

@Test
fun testRDate_withoutDates() {
    val rDate = RDate()
    rDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}

nothing happens. This is because internally this constructor calls an initialization of PeriodList with

public PeriodList(boolean utc, final boolean unmodifiable) {
    this.utc = utc;
    this.unmodifiable = unmodifiable;
    if (unmodifiable) {
     periods = Collections.emptySet();
    }
    else {
     periods = new TreeSet<Period>();
    }
}

unmodifiable set to true, which initializes PeriodList.periods, that then it's used in RDate:

@Override
public final void setTimeZone(TimeZone timezone) {
    if (periods != null && !(periods.isEmpty() && periods.isUnmodifiable())) {
        periods.setTimeZone(timezone);
    } else {
        super.setTimeZone(timezone);
    }
}

So the problematic setTimeZone is not called here 😢. To sum up, I can't find a way to build an invalid RDate, so back to the drawing board.

After seeing this overridden method, I've realized that ExDate doesn't override anything, so maybe the issue is here. However, this test is also not-problematic:

@Test
fun testRDate_withoutDates() {
    val rDate = ExDate()
    rDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}

This is because the empty constructor calls the DateListProperty(final String name, PropertyFactory factory) constructor, which then initializes dates with

this(name, new DateList(Value.DATE_TIME), factory);

Again, so way dates is null.

Now I feel stupid for not having tried passing a null DateList. If this is called, the exception is thrown, so we have to find somewhere that dates might have been null.

@Test
fun testRDate_withoutDates() {
    val list: DateList? = null
    val exDate = ExDate(list)
    exDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}

which btw also fails with RDate, obviously. To search for this, it's important to note that there no way calling setValue or constructing with Strings doesn't end with a null dates, so we have to stick with other constructors.

I can't find anyhere that could be initializing with a null DateList

@rfc2822 I've tried everything I can think of... Isn't there any way to get the original ical?

@rfc2822 I've tried everything I can think of... Isn't there any way to get the original ical?

I have requested it, but I doubt the person is able to fetch & send it… we will see.

I'll leave it open for now, maybe we really get the ICS or manage to somehow reproduce it.

Missing information, reopen when occurring again.