Skip to content

fix: reject unknown IANA time zones in Instant::toString() - #73

Open
ethanhawkes-gif wants to merge 1 commit into
MidnightDesign:masterfrom
ethanhawkes-gif:fix/60-tostring-unknown-timezone
Open

fix: reject unknown IANA time zones in Instant::toString()#73
ethanhawkes-gif wants to merge 1 commit into
MidnightDesign:masterfrom
ethanhawkes-gif:fix/60-tostring-unknown-timezone

Conversation

@ethanhawkes-gif

Copy link
Copy Markdown

Fixes #60.

Problem

validateTimeZoneString() only checks syntax, so a well-formed but unrecognized IANA identifier passes it and reaches the offset lookup, where it is silently rendered as UTC — while toZonedDateTimeISO() correctly throws a RangeError for the same input. The two entry points disagreed.

Instant::fromEpochMilliseconds(0)->toString(['timeZone' => 'Bogus/Zone']);
// before: "1970-01-01T00:00:00+00:00"   (silent UTC)

Instant::fromEpochMilliseconds(0)->toString(['timeZone' => '1970-01-01T00:00+01:00[Bogus/Zone]']);
// before: "1970-01-01T01:00:00+01:00"   (bogus bracket swallowed, inline offset used)

Per TC39 (ToTemporalTimeZoneIdentifier / GetAvailableNamedTimeZoneIdentifier), an unknown time zone identifier is a RangeError.

Fix

Two toString()-path swallow sites now throw instead, matching toZonedDateTimeISO():

  • ianaOffsetSeconds() — a bare unknown IANA name previously fell back to offset 0 (→ UTC).
  • resolveTimeZoneOffsetSeconds() — an unknown bracket zone previously fell through to the inline offset.

Both helpers are private with a single caller each, only in the toString() path.

Tests

Adds Porcelain regression tests for both unknown-zone paths plus positive coverage for valid bare and bracketed IANA zones. Local verification:

  • Full suite: 12,400 tests pass (+4 new, 0 regressions).
  • PHPStan level 9, Psalm level 1, Mago lint + analyze, and Mago format-check all clean (zero new issues).

🤖 Generated with Claude Code

validateTimeZoneString() only checks syntax, so a well-formed but
unrecognized IANA identifier (e.g. "Bogus/Zone") reaches the offset
lookup and is silently rendered as UTC, while toZonedDateTimeISO()
correctly throws a RangeError for the same input. The two entry points
disagreed.

Per TC39 (ToTemporalTimeZoneIdentifier / GetAvailableNamedTimeZoneIdentifier)
an unknown time zone identifier is a RangeError. Make the two toString()
swallow sites throw instead:

- ianaOffsetSeconds(): a bare unknown IANA name (was: return 0 -> UTC)
- resolveTimeZoneOffsetSeconds(): an unknown bracket zone that previously
  fell through to the inline offset

Adds Porcelain regression tests for both unknown-zone paths plus positive
coverage for valid bare and bracketed IANA zones.

Fixes MidnightDesign#60

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: ethanhawkes-gif <259455325+ethanhawkes-gif@users.noreply.github.com>
@MidnightDesign MidnightDesign changed the title Fix Instant::toString() silently accepting unknown IANA time zones (#60) fix: reject unknown IANA time zones in Instant::toString() Aug 17, 2026
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.

Instant::toString() silently treats an unknown IANA time zone as UTC

1 participant