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
312impl ExcludeNewerOverride {
313 pub fn into_value(self) -> Option<ExcludeNewerValue> {
315 match self {
316 Self::Disabled => None,
317 Self::Enabled(value) => Some(*value),
318 }
319 }
320}
321
322impl From<ExcludeNewerValue> for ExcludeNewerOverride {
323 fn from(value: ExcludeNewerValue) -> Self {
324 Self::Enabled(Box::new(value))
325 }
326}
327
328impl FromStr for ExcludeNewerOverride {
329 type Err = String;
330
331 fn from_str(input: &str) -> Result<Self, Self::Err> {
332 if input == "false" {
333 Ok(Self::Disabled)
334 } else {
335 ExcludeNewerValue::from_str(input).map(Self::from)
336 }
337 }
338}
339
340#[cfg(feature = "schemars")]
341impl schemars::JsonSchema for ExcludeNewerOverride {
342 fn schema_name() -> Cow<'static, str> {
343 Cow::Borrowed("ExcludeNewerOverride")
344 }
345
346 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
347 schemars::json_schema!({
348 "oneOf": [
349 {
350 "type": "boolean",
351 "const": false,
352 "description": "Disable exclude-newer."
353 },
354 generator.subschema_for::<ExcludeNewerValue>(),
355 ]
356 })
357 }
358}
359
360impl<'de> serde::Deserialize<'de> for ExcludeNewerOverride {
361 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
362 where
363 D: serde::Deserializer<'de>,
364 {
365 struct Visitor;
366
367 impl<'de> serde::de::Visitor<'de> for Visitor {
368 type Value = ExcludeNewerOverride;
369
370 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
371 formatter.write_str(
372 "a date/timestamp/duration string, false to disable exclude-newer, or a table \
373 with timestamp/span",
374 )
375 }
376
377 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
378 where
379 E: serde::de::Error,
380 {
381 ExcludeNewerValue::from_str(v)
382 .map(ExcludeNewerOverride::from)
383 .map_err(E::custom)
384 }
385
386 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
387 where
388 E: serde::de::Error,
389 {
390 if v {
391 Err(E::custom(
392 "expected false to disable exclude-newer, got true",
393 ))
394 } else {
395 Ok(ExcludeNewerOverride::Disabled)
396 }
397 }
398
399 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
400 where
401 A: serde::de::MapAccess<'de>,
402 {
403 Ok(ExcludeNewerOverride::Enabled(Box::new(
404 ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?,
405 )))
406 }
407 }
408
409 deserializer.deserialize_any(Visitor)
410 }
411}
412
413impl serde::Serialize for ExcludeNewerOverride {
414 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
415 where
416 S: serde::Serializer,
417 {
418 match self {
419 Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer),
420 Self::Disabled => serializer.serialize_bool(false),
421 }
422 }
423}