Skip to main content

uv_distribution_types/
exclude_newer.rs

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 enum ExcludeNewerValue {
65    /// An absolute timestamp.
66    Absolute(Timestamp),
67    /// A span used to compute a timestamp relative to the current time.
68    Relative(ExcludeNewerSpan),
69}
70
71impl ExcludeNewerValue {
72    /// A placeholder timestamp used when serializing a [`Relative`](Self::Relative)
73    /// value to a wire format that requires a timestamp field.
74    pub const PLACEHOLDER: &'static str = "0001-01-01T00:00:00Z";
75
76    /// Return the effective [`Timestamp`].
77    ///
78    /// For [`Relative`](Self::Relative) values this is computed from the span and the current
79    /// time on each call.
80    pub fn timestamp(&self) -> Timestamp {
81        match self {
82            Self::Absolute(timestamp) => *timestamp,
83            Self::Relative(span) => {
84                let now = current_time();
85                now.checked_sub(span.0.abs())
86                    .map_or(now.timestamp(), |cutoff| cutoff.timestamp())
87            }
88        }
89    }
90
91    /// Return the [`ExcludeNewerSpan`], if any.
92    pub fn span(&self) -> Option<&ExcludeNewerSpan> {
93        match self {
94            Self::Absolute(_) => None,
95            Self::Relative(span) => Some(span),
96        }
97    }
98
99    /// Create a new [`ExcludeNewerValue`] from an absolute timestamp.
100    pub fn absolute(timestamp: Timestamp) -> Self {
101        Self::Absolute(timestamp)
102    }
103
104    /// Create a new [`ExcludeNewerValue`] from a relative span.
105    pub fn relative(span: ExcludeNewerSpan) -> Self {
106        Self::Relative(span)
107    }
108}
109
110/// Return the current time, respecting the `UV_TEST_CURRENT_TIMESTAMP` override.
111fn current_time() -> jiff::Zoned {
112    if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
113        test_time
114            .parse::<Timestamp>()
115            .expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
116            .to_zoned(TimeZone::UTC)
117    } else {
118        Timestamp::now().to_zoned(TimeZone::UTC)
119    }
120}
121
122impl serde::Serialize for ExcludeNewerValue {
123    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124    where
125        S: serde::Serializer,
126    {
127        self.timestamp().serialize(serializer)
128    }
129}
130
131impl<'de> serde::Deserialize<'de> for ExcludeNewerValue {
132    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133    where
134        D: serde::Deserializer<'de>,
135    {
136        #[derive(serde::Deserialize)]
137        struct TableForm {
138            timestamp: Timestamp,
139            span: Option<ExcludeNewerSpan>,
140        }
141
142        #[derive(serde::Deserialize)]
143        #[serde(untagged)]
144        enum Helper {
145            String(String),
146            Table(Box<TableForm>),
147        }
148
149        match Helper::deserialize(deserializer)? {
150            Helper::String(s) => Self::from_str(&s).map_err(serde::de::Error::custom),
151            Helper::Table(table) => Ok(match table.span {
152                Some(span) => Self::relative(span),
153                None => Self::absolute(table.timestamp),
154            }),
155        }
156    }
157}
158
159impl From<Timestamp> for ExcludeNewerValue {
160    fn from(timestamp: Timestamp) -> Self {
161        Self::Absolute(timestamp)
162    }
163}
164
165impl From<ExcludeNewerSpan> for ExcludeNewerValue {
166    fn from(span: ExcludeNewerSpan) -> Self {
167        Self::Relative(span)
168    }
169}
170
171fn format_exclude_newer_error(
172    input: &str,
173    date_err: &jiff::Error,
174    span_err: &jiff::Error,
175) -> String {
176    let trimmed = input.trim();
177
178    let after_sign = trimmed.trim_start_matches(['+', '-']);
179    if after_sign.starts_with('P') || after_sign.starts_with('p') {
180        return format!("`{input}` could not be parsed as an ISO 8601 duration: {span_err}");
181    }
182
183    let after_sign_trimmed = after_sign.trim_start();
184    let mut chars = after_sign_trimmed.chars().peekable();
185    if chars.peek().is_some_and(char::is_ascii_digit) {
186        while chars.peek().is_some_and(char::is_ascii_digit) {
187            chars.next();
188        }
189        while chars.peek().is_some_and(|c| c.is_whitespace()) {
190            chars.next();
191        }
192        if chars.peek().is_some_and(char::is_ascii_alphabetic) {
193            return format!("`{input}` could not be parsed as a duration: {span_err}");
194        }
195    }
196
197    let mut chars = after_sign.chars();
198    let looks_like_date = 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.is_ascii_digit())
201        && chars.next().is_some_and(|c| c.is_ascii_digit())
202        && chars.next().is_some_and(|c| c == '-');
203
204    if looks_like_date {
205        return format!("`{input}` could not be parsed as a valid date: {date_err}");
206    }
207
208    format!(
209        "`{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`)"
210    )
211}
212
213impl FromStr for ExcludeNewerValue {
214    type Err = String;
215
216    fn from_str(input: &str) -> Result<Self, Self::Err> {
217        if let Ok(timestamp) = input.parse::<Timestamp>() {
218            return Ok(Self::absolute(timestamp));
219        }
220
221        let date_err = match input.parse::<jiff::civil::Date>() {
222            Ok(date) => {
223                let timestamp = date
224                    .checked_add(1.day())
225                    .and_then(|date| date.to_zoned(TimeZone::system()))
226                    .map(|zdt| zdt.timestamp())
227                    .map_err(|err| {
228                        format!(
229                            "`{input}` parsed to date `{date}`, but could not be converted to a timestamp: {err}",
230                        )
231                    })?;
232                return Ok(Self::absolute(timestamp));
233            }
234            Err(err) => err,
235        };
236
237        let span_err = match input.parse::<Span>() {
238            Ok(span) => {
239                let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
240                    test_time
241                        .parse::<Timestamp>()
242                        .expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
243                        .to_zoned(TimeZone::UTC)
244                } else {
245                    Timestamp::now().to_zoned(TimeZone::UTC)
246                };
247
248                if span.get_years() != 0 {
249                    let years = span
250                        .total((Unit::Year, &now))
251                        .map(f64::ceil)
252                        .unwrap_or(1.0)
253                        .abs();
254                    let days = years * 365.0;
255                    return Err(format!(
256                        "Duration `{input}` uses unit 'years' which is not allowed; use days instead, e.g., `{days:.0} days`.",
257                    ));
258                }
259                if span.get_months() != 0 {
260                    let months = span
261                        .total((Unit::Month, &now))
262                        .map(f64::ceil)
263                        .unwrap_or(1.0)
264                        .abs();
265                    let days = months * 30.0;
266                    return Err(format!(
267                        "Duration `{input}` uses 'months' which is not allowed; use days instead, e.g., `{days:.0} days`."
268                    ));
269                }
270
271                now.checked_sub(span.abs()).map_err(|err| {
272                    format!("Duration `{input}` is too large to subtract from current time: {err}")
273                })?;
274                return Ok(Self::relative(ExcludeNewerSpan(span)));
275            }
276            Err(err) => err,
277        };
278
279        Err(format_exclude_newer_error(input, &date_err, &span_err))
280    }
281}
282
283impl std::fmt::Display for ExcludeNewerValue {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        self.timestamp().fmt(f)
286    }
287}
288
289#[cfg(feature = "schemars")]
290impl schemars::JsonSchema for ExcludeNewerValue {
291    fn schema_name() -> Cow<'static, str> {
292        Cow::Borrowed("ExcludeNewerValue")
293    }
294
295    fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
296        schemars::json_schema!({
297            "type": "string",
298            "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.",
299        })
300    }
301}
302
303/// Whether `exclude-newer` is disabled or enabled with an explicit cutoff.
304#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
305pub enum ExcludeNewerOverride {
306    /// Disable exclude-newer (allow all versions regardless of upload date).
307    Disabled,
308    /// Enable exclude-newer with this cutoff.
309    Enabled(Box<ExcludeNewerValue>),
310}
311
312#[cfg(feature = "schemars")]
313impl schemars::JsonSchema for ExcludeNewerOverride {
314    fn schema_name() -> Cow<'static, str> {
315        Cow::Borrowed("ExcludeNewerOverride")
316    }
317
318    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
319        schemars::json_schema!({
320            "oneOf": [
321                {
322                    "type": "boolean",
323                    "const": false,
324                    "description": "Disable exclude-newer."
325                },
326                generator.subschema_for::<ExcludeNewerValue>(),
327            ]
328        })
329    }
330}
331
332impl<'de> serde::Deserialize<'de> for ExcludeNewerOverride {
333    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
334    where
335        D: serde::Deserializer<'de>,
336    {
337        struct Visitor;
338
339        impl<'de> serde::de::Visitor<'de> for Visitor {
340            type Value = ExcludeNewerOverride;
341
342            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
343                formatter.write_str(
344                    "a date/timestamp/duration string, false to disable exclude-newer, or a table \
345                     with timestamp/span",
346                )
347            }
348
349            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
350            where
351                E: serde::de::Error,
352            {
353                ExcludeNewerValue::from_str(v)
354                    .map(|ts| ExcludeNewerOverride::Enabled(Box::new(ts)))
355                    .map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}")))
356            }
357
358            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
359            where
360                E: serde::de::Error,
361            {
362                if v {
363                    Err(E::custom(
364                        "expected false to disable exclude-newer, got true",
365                    ))
366                } else {
367                    Ok(ExcludeNewerOverride::Disabled)
368                }
369            }
370
371            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
372            where
373                A: serde::de::MapAccess<'de>,
374            {
375                Ok(ExcludeNewerOverride::Enabled(Box::new(
376                    ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?,
377                )))
378            }
379        }
380
381        deserializer.deserialize_any(Visitor)
382    }
383}
384
385impl serde::Serialize for ExcludeNewerOverride {
386    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
387    where
388        S: serde::Serializer,
389    {
390        match self {
391            Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer),
392            Self::Disabled => serializer.serialize_bool(false),
393        }
394    }
395}