66package kotlinx.datetime.internal
77
88import kotlinx.datetime.*
9+ import kotlinx.datetime.format.optional
910
1011internal class TzFileData (
1112 val leapSecondRules : List <LeapSecondRule >,
@@ -204,8 +205,34 @@ private fun BinaryDataReader.readPosixTzString(): PosixTzString? {
204205 fun readName (): String? {
205206 if (c == ' \n ' ) return null
206207 val name = StringBuilder ()
208+ /* This check is a workaround for a bug in our tzdb processor used in kotlinx-datetime-zoneinfo.
209+ In 2024b+, the tzdb includes the `%z` directive instead of the timezone abbreviations in cases where the
210+ abbreviation can be inferred from the offset
211+ (https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/IZ7AO6WRE3W3TWBL5IR6PMQUL433BQIE/):
212+ instead of writing "abbreviation = -03, offset = -3", they now write "abbreviation = %z, offset = -3".
213+ The first-party tzdb compiler zic knows how to support this:
214+ https://github.com/eggert/tz/blob/271a5784a59e454b659d85948b5e65c17c11516a/zic.8#L590-L602
215+ The compiler we're using (`tubular_time_tzdb`) doesn't seem to, though, and generates invalid POSIX strings.
216+ This is a quick and dirty workaround. A proper solution would be to have correct data in `-zoneinfo`, but
217+ it doesn't matter if we publish broken tzdb info now, as we are not planning on supporting consuming old tzdb
218+ versions from new library versions, so the workaround can be removed as soon as the `-zoneinfo` artifact is
219+ fixed. */
220+ if (c == ' %' ) {
221+ c = readAsciiChar()
222+ check(c == ' z' ) { " Invalid directive %$c in the timezone name abbreviation" }
223+ c = readAsciiChar()
224+ return GENERATE_NAME
225+ }
207226 if (c == ' <' ) {
208227 c = readAsciiChar()
228+ if (c == ' %' ) {
229+ c = readAsciiChar()
230+ check(c == ' z' ) { " Invalid directive %$c in the timezone name abbreviation" }
231+ c = readAsciiChar()
232+ check(c == ' >' ) { " <%z> expected, got %$c " }
233+ c = readAsciiChar()
234+ return GENERATE_NAME
235+ }
209236 while (c != ' >' ) {
210237 check(c.isLetterOrDigit() || c == ' -' || c == ' +' ) { " Invalid char '$c ' in the std name in POSIX TZ string" }
211238 name.append(c)
@@ -218,7 +245,7 @@ private fun BinaryDataReader.readPosixTzString(): PosixTzString? {
218245 c = readAsciiChar()
219246 }
220247 }
221- check(name.isNotEmpty()) { " Empty std name in POSIX TZ string" }
248+ check(name.isNotEmpty()) { " Empty std name in POSIX TZ string: got $c " }
222249 return name.toString()
223250 }
224251
@@ -341,13 +368,29 @@ private fun BinaryDataReader.readPosixTzString(): PosixTzString? {
341368
342369 val std = readName() ? : return null
343370 val stdOffset = readOffset() ? : throw IllegalArgumentException (" Could not parse the std offset in POSIX TZ string" )
344- val dst = readName() ? : return PosixTzString (std to stdOffset, null , null )
371+ val stdName = if (std == = GENERATE_NAME ) ISO_OFFSET_BASIC_NO_Z .format(stdOffset) else std
372+ val dst = readName() ? : return PosixTzString (stdName to stdOffset, null , null )
345373 val dstOffset = readOffset() ? : UtcOffset (seconds = stdOffset.totalSeconds + 3600 )
374+ val dstName = if (dst == = GENERATE_NAME ) ISO_OFFSET_BASIC_NO_Z .format(dstOffset) else dst
346375 val startDate = readDate() ? : return PosixTzString (std to stdOffset, dst to dstOffset, null )
347376 val startTime = readTime() ? : MonthDayTime .TransitionLocaltime (2 , 0 , 0 )
348377 val endDate = readDate() ? : throw IllegalArgumentException (" Could not parse the end date in POSIX TZ string" )
349378 val endTime = readTime() ? : MonthDayTime .TransitionLocaltime (2 , 0 , 0 )
350379 val start = MonthDayTime (startDate, startTime, MonthDayTime .OffsetResolver .WallClockOffset )
351380 val end = MonthDayTime (endDate, endTime, MonthDayTime .OffsetResolver .WallClockOffset )
352- return PosixTzString (std to stdOffset, dst to dstOffset, start to end)
381+ return PosixTzString (stdName to stdOffset, dstName to dstOffset, start to end)
382+ }
383+
384+ private const val GENERATE_NAME = " %z"
385+
386+ private val ISO_OFFSET_BASIC_NO_Z by lazy {
387+ UtcOffset .Format {
388+ offsetHours()
389+ optional {
390+ offsetMinutesOfHour()
391+ optional {
392+ offsetSecondsOfMinute()
393+ }
394+ }
395+ }
353396}
0 commit comments