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 self::shared::{
48    util::array_str::Abbreviation, PosixDay, PosixDayTime, PosixDst,
49    PosixOffset, PosixRule, PosixTime, PosixTimeZone, TzifDateTime, TzifFixed,
50    TzifIndicator, TzifLocalTimeType, TzifOwned, TzifTransitionInfo,
51    TzifTransitionKind, TzifTransitionsOwned,
52};
53
54/// A bundle of code copied from `src/shared`.
55///
56/// The main thing we use in here is the parsing routine for TZif data and
57/// shared data types for representing TZif data.
58///
59/// We also squash dead code warnings. This is somewhat precarious since
60/// ideally we wouldn't compile what we don't need. But in practice, it's
61/// annoying to get rid of everything we don't need in this context, and it
62/// should be pretty small anyway.
63#[allow(dead_code)]
64mod shared;
65
66// Public API docs are in Jiff.
67#[proc_macro]
68pub fn include(input: TokenStream) -> TokenStream {
69    let input = syn::parse_macro_input!(input as Include);
70    proc_macro::TokenStream::from(input.quote())
71}
72
73// Public API docs are in Jiff.
74#[cfg(feature = "tzdb")]
75#[proc_macro]
76pub fn get(input: TokenStream) -> TokenStream {
77    let input = syn::parse_macro_input!(input as Get);
78    proc_macro::TokenStream::from(input.quote())
79}
80
81/// The entry point for the `include!` macro.
82#[derive(Debug)]
83struct Include {
84    tzif: TzifOwned,
85}
86
87impl Include {
88    fn from_path_only(path: &str) -> Result<Include, String> {
89        const NEEDLE: &str = "zoneinfo/";
90
91        let Some(zoneinfo) = path.rfind(NEEDLE) else {
92            return Err(format!(
93                "could not extract IANA time zone identifier from \
94                 file path `{path}` \
95                 (could not find `zoneinfo` in path), \
96                 please provide IANA time zone identifier as second \
97                 parameter",
98            ));
99        };
100        let idstart = zoneinfo.saturating_add(NEEDLE.len());
101        let id = &path[idstart..];
102        Include::from_path_with_id(id, path)
103    }
104
105    fn from_path_with_id(id: &str, path: &str) -> Result<Include, String> {
106        let id = id.to_string();
107        let data = std::fs::read(path)
108            .map_err(|e| format!("failed to read {path}: {e}"))?;
109        let tzif = TzifOwned::parse(Some(id.clone()), &data).map_err(|e| {
110            format!("failed to parse TZif data from {path}: {e}")
111        })?;
112        Ok(Include { tzif })
113    }
114
115    fn quote(&self) -> proc_macro2::TokenStream {
116        self.tzif.quote()
117    }
118}
119
120impl syn::parse::Parse for Include {
121    fn parse(input: syn::parse::ParseStream) -> syn::Result<Include> {
122        let lit1 = input.parse::<syn::LitStr>()?.value();
123        if !input.lookahead1().peek(syn::Token![,]) {
124            return Ok(
125                Include::from_path_only(&lit1).map_err(|e| input.error(e))?
126            );
127        }
128        input.parse::<syn::Token![,]>()?;
129        if input.is_empty() {
130            return Ok(
131                Include::from_path_only(&lit1).map_err(|e| input.error(e))?
132            );
133        }
134        let lit2 = input.parse::<syn::LitStr>()?.value();
135        // Permit optional trailing comma.
136        if input.lookahead1().peek(syn::Token![,]) {
137            input.parse::<syn::Token![,]>()?;
138        }
139        Ok(Include::from_path_with_id(&lit2, &lit1)
140            .map_err(|e| input.error(e))?)
141    }
142}
143
144/// The entry point for the `get!` macro.
145#[cfg(feature = "tzdb")]
146#[derive(Debug)]
147struct Get {
148    tzif: TzifOwned,
149}
150
151#[cfg(feature = "tzdb")]
152impl Get {
153    fn from_id(id: &str) -> Result<Get, String> {
154        let (id, data) = jiff_tzdb::get(id).ok_or_else(|| {
155            format!("could not find time zone `{id}` in bundled tzdb")
156        })?;
157        let id = id.to_string();
158        let tzif = TzifOwned::parse(Some(id.clone()), &data).map_err(|e| {
159            format!("failed to parse TZif data from bundled `{id}`: {e}")
160        })?;
161        Ok(Get { tzif })
162    }
163
164    fn quote(&self) -> proc_macro2::TokenStream {
165        self.tzif.quote()
166    }
167}
168
169#[cfg(feature = "tzdb")]
170impl syn::parse::Parse for Get {
171    fn parse(input: syn::parse::ParseStream) -> syn::Result<Get> {
172        let lit1 = input.parse::<syn::LitStr>()?.value();
173        if input.lookahead1().peek(syn::Token![,]) {
174            input.parse::<syn::Token![,]>()?;
175        }
176        Ok(Get::from_id(&lit1).map_err(|e| input.error(e))?)
177    }
178}
179
180// Everything below at this point is quasi-quoting the `shared` data type
181// values into `static` data structures as Rust source code.
182
183impl TzifOwned {
184    fn quote(&self) -> proc_macro2::TokenStream {
185        let TzifOwned { ref fixed, ref types, ref transitions } = *self;
186        let fixed = fixed.quote();
187        let types = types.iter().map(TzifLocalTimeType::quote);
188        let transitions = transitions.quote();
189        quote! {
190            {
191                static TZ: jiff::tz::TimeZone =
192                    jiff::tz::TimeZone::__internal_from_tzif(
193                        &jiff::shared::TzifStatic {
194                            fixed: #fixed,
195                            types: &[#(#types),*],
196                            transitions: #transitions,
197                        }.into_jiff()
198                    );
199                // SAFETY: Since we are guaranteed that the `TimeZone` is
200                // constructed above as a static TZif time zone, it follows
201                // that it is safe to memcpy's its internal representation.
202                //
203                // NOTE: We arrange things this way so that `jiff::tz::get!`
204                // can be used "by value" in most contexts. Basically, we
205                // "pin" the time zone to a static so that it has a guaranteed
206                // static lifetime. Otherwise, since `TimeZone` has a `Drop`
207                // impl, it's easy to run afoul of this and have it be dropped
208                // earlier than you like. Since this particular variant of
209                // `TimeZone` can always be memcpy'd internally, we just do
210                // this dance here to save the user from having to write out
211                // their own `static`.
212                //
213                // NOTE: It would be nice if we could make this `copy` routine
214                // safe, or at least panic if it's misused. But to do that, you
215                // need to know the time zone variant. And to know the time
216                // zone variant, you need to "look" at the tag in the pointer.
217                // And looking at the address of a pointer in a `const` context
218                // is precarious.
219                unsafe { TZ.copy() }
220            }
221        }
222    }
223}
224
225impl TzifFixed<String, Abbreviation> {
226    fn quote(&self) -> proc_macro2::TokenStream {
227        let TzifFixed {
228            ref name,
229            version,
230            checksum,
231            ref designations,
232            ref posix_tz,
233        } = *self;
234        let name = name.as_ref().unwrap();
235        let posix_tz = posix_tz
236            .as_ref()
237            .map(|tz| {
238                let tz = tz.quote();
239                quote!(Some(#tz))
240            })
241            .unwrap_or_else(|| quote!(None));
242        quote! {
243            jiff::shared::TzifFixed {
244                name: Some(#name),
245                version: #version,
246                checksum: #checksum,
247                designations: #designations,
248                posix_tz: #posix_tz,
249            }
250        }
251    }
252}
253
254impl TzifTransitionsOwned {
255    fn quote(&self) -> proc_macro2::TokenStream {
256        let TzifTransitionsOwned {
257            ref timestamps,
258            ref civil_starts,
259            ref civil_ends,
260            ref infos,
261        } = *self;
262        let civil_starts: Vec<_> =
263            civil_starts.iter().map(TzifDateTime::quote).collect();
264        let civil_ends: Vec<_> =
265            civil_ends.iter().map(TzifDateTime::quote).collect();
266        let infos: Vec<_> =
267            infos.iter().map(TzifTransitionInfo::quote).collect();
268        quote! {
269            jiff::shared::TzifTransitions {
270                timestamps: &[#(#timestamps),*],
271                civil_starts: &[#(#civil_starts),*],
272                civil_ends: &[#(#civil_ends),*],
273                infos: &[#(#infos),*],
274            }
275        }
276    }
277}
278
279impl TzifLocalTimeType {
280    fn quote(&self) -> proc_macro2::TokenStream {
281        let TzifLocalTimeType {
282            offset,
283            is_dst,
284            ref designation,
285            ref indicator,
286        } = *self;
287        let desig_start = designation.0;
288        let desig_end = designation.1;
289        let indicator = indicator.quote();
290        quote! {
291            jiff::shared::TzifLocalTimeType {
292                offset: #offset,
293                is_dst: #is_dst,
294                designation: (#desig_start, #desig_end),
295                indicator: #indicator,
296            }
297        }
298    }
299}
300
301impl TzifIndicator {
302    fn quote(&self) -> proc_macro2::TokenStream {
303        match *self {
304            TzifIndicator::LocalWall => quote! {
305                jiff::shared::TzifIndicator::LocalWall
306            },
307            TzifIndicator::LocalStandard => quote! {
308                jiff::shared::TzifIndicator::LocalStandard
309            },
310            TzifIndicator::UTStandard => quote! {
311                jiff::shared::TzifIndicator::UTStandard
312            },
313        }
314    }
315}
316
317impl TzifTransitionInfo {
318    fn quote(&self) -> proc_macro2::TokenStream {
319        let TzifTransitionInfo { type_index, kind } = *self;
320        let kind = kind.quote();
321        quote! {
322            jiff::shared::TzifTransitionInfo {
323                type_index: #type_index,
324                kind: #kind,
325            }
326        }
327    }
328}
329
330impl TzifTransitionKind {
331    fn quote(&self) -> proc_macro2::TokenStream {
332        match *self {
333            TzifTransitionKind::Unambiguous => quote! {
334                jiff::shared::TzifTransitionKind::Unambiguous
335            },
336            TzifTransitionKind::Gap => quote! {
337                jiff::shared::TzifTransitionKind::Gap
338            },
339            TzifTransitionKind::Fold => quote! {
340                jiff::shared::TzifTransitionKind::Fold
341            },
342        }
343    }
344}
345
346impl TzifDateTime {
347    fn quote(&self) -> proc_macro2::TokenStream {
348        let year = self.year();
349        let month = self.month();
350        let day = self.day();
351        let hour = self.hour();
352        let minute = self.minute();
353        let second = self.second();
354        quote! {
355            jiff::shared::TzifDateTime::new(
356                #year,
357                #month,
358                #day,
359                #hour,
360                #minute,
361                #second,
362            )
363        }
364    }
365}
366
367impl PosixTimeZone<Abbreviation> {
368    fn quote(&self) -> proc_macro2::TokenStream {
369        let PosixTimeZone { ref std_abbrev, ref std_offset, ref dst } = *self;
370        let std_abbrev = std_abbrev.as_str();
371        let std_offset = std_offset.quote();
372        let dst = dst
373            .as_ref()
374            .map(|dst| {
375                let dst = dst.quote();
376                quote!(Some(#dst))
377            })
378            .unwrap_or_else(|| quote!(None));
379        quote! {
380            jiff::shared::PosixTimeZone {
381                std_abbrev: #std_abbrev,
382                std_offset: #std_offset,
383                dst: #dst,
384            }
385        }
386    }
387}
388
389impl PosixDst<Abbreviation> {
390    fn quote(&self) -> proc_macro2::TokenStream {
391        let PosixDst { ref abbrev, ref offset, ref rule } = *self;
392        let abbrev = abbrev.as_str();
393        let offset = offset.quote();
394        let rule = rule.quote();
395        quote! {
396            jiff::shared::PosixDst {
397                abbrev: #abbrev,
398                offset: #offset,
399                rule: #rule,
400            }
401        }
402    }
403}
404
405impl PosixRule {
406    fn quote(&self) -> proc_macro2::TokenStream {
407        let start = self.start.quote();
408        let end = self.end.quote();
409        quote! {
410            jiff::shared::PosixRule { start: #start, end: #end }
411        }
412    }
413}
414
415impl PosixDayTime {
416    fn quote(&self) -> proc_macro2::TokenStream {
417        let PosixDayTime { ref date, ref time } = *self;
418        let date = date.quote();
419        let time = time.quote();
420        quote! {
421            jiff::shared::PosixDayTime { date: #date, time: #time }
422        }
423    }
424}
425
426impl PosixDay {
427    fn quote(&self) -> proc_macro2::TokenStream {
428        match *self {
429            PosixDay::JulianOne(day) => quote! {
430                jiff::shared::PosixDay::JulianOne(#day)
431            },
432            PosixDay::JulianZero(day) => quote! {
433                jiff::shared::PosixDay::JulianZero(#day)
434            },
435            PosixDay::WeekdayOfMonth { month, week, weekday } => quote! {
436                jiff::shared::PosixDay::WeekdayOfMonth {
437                    month: #month,
438                    week: #week,
439                    weekday: #weekday,
440                }
441            },
442        }
443    }
444}
445
446impl PosixTime {
447    fn quote(&self) -> proc_macro2::TokenStream {
448        let PosixTime { second } = *self;
449        quote! {
450            jiff::shared::PosixTime { second: #second }
451        }
452    }
453}
454
455impl PosixOffset {
456    fn quote(&self) -> proc_macro2::TokenStream {
457        let PosixOffset { second } = *self;
458        quote! {
459            jiff::shared::PosixOffset { second: #second }
460        }
461    }
462}