/*
 * Authors :
 *  Chenthill Palanisamy <pchenthill@novell.com>
 *
 * SPDX-FileCopyrightText: 2007, Novell, Inc.
 * SPDX-License-Identifier: LGPL-2.1-only OR MPL-2.0
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "icaltimezone_p.h"
#include "icalerror_p.h"
#include "icaltimezone.h"
#include "icalmemory.h"

#include <stdlib.h>
#include <limits.h>

#if defined(HAVE_BYTESWAP_H)
#include <byteswap.h>
#elif defined(_MSC_VER) //krazy:exclude=cpp
// these are provided in stdlib.h since MSVC 2015
#define bswap_32 _byteswap_ulong
#define bswap_64 _byteswap_uint64
#else
#define bswap_32(x)                \
    ((((x) & 0xff000000u) >> 24) | \
     (((x) & 0x00ff0000u) >> 8) |  \
     (((x) & 0x0000ff00u) << 8) |  \
     (((x) & 0x000000ffu) << 24))
#define bswap_64(x)                          \
    ((((x) & 0xff00000000000000ull) >> 56) | \
     (((x) & 0x00ff000000000000ull) >> 40) | \
     (((x) & 0x0000ff0000000000ull) >> 24) | \
     (((x) & 0x000000ff00000000ull) >> 8) |  \
     (((x) & 0x00000000ff000000ull) << 8) |  \
     (((x) & 0x0000000000ff0000ull) << 24) | \
     (((x) & 0x000000000000ff00ull) << 40) | \
     (((x) & 0x00000000000000ffull) << 56))
#endif

#define EFREAD(buf, size, num, fs)                                       \
    if (!ferror(fs) && !feof(fs) && fread(buf, size, num, fs) < (num)) { \
        if (ferror(fs)) {                                                \
            icalerror_set_errno(ICAL_FILE_ERROR);                        \
            goto error;                                                  \
        }                                                                \
    }

typedef struct
{
    char magic[4];
    char version;
    char unused[15];
    char ttisgmtcnt[4];
    char ttisstdcnt[4];
    char leapcnt[4];
    char timecnt[4];
    char typecnt[4];
    char charcnt[4];
} tzinfo;

typedef struct
{
    long int gmtoff;
    unsigned char isdst;
    unsigned int abbr;
    unsigned char isstd;
    unsigned char isgmt;
    char *zname;

} ttinfo;

typedef struct
{
    icaltime_t transition;
    long int change;
} leap;

static int decode(const void *ptr)
{
#if SIZEOF_INT == 4
#if defined(HAVE_WORDS_BIGENDIAN)
    return *(const int *)ptr;
#else
    const unsigned int *p = (const unsigned int *)ptr;
    return (int)bswap_32(*p);
#endif
#else
    const unsigned char *p = ptr;
    unsigned int result = (*p & (1 << (CHAR_BIT - 1))) ? ~0U : 0;

    result = (result << 8) | *p++;
    result = (result << 8) | *p++;
    result = (result << 8) | *p++;
    result = (result << 8) | *p++;

    return (int)result;
#endif
}

static long long int decode64(const void *ptr)
{
#if defined(HAVE_WORDS_BIGENDIAN)
    return *(const long long int *)ptr;
#else
    const unsigned long long int *p = (const unsigned long long int *)ptr;
    return (long long int)bswap_64(*p);
#endif
}

static char *zname_from_stridx(char *str, size_t len_str, size_t idx)
{
    size_t i;
    size_t size;
    char *ret;

    i = idx;
    while (i < len_str && str[i] != '\0') {
        i++;
    }

    size = i - idx;
    str += idx;
    ret = (char *)icalmemory_new_buffer(size + 1);
    ret = strncpy(ret, str, size);
    ret[size] = '\0';

    return ret;
}

static int calculate_pos(icaltimetype icaltime)
{
    static const int r_pos[] = {1, 2, 3, -2, -1};
    int pos;

    pos = (icaltime.day - 1) / 7;

    /* Check if pos 3 is the last occurrence of the week day in the month */
    if (pos == 3 && ((icaltime.day + 7) > icaltime_days_in_month(icaltime.month, icaltime.year))) {
        pos = 4;
    }

    return r_pos[pos];
}

static char *parse_posix_zone(char *p, ttinfo *type)
{
    size_t size;

    /* Zone name */
    if (*p == '<') {
        /* Alphanumeric, '-', or '+' */
        size = strcspn(++p, ">");
    } else {
        /* Alpha ONLY */
        size = strcspn(p, "-+0123456789,\n");
    }

    type->zname = (char *)icalmemory_new_buffer(size + 1);
    strncpy(type->zname, p, size);
    type->zname[size] = '\0';
    p += size;

    if (*p == '>') {
        p++;
    }

    if (*p == ',') {
        return p;
    }

    /* Zone offset: hh[:mm[:ss]] */
    type->gmtoff = strtol(p, &p, 10) * -3600; /* sign of offset is reversed */
    if (*p == ':') {
        type->gmtoff += strtol(++p, &p, 10) * 60;
    }
    if (*p == ':') {
        type->gmtoff += strtol(++p, &p, 10);
    }
    return p;
}

#define nth_weekday(week, day) (icalrecurrencetype_encode_day((enum icalrecurrencetype_weekday)(day), (short)(week)))

static bool icalrecur_set_single_by(icalrecurrence_by_data *by, short value)
{
    if ((by->size != 1) && !icalrecur_resize_by(by, 1)) {
        return false;
    }

    by->data[0] = value;
    return true;
}

static char *parse_posix_rule(char *p,
                              struct icalrecurrencetype *recur, icaltimetype *t)
{
    int month = 0, monthday = 0, week = 0, day;

    /* Parse date */
    if (*p == 'J') {
        /* The Julian day n (1 <= n <= 365).
           Leap days shall not be counted. That is, in all years,
           including leap years, February 28 is day 59 and March 1 is day 60.
           It is impossible to refer explicitly to the occasional February 29.
        */
        day = strtol(++p, &p, 10);
    } else if (*p == 'M') {
        /* The d'th day (0 <= d <= 6)
           of week n of month m of the year (1 <= n <= 5, 1 <= m <= 12,
           where week 5 means "the last d day in month m"
           which may occur in either the fourth or the fifth week).
           Week 1 is the first week in which the d'th day occurs.
           Day zero is Sunday.
        */
        month = strtol(++p, &p, 10);
        week = strtol(++p, &p, 10);
        day = strtol(++p, &p, 10);
        if (week == 5) {
            week = -1;
        }
    } else {
        /* The zero-based Julian day (0 <= n <= 365).
           Leap days shall be counted, and it is possible to refer to February 29.

           Flag this by adding 1001 to the day.
        */
        day = strtol(++p, &p, 10) + 1001;
    }

    /* Parse time */
    *t = icaltime_null_time();
    t->hour = 2; /* default is 02:00 */

    if (*p == '/') {
        t->hour = strtol(++p, &p, 10);
        if (*p == ':') {
            t->minute = strtol(++p, &p, 10);
        }
        if (*p == ':') {
            t->second = strtol(++p, &p, 10);
        }
    }

    /* Do adjustments for extended TZ strings */
    if (t->hour < 0 || t->hour > 23) {
        int days_adjust = t->hour / 24;

        t->hour %= 24;
        day += days_adjust;

        if (t->hour < 0) {
            t->hour += 24;
            day += 6;
        }
        if (month) {
            if (week == -1) {
                int days_in_month = icaltime_days_in_month(month, 1 /* non-leap */);

                monthday = days_in_month + days_adjust - 7;
            } else {
                monthday = 1 + (week - 1) * 7 + days_adjust;
            }
            week = 0;
        }
    }

    /* Create rule */
    recur->freq = ICAL_YEARLY_RECURRENCE;

    int error = 0;

    if (month) {
        error |= !icalrecur_set_single_by(&recur->by[ICAL_BY_DAY], nth_weekday(week, (day % 7) + 1));
        error |= !icalrecur_set_single_by(&recur->by[ICAL_BY_MONTH], (short)month);

        if (monthday) {
            error |= !icalrecur_resize_by(&recur->by[ICAL_BY_MONTH_DAY], 7);
            unsigned i;
            for (i = 0; i < 7; i++) {
                recur->by[ICAL_BY_MONTH_DAY].data[i] = monthday++;
            }
        }
    } else if (day > 1000) {
        error |= !icalrecur_set_single_by(&recur->by[ICAL_BY_YEAR_DAY], day - 1000);
    } else {
        /* Convert day-of-non-leap-year into month/day */
        icaltimetype doy_t = icaltime_from_day_of_year(day, 1 /* non-leap */);

        error |= !icalrecur_set_single_by(&recur->by[ICAL_BY_MONTH], doy_t.month);
        error |= !icalrecur_set_single_by(&recur->by[ICAL_BY_MONTH_DAY], doy_t.day);
    }

    if (error) {
        return NULL;
    }

    return p;
}

struct zone_context {
    enum icalcomponent_kind kind;
    const char *name;
    long gmtoff_from;
    long gmtoff_to;

    icaltimetype time;
    icaltimetype prev_time;

    icalcomponent *rdate_comp;
    icalcomponent *rrule_comp;
    icalproperty *rrule_prop;
    short num_monthdays;
    struct icalrecurrencetype *recur;
    struct icalrecurrencetype *final_recur;
};

static void terminate_rrule(struct zone_context *zone)
{
    if (icaltime_compare(zone->time, zone->prev_time)) {
        // Multiple instances
        // Set UNTIL of the component's recurrence
        zone->recur->until = zone->time;
        icaltime_adjust(&zone->recur->until, 0, 0, 0, -zone->gmtoff_from);
        zone->recur->until.zone = icaltimezone_get_utc_timezone();

        // Remove BYMONTHDAY if BYDAY week != 0
        if ((zone->recur->by[ICAL_BY_DAY].size >= 1) &&
            icalrecurrencetype_day_position(zone->recur->by[ICAL_BY_DAY].data[0])) {
            // Don't bother dealing with success/failure since we're simply removing
            (void)icalrecur_resize_by(&zone->recur->by[ICAL_BY_MONTH_DAY], 0);
        }

        icalproperty_set_rrule(zone->rrule_prop, zone->recur);
        icalrecurrencetype_unref(zone->recur);
        zone->recur = NULL;

        zone->rdate_comp = zone->rrule_comp = NULL;
    } else {
        // Remove the RRULE from the component
        icalcomponent_remove_property(zone->rrule_comp, zone->rrule_prop);
        icalproperty_free(zone->rrule_prop);
    }
}

icalcomponent *icaltimezone_fetch_timezone(const char *location)
{
    tzinfo header = {0};
    size_t i, num_trans, num_chars, num_leaps, num_isstd, num_isgmt;
    size_t num_types = 0;
    size_t size;
    int trans_size = 4;

    const char *zonedir;
    FILE *f = NULL;
    char *full_path = NULL;
    icaltime_t *transitions = NULL;
    char *r_trans = NULL, *temp;
    int *trans_idx = NULL;
    ttinfo *types = NULL;
    char *znames = NULL;
    leap *leaps = NULL;
    char *tzid = NULL;

    char footer[100], *tzstr = NULL;

    int idx, prev_idx;
    icalcomponent *tz_comp = NULL;
    icalproperty *icalprop;
    icaltimetype icaltime;

    struct zone_context standard =
        {ICAL_XSTANDARD_COMPONENT, NULL, LONG_MIN, LONG_MIN,
         ICALTIMETYPE_INITIALIZER, ICALTIMETYPE_INITIALIZER,
         NULL, NULL, NULL, 0,
         icalrecurrencetype_new(), icalrecurrencetype_new()};
    struct zone_context daylight =
        {ICAL_XDAYLIGHT_COMPONENT, NULL, LONG_MIN, LONG_MIN,
         ICALTIMETYPE_INITIALIZER, ICALTIMETYPE_INITIALIZER,
         NULL, NULL, NULL, 0,
         icalrecurrencetype_new(), icalrecurrencetype_new()};
    struct zone_context *zone;

    if (!standard.recur || !standard.final_recur || !daylight.recur || !daylight.final_recur) {
        goto error;
    }

    if (icaltimezone_get_builtin_tzdata()) {
        goto error;
    }

    zonedir = icaltimezone_get_system_zone_directory();
    if (!zonedir) {
        icalerror_set_errno(ICAL_FILE_ERROR);
        goto error;
    }

    size = strlen(zonedir) + strlen(location) + 2;
    full_path = (char *)icalmemory_new_buffer(size);
    if (full_path == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    snprintf(full_path, size, "%s/%s", zonedir, location);
    if ((f = fopen(full_path, "rb")) == 0) {
        icalerror_set_errno(ICAL_FILE_ERROR);
        goto error;
    }

    /* read version 1 header */
    EFREAD(&header, 44, 1, f);
    if (memcmp(header.magic, "TZif", 4) != 0) {
        icalerror_set_errno(ICAL_MALFORMEDDATA_ERROR);
        goto error;
    }
    switch (header.version) {
    case 0:
        break;
    case '2':
    case '3':
        if (sizeof(icaltime_t) == 8) {
            trans_size = 8;
        }
        break;
    default:
        icalerror_set_errno(ICAL_MALFORMEDDATA_ERROR);
        goto error;
    }

    num_isgmt = (size_t)decode(header.ttisgmtcnt);
    num_leaps = (size_t)decode(header.leapcnt);
    num_chars = (size_t)decode(header.charcnt);
    num_trans = (size_t)decode(header.timecnt);
    num_isstd = (size_t)decode(header.ttisstdcnt);
    num_types = (size_t)decode(header.typecnt);

    if (trans_size == 8) {
        size_t skip = num_trans * 5 + num_types * 6 +
                      num_chars + num_leaps * 8 + num_isstd + num_isgmt;

        /* skip version 1 data block */
        if (fseek(f, (long)skip, SEEK_CUR) != 0) {
            icalerror_set_errno(ICAL_FILE_ERROR);
            goto error;
        }

        /* read version 2+ header */
        EFREAD(&header, 44, 1, f);
        if (memcmp(header.magic, "TZif", 4) != 0) {
            icalerror_set_errno(ICAL_MALFORMEDDATA_ERROR);
            goto error;
        }

        num_isgmt = (size_t)decode(header.ttisgmtcnt);
        num_leaps = (size_t)decode(header.leapcnt);
        num_chars = (size_t)decode(header.charcnt);
        num_trans = (size_t)decode(header.timecnt);
        num_isstd = (size_t)decode(header.ttisstdcnt);
        num_types = (size_t)decode(header.typecnt);
    }

    /* read data block */
    transitions = icalmemory_new_buffer((num_trans + 1) * sizeof(icaltime_t)); // +1 for TZ string
    if (transitions == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    r_trans = icalmemory_new_buffer(num_trans * (size_t)trans_size);
    if (r_trans == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    const size_t len_types = (num_types + 2) * sizeof(ttinfo);  // +2 for TZ string
    const size_t len_trans_idx = (num_trans + 1) * sizeof(int); // +1 for TZ string
    trans_idx = icalmemory_new_buffer(len_trans_idx);
    if (trans_idx == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    if (num_trans == 0) {
        // Add one transition using time type 0 at 19011213T204552Z
        transitions[0] = (icaltime_t)INT_MIN;
        trans_idx[0] = 0;
        num_trans = 1;
    } else {
        EFREAD(r_trans, (size_t)trans_size, num_trans, f);
        temp = r_trans;
        for (i = 0; i < num_trans && !feof(f) && !ferror(f); i++) {
            int c = fgetc(f);
            if (c < 0 || c >= (int)len_types) {
                break;
            }
            trans_idx[i] = c; // possibly tainted
            if (trans_size == 8) {
                transitions[i] = (icaltime_t)decode64(r_trans);
            } else {
                transitions[i] = (icaltime_t)decode(r_trans);
            }
            r_trans += trans_size;
        }
        r_trans = temp;
    }
    types = icalmemory_new_buffer(len_types);
    if (types == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    for (i = 0; i < num_types; i++) {
        unsigned char a[4];
        int c;

        EFREAD(a, 4, 1, f);
        if (feof(f) || ferror(f) || (c = fgetc(f)) < 0) {
            break;
        }
        types[i].isdst = (unsigned char)c;
        if (feof(f) || ferror(f) || (c = fgetc(f)) < 0) {
            break;
        }
        types[i].abbr = (unsigned int)c;
        types[i].gmtoff = decode(a);
    }

    znames = (char *)icalmemory_new_buffer(num_chars);
    if (znames == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    EFREAD(znames, num_chars, 1, f);

    /* We got all the information which we need */

    leaps = icalmemory_new_buffer(num_leaps * sizeof(leap));
    if (leaps == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    for (i = 0; i < num_leaps; i++) {
        char c[8];

        EFREAD(c, (size_t)trans_size, 1, f);
        if (trans_size == 8) {
            leaps[i].transition = (icaltime_t)decode64(c);
        } else {
            leaps[i].transition = (icaltime_t)decode(c);
        }

        EFREAD(c, 4, 1, f);
        leaps[i].change = decode(c);
    }

    for (i = 0; i < num_isstd && !feof(f) && !ferror(f); ++i) {
        int c = getc(f);
        types[i].isstd = c != 0;
    }

    while (i < num_types) {
        types[i++].isstd = 0;
    }

    for (i = 0; i < num_isgmt && !feof(f) && !ferror(f); ++i) {
        int c = getc(f);
        types[i].isgmt = c != 0;
    }

    while (i < num_types) {
        types[i++].isgmt = 0;
    }

    for (i = 0; i < num_types; i++) {
        /* coverity[tainted_data] */
        types[i].zname = zname_from_stridx(znames, num_chars, types[i].abbr);
    }

    /* Read the footer */
    if (trans_size == 8 &&
        !feof(f) &&
        !ferror(f) &&
        (footer[0] = (char)fgetc(f)) == '\n' &&
        fgets(footer + 1, (int)sizeof(footer) - 1, f) &&
        footer[strlen(footer) - 1] == '\n') {
        tzstr = footer + 1;
    }
    if (tzstr) {
        /* Parse the TZ string:
           stdoffset[dst[offset][,start[/time],end[/time]]]
        */
        ttinfo *std_type = &types[num_types++];
        ttinfo *dst_type = &types[num_types++];
        char *p = tzstr;

        /* Parse standard zone */
        p = parse_posix_zone(p, std_type);
        if (*p == '\n') {
            /* No DST, so ignore the TZ string */
            tzstr = NULL;
        } else {
            /* Parse DST zone */
            dst_type->isdst = 1;
            dst_type->gmtoff = std_type->gmtoff + 3600; /* default is +1hr */
            p = parse_posix_zone(p, dst_type);

            if (*p != ',') {
                /* No rule, so ignore the TZ string */
                tzstr = NULL;
            } else {
                struct icaltimetype std_trans = {0}, dst_trans = {0};

                /* Parse std->dst rule */
                p = parse_posix_rule(++p, /* skip ',' */
                                     daylight.final_recur, &dst_trans);
                if (!p) {
                    goto error;
                }

                /* Parse dst->std rule */
                p = parse_posix_rule(++p, /* skip ',' */
                                     standard.final_recur, &std_trans);

                if (!p) {
                    goto error;
                }

                if (*p != '\n') {
                    /* Trailing junk, so ignore the TZ string */
                    tzstr = NULL;
                } else {
                    struct icaltimetype last_trans =
                        icaltime_from_timet_with_zone(transitions[num_trans - 1],
                                                      0, NULL);
                    icalrecur_iterator *iter;

                    if (types[trans_idx[num_trans - 1]].isdst) {
                        /* Add next dst->std transition */
                        std_trans.year = last_trans.year;
                        std_trans.month = last_trans.month;
                        std_trans.day = last_trans.day;
                        iter = icalrecur_iterator_new(standard.final_recur,
                                                      std_trans);
                        std_trans = icalrecur_iterator_next(iter);
                        icaltime_adjust(&std_trans, 0, 0, 0, -dst_type->gmtoff);
                        transitions[num_trans] = icaltime_as_timet(std_trans);
                        trans_idx[num_trans++] = (int)num_types - 2;
                        icalrecur_iterator_free(iter);
                    } else {
                        /* Add next std->dst transition */
                        dst_trans.year = last_trans.year;
                        dst_trans.month = last_trans.month;
                        dst_trans.day = last_trans.day;
                        iter = icalrecur_iterator_new(daylight.final_recur,
                                                      dst_trans);
                        dst_trans = icalrecur_iterator_next(iter);
                        icaltime_adjust(&dst_trans, 0, 0, 0, -std_type->gmtoff);
                        transitions[num_trans] = icaltime_as_timet(dst_trans);
                        trans_idx[num_trans++] = (int)num_types - 1;
                        icalrecur_iterator_free(iter);
                    }
                }
            }
        }
    }

    /* Build the VTIMEZONE now */

    tz_comp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);

    /* Add tzid property */
    size = strlen(icaltimezone_tzid_prefix()) + strlen(location) + 1;
    tzid = (char *)icalmemory_new_buffer(size);
    if (tzid == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        goto error;
    }
    snprintf(tzid, size, "%s%s", icaltimezone_tzid_prefix(), location);
    icalprop = icalproperty_new_tzid(tzid);
    icalcomponent_add_property(tz_comp, icalprop);

    icalprop = icalproperty_new_x(location);
    icalproperty_set_x_name(icalprop, "X-LIC-LOCATION");
    icalcomponent_add_property(tz_comp, icalprop);

    idx = 0; // time type 0 is always time prior to first transition

    for (i = 0; i < num_trans; i++) {
        int by_day = 0;
        icaltime_t start;
        enum icalrecurrencetype_weekday dow = ICAL_NO_WEEKDAY;

        prev_idx = idx;
        idx = trans_idx[i];
        if (idx < 0 || idx >= (int)len_types) {
            // tainted data
            goto error;
        }
        start = transitions[i] + types[prev_idx].gmtoff;
        icaltime = icaltime_from_timet_with_zone(start, 0, NULL);

        if (types[idx].isdst) {
            zone = &daylight;
        } else {
            zone = &standard;
        }

        // The last two transition times are DTSTART for the TZ string RRULEs
        if (tzstr && (i >= num_trans - 2)) {
            terminate_rrule(zone);
            zone->rrule_comp = NULL;
        } else {
            dow = (enum icalrecurrencetype_weekday)icaltime_day_of_week(icaltime);
            by_day = nth_weekday(calculate_pos(icaltime), dow);
        }

        if (zone->rrule_comp) {
            int terminate = 0;
            int rdate = 0;

            // Check if the zone name or either of the offsets have changed
            if (types[prev_idx].gmtoff != zone->gmtoff_from ||
                types[idx].gmtoff != zone->gmtoff_to ||
                (types[idx].zname != NULL &&
                 strcmp(types[idx].zname, zone->name) != 0)) {
                zone->rdate_comp = NULL;
                terminate = 1;
            }
            // Check if most of the recurrence pattern is the same
            else if (icaltime.year == zone->time.year + 1 &&
                     icaltime.month == zone->time.month &&
                     icaltime.hour == zone->time.hour &&
                     icaltime.minute == zone->time.minute &&
                     icaltime.second == zone->time.second) {
                if ((zone->recur->by[ICAL_BY_DAY].size >= 1) && (by_day == zone->recur->by[ICAL_BY_DAY].data[0])) {
                    // Same nth weekday of the month - continue
                } else if ((zone->recur->by[ICAL_BY_DAY].size >= 1) &&
                           (dow == icalrecurrencetype_day_day_of_week(zone->recur->by[ICAL_BY_DAY].data[0]))) {
                    // Same weekday in the month
                    if (((zone->recur->by[ICAL_BY_MONTH_DAY].size >= 1) &&
                         (icaltime.day >= zone->recur->by[ICAL_BY_MONTH_DAY].data[0] + 7)) ||
                        (zone->recur->by[ICAL_BY_MONTH_DAY].size <= zone->num_monthdays - 1) ||
                        (icaltime.day + 7 <= zone->recur->by[ICAL_BY_MONTH_DAY].data[zone->num_monthdays - 1])) {
                        // Don't allow two month days with the same weekday -
                        // possible RDATE
                        rdate = terminate = 1;
                    } else {
                        // Insert day of month into the array
                        int j;

                        if (zone->num_monthdays != zone->recur->by[ICAL_BY_MONTH_DAY].size) {
                            icalerror_set_errno(ICAL_INTERNAL_ERROR);
                            break;
                        }

                        for (j = 0; j < zone->recur->by[ICAL_BY_MONTH_DAY].size; j++) {
                            if (icaltime.day <= zone->recur->by[ICAL_BY_MONTH_DAY].data[j]) {
                                break;
                            }
                        }

                        if ((j >= zone->num_monthdays) || (icaltime.day < zone->recur->by[ICAL_BY_MONTH_DAY].data[j])) {
                            if (!icalrecur_resize_by(&zone->recur->by[ICAL_BY_MONTH_DAY], zone->num_monthdays + 1)) {
                                icalerror_set_errno(ICAL_NEWFAILED_ERROR);
                                break;
                            }

                            if (zone->num_monthdays > j) {
                                memmove(&zone->recur->by[ICAL_BY_MONTH_DAY].data[j + 1],
                                        &zone->recur->by[ICAL_BY_MONTH_DAY].data[j],
                                        (size_t)(zone->num_monthdays - j) *
                                            sizeof(zone->recur->by[ICAL_BY_MONTH_DAY].data[0]));
                            }

                            zone->recur->by[ICAL_BY_MONTH_DAY].data[j] = icaltime.day;
                            zone->num_monthdays++;
                        }

                        // Remove week number from BYDAY
                        // zone->recur->by[ICAL_BY_DAY].size cannot be < 1 here
                        zone->recur->by[ICAL_BY_DAY].data[0] = nth_weekday(0, dow);
                    }
                } else if ((zone->recur->by[ICAL_BY_MONTH_DAY].size >= 1) &&
                           (icaltime.day == zone->recur->by[ICAL_BY_MONTH_DAY].data[0])) {
                    // Same day of the month - remove BYDAY
                    if (zone->recur->by[ICAL_BY_DAY].data) {
                        // Ignore return since we're simply removing
                        (void)icalrecur_resize_by(&zone->recur->by[ICAL_BY_DAY], 0);
                    }
                } else {
                    // Different BYDAY and BYMONTHDAY - possible RDATE
                    rdate = terminate = 1;
                }
            } else {
                // Different recurrence pattern entirely - possible RDATE
                rdate = terminate = 1;
            }

            if (terminate) {
                // Terminate the current RRULE
                terminate_rrule(zone);

                if (rdate) {
                    if (zone->rdate_comp) {
                        // Add an RDATE to the previous component
                        // Remove the current RRULE component
                        struct icaldatetimeperiodtype dtp =
                            {zone->time,
                             ICALPERIODTYPE_INITIALIZER};

                        icalprop = icalproperty_new_rdate(dtp);
                        icalcomponent_add_property(zone->rdate_comp, icalprop);

                        icalcomponent_remove_component(tz_comp, zone->rrule_comp);
                        icalcomponent_free(zone->rrule_comp);
                    } else {
                        zone->rdate_comp = zone->rrule_comp;
                    }
                }
                zone->rrule_comp = NULL;
            }
        }

        zone->prev_time = zone->time;
        zone->time = icaltime;
        zone->gmtoff_from = types[prev_idx].gmtoff;
        zone->gmtoff_to = types[idx].gmtoff;

        if (!zone->rrule_comp) {
            int error = 0;
            zone->name = types[idx].zname;
            zone->prev_time = icaltime;

            // Create a recurrence rule for the current set of changes

            if (zone->recur) {
                icalrecurrencetype_unref(zone->recur);
            }

            zone->recur = icalrecurrencetype_new();
            if (!zone->recur) {
                icalerror_set_errno(ICAL_NEWFAILED_ERROR);
                break;
            }

            zone->recur->freq = ICAL_YEARLY_RECURRENCE;
            error |= !icalrecur_set_single_by(&zone->recur->by[ICAL_BY_DAY], by_day);
            error |= !icalrecur_set_single_by(&zone->recur->by[ICAL_BY_MONTH], icaltime.month);
            error |= !icalrecur_set_single_by(&zone->recur->by[ICAL_BY_MONTH_DAY], icaltime.day);
            if (error) {
                icalerror_set_errno(ICAL_NEWFAILED_ERROR);
                break;
            }

            zone->num_monthdays = 1;

            struct icalrecurrencetype *clone = icalrecurrencetype_clone(zone->recur);
            if (!clone) {
                icalerror_set_errno(ICAL_NEWFAILED_ERROR);
                break;
            }

            zone->rrule_prop = icalproperty_new_rrule(clone);
            icalrecurrencetype_unref(clone);

            if (!zone->rrule_prop) {
                icalerror_set_errno(ICAL_NEWFAILED_ERROR);
                break;
            }

            zone->rrule_comp =
                icalcomponent_vanew(zone->kind,
                                    icalproperty_new_tzname(zone->name),
                                    icalproperty_new_tzoffsetfrom(zone->gmtoff_from),
                                    icalproperty_new_tzoffsetto(zone->gmtoff_to),
                                    icalproperty_new_dtstart(zone->time),
                                    zone->rrule_prop,
                                    (void *)0);
            icalcomponent_add_component(tz_comp, zone->rrule_comp);
        }
    }

    if (tzstr) {
        // Replace the last recurrence rules with those from the TZ string
        icalproperty_set_rrule(standard.rrule_prop, standard.final_recur);
        icalproperty_set_rrule(daylight.rrule_prop, daylight.final_recur);
    } else {
        // Terminate the last recurrence rules
        if (standard.rrule_comp) {
            terminate_rrule(&standard);
        }
        if (daylight.rrule_comp) {
            terminate_rrule(&daylight);
        }
    }

error:
    if (standard.recur) {
        icalrecurrencetype_unref(standard.recur);
    }
    if (standard.final_recur) {
        icalrecurrencetype_unref(standard.final_recur);
    }
    if (daylight.recur) {
        icalrecurrencetype_unref(daylight.recur);
    }
    if (daylight.final_recur) {
        icalrecurrencetype_unref(daylight.final_recur);
    }

    if (f) {
        fclose(f);
    }

    icalmemory_free_buffer(full_path);
    icalmemory_free_buffer(transitions);
    icalmemory_free_buffer(r_trans);
    icalmemory_free_buffer(trans_idx);

    if (types) {
        for (i = 0; i < num_types; i++) {
            icalmemory_free_buffer(types[i].zname);
        }
        icalmemory_free_buffer(types);
    }

    icalmemory_free_buffer(znames);
    icalmemory_free_buffer(leaps);
    icalmemory_free_buffer(tzid);

    return tz_comp;
}
