tzif/parse/
tzif.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use super::ensure;
6use super::posix::posix_tz_string;
7use crate::data::posix::PosixTzString;
8use crate::data::time::Seconds;
9use crate::data::tzif::{
10    DataBlock, LeapSecondRecord, LocalTimeTypeRecord, StandardWallIndicator, TzifData, TzifHeader,
11    UtLocalIndicator,
12};
13use combine::parser::byte::byte;
14use combine::parser::byte::num::{be_i32, be_i64, be_u32};
15use combine::{
16    any, between, choice, count_min_max, one_of, skip_count, value, ParseError, Parser, Stream,
17};
18
19/// Parses the four-byte ASCII \[RFC20\] sequence `"TZif"` (0x54 0x5A 0x69 0x42),
20/// which identifies the file as utilizing the Time Zone Information Format.
21fn magic_sequence<Input>() -> impl Parser<Input, Output = u8>
22where
23    Input: Stream<Token = u8>,
24    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
25{
26    byte(b'T')
27        .with(byte(b'Z'))
28        .with(byte(b'i'))
29        .with(byte(b'f'))
30}
31
32/// Parse the `TZif` version number specified by <https://datatracker.ietf.org/doc/html/rfc8536>
33/// > A byte identifying the version of the file's format.
34/// > The value MUST be one of the following:
35/// >
36/// > NUL (0x00)  Version 1
37/// >
38/// > '2' (0x32)  Version 2
39/// >
40/// > '3' (0x33)  Version 3
41fn version<Input>() -> impl Parser<Input, Output = usize>
42where
43    Input: Stream<Token = u8>,
44    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
45{
46    one_of([0, b'2', b'3'])
47        .map(|byte: u8| byte.saturating_sub(b'0') as usize)
48        .map(|version| if version == 0 { 1 } else { version })
49}
50
51/// Parse the `TZif` `isutcnt` value specified by <https://datatracker.ietf.org/doc/html/rfc8536>
52/// > A four-byte unsigned integer specifying the number of UT/
53/// > local indicators contained in the data block -- MUST either be
54/// > zero or equal to "typecnt".
55fn isutcnt<Input>() -> impl Parser<Input, Output = usize>
56where
57    Input: Stream<Token = u8>,
58    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
59{
60    be_u32().map(|u32| u32 as usize)
61}
62
63/// Parse the `TZif` `isstdcnt` value specified by <https://datatracker.ietf.org/doc/html/rfc8536>
64/// > A four-byte unsigned integer specifying the number of
65/// > standard/wall indicators contained in the data block -- MUST
66/// > either be zero or equal to "typecnt".
67fn isstdcnt<Input>() -> impl Parser<Input, Output = usize>
68where
69    Input: Stream<Token = u8>,
70    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
71{
72    be_u32().map(|u32| u32 as usize)
73}
74
75/// Parse the `TZif` `leapcnt` value specified by <https://datatracker.ietf.org/doc/html/rfc8536>
76/// > A four-byte unsigned integer specifying the number of
77/// > leap-second records contained in the data block.
78fn leapcnt<Input>() -> impl Parser<Input, Output = usize>
79where
80    Input: Stream<Token = u8>,
81    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
82{
83    be_u32().map(|u32| u32 as usize)
84}
85
86/// Parse the `TZif` `timecnt` value specified by <https://datatracker.ietf.org/doc/html/rfc8536>
87/// > A four-byte unsigned integer specifying the number of
88/// > transition times contained in the data block.
89fn timecnt<Input>() -> impl Parser<Input, Output = usize>
90where
91    Input: Stream<Token = u8>,
92    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
93{
94    be_u32().map(|u32| u32 as usize)
95}
96
97/// Parse the `TZif` `typecnt` value specified by <https://datatracker.ietf.org/doc/html/rfc8536>
98/// > A four-byte unsigned integer specifying the number of
99/// > local time type records contained in the data block -- MUST NOT be
100/// > zero. (Although local time type records convey no useful
101/// > information in files that have nonempty TZ strings but no
102/// > transitions, at least one such record is nevertheless required
103/// > because many `TZif` readers reject files that have zero time types.)
104///
105/// Takes `isutcnt` and `isstdcnt` as arguments. If either of these values are
106/// non-zero, then they must be equal to the parsed `typecnt`.
107fn typecnt<Input>(isutcnt: usize, isstdcnt: usize) -> impl Parser<Input, Output = usize>
108where
109    Input: Stream<Token = u8>,
110    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
111{
112    be_u32()
113        .map(|u32| u32 as usize)
114        .then(|typecnt| {
115            ensure(
116                typecnt,
117                |&typecnt| typecnt != 0,
118                "typecnt should never be equal to zero",
119            )
120        })
121        .then(move |typecnt| {
122            ensure(
123                typecnt,
124                |&typecnt| isutcnt == 0 || isutcnt == typecnt,
125                "if isutcnt is non-zero it should be equal to typecnt",
126            )
127        })
128        .then(move |typecnt| {
129            ensure(
130                typecnt,
131                |&typecnt| isstdcnt == 0 || isstdcnt == typecnt,
132                "if isstdcnt is non-zero it should be equal to typecnt",
133            )
134        })
135}
136
137/// Parse the `TZif` `charcnt` value specified by <https://datatracker.ietf.org/doc/html/rfc8536>
138/// > A four-byte unsigned integer specifying the total number
139/// > of bytes used by the set of time zone designations contained in
140/// > the data block - MUST NOT be zero. The count includes the
141/// > trailing NUL (0x00) byte at the end of the last time zone
142/// > designation.
143fn charcnt<Input>() -> impl Parser<Input, Output = usize>
144where
145    Input: Stream<Token = u8>,
146    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
147{
148    be_u32().map(|u32| u32 as usize).then(|charcnt| {
149        ensure(
150            charcnt,
151            |&charcnt| charcnt != 0,
152            "charcnt should never be zero",
153        )
154    })
155}
156
157/// Parse a `TZif` file header specified by <https://datatracker.ietf.org/doc/html/rfc8536>
158/// > A `TZif` header is structured as follows (the lengths of multi-byte
159/// > fields are shown in parentheses):
160/// > ```text
161/// > +---------------+---+
162/// > |  magic    (4) |ver|
163/// > +---------------+---+---------------------------------------+
164/// > |           [unused - reserved for future use] (15)         |
165/// > +---------------+---------------+---------------+-----------+
166/// > |  isutcnt  (4) |  isstdcnt (4) |  leapcnt  (4) |
167/// > +---------------+---------------+---------------+
168/// > |  timecnt  (4) |  typecnt  (4) |  charcnt  (4) |
169/// > +---------------+---------------+---------------+
170/// > ```
171fn header<Input>() -> impl Parser<Input, Output = TzifHeader>
172where
173    Input: Stream<Token = u8>,
174    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
175{
176    magic_sequence()
177        .with((
178            version(),
179            skip_count(15, any()).with(isutcnt()),
180            isstdcnt(),
181            leapcnt(),
182            timecnt(),
183        ))
184        .then(|(version, isutcnt, isstdcnt, leapcnt, timecnt)| {
185            combine::struct_parser! {
186                TzifHeader {
187                    version: value(version),
188                    isutcnt: value(isutcnt),
189                    isstdcnt: value(isstdcnt),
190                    leapcnt: value(leapcnt),
191                    timecnt: value(timecnt),
192                    typecnt: typecnt(isutcnt, isstdcnt),
193                    charcnt: charcnt(),
194                }
195            }
196        })
197}
198
199/// A four- or eight-byte UNIX leap-time value.
200/// Each value is used as a transition time at which the rules for
201/// computing local time may change. Each time value SHOULD be at least -2**59.
202///
203/// (-2**59 is the greatest negated power of 2 that predates the Big
204/// Bang, and avoiding earlier timestamps works around known `TZif`
205/// reader bugs relating to outlandishly negative timestamps.)
206fn historic_transition_time<const V: usize, Input>() -> impl Parser<Input, Output = Seconds>
207where
208    Input: Stream<Token = u8>,
209    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
210{
211    match V {
212        1 => be_i32().map(i64::from).left(),
213        _ => be_i64().right(),
214    }
215    .then(|time| {
216        ensure(
217            time,
218            |&time| time >= (-2_i64).pow(59),
219            "transition time should not be less than -2.pow(59)",
220        )
221    })
222    .map(Seconds)
223}
224
225/// Parse a series of transition times sorted in strictly ascending order.
226/// The number of time values is specified by the `timecnt`
227/// field in the header.
228fn historic_transition_times<const V: usize, Input>(
229    timecnt: usize,
230) -> impl Parser<Input, Output = Vec<Seconds>>
231where
232    Input: Stream<Token = u8>,
233    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
234{
235    count_min_max(timecnt, timecnt, historic_transition_time::<V, _>()).then(
236        |times: Vec<Seconds>| {
237            ensure(
238                times,
239                |times| {
240                    times
241                        .iter()
242                        .zip(times.iter().skip(1))
243                        .all(|(lhs, rhs)| lhs <= rhs)
244                },
245                "historic transition times should be in ascenting order",
246            )
247        },
248    )
249}
250
251/// A series of one-byte unsigned integers specifying
252/// the type of local time of the corresponding transition time.
253///
254/// These values serve as zero-based indices into the array of local
255/// time type records. The number of type indices is specified by the
256/// `timecnt` field in the header. Each type index MUST be in the
257/// range `[0, typecnt - 1]`
258fn transition_types<Input>(
259    timecnt: usize,
260    typecnt: usize,
261) -> impl Parser<Input, Output = Vec<usize>>
262where
263    Input: Stream<Token = u8>,
264    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
265{
266    count_min_max(timecnt, timecnt, any().map(|byte| byte as usize)).then(
267        move |types: Vec<usize>| {
268            ensure(
269                types,
270                |types| types.iter().all(|&t| t < typecnt),
271                "all transition types should be in range [0, typecnt - 1]",
272            )
273        },
274    )
275}
276
277/// A four-byte signed integer specifying the number of
278/// seconds to be added to UT in order to determine local time.
279/// The value MUST NOT be -2**31 and SHOULD be in the range
280/// [-89999, 93599] (i.e., its value SHOULD be more than -25 hours
281/// and less than 26 hours). Avoiding -2**31 allows 32-bit clients
282/// to negate the value without overflow. Restricting it to
283/// [-89999, 93599] allows easy support by implementations that
284/// already support the POSIX-required range [-24:59:59, 25:59:59].
285fn utoff<Input>() -> impl Parser<Input, Output = Seconds>
286where
287    Input: Stream<Token = u8>,
288    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
289{
290    be_i32()
291        .then(|utoff| {
292            ensure(
293                utoff,
294                |&utoff| utoff != (-2i32).pow(31),
295                "utoff should never be equal to -2.pow(31)",
296            )
297        })
298        .map(|utoff| Seconds(i64::from(utoff)))
299}
300
301/// Parses a byte as a boolean value. The value must be exactly
302/// the numeric digit 0 (false) or the numeric digit 1 (true).
303fn boolean<Input>() -> impl Parser<Input, Output = bool>
304where
305    Input: Stream<Token = u8>,
306    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
307{
308    choice((byte(b'\x00').map(|_| false), byte(b'\x01').map(|_| true)))
309}
310
311/// A one-byte value indicating whether local time should
312/// be considered Daylight Saving Time (DST). The value MUST be 0
313/// or 1. A value of one (1) indicates that this type of time is
314/// DST. A value of zero (0) indicates that this time type is
315/// standard time.
316fn is_dst<Input>() -> impl Parser<Input, Output = bool>
317where
318    Input: Stream<Token = u8>,
319    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
320{
321    boolean()
322}
323
324/// A one-byte unsigned integer specifying a zero-based
325/// index into the series of time zone designation bytes, thereby
326/// selecting a particular designation string. Each index MUST be
327/// in the range [0, charcnt - 1]; it designates the
328/// NUL-terminated string of bytes starting at position "idx" in
329/// the time zone designations. (This string MAY be empty.) A NUL
330/// byte MUST exist in the time zone designations at or after
331/// position "idx".
332fn idx<Input>(charcnt: usize) -> impl Parser<Input, Output = usize>
333where
334    Input: Stream<Token = u8>,
335    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
336{
337    any()
338        .map(|byte| byte as usize)
339        .then(move |idx| ensure(idx, |&idx| idx < charcnt, "idx should be less than charcnt"))
340}
341
342/// A series of six-byte records specifying a
343/// local time type. Each record has the following
344/// format (the lengths of multi-byte fields are shown in
345/// parentheses):
346///
347/// > ```text
348/// > +---------------+---+---+
349/// > |  utoff (4)    |dst|idx|
350/// > +---------------+---+---+
351/// > ```
352fn local_time_type_record<Input>(charcnt: usize) -> impl Parser<Input, Output = LocalTimeTypeRecord>
353where
354    Input: Stream<Token = u8>,
355    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
356{
357    combine::struct_parser! {
358        LocalTimeTypeRecord {
359            utoff: utoff(),
360            is_dst: is_dst(),
361            idx: idx(charcnt),
362        }
363    }
364}
365
366/// A series of local time type records.
367/// The number of records is specified by the "typecnt" field in the header.
368fn local_time_type_records<Input>(
369    typecnt: usize,
370    charcnt: usize,
371) -> impl Parser<Input, Output = Vec<LocalTimeTypeRecord>>
372where
373    Input: Stream<Token = u8>,
374    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
375{
376    count_min_max(typecnt, typecnt, local_time_type_record(charcnt))
377}
378
379/// A series of bytes constituting an array of
380/// NUL-terminated (0x00) time zone designation strings. The total
381/// number of bytes is specified by the "charcnt" field in the
382/// header.
383///
384/// Splits the list of bytes by the NULL-terminator (0x00) character
385/// and puts each designation into a [`String`].
386///
387/// > e.g.
388/// > ```text
389/// > "LMT\u{0}HMT\u{0}MMT\u{0}IST\u{0}+0630\u{0}"
390/// > ```
391///
392/// Note that a local time record index might point in the middle of a
393/// designation. In that case the record's designation is the specified
394/// suffix. The [DataBlock::time_zone_designation] method can be used to
395/// access the correct designation string given an index.
396///
397/// The character encoding of time zone designation strings is not specified.
398/// However, time zone designations SHOULD consist of at least three (3) and no
399/// more than six (6) ASCII characters from the set of alphanumerics,
400/// '-', and '+'. This is for compatibility with POSIX requirements
401/// for time zone abbreviations, so this parser enforces a UTF-8 ASCII encoding,
402/// to ensure compatability with Rust strings.
403fn time_zone_designations<Input>(charcnt: usize) -> impl Parser<Input, Output = Vec<String>>
404where
405    Input: Stream<Token = u8>,
406    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
407{
408    count_min_max(charcnt, charcnt, any()).map(|bytes: Vec<u8>| {
409        bytes
410            .split_inclusive(|&b| b == b'\0')
411            .map(|s| String::from_utf8_lossy(&s[0..s.len() - 1]).into_owned())
412            .collect()
413    })
414}
415
416/// A four- or eight-byte UNIX leap time value specifying the time at which a leap-second
417/// correction occurs.
418fn leap_second_occurrence<const V: usize, Input>() -> impl Parser<Input, Output = Seconds>
419where
420    Input: Stream<Token = u8>,
421    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
422{
423    match V {
424        1 => be_i32().map(i64::from).left(),
425        _ => be_i64().right(),
426    }
427    .map(Seconds)
428}
429
430/// A four-byte signed integer specifying the value of LEAPCORR on or after the
431/// occurrence. The correction value in the first leap-second record, if present,
432/// MUST be either one (1) or minus one (-1).
433fn leap_second_correction<Input>() -> impl Parser<Input, Output = i32>
434where
435    Input: Stream<Token = u8>,
436    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
437{
438    be_i32()
439}
440
441/// A series of eight- or twelve-byte records
442/// specifying the corrections that need to be applied to UTC in order
443/// to determine TAI. The records are sorted by the occurrence time
444/// in strictly ascending order. The number of records is specified
445/// by the "leapcnt" field in the header. Each record has one of the
446/// following structures (the lengths of multi-byte fields are shown
447/// in parentheses):
448///
449/// > Version 1 Data Block:
450/// >
451/// > ```text
452/// > +---------------+---------------+
453/// > |  occur (4)    |  corr (4)     |
454/// > +---------------+---------------+
455/// > ```
456/// >
457/// > version-2+ Data Block:
458/// >
459/// > ```text
460/// > +---------------+---------------+---------------+
461/// > |  occur (8)                    |  corr (4)     |
462/// > +---------------+---------------+---------------+
463/// > ```
464fn leap_second_record<const V: usize, Input>() -> impl Parser<Input, Output = LeapSecondRecord>
465where
466    Input: Stream<Token = u8>,
467    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
468{
469    combine::struct_parser! {
470        LeapSecondRecord {
471            occurrence: leap_second_occurrence::<V, _>(),
472            correction: leap_second_correction(),
473        }
474    }
475}
476
477/// A series of leap second records.
478///
479/// Regarding the "occurence" value:
480/// > The first value, if present, MUST be nonnegative, and each
481/// > later value MUST be at least 2419199 greater than the previous
482/// > value. (This is 28 days' worth of seconds, minus a potential
483/// > negative leap second.)
484///
485/// Regarding the "correction" value:
486/// > The correction value in the first leap-second record, if present,
487/// > MUST be either one (1) or minus one (-1).
488/// >
489/// > The correction values in adjacent leap-second
490/// > records MUST differ by exactly one (1). The value of
491/// > LEAPCORR is zero for timestamps that occur before the
492/// > occurrence time in the first leap-second record (or for all
493/// > timestamps if there are no leap-second records).
494fn leap_second_records<const V: usize, Input>(
495    leapcnt: usize,
496) -> impl Parser<Input, Output = Vec<LeapSecondRecord>>
497where
498    Input: Stream<Token = u8>,
499    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
500{
501    count_min_max(leapcnt, leapcnt, leap_second_record::<V, _>())
502        .then(|records: Vec<LeapSecondRecord>| {
503            ensure(
504                records,
505                |records| {
506                    records
507                        .first()
508                        .map_or(true, |first| first.occurrence >= Seconds(0))
509                },
510                "The first leap-second occurrence, if present, must be non-negative",
511            )
512        })
513        .then(|records: Vec<LeapSecondRecord>| {
514            ensure(
515                records,
516                |records| {
517                    records
518                        .first()
519                        .map_or(true, |first| first.correction == 1 || first.correction == -1)
520                },
521                "The first leap-second correction, if present, must be 1 or -1",
522            )
523        })
524        .then(|records: Vec<LeapSecondRecord>| {
525            ensure(
526                records,
527                |records| {
528                    records
529                        .iter()
530                        .zip(records.iter().skip(1))
531                        .all(|(prev, next)| next.occurrence - prev.occurrence >= Seconds(2_419_199))
532                },
533                "Each subsequent leap-second occurrence must be at least 2419199 greater than the previous value",
534            )
535        })
536        .then(|records: Vec<LeapSecondRecord>| {
537            ensure(
538                records,
539                |records| {
540                    records
541                        .iter()
542                        .zip(records.iter().skip(1))
543                        .all(|(prev, next)| (next.correction - prev.correction).abs() == 1)
544                },
545                "Adjacent leap-second corrections must differ by exactly 1",
546            )
547        })
548}
549
550/// A one-byte value indicating whether the
551/// transition times associated with local time types were
552/// specified as standard time or wall-clock time. Each value MUST be
553/// 0 or 1. A value of one (1) indicates standard time. The value
554/// MUST be set to one (1) if the corresponding UT/local indicator is
555/// set to one (1). A value of zero (0) indicates wall time.
556fn standard_wall_indicator<Input>() -> impl Parser<Input, Output = StandardWallIndicator>
557where
558    Input: Stream<Token = u8>,
559    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
560{
561    boolean().map(|bool| {
562        if bool {
563            StandardWallIndicator::Standard
564        } else {
565            StandardWallIndicator::Wall
566        }
567    })
568}
569
570/// A series of standard/wall indicators.
571/// The number of values is specified by the "isstdcnt" field in the
572/// header. If "isstdcnt" is zero (0), all transition times
573/// associated with local time types are assumed to be specified as
574/// wall time.
575fn standard_wall_indicators<Input>(
576    isstdcnt: usize,
577) -> impl Parser<Input, Output = Vec<StandardWallIndicator>>
578where
579    Input: Stream<Token = u8>,
580    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
581{
582    count_min_max(isstdcnt, isstdcnt, standard_wall_indicator())
583}
584
585/// A one-byte value indicating whether the
586/// transition times associated with local time types were
587/// specified as UT or local time. Each value MUST be 0 or 1. A
588/// value of one (1) indicates UT, and the corresponding standard/wall
589/// indicator MUST also be set to one (1). A value of zero (0)
590/// indicates local time.
591fn ut_local_indicator<Input>() -> impl Parser<Input, Output = UtLocalIndicator>
592where
593    Input: Stream<Token = u8>,
594    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
595{
596    boolean().map(|bool| {
597        if bool {
598            UtLocalIndicator::Ut
599        } else {
600            UtLocalIndicator::Local
601        }
602    })
603}
604
605/// A series of ut/local indicators
606/// The number of values is specified by the
607/// "isutcnt" field in the header. If "isutcnt" is zero (0), all
608/// transition times associated with local time types are assumed to
609/// be specified as local time.
610fn ut_local_indicators<Input>(isstdcnt: usize) -> impl Parser<Input, Output = Vec<UtLocalIndicator>>
611where
612    Input: Stream<Token = u8>,
613    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
614{
615    count_min_max(isstdcnt, isstdcnt, ut_local_indicator())
616}
617
618/// Parses a `TZif` data block.
619/// A `TZif` data block consists of seven variable-length elements, each of
620/// which is a series of items.  The number of items in each series is
621/// determined by the corresponding count field in the header.  The total
622/// length of each element is calculated by multiplying the number of
623/// items by the size of each item.  Therefore, implementations that do
624/// not wish to parse or use the version 1 data block can calculate its
625/// total length and skip directly to the header of the version-2+ data
626/// block.
627///
628/// In the version 1 data block, time values are 32 bits (`TIME_SIZE` = 4
629/// bytes).  In the version-2+ data block, present only in version 2 and
630/// 3 files, time values are 64 bits (`TIME_SIZE` = 8 bytes).
631///
632/// The data block is structured as follows (the lengths of multi-byte
633/// fields are shown in parentheses):
634/// > ```text
635/// >    +---------------------------------------------------------+
636/// >    |  transition times          (timecnt x TIME_SIZE)        |
637/// >    +---------------------------------------------------------+
638/// >    |  transition types          (timecnt)                    |
639/// >    +---------------------------------------------------------+
640/// >    |  local time type records   (typecnt x 6)                |
641/// >    +---------------------------------------------------------+
642/// >    |  time zone designations    (charcnt)                    |
643/// >    +---------------------------------------------------------+
644/// >    |  leap-second records       (leapcnt x (TIME_SIZE + 4))  |
645/// >    +---------------------------------------------------------+
646/// >    |  standard/wall indicators  (isstdcnt)                   |
647/// >    +---------------------------------------------------------+
648/// >    |  UT/local indicators       (isutcnt)                    |
649/// >    +---------------------------------------------------------+
650/// > ```
651fn data_block<const V: usize, Input>(header: TzifHeader) -> impl Parser<Input, Output = DataBlock>
652where
653    Input: Stream<Token = u8>,
654    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
655{
656    combine::struct_parser! {
657        DataBlock {
658            transition_times: historic_transition_times::<V, _>(header.timecnt),
659            transition_types: transition_types(header.timecnt, header.typecnt),
660            local_time_type_records: local_time_type_records(header.typecnt, header.charcnt),
661            time_zone_designations: time_zone_designations(header.charcnt),
662            leap_second_records: leap_second_records::<V, _>(header.leapcnt),
663            standard_wall_indicators: standard_wall_indicators(header.isstdcnt),
664            ut_local_indicators: ut_local_indicators(header.isutcnt),
665        }
666    }
667}
668
669/// Parses a `TZif` footer.
670/// The `TZif` footer is structured as follows (the lengths of multi-byte
671/// fields are shown in parentheses):
672///
673/// > ```text
674/// > +---+--------------------+---+
675/// > | NL|  TZ string (0...)  |NL |
676/// > +---+--------------------+---+
677/// >
678/// >           `TZif` Footer
679/// > ```
680///
681/// The elements of the footer are defined as follows:
682///
683/// > NL:
684/// > > An ASCII new line character (0x0A).
685/// >
686/// > TZ string:
687/// > > A rule for computing local time changes after the last
688/// > > transition time stored in the version-2+ data block.  The string
689/// > > is either empty or uses the expanded format of the "TZ"
690/// > > environment variable as defined in Section 8.3 of the "Base
691/// > > Definitions" volume of \[POSIX\] with ASCII encoding, possibly
692/// > > utilizing extensions described below (Section 3.3.1) in version 3
693/// > > files.  If the string is empty, the corresponding information is
694/// > > not available.  If the string is nonempty and one or more
695/// > > transitions appear in the version-2+ data, the string MUST be
696/// > > consistent with the last version-2+ transition.  In other words,
697/// > > evaluating the TZ string at the time of the last transition should
698/// > > yield the same time type as was specified in the last transition.
699/// > > The string MUST NOT contain NUL bytes or be NUL-terminated, and
700/// > > it SHOULD NOT begin with the ':' (colon) character.
701///
702/// The `TZif` footer is present only in version 2 and 3 files, as the
703/// obsolescent version 1 format was designed before the need for a
704/// footer was apparent.
705fn footer<Input>() -> impl Parser<Input, Output = PosixTzString>
706where
707    Input: Stream<Token = u8>,
708    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
709{
710    between(byte(b'\n'), byte(b'\n'), posix_tz_string())
711}
712
713/// Parses a `TZif` binary file according to the following specification:
714/// <https://datatracker.ietf.org/doc/html/rfc8536>
715#[must_use]
716pub fn tzif<Input>() -> impl Parser<Input, Output = TzifData>
717where
718    Input: Stream<Token = u8>,
719    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
720{
721    header()
722        .then(|header1| {
723            if header1.version() == 1 {
724                (
725                    value(header1),
726                    data_block::<1, _>(header1),
727                    value(None).left(),
728                )
729            } else {
730                (
731                    value(header1),
732                    data_block::<1, _>(header1),
733                    header().map(Some).right(),
734                )
735            }
736        })
737        .then(|(header1, block1, header2)| match header2 {
738            None => combine::struct_parser! {
739                TzifData {
740                    header1: value(header1),
741                    data_block1: value(block1),
742                    header2: value(header2),
743                    data_block2: value(None),
744                    footer: value(None),
745                }
746            }
747            .left(),
748            Some(header) => (match header.version() {
749                2 => combine::struct_parser! {
750                    TzifData {
751                        header1: value(header1),
752                        data_block1: value(block1),
753                        header2: value(header2),
754                        data_block2: data_block::<2, _>(header).map(Some),
755                        footer: footer().map(Some),
756                    }
757                }
758                .left(),
759                _ => combine::struct_parser! {
760                    TzifData {
761                        header1: value(header1),
762                        data_block1: value(block1),
763                        header2: value(header2),
764                        data_block2: data_block::<3, _>(header).map(Some),
765                        footer: footer().map(Some),
766                    }
767                }
768                .right(),
769            })
770            .right(),
771        })
772}
773
774#[cfg(test)]
775mod test {
776    use super::*;
777    use crate::data::posix::{
778        DstTransitionInfo, TimeZoneVariantInfo, TransitionDate, TransitionDay,
779    };
780    use crate::data::time::Hours;
781    use crate::{assert_parse_eq, assert_parse_err, assert_parse_ok};
782    use combine::EasyParser;
783
784    // Test constants
785    const A: usize = b'A' as usize;
786    const B: usize = b'B' as usize;
787    const C: usize = b'C' as usize;
788    const D: usize = b'D' as usize;
789
790    #[test]
791    fn parse_magic_sequence() {
792        // emtpy
793        assert_parse_err!(magic_sequence(), "");
794
795        // invalid magic sequences
796        assert_parse_err!(magic_sequence(), "asdf");
797        assert_parse_err!(magic_sequence(), "tzif");
798        assert_parse_err!(magic_sequence(), "TZIF");
799
800        // valid magic sequence
801        assert_parse_ok!(magic_sequence(), "TZif");
802    }
803
804    #[test]
805    fn parse_version() {
806        // emtpy
807        assert_parse_err!(version(), "");
808
809        // invalid versions
810        assert_parse_err!(version(), "0");
811        assert_parse_err!(version(), "1");
812        assert_parse_err!(version(), "4");
813
814        // valid versions
815        assert_parse_eq!(version(), "\x00", 1);
816        assert_parse_eq!(version(), "2", 2);
817        assert_parse_eq!(version(), "3", 3);
818    }
819
820    #[test]
821    fn parse_trivial_count_values() {
822        // empty
823        assert_parse_err!(isutcnt(), "");
824        assert_parse_err!(isstdcnt(), "");
825        assert_parse_err!(leapcnt(), "");
826        assert_parse_err!(timecnt(), "");
827
828        // invalid count
829        assert_parse_err!(isutcnt(), "\x00");
830        assert_parse_err!(isutcnt(), "\x00\x00");
831        assert_parse_err!(isutcnt(), "\x00\x00\x00");
832
833        assert_parse_err!(isstdcnt(), "\x00");
834        assert_parse_err!(isstdcnt(), "\x00\x00");
835        assert_parse_err!(isstdcnt(), "\x00\x00\x00");
836
837        assert_parse_err!(leapcnt(), "\x00");
838        assert_parse_err!(leapcnt(), "\x00\x00");
839        assert_parse_err!(leapcnt(), "\x00\x00\x00");
840
841        assert_parse_err!(timecnt(), "\x00");
842        assert_parse_err!(timecnt(), "\x00\x00");
843        assert_parse_err!(timecnt(), "\x00\x00\x00");
844
845        // valid count
846        assert_parse_eq!(isutcnt(), "\x00\x00\x00\x41", A);
847        assert_parse_eq!(isutcnt(), "\x00\x00\x00\x00", 0);
848
849        assert_parse_eq!(isstdcnt(), "\x00\x00\x00\x42", B);
850        assert_parse_eq!(isstdcnt(), "\x00\x00\x00\x00", 0);
851
852        assert_parse_eq!(leapcnt(), "\x00\x00\x00\x43", C);
853        assert_parse_eq!(leapcnt(), "\x00\x00\x00\x00", 0);
854
855        assert_parse_eq!(timecnt(), "\x00\x00\x00\x44", D);
856        assert_parse_eq!(timecnt(), "\x00\x00\x00\x00", 0);
857    }
858
859    #[test]
860    fn parse_typecnt() {
861        // empty
862        assert_parse_err!(typecnt(0, 0), "");
863
864        // invalid count
865        assert_parse_err!(typecnt(0, 0), "\x00");
866        assert_parse_err!(typecnt(0, 0), "\x00\x00");
867        assert_parse_err!(typecnt(0, 0), "\x00\x00\x00");
868
869        // invalid must be non-zero
870        assert_parse_err!(typecnt(0, 0), "\x00\x00\x00\x00");
871
872        // invalid if params are non-zero, must be equal to count.
873        assert_parse_err!(typecnt(B, 0), "\x00\x00\x00\x41");
874        assert_parse_err!(typecnt(0, B), "\x00\x00\x00\x41");
875        assert_parse_err!(typecnt(A, B), "\x00\x00\x00\x41");
876        assert_parse_err!(typecnt(B, A), "\x00\x00\x00\x41");
877
878        // valid typecnt
879        assert_parse_eq!(typecnt(0, 0), "\x00\x00\x00\x41", A);
880        assert_parse_eq!(typecnt(B, B), "\x00\x00\x00\x42", B);
881    }
882
883    #[test]
884    fn parse_charcnt() {
885        // empty
886        assert_parse_err!(charcnt(), "");
887
888        // invalid count
889        assert_parse_err!(charcnt(), "\x00");
890        assert_parse_err!(charcnt(), "\x00\x00");
891        assert_parse_err!(charcnt(), "\x00\x00\x00");
892
893        // invalid must be non-zero
894        assert_parse_err!(charcnt(), "\x00\x00\x00\x00");
895
896        // valid count
897        assert_parse_eq!(charcnt(), "\x00\x00\x00\x41", A);
898        assert_parse_eq!(charcnt(), "\x00\x00\x00\x42", B);
899    }
900
901    #[test]
902    fn parse_header() {
903        // empty
904        assert_parse_err!(header(), "");
905
906        // invalid magic number
907        assert_parse_err!(
908            header(),
909            "TZif1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0",
910        );
911
912        // invalid typecnt
913        assert_parse_err!(
914            header(),
915            "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0",
916        );
917
918        // invalid charcnt
919        assert_parse_err!(
920            header(),
921            "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0",
922        );
923
924        // invalid isutcnt
925        assert_parse_err!(
926            header(),
927            "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0",
928        );
929
930        // invalid isstdcnt
931        assert_parse_err!(
932            header(),
933            "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0",
934        );
935
936        // valid header
937        assert_parse_eq!(
938            header(),
939            "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0",
940            TzifHeader {
941                version: 2,
942                isutcnt: 0,
943                isstdcnt: 0,
944                leapcnt: 0,
945                timecnt: 0,
946                typecnt: 1,
947                charcnt:1,
948            }
949        );
950    }
951
952    #[test]
953    fn parse_historic_transition_time() {
954        const ONE: &[u8] = 1i64.to_be_bytes().as_slice();
955        const AT_BOUNDARY: &[u8] = (-2_i64).pow(59).to_be_bytes().as_slice();
956        const OUT_OF_BOUNDS: &[u8] = ((-2_i64).pow(59) - 1).to_be_bytes().as_slice();
957
958        // invalid transition time
959        assert_parse_err!(
960            historic_transition_time::<2, _>(),
961            bytes OUT_OF_BOUNDS,
962        );
963
964        // valid transition times
965        assert_parse_eq!(
966            historic_transition_time::<2, _>(),
967            bytes ONE,
968            Seconds(1),
969        );
970
971        // version 1 reads only the first 32 bits
972        // so this will read all zeros
973        assert_parse_eq!(
974            historic_transition_time::<1, _>(),
975            bytes ONE,
976            Seconds(0),
977        );
978
979        // reading the last 32 bits of the 64-bit ONE
980        // should still parse to 1.
981        assert_parse_eq!(
982            historic_transition_time::<1, _>(),
983            bytes ONE[ONE.len() / 2..].as_ref(),
984            Seconds(1),
985        );
986
987        assert_parse_eq!(
988            historic_transition_time::<3, _>(),
989            bytes AT_BOUNDARY,
990            Seconds((-2_i64).pow(59)),
991        );
992    }
993
994    #[test]
995    fn parse_historic_transition_times() {
996        const ONE: &[u8] = 1i64.to_be_bytes().as_slice();
997        const TWO: &[u8] = 2i64.to_be_bytes().as_slice();
998        const SIX: &[u8] = 6i64.to_be_bytes().as_slice();
999        let ascending = ONE
1000            .iter()
1001            .chain(TWO)
1002            .chain(SIX)
1003            .copied()
1004            .collect::<Vec<u8>>();
1005        let descending = SIX
1006            .iter()
1007            .chain(TWO)
1008            .chain(ONE)
1009            .copied()
1010            .collect::<Vec<u8>>();
1011
1012        // invalid descending order
1013        assert_parse_err!(
1014            historic_transition_times::<2, _>(3),
1015            bytes descending.as_slice(),
1016        );
1017
1018        // invalid wrong count
1019        assert_parse_err!(
1020            historic_transition_times::<2, _>(4),
1021            bytes ascending.as_slice(),
1022        );
1023
1024        // valid times
1025        assert_parse_eq!(
1026            historic_transition_times::<2, _>(3),
1027            bytes ascending.as_slice(),
1028            vec![
1029                Seconds(1),
1030                Seconds(2),
1031                Seconds(6),
1032            ]
1033        );
1034    }
1035
1036    #[test]
1037    fn parse_transition_types() {
1038        // empty
1039        assert_parse_err!(transition_types(3, 3), "");
1040
1041        // invalid wrong count
1042        assert_parse_err!(transition_types(3, 3), "\x00\x01");
1043
1044        // invalid ranges
1045        assert_parse_err!(transition_types(3, 3), "\x00\x01\x03");
1046
1047        // valid types
1048        assert_parse_eq!(transition_types(3, 3), "\x00\x01\x02", vec![0, 1, 2],);
1049        assert_parse_eq!(transition_types(3, 3), "\x02\x01\x01", vec![2, 1, 1],);
1050    }
1051
1052    #[test]
1053    fn parse_utoff() {
1054        const ONE: &[u8] = 1i32.to_be_bytes().as_slice();
1055        const TWO: &[u8] = 2i32.to_be_bytes().as_slice();
1056        const SIX: &[u8] = 6i32.to_be_bytes().as_slice();
1057        const INVALID: &[u8] = ((-2i32).pow(31)).to_be_bytes().as_slice();
1058
1059        // invalid utoff
1060        assert_parse_err!(utoff(), bytes INVALID);
1061
1062        // valid utoff
1063        assert_parse_eq!(utoff(), bytes ONE, Seconds(1));
1064        assert_parse_eq!(utoff(), bytes TWO, Seconds(2));
1065        assert_parse_eq!(utoff(), bytes SIX, Seconds(6));
1066    }
1067
1068    #[test]
1069    fn parse_is_dst() {
1070        // empty
1071        assert_parse_err!(is_dst(), "");
1072
1073        // invalid boolean byte
1074        assert_parse_err!(is_dst(), "0");
1075        assert_parse_err!(is_dst(), "1");
1076
1077        // valid boolean bytes
1078        assert_parse_eq!(is_dst(), "\x00", false);
1079        assert_parse_eq!(is_dst(), "\x01", true);
1080    }
1081
1082    #[test]
1083    fn parse_idx() {
1084        // empty
1085        assert_parse_err!(idx(0), "");
1086
1087        // invalid index too large
1088        assert_parse_err!(idx(3), "\x03");
1089
1090        // valid indices
1091        assert_parse_eq!(idx(3), "\x00", 0);
1092        assert_parse_eq!(idx(3), "\x01", 1);
1093        assert_parse_eq!(idx(3), "\x02", 2);
1094    }
1095
1096    #[test]
1097    fn parse_local_time_type_record() {
1098        assert_parse_eq!(
1099            local_time_type_record(3),
1100            "\x00\x00\x00\x10\x01\x02",
1101            LocalTimeTypeRecord {
1102                utoff: Seconds(16),
1103                is_dst: true,
1104                idx: 2
1105            }
1106        );
1107        assert_parse_eq!(
1108            local_time_type_record(3),
1109            "\x00\x00\x10\x10\x00\x01",
1110            LocalTimeTypeRecord {
1111                utoff: Seconds(16 * 16 * 16 + 16),
1112                is_dst: false,
1113                idx: 1,
1114            }
1115        );
1116    }
1117
1118    #[test]
1119    fn parse_local_time_type_records() {
1120        // invalid count
1121        assert_parse_err!(
1122            local_time_type_records(3, 3),
1123            "\x00\x00\x00\x10\x01\x02\x00\x00\x10\x10\x00\x01",
1124        );
1125
1126        // valid records
1127        assert_parse_eq!(
1128            local_time_type_records(2, 3),
1129            "\x00\x00\x00\x10\x01\x02\x00\x00\x10\x10\x00\x01",
1130            vec![
1131                LocalTimeTypeRecord {
1132                    utoff: Seconds(16),
1133                    is_dst: true,
1134                    idx: 2
1135                },
1136                LocalTimeTypeRecord {
1137                    utoff: Seconds(16 * 16 * 16 + 16),
1138                    is_dst: false,
1139                    idx: 1,
1140                },
1141            ]
1142        );
1143    }
1144
1145    #[test]
1146    fn parse_time_zone_designations() {
1147        assert_parse_eq!(
1148            time_zone_designations(14),
1149            "LMT\0AEDT\0AEST\0",
1150            vec!["LMT".to_owned(), "AEDT".to_owned(), "AEST".to_owned()],
1151        );
1152    }
1153
1154    #[test]
1155    fn time_zone_designation_indexing() {
1156        let block: &[u8] = &[
1157            0x00, 0x00, 0x00, 0x10, 0x01, 0x00, // local time record 0
1158            0x00, 0x00, 0x00, 0x10, 0x01, 0x03, // local time record 1
1159            0x00, 0x00, 0x00, 0x10, 0x01, 0x04, // local time record 2
1160            0x00, 0x00, 0x00, 0x10, 0x01, 0x05, // local time record 3
1161            b'L', b'M', b'T', 0x00, b'A', b'E', b'D', b'T', 0x00, // timezone designations
1162        ];
1163        let header = TzifHeader {
1164            version: 0,
1165            isutcnt: 0,
1166            isstdcnt: 0,
1167            leapcnt: 0,
1168            timecnt: 0,
1169            typecnt: 4,
1170            charcnt: 9,
1171        };
1172        let (block, _) = data_block::<1, _>(header).parse(block).unwrap();
1173        assert_eq!(
1174            block.time_zone_designation(block.local_time_type_records[0].idx),
1175            Some("LMT")
1176        );
1177        assert_eq!(
1178            block.time_zone_designation(block.local_time_type_records[1].idx),
1179            Some("")
1180        );
1181        assert_eq!(
1182            block.time_zone_designation(block.local_time_type_records[2].idx),
1183            Some("AEDT")
1184        );
1185        assert_eq!(
1186            block.time_zone_designation(block.local_time_type_records[3].idx),
1187            Some("EDT")
1188        );
1189        assert_eq!(block.time_zone_designation(8), Some(""));
1190        assert_eq!(block.time_zone_designation(9), None);
1191    }
1192
1193    #[test]
1194    fn parse_leap_second_occurrence() {
1195        const FIVE: &[u8] = 5i64.to_be_bytes().as_slice();
1196
1197        // version 1 reads only the first 32 bits
1198        // so this will read all zeros
1199        assert_parse_eq!(
1200            historic_transition_time::<1, _>(),
1201            bytes FIVE,
1202            Seconds(0),
1203        );
1204
1205        // reading the last 32 bits of the 64-bit ONE
1206        // should still parse to 5.
1207        assert_parse_eq!(
1208            historic_transition_time::<1, _>(),
1209            bytes FIVE[FIVE.len() / 2..].as_ref(),
1210            Seconds(5),
1211        );
1212
1213        // version 2
1214        assert_parse_eq!(
1215            historic_transition_time::<2, _>(),
1216            bytes FIVE,
1217            Seconds(5),
1218        );
1219
1220        // version 3
1221        assert_parse_eq!(
1222            historic_transition_time::<3, _>(),
1223            bytes FIVE,
1224            Seconds(5),
1225        );
1226    }
1227
1228    #[test]
1229    fn parse_leap_second_record() {
1230        const ONE_64BIT: &[u8] = 1i64.to_be_bytes().as_slice();
1231        const ONE_32BIT: &[u8] = 1i32.to_be_bytes().as_slice();
1232
1233        let record_v1 = ONE_32BIT
1234            .iter()
1235            .chain(ONE_32BIT)
1236            .copied()
1237            .collect::<Vec<u8>>();
1238        let record_v2p = ONE_64BIT
1239            .iter()
1240            .chain(ONE_32BIT)
1241            .copied()
1242            .collect::<Vec<u8>>();
1243
1244        // version 1
1245        assert_parse_eq!(
1246            leap_second_record::<1, _>(),
1247            bytes record_v1.as_slice(),
1248            LeapSecondRecord {
1249                occurrence: Seconds(1),
1250                correction: 1,
1251            }
1252        );
1253
1254        // version 2
1255        assert_parse_eq!(
1256            leap_second_record::<2, _>(),
1257            bytes record_v2p.as_slice(),
1258            LeapSecondRecord {
1259                occurrence: Seconds(1),
1260                correction: 1,
1261            }
1262        );
1263
1264        // version 3
1265        assert_parse_eq!(
1266            leap_second_record::<3, _>(),
1267            bytes record_v2p.as_slice(),
1268            LeapSecondRecord {
1269                occurrence: Seconds(1),
1270                correction: 1,
1271            }
1272        );
1273    }
1274
1275    #[test]
1276    fn parse_leap_second_records() {
1277        let invalid_first_occurrence = (-5i64)
1278            .to_be_bytes()
1279            .iter()
1280            .copied()
1281            .chain(1i32.to_be_bytes().iter().copied())
1282            .collect::<Vec<u8>>();
1283        let invalid_first_correction = 0i64
1284            .to_be_bytes()
1285            .iter()
1286            .copied()
1287            .chain(0i32.to_be_bytes().iter().copied())
1288            .collect::<Vec<u8>>();
1289        let invalid_second_occurrence = 0i64
1290            .to_be_bytes()
1291            .iter()
1292            .copied()
1293            .chain(1i32.to_be_bytes().iter().copied())
1294            .chain(2419198i64.to_be_bytes().iter().copied())
1295            .chain(2i32.to_be_bytes().iter().copied())
1296            .chain((2 * 2419199i64).to_be_bytes().iter().copied())
1297            .chain(3i32.to_be_bytes().iter().copied())
1298            .collect::<Vec<u8>>();
1299        let invalid_second_correction = 0i64
1300            .to_be_bytes()
1301            .iter()
1302            .copied()
1303            .chain(1i32.to_be_bytes().iter().copied())
1304            .chain(2419199i64.to_be_bytes().iter().copied())
1305            .chain(3i32.to_be_bytes().iter().copied())
1306            .chain((2 * 2419199i64).to_be_bytes().iter().copied())
1307            .chain(4i32.to_be_bytes().iter().copied())
1308            .collect::<Vec<u8>>();
1309        let valid_v1 = 0i32
1310            .to_be_bytes()
1311            .iter()
1312            .copied()
1313            .chain(1i32.to_be_bytes().iter().copied())
1314            .chain(2419199i32.to_be_bytes().iter().copied())
1315            .chain(2i32.to_be_bytes().iter().copied())
1316            .chain((2 * 2419199i32).to_be_bytes().iter().copied())
1317            .chain(3i32.to_be_bytes().iter().copied())
1318            .collect::<Vec<u8>>();
1319        let valid_v2p = 0i64
1320            .to_be_bytes()
1321            .iter()
1322            .copied()
1323            .chain(1i32.to_be_bytes().iter().copied())
1324            .chain(2419199i64.to_be_bytes().iter().copied())
1325            .chain(2i32.to_be_bytes().iter().copied())
1326            .chain((2 * 2419199i64).to_be_bytes().iter().copied())
1327            .chain(3i32.to_be_bytes().iter().copied())
1328            .collect::<Vec<u8>>();
1329
1330        // invalid count
1331        assert_parse_err!(
1332            leap_second_records::<2, _>(4),
1333            bytes valid_v2p.as_slice(),
1334        );
1335
1336        // invalid records
1337        assert_parse_err!(
1338            leap_second_records::<2, _>(1),
1339            bytes invalid_first_correction.as_slice(),
1340        );
1341
1342        assert_parse_err!(
1343            leap_second_records::<2, _>(1),
1344            bytes invalid_first_occurrence.as_slice(),
1345        );
1346
1347        assert_parse_err!(
1348            leap_second_records::<2, _>(2),
1349            bytes invalid_second_correction.as_slice(),
1350        );
1351
1352        assert_parse_err!(
1353            leap_second_records::<2, _>(2),
1354            bytes invalid_second_occurrence.as_slice(),
1355        );
1356
1357        // valid records
1358        assert_parse_eq!(
1359            leap_second_records::<1, _>(2),
1360            bytes valid_v1.as_slice(),
1361            vec![
1362                LeapSecondRecord {
1363                    occurrence: Seconds(0),
1364                    correction: 1,
1365                },
1366                LeapSecondRecord {
1367                    occurrence: Seconds(2419199),
1368                    correction: 2,
1369                },
1370            ],
1371        );
1372
1373        assert_parse_eq!(
1374            leap_second_records::<2, _>(2),
1375            bytes valid_v2p.as_slice(),
1376            vec![
1377                LeapSecondRecord {
1378                    occurrence: Seconds(0),
1379                    correction: 1,
1380                },
1381                LeapSecondRecord {
1382                    occurrence: Seconds(2419199),
1383                    correction: 2,
1384                },
1385            ],
1386        );
1387    }
1388
1389    #[test]
1390    fn parse_standard_wall_indicators() {
1391        // empty
1392        assert_parse_err!(standard_wall_indicators(3), "");
1393
1394        // invalid count
1395        assert_parse_err!(standard_wall_indicators(3), "\x00\x01");
1396
1397        // invalid standard-wall indicator
1398        assert_parse_err!(standard_wall_indicators(3), "\x00\x01\x02");
1399
1400        // valid standard-wall indicators
1401        assert_parse_eq!(
1402            standard_wall_indicators(0),
1403            "",
1404            Vec::<StandardWallIndicator>::new()
1405        );
1406        assert_parse_eq!(
1407            standard_wall_indicators(4),
1408            "\x00\x01\x01\x00",
1409            vec![
1410                StandardWallIndicator::Wall,
1411                StandardWallIndicator::Standard,
1412                StandardWallIndicator::Standard,
1413                StandardWallIndicator::Wall,
1414            ]
1415        );
1416    }
1417
1418    #[test]
1419    fn parse_ut_local_indicators() {
1420        // empty
1421        assert_parse_err!(ut_local_indicators(3), "");
1422
1423        // invalid count
1424        assert_parse_err!(ut_local_indicators(3), "\x00\x01");
1425
1426        // invalid standard-wall indicator
1427        assert_parse_err!(ut_local_indicators(3), "\x00\x01\x02");
1428
1429        // valid standard-wall indicators
1430        assert_parse_eq!(ut_local_indicators(0), "", Vec::<UtLocalIndicator>::new());
1431        assert_parse_eq!(
1432            ut_local_indicators(4),
1433            "\x01\x00\x00\x01",
1434            vec![
1435                UtLocalIndicator::Ut,
1436                UtLocalIndicator::Local,
1437                UtLocalIndicator::Local,
1438                UtLocalIndicator::Ut,
1439            ]
1440        );
1441    }
1442
1443    #[test]
1444    fn parse_footer() {
1445        // missing leading newline
1446        assert_parse_err!(footer(), "EST+5EDT,M3.2.0/2,M11.1.0/2\n");
1447
1448        // missing final newline
1449        assert_parse_err!(footer(), "\nEST+5EDT,M3.2.0/2,M11.1.0/2");
1450
1451        // valid footer
1452        assert_parse_eq!(
1453            footer(),
1454            "\nEST+5EDT,M3.2.0/2,M11.1.0/2\n",
1455            PosixTzString {
1456                std_info: TimeZoneVariantInfo {
1457                    name: "EST".to_owned(),
1458                    offset: Hours(5).as_seconds(),
1459                },
1460                dst_info: Some(DstTransitionInfo {
1461                    variant_info: TimeZoneVariantInfo {
1462                        name: "EDT".to_owned(),
1463                        offset: Hours(4).as_seconds()
1464                    },
1465                    start_date: TransitionDate {
1466                        day: TransitionDay::Mwd(3, 2, 0),
1467                        time: Hours(2).as_seconds(),
1468                    },
1469                    end_date: TransitionDate {
1470                        day: TransitionDay::Mwd(11, 1, 0),
1471                        time: Hours(2).as_seconds(),
1472                    },
1473                })
1474            }
1475        );
1476    }
1477}