1use std::borrow::Cow;
2use std::str::FromStr;
3
4use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone};
5use serde::Deserialize;
6use serde::de::value::MapAccessDeserializer;
7
8#[derive(Debug, Copy, Clone)]
9pub struct ExcludeNewerSpan(Span);
10
11impl std::fmt::Display for ExcludeNewerSpan {
12 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 self.0.fmt(f)
14 }
15}
16
17impl PartialEq for ExcludeNewerSpan {
18 fn eq(&self, other: &Self) -> bool {
19 self.0.fieldwise() == other.0.fieldwise()
20 }
21}
22
23impl Eq for ExcludeNewerSpan {}
24
25impl PartialOrd for ExcludeNewerSpan {
26 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
27 Some(self.cmp(other))
28 }
29}
30
31impl Ord for ExcludeNewerSpan {
32 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
33 self.0.to_string().cmp(&other.0.to_string())
34 }
35}
36
37impl std::hash::Hash for ExcludeNewerSpan {
38 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
39 self.0.to_string().hash(state);
40 }
41}
42
43impl serde::Serialize for ExcludeNewerSpan {
44 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
45 where
46 S: serde::Serializer,
47 {
48 serializer.serialize_str(&self.0.to_string())
49 }
50}
51
52impl<'de> serde::Deserialize<'de> for ExcludeNewerSpan {
53 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54 where
55 D: serde::Deserializer<'de>,
56 {
57 let s = <Cow<'_, str>>::deserialize(deserializer)?;
58 let span: Span = s.parse().map_err(serde::de::Error::custom)?;
59 Ok(Self(span))
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub struct ExcludeNewerValue {
65 timestamp: Timestamp,
66 span: Option<ExcludeNewerSpan>,
67}
68
69impl ExcludeNewerValue {
70 pub fn into_parts(self) -> (Timestamp, Option<ExcludeNewerSpan>) {
71 (self.timestamp, self.span)
72 }
73
74 pub fn timestamp_millis(&self) -> i64 {
76 self.timestamp.as_millisecond()
77 }
78
79 pub fn timestamp(&self) -> Timestamp {
81 self.timestamp
82 }
83
84 pub fn span(&self) -> Option<&ExcludeNewerSpan> {
86 self.span.as_ref()
87 }
88
89 pub fn new(timestamp: Timestamp, span: Option<ExcludeNewerSpan>) -> Self {
91 Self { timestamp, span }
92 }
93
94 #[must_use]
98 pub fn recompute(self) -> Self {
99 let Some(span) = self.span else {
100 return self;
101 };
102
103 let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
104 test_time
105 .parse::<Timestamp>()
106 .expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
107 .to_zoned(TimeZone::UTC)
108 } else {
109 Timestamp::now().to_zoned(TimeZone::UTC)
110 };
111
112 let Ok(cutoff) = now.checked_sub(span.0.abs()) else {
113 return Self {
114 timestamp: self.timestamp,
115 span: Some(span),
116 };
117 };
118
119 Self {
120 timestamp: cutoff.into(),
121 span: Some(span),
122 }
123 }
124}
125
126impl serde::Serialize for ExcludeNewerValue {
127 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
128 where
129 S: serde::Serializer,
130 {
131 self.timestamp.serialize(serializer)
132 }
133}
134
135impl<'de> serde::Deserialize<'de> for ExcludeNewerValue {
136 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137 where
138 D: serde::Deserializer<'de>,
139 {
140 #[derive(serde::Deserialize)]
141 struct TableForm {
142 timestamp: Timestamp,
143 span: Option<ExcludeNewerSpan>,
144 }
145
146 #[derive(serde::Deserialize)]
147 #[serde(untagged)]
148 enum Helper {
149 String(String),
150 Table(Box<TableForm>),
151 }
152
153 match Helper::deserialize(deserializer)? {
154 Helper::String(s) => Self::from_str(&s).map_err(serde::de::Error::custom),
155 Helper::Table(table) => Ok(Self::new(table.timestamp, table.span)),
156 }
157 }
158}
159
160impl From<Timestamp> for ExcludeNewerValue {
161 fn from(timestamp: Timestamp) -> Self {
162 Self {
163 timestamp,
164 span: None,
165 }
166 }
167}
168
169fn format_exclude_newer_error(
170 input: &str,
171 date_err: &jiff::Error,
172 span_err: &jiff::Error,
173) -> String {
174 let trimmed = input.trim();
175
176 let after_sign = trimmed.trim_start_matches(['+', '-']);
177 if after_sign.starts_with('P') || after_sign.starts_with('p') {
178 return format!("`{input}` could not be parsed as an ISO 8601 duration: {span_err}");
179 }
180
181 let after_sign_trimmed = after_sign.trim_start();
182 let mut chars = after_sign_trimmed.chars().peekable();
183 if chars.peek().is_some_and(char::is_ascii_digit) {
184 while chars.peek().is_some_and(char::is_ascii_digit) {
185 chars.next();
186 }
187 while chars.peek().is_some_and(|c| c.is_whitespace()) {
188 chars.next();
189 }
190 if chars.peek().is_some_and(char::is_ascii_alphabetic) {
191 return format!("`{input}` could not be parsed as a duration: {span_err}");
192 }
193 }
194
195 let mut chars = after_sign.chars();
196 let looks_like_date = chars.next().is_some_and(|c| c.is_ascii_digit())
197 && chars.next().is_some_and(|c| c.is_ascii_digit())
198 && chars.next().is_some_and(|c| c.is_ascii_digit())
199 && chars.next().is_some_and(|c| c.is_ascii_digit())
200 && chars.next().is_some_and(|c| c == '-');
201
202 if looks_like_date {
203 return format!("`{input}` could not be parsed as a valid date: {date_err}");
204 }
205
206 format!(
207 "`{input}` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`)"
208 )
209}
210
211impl FromStr for ExcludeNewerValue {
212 type Err = String;
213
214 fn from_str(input: &str) -> Result<Self, Self::Err> {
215 if let Ok(timestamp) = input.parse::<Timestamp>() {
216 return Ok(Self::new(timestamp, None));
217 }
218
219 let date_err = match input.parse::<jiff::civil::Date>() {
220 Ok(date) => {
221 let timestamp = date
222 .checked_add(1.day())
223 .and_then(|date| date.to_zoned(TimeZone::system()))
224 .map(|zdt| zdt.timestamp())
225 .map_err(|err| {
226 format!(
227 "`{input}` parsed to date `{date}`, but could not be converted to a timestamp: {err}",
228 )
229 })?;
230 return Ok(Self::new(timestamp, None));
231 }
232 Err(err) => err,
233 };
234
235 let span_err = match input.parse::<Span>() {
236 Ok(span) => {
237 let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
238 test_time
239 .parse::<Timestamp>()
240 .expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
241 .to_zoned(TimeZone::UTC)
242 } else {
243 Timestamp::now().to_zoned(TimeZone::UTC)
244 };
245
246 if span.get_years() != 0 {
247 let years = span
248 .total((Unit::Year, &now))
249 .map(f64::ceil)
250 .unwrap_or(1.0)
251 .abs();
252 let days = years * 365.0;
253 return Err(format!(
254 "Duration `{input}` uses unit 'years' which is not allowed; use days instead, e.g., `{days:.0} days`.",
255 ));
256 }
257 if span.get_months() != 0 {
258 let months = span
259 .total((Unit::Month, &now))
260 .map(f64::ceil)
261 .unwrap_or(1.0)
262 .abs();
263 let days = months * 30.0;
264 return Err(format!(
265 "Duration `{input}` uses 'months' which is not allowed; use days instead, e.g., `{days:.0} days`."
266 ));
267 }
268
269 let cutoff = now.checked_sub(span.abs()).map_err(|err| {
270 format!("Duration `{input}` is too large to subtract from current time: {err}")
271 })?;
272 return Ok(Self::new(cutoff.into(), Some(ExcludeNewerSpan(span))));
273 }
274 Err(err) => err,
275 };
276
277 Err(format_exclude_newer_error(input, &date_err, &span_err))
278 }
279}
280
281impl std::fmt::Display for ExcludeNewerValue {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 self.timestamp.fmt(f)
284 }
285}
286
287#[cfg(feature = "schemars")]
288impl schemars::JsonSchema for ExcludeNewerValue {
289 fn schema_name() -> Cow<'static, str> {
290 Cow::Borrowed("ExcludeNewerValue")
291 }
292
293 fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
294 schemars::json_schema!({
295 "type": "string",
296 "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`), as well as relative durations (e.g., `1 week`, `30 days`, `6 months`). Relative durations are resolved to a timestamp at lock time.",
297 })
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
303pub enum ExcludeNewerOverride {
304 Disabled,
306 Enabled(Box<ExcludeNewerValue>),
308}
309
310#[cfg(feature = "schemars")]
311impl schemars::JsonSchema for ExcludeNewerOverride {
312 fn schema_name() -> Cow<'static, str> {
313 Cow::Borrowed("ExcludeNewerOverride")
314 }
315
316 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
317 schemars::json_schema!({
318 "oneOf": [
319 {
320 "type": "boolean",
321 "const": false,
322 "description": "Disable exclude-newer."
323 },
324 generator.subschema_for::<ExcludeNewerValue>(),
325 ]
326 })
327 }
328}
329
330impl<'de> serde::Deserialize<'de> for ExcludeNewerOverride {
331 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332 where
333 D: serde::Deserializer<'de>,
334 {
335 struct Visitor;
336
337 impl<'de> serde::de::Visitor<'de> for Visitor {
338 type Value = ExcludeNewerOverride;
339
340 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
341 formatter.write_str(
342 "a date/timestamp/duration string, false to disable exclude-newer, or a table \
343 with timestamp/span",
344 )
345 }
346
347 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
348 where
349 E: serde::de::Error,
350 {
351 ExcludeNewerValue::from_str(v)
352 .map(|ts| ExcludeNewerOverride::Enabled(Box::new(ts)))
353 .map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}")))
354 }
355
356 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
357 where
358 E: serde::de::Error,
359 {
360 if v {
361 Err(E::custom(
362 "expected false to disable exclude-newer, got true",
363 ))
364 } else {
365 Ok(ExcludeNewerOverride::Disabled)
366 }
367 }
368
369 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
370 where
371 A: serde::de::MapAccess<'de>,
372 {
373 Ok(ExcludeNewerOverride::Enabled(Box::new(
374 ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?,
375 )))
376 }
377 }
378
379 deserializer.deserialize_any(Visitor)
380 }
381}
382
383impl serde::Serialize for ExcludeNewerOverride {
384 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
385 where
386 S: serde::Serializer,
387 {
388 match self {
389 Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer),
390 Self::Disabled => serializer.serialize_bool(false),
391 }
392 }
393}