diff --git a/Cargo.lock b/Cargo.lock index 47e695e49d79..5c0c40925b1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,12 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -387,6 +393,42 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "ciborium" version = "0.2.0" @@ -2035,6 +2077,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.7" @@ -2047,6 +2098,44 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -2537,6 +2626,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.7" @@ -3857,6 +3952,8 @@ dependencies = [ "cap-rand", "cap-std", "cap-time-ext", + "chrono", + "chrono-tz", "fs-set-times", "futures", "io-extras", diff --git a/Cargo.toml b/Cargo.toml index ff42046e1f39..f6b9b11c4b9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -322,6 +322,8 @@ criterion = { version = "0.5.0", default-features = false, features = ["html_rep rustc-hash = "1.1.0" libtest-mimic = "0.7.0" semver = { version = "1.0.17", default-features = false } +chrono = "0.4.26" +chrono-tz = "0.8.3" # ============================================================================= # diff --git a/crates/test-programs/src/lib.rs b/crates/test-programs/src/lib.rs index dfa4541a8fc2..750ec7748dc8 100644 --- a/crates/test-programs/src/lib.rs +++ b/crates/test-programs/src/lib.rs @@ -3,7 +3,7 @@ pub mod nn; pub mod preview1; pub mod sockets; -wit_bindgen::generate!("test-command" in "../wasi/wit"); +wit_bindgen::generate!("test-command" in "../wasi/wit", features: ["clocks-timezone"]); pub mod proxy { wit_bindgen::generate!({ @@ -15,5 +15,6 @@ pub mod proxy { "wasi:http/types@0.2.0": crate::wasi::http::types, "wasi:http/outgoing-handler@0.2.0": crate::wasi::http::outgoing_handler, }, + features: ["clocks-timezone"], }); } diff --git a/crates/wasi-http/src/bindings.rs b/crates/wasi-http/src/bindings.rs index d48cbd6b52b5..a9a2a7680971 100644 --- a/crates/wasi-http/src/bindings.rs +++ b/crates/wasi-http/src/bindings.rs @@ -38,6 +38,7 @@ mod generated { trappable_error_type: { "wasi:http/types/error-code" => crate::HttpError, }, + features: ["clocks-timezone"], }); } @@ -64,6 +65,7 @@ pub mod sync { "wasi": wasmtime_wasi::bindings, // everything else }, require_store_data_send: true, + features: ["clocks-timezone"], }); } diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs index 8c5c0f509f03..cbe11921817d 100644 --- a/crates/wasi-http/src/lib.rs +++ b/crates/wasi-http/src/lib.rs @@ -283,6 +283,7 @@ where let closure = type_annotate_wasi::(|t| wasmtime_wasi::WasiImpl(t)); wasmtime_wasi::bindings::clocks::wall_clock::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::clocks::monotonic_clock::add_to_linker_get_host(l, closure)?; + wasmtime_wasi::bindings::clocks::timezone::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::io::poll::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::io::error::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::io::streams::add_to_linker_get_host(l, closure)?; @@ -375,6 +376,7 @@ where wasmtime_wasi::bindings::clocks::wall_clock::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::clocks::monotonic_clock::add_to_linker_get_host(l, closure)?; + wasmtime_wasi::bindings::clocks::timezone::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::sync::io::poll::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::sync::io::streams::add_to_linker_get_host(l, closure)?; wasmtime_wasi::bindings::io::error::add_to_linker_get_host(l, closure)?; diff --git a/crates/wasi-http/wit/deps/clocks/timezone.wit b/crates/wasi-http/wit/deps/clocks/timezone.wit new file mode 100644 index 000000000000..38e03e05e144 --- /dev/null +++ b/crates/wasi-http/wit/deps/clocks/timezone.wit @@ -0,0 +1,54 @@ +package wasi:clocks@0.2.0; + +@unstable(feature = clocks-timezone) +interface timezone { + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi-http/wit/deps/clocks/world.wit b/crates/wasi-http/wit/deps/clocks/world.wit index c0224572a55b..b76a005145f1 100644 --- a/crates/wasi-http/wit/deps/clocks/world.wit +++ b/crates/wasi-http/wit/deps/clocks/world.wit @@ -3,4 +3,6 @@ package wasi:clocks@0.2.0; world imports { import monotonic-clock; import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; } diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs index 83b0475a20f8..9a4428c8c7de 100644 --- a/crates/wasi-preview1-component-adapter/src/lib.rs +++ b/crates/wasi-preview1-component-adapter/src/lib.rs @@ -62,6 +62,7 @@ pub mod bindings { // Instead, we manually define the bindings for these functions in // terms of raw pointers. skip: ["run", "get-environment", "poll"], + features: ["clocks-timezone"], }); #[cfg(feature = "reactor")] @@ -78,6 +79,7 @@ pub mod bindings { // Instead, we manually define the bindings for these functions in // terms of raw pointers. skip: ["get-environment", "poll"], + features: ["clocks-timezone"], }); #[cfg(feature = "proxy")] @@ -99,6 +101,7 @@ pub mod bindings { raw_strings, runtime_path: "crate::bindings::wit_bindgen_rt_shim", skip: ["poll"], + features: ["clocks-timezone"], }); pub mod wit_bindgen_rt_shim { diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 2816f16c9d29..22a7828e3ffb 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -27,6 +27,8 @@ cap-rand = { workspace = true } cap-fs-ext = { workspace = true } cap-net-ext = { workspace = true } cap-time-ext = { workspace = true } +chrono = { workspace = true } +chrono-tz = { workspace = true } io-lifetimes = { workspace = true } fs-set-times = { workspace = true } bitflags = { workspace = true } diff --git a/crates/wasi/src/bindings.rs b/crates/wasi/src/bindings.rs index 62a0ed020938..33e05b327533 100644 --- a/crates/wasi/src/bindings.rs +++ b/crates/wasi/src/bindings.rs @@ -179,6 +179,7 @@ pub mod sync { "wasi:sockets/udp/udp-socket": super::super::sockets::udp::UdpSocket, }, require_store_data_send: true, + features: ["clocks-timezone"], }); } pub use self::generated::exports; @@ -413,6 +414,7 @@ mod async_io { "wasi:cli/terminal-input/terminal-input": crate::stdio::TerminalInput, "wasi:cli/terminal-output/terminal-output": crate::stdio::TerminalOutput, }, + features: ["clocks-timezone"], }); } diff --git a/crates/wasi/src/clocks.rs b/crates/wasi/src/clocks.rs index 175ac1990ffc..1f6ef9f9fcb8 100644 --- a/crates/wasi/src/clocks.rs +++ b/crates/wasi/src/clocks.rs @@ -1,4 +1,5 @@ pub mod host; +use crate::bindings::clocks::timezone::TimezoneDisplay; use cap_std::time::Duration; pub trait HostWallClock: Send { @@ -10,3 +11,8 @@ pub trait HostMonotonicClock: Send { fn resolution(&self) -> u64; fn now(&self) -> u64; } + +pub trait HostTimezone: Send { + fn display(&self, datetime: Duration) -> TimezoneDisplay; + fn utc_offset(&self, datetime: Duration) -> i32; +} diff --git a/crates/wasi/src/clocks/host.rs b/crates/wasi/src/clocks/host.rs index 20b35ce6438f..bc812382ada4 100644 --- a/crates/wasi/src/clocks/host.rs +++ b/crates/wasi/src/clocks/host.rs @@ -1,7 +1,10 @@ -use super::{HostMonotonicClock, HostWallClock}; +use super::{HostMonotonicClock, HostTimezone, HostWallClock}; +use crate::bindings::clocks::timezone::TimezoneDisplay; use cap_std::time::{Duration, Instant, SystemClock}; use cap_std::{ambient_authority, AmbientAuthority}; use cap_time_ext::{MonotonicClockExt, SystemClockExt}; +use chrono::{NaiveDateTime, TimeZone}; +use chrono_tz::{OffsetComponents, Tz, TZ_VARIANTS}; pub struct WallClock { /// The underlying system clock. @@ -64,6 +67,53 @@ impl HostMonotonicClock for MonotonicClock { } } +pub struct Timezone { + // The underlying system timezone. + timezone: cap_time_ext::Timezone, +} + +impl Timezone { + pub fn new(ambient_authority: AmbientAuthority) -> Self { + Self { + timezone: cap_time_ext::Timezone::new(ambient_authority), + } + } + + fn timezone_from_duration(&self, datetime: Duration) -> Option { + let name = self.timezone.timezone_name().ok()?; + let tz: Tz = TZ_VARIANTS.into_iter().find(|tz| tz.to_string() == name)?; + let naive_datetime = NaiveDateTime::from_timestamp_opt(datetime.as_secs() as i64, 0)?; + let tz_offset = tz.offset_from_local_datetime(&naive_datetime).single()?; + let utc_offset = tz_offset.base_utc_offset().num_hours() as i32; + let in_daylight_saving_time = !tz_offset.dst_offset().is_zero(); + Some(TimezoneDisplay { + utc_offset, + name, + in_daylight_saving_time, + }) + } +} + +impl HostTimezone for Timezone { + fn display(&self, datetime: Duration) -> TimezoneDisplay { + match self.timezone_from_duration(datetime) { + None => TimezoneDisplay { + utc_offset: 0, + name: "UTC".to_string(), + in_daylight_saving_time: false, + }, + Some(timezone_display) => timezone_display, + } + } + + fn utc_offset(&self, datetime: Duration) -> i32 { + match self.timezone_from_duration(datetime) { + None => 0, + Some(timezone_display) => timezone_display.utc_offset, + } + } +} + pub fn monotonic_clock() -> Box { Box::new(MonotonicClock::new(ambient_authority())) } @@ -71,3 +121,7 @@ pub fn monotonic_clock() -> Box { pub fn wall_clock() -> Box { Box::new(WallClock::new(ambient_authority())) } + +pub fn timezone() -> Box { + Box::new(Timezone::new(ambient_authority())) +} diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs index 136047b9c887..6186e86ec3d6 100644 --- a/crates/wasi/src/ctx.rs +++ b/crates/wasi/src/ctx.rs @@ -1,7 +1,7 @@ use crate::{ clocks::{ - host::{monotonic_clock, wall_clock}, - HostMonotonicClock, HostWallClock, + host::{monotonic_clock, timezone, wall_clock}, + HostMonotonicClock, HostTimezone, HostWallClock, }, filesystem::{Dir, OpenMode}, network::{SocketAddrCheck, SocketAddrUse}, @@ -53,6 +53,7 @@ pub struct WasiCtxBuilder { monotonic_clock: Box, allowed_network_uses: AllowedNetworkUses, allow_blocking_current_thread: bool, + timezone: Box, built: bool, } @@ -102,6 +103,7 @@ impl WasiCtxBuilder { monotonic_clock: monotonic_clock(), allowed_network_uses: AllowedNetworkUses::default(), allow_blocking_current_thread: false, + timezone: timezone(), built: false, } } @@ -481,6 +483,7 @@ impl WasiCtxBuilder { monotonic_clock, allowed_network_uses, allow_blocking_current_thread, + timezone, built: _, } = mem::replace(self, Self::new()); self.built = true; @@ -500,6 +503,7 @@ impl WasiCtxBuilder { monotonic_clock, allowed_network_uses, allow_blocking_current_thread, + timezone, } } @@ -645,6 +649,7 @@ pub struct WasiCtx { pub(crate) insecure_random_seed: u128, pub(crate) wall_clock: Box, pub(crate) monotonic_clock: Box, + pub(crate) timezone: Box, pub(crate) env: Vec<(String, String)>, pub(crate) args: Vec, pub(crate) preopens: Vec<(Dir, String)>, diff --git a/crates/wasi/src/host/clocks.rs b/crates/wasi/src/host/clocks.rs index e94ca98555ff..0755d0c5c35e 100644 --- a/crates/wasi/src/host/clocks.rs +++ b/crates/wasi/src/host/clocks.rs @@ -2,6 +2,7 @@ use crate::bindings::{ clocks::monotonic_clock::{self, Duration as WasiDuration, Instant}, + clocks::timezone::{self, TimezoneDisplay}, clocks::wall_clock::{self, Datetime}, }; use crate::poll::{subscribe, Subscribe}; @@ -107,3 +108,18 @@ impl Subscribe for Deadline { } } } + +impl timezone::Host for WasiImpl +where + T: WasiView, +{ + fn display(&mut self, when: Datetime) -> anyhow::Result { + let duration = std::time::Duration::new(when.seconds, when.nanoseconds); + Ok(self.ctx().timezone.display(duration)) + } + + fn utc_offset(&mut self, when: Datetime) -> anyhow::Result { + let duration = std::time::Duration::new(when.seconds, when.nanoseconds); + Ok(self.ctx().timezone.utc_offset(duration)) + } +} diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index fe8594c74c75..64dbe3b89be8 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -291,6 +291,7 @@ pub fn add_to_linker_async(linker: &mut Linker) -> anyhow::Resul crate::bindings::clocks::wall_clock::add_to_linker_get_host(l, closure)?; crate::bindings::clocks::monotonic_clock::add_to_linker_get_host(l, closure)?; + crate::bindings::clocks::timezone::add_to_linker_get_host(l, closure)?; crate::bindings::filesystem::types::add_to_linker_get_host(l, closure)?; crate::bindings::filesystem::preopens::add_to_linker_get_host(l, closure)?; crate::bindings::io::error::add_to_linker_get_host(l, closure)?; @@ -381,6 +382,7 @@ pub fn add_to_linker_sync( crate::bindings::clocks::wall_clock::add_to_linker_get_host(l, closure)?; crate::bindings::clocks::monotonic_clock::add_to_linker_get_host(l, closure)?; + crate::bindings::clocks::timezone::add_to_linker_get_host(l, closure)?; crate::bindings::sync::filesystem::types::add_to_linker_get_host(l, closure)?; crate::bindings::filesystem::preopens::add_to_linker_get_host(l, closure)?; crate::bindings::io::error::add_to_linker_get_host(l, closure)?; diff --git a/crates/wasi/wit/deps/cli/imports.wit b/crates/wasi/wit/deps/cli/imports.wit index 083b84a036d7..901c04842bfe 100644 --- a/crates/wasi/wit/deps/cli/imports.wit +++ b/crates/wasi/wit/deps/cli/imports.wit @@ -1,6 +1,7 @@ package wasi:cli@0.2.0; world imports { + @unstable(feature = clocks-timezone) include wasi:clocks/imports@0.2.0; include wasi:filesystem/imports@0.2.0; include wasi:sockets/imports@0.2.0; diff --git a/crates/wasi/wit/deps/clocks/timezone.wit b/crates/wasi/wit/deps/clocks/timezone.wit new file mode 100644 index 000000000000..38e03e05e144 --- /dev/null +++ b/crates/wasi/wit/deps/clocks/timezone.wit @@ -0,0 +1,54 @@ +package wasi:clocks@0.2.0; + +@unstable(feature = clocks-timezone) +interface timezone { + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi/wit/deps/clocks/world.wit b/crates/wasi/wit/deps/clocks/world.wit index c0224572a55b..b76a005145f1 100644 --- a/crates/wasi/wit/deps/clocks/world.wit +++ b/crates/wasi/wit/deps/clocks/world.wit @@ -3,4 +3,6 @@ package wasi:clocks@0.2.0; world imports { import monotonic-clock; import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; }