Skip to main content

jiff_static/
lib.rs

1/*!
2This crate provides macros for defining `static` data structures for Jiff.
3
4The macros in this crate are re-exported in the [`jiff::tz`] sub-module.
5Users should _not_ depend on this crate directly or import from it. Instead,
6enable the `static` or `static-tz` features of Jiff and use the re-exports in
7`jiff::tz`.
8
9At present, the macros in this crate are limited to creating `TimeZone`
10in a `const` context. This works by reading TZif data (e.g., from
11`/usr/share/zoneinfo/America/New_York` or from [`jiff-tzdb`]) at compile
12time and generating Rust source code that builds a `TimeZone`.
13
14# Documentation
15
16The macros defined in this crate are documented on their corresponding
17re-exports in Jiff:
18
19* `get` is documented at [`jiff::tz::get`].
20* `include` is documented at [`jiff::tz::include`].
21
22# Compatibility
23
24The APIs required to build a `TimeZone` in a `const` context are exposed by
25Jiff but not part of Jiff's public API for the purposes of semver (and do not
26appear in `rustdoc`). The only guarantee provided by `jiff` and `jiff-static`
27is that there is exactly one version of `jiff` that `jiff-static` works with.
28Conventionally, this is indicated by the exact same version string. That is,
29`jiff-static 0.2.2` is only guaranteed to work with `jiff 0.2.2`.
30
31This compatibility constraint is managed by Jiff, so that you should never
32need to worry about it. In particular, users should never directly depend on
33this crate. Everything should be managed through the `jiff` crate.
34
35[`jiff-tzdb`]: https://docs.rs/jiff-tzdb
36[`jiff::tz`]: https://docs.rs/jiff/0.2/jiff/tz/index.html
37[`jiff::tz::get`]: https://docs.rs/jiff/0.2/jiff/tz/macro.get.html
38[`jiff::tz::include`]: https://docs.rs/jiff/0.2/jiff/tz/macro.include.html
39*/
40
41extern crate alloc;
42extern crate proc_macro;
43
44use proc_macro::TokenStream;
45use quote::quote;
46
47use jcore::tz::{posix, tzif, TimeZoneId};
48
49// Public API docs are in Jiff.
50#[proc_macro]
51pub fn include(input: TokenStream) -> TokenStream {
52    let input = syn::parse_macro_input!(input as Include);
53    proc_macro::TokenStream::from(input.quote())
54}
55
56// Public API docs are in Jiff.
57#[cfg(feature = "tzdb")]
58#[proc_macro]
59pub fn get(input: TokenStream) -> TokenStream {
60    let input = syn::parse_macro_input!(input as Get);
61    proc_macro::TokenStream::from(input.quote())
62}
63
64/// The entry point for the `include!` macro.
65#[derive(Debug)]
66struct Include {
67    tzif: tzif::MaybeNamedTimeZone,
68}
69
70impl Include {
71    fn from_path_only(path: &str) -> Result<Include, String> {
72        const NEEDLE: &str = "zoneinfo/";
73
74        let Some(zoneinfo) = path.rfind(NEEDLE) else {
75            return Err(format!(
76                "could not extract IANA time zone identifier from \
77                 file path `{path}` \
78                 (could not find `zoneinfo` in path), \
79                 please provide IANA time zone identifier as second \
80                 parameter",
81            ));
82        };
83        let idstart = zoneinfo.saturating_add(NEEDLE.len());
84        let id = &path[idstart..];
85        Include::from_path_with_id(id, path)
86    }
87
88    fn from_path_with_id(id: &str, path: &str) -> Result<Include, String> {
89        let id = TimeZoneId::new_or_heap(id);
90        let data = std::fs::read(path)
91            .map_err(|e| format!("failed to read {path}: {e}"))?;
92        let tzif = tzif::TimeZone::parse(&data)
93            .map_err(|e| {
94                format!("failed to parse TZif data from {path}: {e}")
95            })?
96            .into_named(id);
97        Ok(Include { tzif })
98    }
99
100    fn quote(&self) -> proc_macro2::TokenStream {
101        self.tzif.quote()
102    }
103}
104
105impl syn::parse::Parse for Include {
106    fn parse(input: syn::parse::ParseStream) -> syn::Result<Include> {
107        let lit1 = input.parse::<syn::LitStr>()?.value();
108        if !input.lookahead1().peek(syn::Token![,]) {
109            return Ok(
110                Include::from_path_only(&lit1).map_err(|e| input.error(e))?
111            );
112        }
113        input.parse::<syn::Token![,]>()?;
114        if input.is_empty() {
115            return Ok(
116                Include::from_path_only(&lit1).map_err(|e| input.error(e))?
117            );
118        }
119        let lit2 = input.parse::<syn::LitStr>()?.value();
120        // Permit optional trailing comma.
121        if input.lookahead1().peek(syn::Token![,]) {
122            input.parse::<syn::Token![,]>()?;
123        }
124        Ok(Include::from_path_with_id(&lit2, &lit1)
125            .map_err(|e| input.error(e))?)
126    }
127}
128
129/// The entry point for the `get!` macro.
130#[cfg(feature = "tzdb")]
131#[derive(Debug)]
132struct Get {
133    tzif: tzif::MaybeNamedTimeZone,
134}
135
136#[cfg(feature = "tzdb")]
137impl Get {
138    fn from_id(id: &str) -> Result<Get, String> {
139        let (id, data) = jiff_tzdb::get(id).ok_or_else(|| {
140            format!("could not find time zone `{id}` in bundled tzdb")
141        })?;
142        let id = TimeZoneId::statik(id);
143        let tzif = tzif::TimeZone::parse(&data)
144            .map_err(|e| {
145                format!("failed to parse TZif data from bundled `{id}`: {e}")
146            })?
147            .into_named(id);
148        Ok(Get { tzif })
149    }
150
151    fn quote(&self) -> proc_macro2::TokenStream {
152        self.tzif.quote()
153    }
154}
155
156#[cfg(feature = "tzdb")]
157impl syn::parse::Parse for Get {
158    fn parse(input: syn::parse::ParseStream) -> syn::Result<Get> {
159        let lit1 = input.parse::<syn::LitStr>()?.value();
160        if input.lookahead1().peek(syn::Token![,]) {
161            input.parse::<syn::Token![,]>()?;
162        }
163        Ok(Get::from_id(&lit1).map_err(|e| input.error(e))?)
164    }
165}
166
167// Everything below at this point is quasi-quoting the `shared` data type
168// values into `static` data structures as Rust source code.
169
170trait Quote {
171    fn quote(&self) -> proc_macro2::TokenStream;
172}
173
174impl Quote for tzif::MaybeNamedTimeZone {
175    fn quote(&self) -> proc_macro2::TokenStream {
176        let tzif::MaybeNamedTimeZone { ref name, ref tz } = *self;
177        let tzif::TimeZone {
178            version,
179            checksum,
180            ref designations,
181            ref posix_tz,
182            ref types,
183            ref transitions,
184        } = *tz;
185        // We are guaranteed to always have a name in this context.
186        let name = name.as_ref().unwrap().quote();
187        let designations = designations.iter().map(Quote::quote);
188        let posix_tz = posix_tz
189            .as_ref()
190            .map(|tz| {
191                let tz = tz.quote();
192                quote!(Some(#tz))
193            })
194            .unwrap_or_else(|| quote!(None));
195        let types = types.iter().map(tzif::LocalTimeType::quote);
196        let transitions = transitions.quote();
197        quote! {{
198            static __TZ_DESIGNATIONS: &[jiff::__jcore::tz::Abbreviation] = &[#(#designations),*];
199            static __TZ_INTERNAL: jiff::__jcore::tz::tzif::MaybeNamedTimeZone =
200                jiff::__jcore::tz::tzif::MaybeNamedTimeZone {
201                    name: Some(#name),
202                    tz: jiff::__jcore::tz::tzif::TimeZone {
203                        version: #version,
204                        checksum: #checksum,
205                        designations: jiff::__jcore::util::MaybeStaticSlice::statik(__TZ_DESIGNATIONS),
206                        posix_tz: #posix_tz,
207                        types: jiff::__jcore::util::MaybeStaticSlice::statik(&[#(#types),*]),
208                        transitions: #transitions,
209                    },
210                };
211            static TZ: jiff::tz::TimeZone =
212                jiff::tz::TimeZone::__internal_from_tzif(&__TZ_INTERNAL);
213            // SAFETY: Since we are guaranteed that the `TimeZone` is
214            // constructed above as a static TZif time zone, it follows
215            // that it is safe to memcpy's its internal representation.
216            //
217            // NOTE: We arrange things this way so that `jiff::tz::get!`
218            // can be used "by value" in most contexts. Basically, we
219            // "pin" the time zone to a static so that it has a guaranteed
220            // static lifetime. Otherwise, since `TimeZone` has a `Drop`
221            // impl, it's easy to run afoul of this and have it be dropped
222            // earlier than you like. Since this particular variant of
223            // `TimeZone` can always be memcpy'd internally, we just do
224            // this dance here to save the user from having to write out
225            // their own `static`.
226            //
227            // NOTE: It would be nice if we could make this `copy` routine
228            // safe, or at least panic if it's misused. But to do that, you
229            // need to know the time zone variant. And to know the time
230            // zone variant, you need to "look" at the tag in the pointer.
231            // And looking at the address of a pointer in a `const` context
232            // is precarious.
233            unsafe { TZ.copy() }
234        }}
235    }
236}
237
238impl Quote for tzif::Transitions {
239    fn quote(&self) -> proc_macro2::TokenStream {
240        let tzif::Transitions {
241            ref timestamps,
242            ref civil_starts,
243            ref civil_ends,
244            ref infos,
245        } = *self;
246        let timestamps = timestamps.iter().map(tzif::Timestamp::quote);
247        let civil_starts = civil_starts.iter().map(tzif::DateTime::quote);
248        let civil_ends = civil_ends.iter().map(tzif::DateTime::quote);
249        let infos = infos.iter().map(tzif::TransitionInfo::quote);
250        quote! {
251            jiff::__jcore::tz::tzif::Transitions {
252                timestamps: jiff::__jcore::util::MaybeStaticSlice::statik(&[#(#timestamps),*]),
253                civil_starts: jiff::__jcore::util::MaybeStaticSlice::statik(&[#(#civil_starts),*]),
254                civil_ends: jiff::__jcore::util::MaybeStaticSlice::statik(&[#(#civil_ends),*]),
255                infos: jiff::__jcore::util::MaybeStaticSlice::statik(&[#(#infos),*]),
256            }
257        }
258    }
259}
260
261impl Quote for tzif::LocalTimeType {
262    fn quote(&self) -> proc_macro2::TokenStream {
263        let tzif::LocalTimeType { offset, dst, designation, indicator } =
264            *self;
265        let offset = offset.quote();
266        let dst = dst.quote();
267        let indicator = indicator.quote();
268        quote! {
269            jiff::__jcore::tz::tzif::LocalTimeType {
270                offset: #offset,
271                dst: #dst,
272                designation: #designation,
273                indicator: #indicator,
274            }
275        }
276    }
277}
278
279impl Quote for tzif::Indicator {
280    fn quote(&self) -> proc_macro2::TokenStream {
281        match *self {
282            tzif::Indicator::LocalWall => quote! {
283                jiff::__jcore::tz::tzif::Indicator::LocalWall
284            },
285            tzif::Indicator::LocalStandard => quote! {
286                jiff::__jcore::tz::tzif::Indicator::LocalStandard
287            },
288            tzif::Indicator::UTStandard => quote! {
289                jiff::__jcore::tz::tzif::Indicator::UTStandard
290            },
291        }
292    }
293}
294
295impl Quote for tzif::TransitionInfo {
296    fn quote(&self) -> proc_macro2::TokenStream {
297        let tzif::TransitionInfo { type_index, kind } = *self;
298        let kind = kind.quote();
299        quote! {
300            jiff::__jcore::tz::tzif::TransitionInfo {
301                type_index: #type_index,
302                kind: #kind,
303            }
304        }
305    }
306}
307
308impl Quote for tzif::TransitionKind {
309    fn quote(&self) -> proc_macro2::TokenStream {
310        match *self {
311            tzif::TransitionKind::Unambiguous => quote! {
312                jiff::__jcore::tz::tzif::TransitionKind::Unambiguous
313            },
314            tzif::TransitionKind::Gap => quote! {
315                jiff::__jcore::tz::tzif::TransitionKind::Gap
316            },
317            tzif::TransitionKind::Fold => quote! {
318                jiff::__jcore::tz::tzif::TransitionKind::Fold
319            },
320        }
321    }
322}
323
324impl Quote for tzif::DateTime {
325    fn quote(&self) -> proc_macro2::TokenStream {
326        let year = self.year();
327        let month = self.month();
328        let day = self.day();
329        let hour = self.hour();
330        let minute = self.minute();
331        let second = self.second();
332        quote! {
333            jiff::__jcore::tz::tzif::DateTime::new(jiff::__jcore::civil::datetime(
334                #year,
335                #month,
336                #day,
337                #hour,
338                #minute,
339                #second,
340                0
341            ))
342        }
343    }
344}
345
346impl Quote for posix::TimeZone {
347    fn quote(&self) -> proc_macro2::TokenStream {
348        let posix::TimeZone { ref std_abbrev, ref std_offset, ref dst } =
349            *self;
350        let std_abbrev = std_abbrev.quote();
351        let std_offset = std_offset.quote();
352        let dst = dst
353            .as_ref()
354            .map(|dst| {
355                let dst = dst.quote();
356                quote!(Some(#dst))
357            })
358            .unwrap_or_else(|| quote!(None));
359        quote! {
360            jiff::__jcore::tz::posix::TimeZone {
361                std_abbrev: #std_abbrev,
362                std_offset: #std_offset,
363                dst: #dst,
364            }
365        }
366    }
367}
368
369impl Quote for posix::Dst {
370    fn quote(&self) -> proc_macro2::TokenStream {
371        let posix::Dst { ref abbrev, ref offset, ref rule } = *self;
372        let abbrev = abbrev.quote();
373        let offset = offset.quote();
374        let rule = rule.quote();
375        quote! {
376            jiff::__jcore::tz::posix::Dst {
377                abbrev: #abbrev,
378                offset: #offset,
379                rule: #rule,
380            }
381        }
382    }
383}
384
385impl Quote for posix::Rule {
386    fn quote(&self) -> proc_macro2::TokenStream {
387        let start = self.start.quote();
388        let end = self.end.quote();
389        quote! {
390            jiff::__jcore::tz::posix::Rule { start: #start, end: #end }
391        }
392    }
393}
394
395impl Quote for posix::DayTime {
396    fn quote(&self) -> proc_macro2::TokenStream {
397        let posix::DayTime { ref date, ref time } = *self;
398        let date = date.quote();
399        let time = time.quote();
400        quote! {
401            jiff::__jcore::tz::posix::DayTime { date: #date, time: #time }
402        }
403    }
404}
405
406impl Quote for posix::Day {
407    fn quote(&self) -> proc_macro2::TokenStream {
408        match *self {
409            posix::Day::JulianOne(day) => quote! {
410                jiff::__jcore::tz::posix::Day::JulianOne(#day)
411            },
412            posix::Day::JulianZero(day) => quote! {
413                jiff::__jcore::tz::posix::Day::JulianZero(#day)
414            },
415            posix::Day::WeekdayOfMonth { month, week, weekday } => {
416                let weekday = weekday.quote();
417                quote! {
418                    jiff::__jcore::tz::posix::Day::WeekdayOfMonth {
419                        month: #month,
420                        week: #week,
421                        weekday: #weekday,
422                    }
423                }
424            }
425        }
426    }
427}
428
429impl Quote for jcore::civil::Weekday {
430    fn quote(&self) -> proc_macro2::TokenStream {
431        use jcore::civil::Weekday::*;
432        match *self {
433            Sunday => quote!(jiff::__jcore::civil::Weekday::Sunday),
434            Monday => quote!(jiff::__jcore::civil::Weekday::Monday),
435            Tuesday => quote!(jiff::__jcore::civil::Weekday::Tuesday),
436            Wednesday => quote!(jiff::__jcore::civil::Weekday::Wednesday),
437            Thursday => quote!(jiff::__jcore::civil::Weekday::Thursday),
438            Friday => quote!(jiff::__jcore::civil::Weekday::Friday),
439            Saturday => quote!(jiff::__jcore::civil::Weekday::Saturday),
440        }
441    }
442}
443
444impl Quote for jcore::tz::posix::TransitionCivilTime {
445    fn quote(&self) -> proc_macro2::TokenStream {
446        let posix::TransitionCivilTime { second } = *self;
447        quote! {
448            jiff::__jcore::tz::posix::TransitionCivilTime { second: #second }
449        }
450    }
451}
452
453impl Quote for tzif::Timestamp {
454    fn quote(&self) -> proc_macro2::TokenStream {
455        let second = self.as_second();
456        quote! {
457            jiff::__jcore::tz::tzif::Timestamp::new(
458                jiff::__jcore::Timestamp::constant(#second, 0),
459            )
460        }
461    }
462}
463
464impl Quote for jcore::tz::Offset {
465    fn quote(&self) -> proc_macro2::TokenStream {
466        let seconds = self.seconds();
467        quote! {
468            jiff::__jcore::tz::Offset::constant_seconds(#seconds)
469        }
470    }
471}
472
473impl Quote for jcore::tz::Dst {
474    fn quote(&self) -> proc_macro2::TokenStream {
475        match *self {
476            jcore::tz::Dst::Yes => quote! { jiff::__jcore::tz::Dst::Yes },
477            jcore::tz::Dst::No => quote! { jiff::__jcore::tz::Dst::No },
478        }
479    }
480}
481
482impl<const N: usize> Quote for jcore::util::SmallStr<N> {
483    fn quote(&self) -> proc_macro2::TokenStream {
484        let s = self.as_str();
485        quote! {
486            jiff::__jcore::util::SmallStr::statik(#s)
487        }
488    }
489}
490
491impl<T: Quote + 'static> Quote for jcore::util::MaybeStaticSlice<T> {
492    fn quote(&self) -> proc_macro2::TokenStream {
493        let slice = self.as_slice().iter().map(Quote::quote);
494        quote! {{
495            jiff::__jcore::util::MaybeStaticSlice::statik(&[#(#slice),*])
496        }}
497    }
498}