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 Absolute(Timestamp),
67 Relative(ExcludeNewerSpan),
69}
70
71impl ExcludeNewerValue {
72 pub const PLACEHOLDER: &'static str = "0001-01-01T00:00:00Z";
75
76 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 pub fn span(&self) -> Option<&ExcludeNewerSpan> {
93 match self {
94 Self::Absolute(_) => None,
95 Self::Relative(span) => Some(span),
96 }
97 }
98
99 pub fn absolute(timestamp: Timestamp) -> Self {
101 Self::Absolute(timestamp)
102 }
103
104 pub fn relative(span: ExcludeNewerSpan) -> Self {
106 Self::Relative(span)
107 }
108}
109
110fn 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
305pub enum ExcludeNewerOverride {
306 Disabled,
308 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}