1use crate::{interop::is_placeholder_type, validate::validate_file, TzdistError, TzifFile};
2
3const APPLICATION_TZIF: &str = "application/tzif";
4const APPLICATION_TZIF_LEAP: &str = "application/tzif-leap";
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum TzifMediaType {
8 Tzif,
9 TzifLeap,
10}
11
12impl TzifMediaType {
13 pub const APPLICATION_TZIF: &'static str = APPLICATION_TZIF;
14 pub const APPLICATION_TZIF_LEAP: &'static str = APPLICATION_TZIF_LEAP;
15
16 #[must_use]
17 pub const fn as_str(self) -> &'static str {
18 match self {
19 Self::Tzif => APPLICATION_TZIF,
20 Self::TzifLeap => APPLICATION_TZIF_LEAP,
21 }
22 }
23}
24
25impl TryFrom<&str> for TzifMediaType {
26 type Error = TzdistError;
27
28 fn try_from(value: &str) -> Result<Self, Self::Error> {
29 match value {
30 APPLICATION_TZIF => Ok(Self::Tzif),
31 APPLICATION_TZIF_LEAP => Ok(Self::TzifLeap),
32 value => Err(TzdistError::UnsupportedMediaType(value.to_string())),
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct TzdistTruncation {
39 pub start: Option<i64>,
40 pub end: Option<i64>,
41}
42
43impl TzdistTruncation {
44 #[must_use]
45 pub const fn start(start: i64) -> Self {
46 Self {
47 start: Some(start),
48 end: None,
49 }
50 }
51
52 #[must_use]
53 pub const fn end(end: i64) -> Self {
54 Self {
55 start: None,
56 end: Some(end),
57 }
58 }
59
60 #[must_use]
61 pub const fn range(start: i64, end: i64) -> Self {
62 Self {
63 start: Some(start),
64 end: Some(end),
65 }
66 }
67}
68
69pub fn validate_tzdist_capability_formats<'a, I>(formats: I) -> Result<(), TzdistError>
76where
77 I: IntoIterator<Item = &'a str>,
78{
79 let mut has_tzif = false;
80 let mut has_tzif_leap = false;
81 for format in formats {
82 has_tzif |= format == APPLICATION_TZIF;
83 has_tzif_leap |= format == APPLICATION_TZIF_LEAP;
84 }
85 if has_tzif_leap && !has_tzif {
86 return Err(TzdistError::TzifLeapCapabilityRequiresTzif);
87 }
88 Ok(())
89}
90
91impl TzifFile {
92 #[must_use]
93 pub fn has_leap_seconds(&self) -> bool {
94 !self.v1.leap_seconds.is_empty()
95 || self
96 .v2_plus
97 .as_ref()
98 .is_some_and(|block| !block.leap_seconds.is_empty())
99 }
100
101 #[must_use]
102 pub fn suggested_media_type(&self) -> &'static str {
103 if self.has_leap_seconds() {
104 APPLICATION_TZIF_LEAP
105 } else {
106 APPLICATION_TZIF
107 }
108 }
109
110 pub fn validate_for_media_type(&self, media_type: TzifMediaType) -> Result<(), TzdistError> {
117 validate_file(self).map_err(TzdistError::InvalidTzif)?;
118 if media_type == TzifMediaType::Tzif && self.has_leap_seconds() {
119 return Err(TzdistError::LeapSecondsNotAllowedForApplicationTzif);
120 }
121 Ok(())
122 }
123
124 pub fn validate_tzdist_truncation(
132 &self,
133 truncation: TzdistTruncation,
134 ) -> Result<(), TzdistError> {
135 let block = self
136 .v2_plus
137 .as_ref()
138 .ok_or(TzdistError::TruncationRequiresVersion2Plus)?;
139 if block.transition_times.is_empty() {
140 return Err(TzdistError::TruncationRequiresVersion2PlusTransitions);
141 }
142
143 if let Some(start) = truncation.start {
144 let first = *block
145 .transition_times
146 .first()
147 .ok_or(TzdistError::TruncationRequiresVersion2PlusTransitions)?;
148 if first != start {
149 return Err(TzdistError::StartTruncationTransitionMismatch {
150 expected: start,
151 actual: first,
152 });
153 }
154 }
155
156 if let Some(end) = truncation.end {
157 let last = *block
158 .transition_times
159 .last()
160 .ok_or(TzdistError::TruncationRequiresVersion2PlusTransitions)?;
161 if last != end {
162 return Err(TzdistError::EndTruncationTransitionMismatch {
163 expected: end,
164 actual: last,
165 });
166 }
167 if self
168 .footer
169 .as_deref()
170 .is_some_and(|footer| !footer.is_empty())
171 {
172 return Err(TzdistError::EndTruncationRequiresEmptyFooter);
173 }
174 }
175
176 validate_file(self).map_err(TzdistError::InvalidTzif)?;
177
178 if let Some(start) = truncation.start {
179 let first = *block
180 .transition_times
181 .first()
182 .ok_or(TzdistError::TruncationRequiresVersion2PlusTransitions)?;
183 if first != start {
184 return Err(TzdistError::StartTruncationTransitionMismatch {
185 expected: start,
186 actual: first,
187 });
188 }
189 if !is_placeholder_type(block, 0) {
190 return Err(TzdistError::StartTruncationTypeZeroMustBePlaceholder);
191 }
192 }
193
194 if truncation.end.is_some() {
195 let last_type = usize::from(
196 *block
197 .transition_types
198 .last()
199 .ok_or(TzdistError::TruncationRequiresVersion2PlusTransitions)?,
200 );
201 if !is_placeholder_type(block, last_type) {
202 return Err(TzdistError::EndTruncationLastTypeMustBePlaceholder);
203 }
204 }
205
206 if let (Some(start), Some(end)) = (truncation.start, truncation.end) {
207 if start >= end {
208 return Err(TzdistError::InvalidTruncationRange { start, end });
209 }
210 }
211
212 Ok(())
213 }
214}