Skip to content

Add parsing for TZif v2+ (tzdata2 APEX) on Android #5235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

andriydruk
Copy link
Contributor

Motivation

Starting with Android 11, the platform ships its time-zone package as com.google.android.tzdata2, whose files use the TZif v2/v3 specification (RFC 8536) with 64-bit transition timestamps (Google also introduced a new APEX-based mechanism for updating time zones on all Android 10+ devices)
https://source.android.com/docs/core/ota/modular-system/timezone

CFTimeZone.c currently only understands v1 layout, so CoreFoundation on Android silently falls back to “GMT” for any device that only contains the new format

What this patch does

  • Adds support for the new APEX tzdata path
  • Extends struct tzhead to include the one-byte version field
  • Introduces __CFDetzcode64 to decode 64-bit big-endian integers safely (no UB on signed shifts)
  • During parsing
    • read the first header, determine version, and set trans_size to 4 (v1) or 8 (v2+)
    • if version > 1, skip the v1 data block and read the second header that precedes the 64-bit section
    • use trans_size consistently when advancing the transition and type pointers
  • For TZif v1 files, the parsing path is identical to the original implementation

Testing

This patch affects CFTimeZone in CoreFoundation only. It does not affect Foundation.TimeZone, which continues to use ICU-based time zone data:

  • APEX tzdata version on Android device: 2025b
  • Swift Foundation (ICU) tzdata version: 2023c

I wrote a test case verifying the recent time zone change in Asia/Almaty, where Kazakhstan unified to UTC+5 in March 2024. However, I didn't commit it, as it would fail on all platforms using older tzdata (e.g. ICU 2023c).

    /// Kazakhstan unified on UTC +5 at 00:00 local, 1 March 2024.
    /// After that moment Asia/Almaty should report +05:00.
    /// ICU/tzdata 2023c still returns +06:00, so the
    /// assertion below fails on tzdata before 2024a+.
    func testAsiaAlmatyOffsetAfter2024Change() {
        guard let almaty = TimeZone(identifier: "Asia/Almaty") else {
            XCTFail("Asia/Almaty not found")
            return
        }

        // Local time 2024-06-01 12:00 in Almaty
        var comps = DateComponents()
        comps.year = 2024
        comps.month = 6
        comps.day = 1
        comps.hour = 12
        comps.timeZone = almaty

        let calendar = Calendar(identifier: .gregorian)
        let date = calendar.date(from: comps)!

        // Expected offset (UTC+5) after the March 2024 change
        let expectedSeconds = 5 * 3_600
        XCTAssertEqual(almaty.secondsFromGMT(for: date), expectedSeconds)
    }

    func testAsiaAlmatyOffsetAfter2024ChangeCF() {
        guard let timeZoneName = CFStringCreateWithCString(nil, "Asia/Almaty", CFStringBuiltInEncodings.UTF8.rawValue),
              let almaty = CFTimeZoneCreateWithName(nil, timeZoneName, false) else {
            XCTFail("Asia/Almaty not found")
            return
        }
        
        var gregorianDate = CFGregorianDate(
            year: 2024,
            month: 6,
            day: 1,
            hour: 12,
            minute: 0,
            second: 0.0
        )
        
        let absoluteTime = CFGregorianDateGetAbsoluteTime(gregorianDate, almaty)
        let offsetSeconds = CFTimeZoneGetSecondsFromGMT(almaty, absoluteTime)
        
        // Expected offset (UTC+5) after the March 2024 change
        let expectedSeconds: CFTimeInterval = 5 * 3_600
        XCTAssertEqual(offsetSeconds, expectedSeconds)
    }

Result on Android with tzdata 2025b
❌ Foundation.TimeZone still returns outdated UTC+6 due to bundled ICU 2023c
✅ CFTimeZone reports correct offset (UTC+5)

Test Case 'TestTimeZone.testAsiaAlmatyOffsetAfter2024Change' started at 2025-07-05 19:39:00.480
TestFoundation/TestTimeZone.swift:297: error: TestTimeZone.testAsiaAlmatyOffsetAfter2024Change : XCTAssertEqual failed: ("21600") is not equal to ("18000") - Asia/Almaty should be UTC+5 after 2024-03-01. If this assertion fails (got 6 h), the device is running tzdata 2023c or older.
Test Case 'TestTimeZone.testAsiaAlmatyOffsetAfter2024Change' failed (0.003 seconds)
Test Case 'TestTimeZone.testAsiaAlmatyOffsetAfter2024ChangeCF' started at 2025-07-05 19:39:00.483
Test Case 'TestTimeZone.testAsiaAlmatyOffsetAfter2024ChangeCF' passed (0.001 seconds)

Next steps

Explore how to make Foundation.TimeZone use the system-provided tzdata on Android. Currently, it relies on ICU's bundled data, which may be outdated compared to the APEX module. Aligning both would ensure consistent and up-to-date time zone behavior across the system.

This updates CFTimeZone to support time zone files used by tzdata2 APEX
modules on Android 10 and later.

Co-authored-by: Yaroslav Biletskyi <ybiletskyi@readdle.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant