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 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    /// Return the [`Timestamp`] in milliseconds.
75    pub fn timestamp_millis(&self) -> i64 {
76        self.timestamp.as_millisecond()
77    }
78
79    /// Return the [`Timestamp`].
80    pub fn timestamp(&self) -> Timestamp {
81        self.timestamp
82    }
83
84    /// Return the [`ExcludeNewerSpan`] used to construct the [`Timestamp`], if any.
85    pub fn span(&self) -> Option<&ExcludeNewerSpan> {
86        self.span.as_ref()
87    }
88
89    /// Create a new [`ExcludeNewerValue`].
90    pub fn new(timestamp: Timestamp, span: Option<ExcludeNewerSpan>) -> Self {
91        Self { timestamp, span }
92    }
93
94    /// If this value was derived from a relative span, recompute the timestamp relative to now.
95    ///
96    /// Returns `self` unchanged if there is no span (i.e., the timestamp is absolute).
97    #[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/// Whether `exclude-newer` is disabled or enabled with an explicit cutoff.
302#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
303pub enum ExcludeNewerOverride {
304    /// Disable exclude-newer (allow all versions regardless of upload date).
305    Disabled,
306    /// Enable exclude-newer with this cutoff.
307    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}