aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-16 00:29:03 +0900
committernsfisis <nsfisis@gmail.com>2026-08-16 01:03:45 +0900
commitaad468e8b75ffc3e87ea6dfa22c53a8299fc08da (patch)
tree8864fec7343dfb91c7c12e35a2989081ad197fd7 /crates/shirabe-php-shim/src
parent2936cb809da433a92f267f3bc232d88ed4e315ac (diff)
downloadphp-shirabe-aad468e8b75ffc3e87ea6dfa22c53a8299fc08da.tar.gz
php-shirabe-aad468e8b75ffc3e87ea6dfa22c53a8299fc08da.tar.zst
php-shirabe-aad468e8b75ffc3e87ea6dfa22c53a8299fc08da.zip
feat(php-shim): render human-facing timestamps in the local timezone
Split date() into date_utc() and date_local(), the latter resolving the system's local timezone through the tzfile crate ($TZ, then /etc/localtime, falling back to UTC when neither is readable). The timestamps Composer renders for humans -- the GitHub OAuth token note, the GitHub API rate limit reset time, the Perforce client spec fields and the "today" check of the show command -- now go through date_local(). PHP resolves its default timezone from the date.timezone ini setting, which Shirabe does not read, so date_default_timezone_get/set have no input left to model and are dropped from the shim and its callers. The resulting difference is recorded in docs/known-incompatibilities.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim/src')
-rw-r--r--crates/shirabe-php-shim/src/datetime.rs58
-rw-r--r--crates/shirabe-php-shim/src/runtime.rs2
2 files changed, 28 insertions, 32 deletions
diff --git a/crates/shirabe-php-shim/src/datetime.rs b/crates/shirabe-php-shim/src/datetime.rs
index fd0348ff..28ec52af 100644
--- a/crates/shirabe-php-shim/src/datetime.rs
+++ b/crates/shirabe-php-shim/src/datetime.rs
@@ -1,11 +1,8 @@
-static DEFAULT_TIMEZONE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
-
/// Parse the subset of the strtotime()/date_create() grammar that Composer actually emits.
///
/// Supported: ISO8601/RFC3339 (`2023-01-15T12:34:56Z`, `...+00:00`), `Y-m-d H:i:s`, `Y-m-d`,
-/// and `@<unixtime>`. Inputs without an explicit offset are interpreted as UTC, matching the
-/// default timezone this shim assumes elsewhere. Anything else returns `None` rather than
-/// guessing, mirroring PHP returning `false` on unrecognized input.
+/// and `@<unixtime>`. Inputs without an explicit offset are interpreted as UTC. Anything else
+/// returns `None` rather than guessing, mirroring PHP returning `false` on unrecognized input.
fn parse_to_fixed(s: &str) -> Option<chrono::DateTime<chrono::FixedOffset>> {
let s = s.trim();
if s.is_empty() {
@@ -126,34 +123,35 @@ pub fn microtime() -> f64 {
duration.as_secs_f64()
}
-// PHP defaults to "UTC" when no default timezone has been configured.
-pub fn date_default_timezone_get() -> String {
- DEFAULT_TIMEZONE
- .lock()
- .unwrap()
- .clone()
- .unwrap_or_else(|| "UTC".to_string())
-}
-
-pub fn date_default_timezone_set(tz: &str) -> bool {
- *DEFAULT_TIMEZONE.lock().unwrap() = Some(tz.to_string());
- true
+/// PHP: `date()`, rendering in UTC.
+pub fn date_utc(format: &str, timestamp: Option<i64>) -> String {
+ let timestamp = timestamp.unwrap_or_else(time);
+ let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)
+ .expect("date() timestamp out of range");
+ dt.format(date_format_to_strftime(format)).to_string()
}
-pub fn date(format: &str, timestamp: Option<i64>) -> String {
+/// PHP: `date()`, rendering in the system's local timezone.
+pub fn date_local(format: &str, timestamp: Option<i64>) -> String {
let timestamp = timestamp.unwrap_or_else(time);
- // TODO(php-semantics): model the system default timezone. PHP `date()` renders in the default
- // timezone (usually the system's local zone); without a timezone database only "UTC" can be
- // resolved here, so on a non-UTC machine this diverges whenever the local date differs from
- // the UTC date (e.g. daily 00:00-09:00 JST). Fixing this needs a timezone database (a new
- // crate). Any named zone is rejected loudly rather than silently rendered in the wrong zone.
- let tz = date_default_timezone_get();
- if tz != "UTC" {
- panic!(
- "date() with non-UTC default timezone {tz:?} is not supported (no timezone database)"
- );
- }
let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)
.expect("date() timestamp out of range");
- dt.format(date_format_to_strftime(format)).to_string()
+ let tz = local_timezone();
+ dt.with_timezone(&&tz)
+ .format(date_format_to_strftime(format))
+ .to_string()
+}
+
+/// The zone `$TZ` names, or the one `/etc/localtime` describes. Falls back to UTC when no tz
+/// database is readable, as PHP does when `date.timezone` is unset.
+fn local_timezone() -> tzfile::Tz {
+ #[cfg(unix)]
+ {
+ tzfile::Tz::local().unwrap_or_else(|_| tzfile::Tz::from(chrono::Utc))
+ }
+ #[cfg(not(unix))]
+ {
+ // TODO(windows): `tzfile::Tz::local()` is Unix-only.
+ todo!()
+ }
}
diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs
index 6fba5774..fcd6a50f 100644
--- a/crates/shirabe-php-shim/src/runtime.rs
+++ b/crates/shirabe-php-shim/src/runtime.rs
@@ -67,8 +67,6 @@ pub fn function_exists(name: &str) -> bool {
| "curl_multi_setopt"
| "curl_share_init"
| "curl_strerror"
- | "date_default_timezone_get"
- | "date_default_timezone_set"
| "disk_free_space"
| "exec"
| "filter_var"