/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/config/util.rs
483 строки
14 KB
cui fliter
fix(config): reject out-of-range minimum dependency ages (#36051)
15 июл 2026, 12:05
Не верифицирован
15 июл 2026, 12:05
e724d7f
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use chrono::DateTime; use chrono::Duration; use thiserror::Error; use crate::deno_json::NewestDependencyDate; pub fn is_skippable_io_error(e: &std::io::Error) -> bool { use std::io::ErrorKind::*; // Match `ErrorKind` directly (rather than raw errno values gated by `cfg!`) // so config-file auto-discovery behaves identically on every OS — see // denoland/deno#17428. matches!( e.kind(), InvalidInput | InvalidFilename | PermissionDenied | NotFound | NotADirectory | IsADirectory ) } #[derive(Debug, Error)] pub enum ParseDateOrDurationError { #[error("failed parsing integer to minutes")] InvalidMinutes(#[source] std::num::ParseIntError), #[error("expected minutes, RFC3339 datetime, or ISO-8601 duration")] InvalidDateTime(#[source] chrono::ParseError), #[error("expected minutes, RFC3339 datetime, or ISO-8601 duration")] InvalidDuration(#[source] ParseIso8601DurationError), #[error("date or duration is out of range")] OutOfRange, } /// Parses a string that could be an integer for number of minutes, /// ISO-8601 duration, or RFC3339 date. pub fn parse_minutes_duration_or_date( sys: &impl sys_traits::SystemTimeNow, s: &str, ) -> Result<NewestDependencyDate, ParseDateOrDurationError> { if s == "0" { // consider 0 as disabled in order to not cause // issues when a user's clock is wrong Ok(NewestDependencyDate::Disabled) } else { parse_enabled_minutes_duration_or_date(sys, s) .map(NewestDependencyDate::Enabled) } } fn parse_enabled_minutes_duration_or_date( sys: &impl sys_traits::SystemTimeNow, s: &str, ) -> Result<chrono::DateTime<chrono::Utc>, ParseDateOrDurationError> { if s.chars().all(|c| c.is_ascii_digit()) { let minutes: i64 = s .parse() .map_err(ParseDateOrDurationError::InvalidMinutes)?; return date_from_minutes(sys, minutes); } let datetime_parse_err = match DateTime::parse_from_rfc3339(s) { Ok(dt) => return Ok(dt.with_timezone(&chrono::Utc)), Err(err) => err, }; // accept offsets without colon (e.g., +0900) and optional seconds if let Ok(dt) = DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%z") .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M%z")) { return Ok(dt.with_timezone(&chrono::Utc)); } // accept simple date format (YYYY-MM-DD) and treat as midnight UTC if let Ok(date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") { return Ok(date.and_hms_opt(0, 0, 0).unwrap().and_utc()); } // try duration match parse_iso8601_duration(s) { Ok(duration) => date_from_duration(sys, duration), Err(ParseIso8601DurationError::MissingP) => Err( ParseDateOrDurationError::InvalidDateTime(datetime_parse_err), ), Err(err) => Err(ParseDateOrDurationError::InvalidDuration(err)), } } pub(crate) fn date_from_minutes( sys: &impl sys_traits::SystemTimeNow, minutes: i64, ) -> Result<chrono::DateTime<chrono::Utc>, ParseDateOrDurationError> { let duration = chrono::Duration::try_minutes(minutes) .ok_or(ParseDateOrDurationError::OutOfRange)?; date_from_duration(sys, duration) } fn date_from_duration( sys: &impl sys_traits::SystemTimeNow, duration: chrono::Duration, ) -> Result<chrono::DateTime<chrono::Utc>, ParseDateOrDurationError> { let now = chrono::DateTime::<chrono::Utc>::from(sys.sys_time_now()); now .checked_sub_signed(duration) .ok_or(ParseDateOrDurationError::OutOfRange) } #[derive(Debug, Error)] pub enum ParseIso8601DurationError { #[error("empty duration string")] Empty, #[error("duration must start with 'P'")] MissingP, #[error("expected designators after 'P'")] MissingDesignator, #[error("expected time component after 'T'")] MissingTimeAfterT, #[error("duplicate 'T' designator")] DuplicateT, #[error("expected number")] ExpectedNumber, #[error("expected digits after decimal point")] ExpectedFraction, #[error("missing unit designator")] MissingUnit, #[error("invalid integer number")] InvalidNumber, #[error("invalid fractional seconds")] InvalidFractionalSeconds, #[error("months not supported")] MonthsNotSupported, #[error("years not supported")] YearsNotSupported, #[error("weeks must be the only component (use PnW)")] WeeksMustBeAlone, #[error("fractional value is only allowed for seconds (S)")] FractionalNotAllowed, #[error("invalid unit designator")] InvalidUnit, #[error("duration overflow")] Overflow, } /// Parses ISO-8601 durations of the form: /// PnW | PnDTnHnMnS | PnD | PTnH | PTnM | PTnS | combinations /// Notes: /// - Supports optional leading '-' for negative durations. /// - Supports weeks (W), days (D), hours (H), minutes (M in the *time* part), seconds (S). /// - DOES NOT support years or months (ambiguous length); will error if present. fn parse_iso8601_duration( input: &str, ) -> Result<Duration, ParseIso8601DurationError> { if input.is_empty() { return Err(ParseIso8601DurationError::Empty); } // accept optional '+' or '-' sign let (s, neg) = match input.strip_prefix(['-', '+']) { Some(rest) if input.starts_with('-') => (rest, true), Some(rest) => (rest, false), None => (input, false), }; let Some(s) = s.strip_prefix('P') else { return Err(ParseIso8601DurationError::MissingP); }; if s.is_empty() { return Err(ParseIso8601DurationError::MissingDesignator); } // weeks-only short form: PnW if let Some(num) = s.strip_suffix(['W', 'w']) { let weeks: i64 = num .parse() .map_err(|_| ParseIso8601DurationError::InvalidNumber)?; let d = Duration::try_weeks(weeks).ok_or(ParseIso8601DurationError::Overflow)?; return if neg { Duration::zero() .checked_sub(&d) .ok_or(ParseIso8601DurationError::Overflow) } else { Ok(d) }; } let bytes = s.as_bytes(); let mut i = 0usize; let mut in_time = false; let mut total = Duration::zero(); while i < bytes.len() { if !in_time && bytes[i] == b'T' { in_time = true; i += 1; if i == bytes.len() { return Err(ParseIso8601DurationError::MissingTimeAfterT); } continue; } else if in_time && bytes[i] == b'T' { return Err(ParseIso8601DurationError::DuplicateT); } // parse integer part let start = i; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } if start == i { return Err(ParseIso8601DurationError::ExpectedNumber); } // optional fractional part ONLY allowed for seconds let mut frac_start = i; let mut frac_len = 0usize; if i < bytes.len() && bytes[i] == b'.' { i += 1; // skip '.' frac_start = i; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; frac_len += 1; } if frac_len == 0 { return Err(ParseIso8601DurationError::ExpectedFraction); } } // end of the integer slice (just before '.' if present) let int_end = if frac_len > 0 { frac_start - 1 } else { i }; if i >= bytes.len() { return Err(ParseIso8601DurationError::MissingUnit); } let mut unit = bytes[i] as char; unit.make_ascii_uppercase(); i += 1; // integral value let int_val: i64 = s[start..int_end] .parse() .map_err(|_| ParseIso8601DurationError::InvalidNumber)?; // add to total let add = match (in_time, unit, frac_len) { (false, 'D', 0) => Duration::try_days(int_val) .ok_or(ParseIso8601DurationError::Overflow)?, (false, 'M', _) => { return Err(ParseIso8601DurationError::MonthsNotSupported); } (false, 'Y', _) => { return Err(ParseIso8601DurationError::YearsNotSupported); } (false, 'W', _) => { return Err(ParseIso8601DurationError::WeeksMustBeAlone); } (true, 'H', 0) => Duration::try_hours(int_val) .ok_or(ParseIso8601DurationError::Overflow)?, (true, 'M', 0) => Duration::try_minutes(int_val) .ok_or(ParseIso8601DurationError::Overflow)?, // Seconds may be fractional: PT1.5S (true, 'S', _) => { let mut d = Duration::try_seconds(int_val) .ok_or(ParseIso8601DurationError::Overflow)?; if frac_len > 0 { let frac_str = &s[frac_start..(frac_start + frac_len)]; // take up to 9 digits for nanoseconds, right-pad with zeros let n = frac_str.chars().take(9).collect::<String>(); let scale = 9 - n.len(); let base: i64 = n .parse() .map_err(|_| ParseIso8601DurationError::InvalidFractionalSeconds)?; let nanos = base .checked_mul(10_i64.pow(scale as u32)) .ok_or(ParseIso8601DurationError::Overflow)?; d = d .checked_add(&Duration::nanoseconds(nanos)) .ok_or(ParseIso8601DurationError::Overflow)?; } d } // any other time-unit with a fraction is invalid because only seconds allow fractions (true, _, f) if f > 0 => { return Err(ParseIso8601DurationError::FractionalNotAllowed); } // unknown/invalid unit in time section (without fraction) (true, _, _) => return Err(ParseIso8601DurationError::InvalidUnit), _ => return Err(ParseIso8601DurationError::InvalidUnit), }; total = total .checked_add(&add) .ok_or(ParseIso8601DurationError::Overflow)?; } if neg { Duration::zero() .checked_sub(&total) .ok_or(ParseIso8601DurationError::Overflow) } else { Ok(total) } } #[cfg(test)] mod tests { use std::time::SystemTime; use chrono::TimeZone; use chrono::Utc; use super::*; #[cfg(windows)] #[test] fn is_skippable_io_error_win_invalid_filename() { let error = std::io::Error::from_raw_os_error(123); assert!(super::is_skippable_io_error(&error)); } #[cfg(unix)] #[test] fn is_skippable_io_error_unix_enotdir() { let error = std::io::Error::from_raw_os_error(20); assert!(super::is_skippable_io_error(&error)); } #[test] fn is_skippable_io_error_kinds() { use std::io::Error; use std::io::ErrorKind; // these should be skipped on every platform so config discovery walks // past non-directory parent components, permission-denied folders, and // missing files identically on windows and unix for kind in [ ErrorKind::InvalidInput, ErrorKind::InvalidFilename, ErrorKind::PermissionDenied, ErrorKind::NotFound, ErrorKind::NotADirectory, ErrorKind::IsADirectory, ] { assert!( super::is_skippable_io_error(&Error::from(kind)), "expected {kind:?} to be skippable" ); } // sanity check: unrelated kinds are not skipped assert!(!super::is_skippable_io_error(&Error::from( ErrorKind::ConnectionRefused, ))); assert!(!super::is_skippable_io_error(&Error::from( ErrorKind::UnexpectedEof, ))); } #[test] fn ok_basic() { assert_eq!( parse_iso8601_duration("P3DT4H12M6S").unwrap(), Duration::days(3) + Duration::hours(4) + Duration::minutes(12) + Duration::seconds(6) ); assert_eq!( parse_iso8601_duration("+PT90S").unwrap(), Duration::seconds(90) ); assert_eq!(parse_iso8601_duration("P2W").unwrap(), Duration::days(14)); assert_eq!( parse_iso8601_duration("PT1.5S").unwrap(), Duration::milliseconds(1500) ); assert_eq!( parse_iso8601_duration("-PT5M").unwrap(), -Duration::minutes(5) ); } #[test] fn errs() { assert!(parse_iso8601_duration("P1Y").is_err()); assert!(parse_iso8601_duration("P1M").is_err()); assert!(parse_iso8601_duration("PT").is_err()); assert!(parse_iso8601_duration("PT1.2M").is_err()); // fractional minutes rejected assert!(parse_iso8601_duration("P1WT1H").is_err()); // W must be alone assert!(matches!( parse_iso8601_duration("PT153722867280913M"), Err(ParseIso8601DurationError::Overflow) )); } #[test] fn test_parse_minutes_duration_or_date() { struct TestEnv; impl sys_traits::SystemTimeNow for TestEnv { fn sys_time_now(&self) -> SystemTime { let datetime = Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap(); SystemTime::from(datetime) } } // zero becomes disabled to prevent issues with clock drift assert_eq!( parse_minutes_duration_or_date(&TestEnv, "0").unwrap(), NewestDependencyDate::Disabled ); assert_eq!( parse_minutes_duration_or_date(&TestEnv, "120").unwrap(), NewestDependencyDate::Enabled( Utc.with_ymd_and_hms(2025, 5, 31, 22, 0, 0).unwrap() ) ); assert_eq!( parse_minutes_duration_or_date(&TestEnv, "2025-01-01").unwrap(), NewestDependencyDate::Enabled( Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap() ) ); assert_eq!( parse_minutes_duration_or_date(&TestEnv, "2025-01-01").unwrap(), NewestDependencyDate::Enabled( Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap() ) ); assert_eq!( parse_minutes_duration_or_date(&TestEnv, "2025-09-16T12:50:10+00:00") .unwrap(), NewestDependencyDate::Enabled( Utc.with_ymd_and_hms(2025, 9, 16, 12, 50, 10).unwrap() ) ); assert_eq!( parse_minutes_duration_or_date(&TestEnv, "P2D").unwrap(), NewestDependencyDate::Enabled( Utc.with_ymd_and_hms(2025, 5, 30, 0, 0, 0).unwrap() ) ); for value in [ "138939752218", "153722867280913", "9223372036854775807", "P100000000D", ] { assert!(matches!( parse_minutes_duration_or_date(&TestEnv, value), Err(ParseDateOrDurationError::OutOfRange) )); } } }