use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs};
use super::policy::PacPolicy;
use super::time::{self, Civil};
use crate::util::strip_brackets;
pub(crate) fn is_plain_host_name(host: &str) -> bool {
!host.contains('.') && !host.contains(':')
}
pub(crate) fn dns_domain_is(host: &str, domain: &str) -> bool {
host.to_ascii_lowercase()
.ends_with(&domain.to_ascii_lowercase())
}
pub(crate) fn local_host_or_domain_is(host: &str, hostdom: &str) -> bool {
let host = host.to_ascii_lowercase();
let hostdom = hostdom.to_ascii_lowercase();
host == hostdom || (!host.contains('.') && hostdom.starts_with(&format!("{host}.")))
}
pub(crate) fn dns_domain_levels(host: &str) -> usize {
host.matches('.').count()
}
pub(crate) fn sh_exp_match(text: &str, pattern: &str) -> bool {
if !pattern.contains('?') {
return crate::util::glob_match(pattern, text);
}
wildcard_match(pattern, text)
}
const MAX_MATCH_STEPS: usize = 1 << 24;
fn wildcard_match(pattern: &str, text: &str) -> bool {
let (mut p, mut t) = (0usize, 0usize);
let mut star: Option<usize> = None;
let mut resume = 0usize;
let mut budget = MAX_MATCH_STEPS;
while let Some(tc) = text[t..].chars().next() {
match budget.checked_sub(1) {
Some(left) => budget = left,
None => return false,
}
let pc = pattern[p..].chars().next();
if pc == Some('*') {
star = Some(p);
resume = t;
p += 1;
} else if let Some(pc) = pc.filter(|&pc| pc == '?' || pc == tc) {
p += pc.len_utf8();
t += tc.len_utf8();
} else if let Some(star) = star {
p = star + 1;
resume += text[resume..].chars().next().map_or(1, char::len_utf8);
t = resume;
} else {
return false;
}
}
pattern[p..].chars().all(|c| c == '*')
}
pub(crate) fn convert_addr(addr: &str) -> i32 {
let mut out = 0u32;
for (index, part) in addr.split('.').take(4).enumerate() {
let octet = part.trim().parse::<u32>().unwrap_or(0) & 0xff;
out |= octet << (24 - 8 * index);
}
out as i32
}
pub(crate) fn alert(message: &str) {
crate::trace::pac_alert(message);
}
pub(crate) fn my_ip_address(policy: &PacPolicy) -> IpAddr {
policy.my_ip_address()
}
fn literal_ipv4(host: &str) -> Option<Ipv4Addr> {
match crate::endpoint::parse_host(host) {
Ok(url::Host::Ipv4(v4)) => Some(v4),
_ => None,
}
}
fn unbracket_ipv6(host: &str) -> &str {
let inner = strip_brackets(host);
if inner.parse::<Ipv6Addr>().is_ok() {
inner
} else {
host
}
}
pub(crate) fn dns_resolve(host: &str, policy: &PacPolicy) -> Option<Ipv4Addr> {
let host = unbracket_ipv6(host.trim());
if let Some(literal) = literal_ipv4(host) {
return Some(literal);
}
if host.parse::<Ipv6Addr>().is_ok() {
return None;
}
resolve_ipv4(host, policy)
}
pub(crate) fn is_resolvable(host: &str, policy: &PacPolicy) -> bool {
let host = unbracket_ipv6(host.trim());
if literal_ipv4(host).is_some() || host.parse::<Ipv6Addr>().is_ok() {
return true;
}
resolve_ipv4(host, policy).is_some()
}
pub(crate) fn is_in_net(host: &str, pattern: &str, mask: &str, policy: &PacPolicy) -> bool {
let (Some(pattern), Some(mask)) = (reference_ipv4(pattern), reference_ipv4(mask)) else {
return false;
};
let address = match reference_ipv4(host) {
Some(literal) => literal,
None => match dns_resolve(host, policy) {
Some(address) => address,
None => return false,
},
};
let (address, pattern, mask) = (u32::from(address), u32::from(pattern), u32::from(mask));
address & mask == pattern & mask
}
fn reference_ipv4(text: &str) -> Option<Ipv4Addr> {
let mut octets = [0u8; 4];
let mut groups = text.split('.');
for octet in &mut octets {
let group = groups.next()?;
if group.is_empty() || group.len() > 3 || !group.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
*octet = u8::try_from(group.parse::<u16>().ok()?).ok()?;
}
if groups.next().is_some() {
return None;
}
Some(Ipv4Addr::from(octets))
}
pub(super) fn resolve_ipv4(host: &str, policy: &PacPolicy) -> Option<Ipv4Addr> {
#[cfg(all(feature = "pac-subprocess", pac_quickjs))]
if let Some(answer) = super::subprocess::ask_parent(host) {
return answer;
}
if !policy.resolve_dns() || host.is_empty() {
return None;
}
let addresses = (host, 0u16).to_socket_addrs().ok()?;
addresses
.filter_map(|address| match address.ip() {
IpAddr::V4(v4) => Some(v4),
IpAddr::V6(v6) => v6.to_ipv4_mapped(),
})
.find(|address| policy.allow_internal_addresses() || !is_internal(IpAddr::V4(*address)))
}
pub(crate) fn is_internal(address: IpAddr) -> bool {
match address {
IpAddr::V4(v4) => {
let octets = v4.octets();
v4.is_loopback()
|| v4.is_private()
|| v4.is_link_local()
|| v4.is_multicast()
|| octets[0] == 0
|| (octets[0] == 100 && (64..=127).contains(&octets[1]))
|| (octets[0] == 192 && octets[1] == 0 && octets[2] == 0)
|| (octets[0] == 192 && octets[1] == 0 && octets[2] == 2)
|| (octets[0] == 198 && octets[1] == 51 && octets[2] == 100)
|| (octets[0] == 203 && octets[1] == 0 && octets[2] == 113)
|| (octets[0] == 192 && octets[1] == 88 && octets[2] == 99)
|| (octets[0] == 198 && (octets[1] & 0xfe) == 18)
|| octets[0] >= 240
}
IpAddr::V6(v6) => {
if let Some(mapped) = v6.to_ipv4_mapped() {
return is_internal(IpAddr::V4(mapped));
}
let segments = v6.segments();
v6.is_loopback()
|| v6.is_unspecified()
|| v6.is_multicast()
|| (segments[0] & 0xfe00) == 0xfc00
|| (segments[0] & 0xffc0) == 0xfe80
}
}
}
pub(crate) fn weekday_range(args: &[String], policy: &PacPolicy) -> bool {
let (args, gmt) = time::split_gmt(args);
if args.is_empty() || args.len() > 2 {
return false;
}
let Some(first) = time::weekday_index(&args[0]) else {
return false;
};
let last = match args.get(1) {
Some(name) => match time::weekday_index(name) {
Some(index) => index,
None => return false,
},
None => first,
};
time::in_cyclic_range(time::civil_now(policy, gmt).weekday, first, last)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DatePart {
Day(i64),
Month(i64),
Year(i64),
}
fn classify_date_part(text: &str) -> Option<DatePart> {
if let Some(month) = time::month_index(text) {
return Some(DatePart::Month(month));
}
let value = time::number(text)?;
if (1..=31).contains(&value) {
Some(DatePart::Day(value))
} else if value >= 1000 {
Some(DatePart::Year(value))
} else {
None
}
}
pub(crate) fn date_range(args: &[String], policy: &PacPolicy) -> bool {
let (args, gmt) = time::split_gmt(args);
let parts: Option<Vec<DatePart>> = args
.iter()
.map(|arg| classify_date_part(arg.as_str()))
.collect();
let Some(parts) = parts else {
return false;
};
let now = time::civil_now(policy, gmt);
match parts.as_slice() {
[DatePart::Day(day)] => now.day == *day,
[DatePart::Month(month)] => now.month == *month,
[DatePart::Year(year)] => now.year == *year,
[DatePart::Day(lo), DatePart::Day(hi)] => time::in_cyclic_range(now.day, *lo, *hi),
[DatePart::Month(lo), DatePart::Month(hi)] => time::in_cyclic_range(now.month, *lo, *hi),
[DatePart::Year(lo), DatePart::Year(hi)] => time::in_cyclic_range(now.year, *lo, *hi),
[
DatePart::Day(d1),
DatePart::Month(m1),
DatePart::Day(d2),
DatePart::Month(m2),
] => time::in_cyclic_range(now.month * 100 + now.day, m1 * 100 + d1, m2 * 100 + d2),
[
DatePart::Month(m1),
DatePart::Year(y1),
DatePart::Month(m2),
DatePart::Year(y2),
] => match (checked_my(*y1, *m1), checked_my(*y2, *m2)) {
(Some(lo), Some(hi)) => time::in_cyclic_range(now.year * 100 + now.month, lo, hi),
_ => false,
},
[
DatePart::Day(d1),
DatePart::Month(m1),
DatePart::Year(y1),
DatePart::Day(d2),
DatePart::Month(m2),
DatePart::Year(y2),
] => match (checked_ymd(*y1, *m1, *d1), checked_ymd(*y2, *m2, *d2)) {
(Some(lo), Some(hi)) => time::in_cyclic_range(date_key(now), lo, hi),
_ => false,
},
_ => false,
}
}
fn date_key(now: Civil) -> i64 {
now.year * 10_000 + now.month * 100 + now.day
}
fn checked_my(year: i64, month: i64) -> Option<i64> {
year.checked_mul(100)?.checked_add(month)
}
fn checked_ymd(year: i64, month: i64, day: i64) -> Option<i64> {
year.checked_mul(10_000)?.checked_add(month * 100 + day)
}
pub(crate) fn time_range(args: &[String], policy: &PacPolicy) -> bool {
let (args, gmt) = time::split_gmt(args);
let values: Option<Vec<i64>> = args.iter().map(|arg| time::number(arg.as_str())).collect();
let Some(values) = values else {
return false;
};
let now = time::civil_now(policy, gmt);
let seconds = now.hour * 3600 + now.minute * 60 + now.second;
match values.as_slice() {
[hour] => now.hour == *hour,
[lo, hi] => *lo <= now.hour && now.hour <= *hi,
[h1, m1, h2, m2] => match (checked_hms(*h1, *m1, 0), checked_hms(*h2, *m2, 59)) {
(Some(lo), Some(hi)) => time::in_cyclic_range(seconds, lo, hi),
_ => false,
},
[h1, m1, s1, h2, m2, s2] => {
match (checked_hms(*h1, *m1, *s1), checked_hms(*h2, *m2, *s2)) {
(Some(lo), Some(hi)) => time::in_cyclic_range(seconds, lo, hi),
_ => false,
}
}
_ => false,
}
}
fn checked_hms(hour: i64, minute: i64, second: i64) -> Option<i64> {
hour.checked_mul(3600)?
.checked_add(minute.checked_mul(60)?)?
.checked_add(second)
}
#[cfg(test)]
mod tests {
use std::time::{Duration, UNIX_EPOCH};
use super::*;
fn pinned() -> PacPolicy {
PacPolicy::new().with_now(UNIX_EPOCH + Duration::from_secs(1_709_214_307))
}
#[test]
fn plain_host_names_and_domains() {
assert!(is_plain_host_name("intranet"));
assert!(!is_plain_host_name("intranet.corp.example"));
assert!(!is_plain_host_name("::1"));
assert!(!is_plain_host_name("[::1]"));
assert!(!is_plain_host_name("2001:db8::1"));
assert!(!is_plain_host_name("10.0.0.1"));
assert!(dns_domain_is("www.corp.example", ".corp.example"));
assert!(dns_domain_is("WWW.Corp.Example", ".corp.example"));
assert!(!dns_domain_is("www.other.example", ".corp.example"));
assert!(dns_domain_is("evilcorp.example", "corp.example"));
assert!(dns_domain_is("corp.example", "corp.example"));
assert!(!dns_domain_is(
"www.corp.example.attacker.test",
".corp.example"
));
assert!(local_host_or_domain_is(
"www.corp.example",
"www.corp.example"
));
assert!(local_host_or_domain_is("www", "www.corp.example"));
assert!(!local_host_or_domain_is(
"home.corp.example",
"www.corp.example"
));
assert!(!local_host_or_domain_is("ww", "www.corp.example"));
assert!(local_host_or_domain_is("WWW", "www.corp.example"));
assert!(local_host_or_domain_is(
"WWW.Corp.Example",
"www.corp.example"
));
assert!(!local_host_or_domain_is(
"www.corp.example",
"www.corp.example.attacker.test"
));
assert_eq!(dns_domain_levels("host"), 0);
assert_eq!(dns_domain_levels("host.corp.example"), 2);
}
#[test]
fn glob_matching_handles_star_and_question_mark() {
assert!(sh_exp_match(
"http://home.example/people/x.html",
"*/people/*"
));
assert!(!sh_exp_match("http://home.example/x.html", "*/people/*"));
assert!(sh_exp_match("abc", "a?c"));
assert!(!sh_exp_match("ac", "a?c"));
assert!(sh_exp_match("aXbYc", "a?b?c"));
assert!(sh_exp_match("anything", "*"));
assert!(sh_exp_match("", "*"));
assert!(!sh_exp_match("", "?"));
assert!(sh_exp_match("a.b.c", "a.*.?"));
assert!(sh_exp_match("a\nb", "a*b"));
assert!(sh_exp_match("a\nb", "a?b"));
assert!(sh_exp_match("ab", "a?*"));
assert!(sh_exp_match("ab", "a?**"));
assert!(!sh_exp_match("ab", "a?*c"));
assert!(sh_exp_match("é", "?"));
assert!(!sh_exp_match("é", "??"));
assert!(sh_exp_match("aébéc", "*é?"));
}
#[test]
fn a_star_in_the_subject_does_not_consume_the_pattern_s_star() {
assert!(sh_exp_match("*ab", "*?"));
assert!(sh_exp_match("http://home.example/a*b/x.html", "*a*?"));
assert!(sh_exp_match("x*ab", "x*?"));
assert!(sh_exp_match("*ab", "*b"));
assert!(!sh_exp_match("*", "*??"));
}
#[test]
fn a_pathological_pattern_does_not_stall_the_matcher() {
let text = "a".repeat(200_000);
let pattern = format!("*{}?b", "a".repeat(100_000));
let start = std::time::Instant::now();
assert!(!sh_exp_match(&text, &pattern));
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(20),
"matching took {elapsed:?}"
);
}
#[test]
fn an_ordinary_pattern_is_not_cut_short_by_the_step_budget() {
let subject = format!("{}.corp.example.com", "a".repeat(8 * 1024));
let pattern = format!("*{}?*.corp.example.com", "a".repeat(180));
assert!(
pattern.chars().count() >= 200,
"the pattern has to be the size the budget was chosen against"
);
assert!(sh_exp_match(&subject, &pattern));
}
#[test]
fn addresses_convert_to_integers() {
assert_eq!(convert_addr("0.0.0.0"), 0);
assert_eq!(convert_addr("127.0.0.1"), 0x7f00_0001);
assert_eq!(convert_addr("nonsense"), 0);
assert_eq!(convert_addr("1.2"), 0x0102_0000);
assert_eq!(convert_addr("1.2.3.4.5"), 0x0102_0304);
assert_eq!(convert_addr("127.0.0. 1"), 0x7f00_0001);
}
#[test]
fn the_high_bit_makes_the_result_negative_as_in_the_reference() {
assert_eq!(convert_addr("255.255.255.255"), -1);
assert_eq!(convert_addr("128.0.0.0"), i32::MIN);
assert_eq!(convert_addr("192.168.0.1"), 0xc0a8_0001_u32 as i32);
assert!(
convert_addr("200.0.0.0") < convert_addr("10.0.0.0"),
"the browser ordering is what a PAC file was written against"
);
}
#[test]
fn the_default_policy_makes_the_network_functions_inert() {
let policy = PacPolicy::new();
assert_eq!(dns_resolve("example.com", &policy), None);
assert!(!is_resolvable("example.com", &policy));
assert!(!is_in_net("example.com", "10.0.0.0", "255.0.0.0", &policy));
assert_eq!(my_ip_address(&policy), IpAddr::V4(Ipv4Addr::LOCALHOST));
}
#[test]
fn literals_never_need_the_resolver() {
let policy = PacPolicy::new();
assert_eq!(
dns_resolve("10.1.2.3", &policy),
Some(Ipv4Addr::new(10, 1, 2, 3))
);
assert!(is_resolvable("10.1.2.3", &policy));
assert!(is_resolvable(" ::1 ", &policy));
assert!(is_resolvable(" [::1] ", &policy));
assert!(is_in_net("10.1.2.3", "10.0.0.0", "255.0.0.0", &policy));
assert!(!is_in_net("11.1.2.3", "10.0.0.0", "255.0.0.0", &policy));
assert!(!is_in_net("10.1.2.3", "10.0.0.0", "not-a-mask", &policy));
}
#[test]
fn dns_resolve_reads_a_literal_the_way_a_resolver_does() {
let policy = PacPolicy::new();
assert_eq!(
dns_resolve("010.1.2.3", &policy),
Some(Ipv4Addr::new(8, 1, 2, 3)),
"a leading zero is octal"
);
assert_eq!(
dns_resolve("0x7f.0.0.1", &policy),
Some(Ipv4Addr::LOCALHOST)
);
assert_eq!(
dns_resolve("123", &policy),
Some(Ipv4Addr::new(0, 0, 0, 123)),
"a short form fills from the right"
);
assert!(is_resolvable("192.168.001.007", &policy));
assert!(is_in_net(
"010.1.2.3",
"10.1.2.3",
"255.255.255.255",
&policy
));
assert!(!is_in_net(
"010.1.2.3",
"8.1.2.3",
"255.255.255.255",
&policy
));
assert_eq!(dns_resolve("example.com", &policy), None);
assert!(!is_resolvable("1.2.3.4.5", &policy));
assert_eq!(dns_resolve("256.0.0.1", &policy), None);
}
#[test]
fn is_in_net_reads_addresses_with_the_reference_grammar() {
let policy = PacPolicy::new();
let mask = "255.255.255.000";
assert!(is_in_net(
"192.168.001.007",
"192.168.001.000",
mask,
&policy
));
assert!(!is_in_net(
"192.168.002.007",
"192.168.001.000",
mask,
&policy
));
assert!(is_in_net(
"192.168.1.7",
"192.168.001.000",
"255.255.255.0",
&policy
));
assert!(!is_in_net("10.1.2.3", " 10.0.0.0", "255.0.0.0", &policy));
assert!(!is_in_net("10.1.2.3", "10.0.0.0", "255.0.0.0 ", &policy));
assert!(!is_in_net("10.1.2.3", "0010.0.0.0", "255.0.0.0", &policy));
assert!(!is_in_net("10.1.2.3", "10.0.0.0.9", "255.0.0.0", &policy));
assert!(!is_in_net("10.1.2.3", "10.0.0.0", "255.0.0.0.9", &policy));
assert!(!is_in_net("10.1.2.3", "256.0.0.0", "255.0.0.0", &policy));
assert!(!is_in_net("10.1.2.3", "10.0.0.0", "255.0.0.256", &policy));
}
#[test]
fn bracketed_ipv6_literals_count_as_resolvable() {
let policy = PacPolicy::new();
assert_eq!(dns_resolve("[::1]", &policy), None);
assert!(is_resolvable("[::1]", &policy));
assert!(is_resolvable("[2001:db8::1]", &policy));
}
#[test]
fn only_an_ipv6_literal_loses_its_brackets() {
let policy = PacPolicy::new();
assert_eq!(dns_resolve("[10.1.2.3]", &policy), None);
assert!(!is_resolvable("[10.1.2.3]", &policy));
assert!(!is_resolvable("[example.com]", &policy));
assert_eq!(
dns_resolve("10.1.2.3", &policy),
Some(Ipv4Addr::new(10, 1, 2, 3))
);
assert!(!is_resolvable("[::1", &policy));
assert!(!is_resolvable("::1]", &policy));
}
#[test]
fn internal_ranges_are_recognised() {
for internal in [
"127.0.0.1",
"10.0.0.1",
"172.16.0.1",
"192.168.1.1",
"169.254.1.1",
"100.64.0.1",
"100.127.255.255",
"198.18.0.1",
"198.19.255.255",
"0.0.0.0",
"0.1.2.3",
"0.255.255.255",
"240.0.0.1",
"255.255.255.255",
"224.0.0.1",
"192.0.0.1",
"192.0.2.1",
"198.51.100.1",
"203.0.113.1",
"192.88.99.1",
"::1",
"::",
"ff02::1",
"fe80::1",
"fd00::1",
"fc00::1",
"::ffff:10.0.0.1",
] {
assert!(is_internal(internal.parse().unwrap()), "{internal}");
}
for external in [
"8.8.8.8",
"93.184.216.34",
"192.0.1.1",
"192.0.3.1",
"192.88.100.1",
"198.51.101.1",
"203.0.114.1",
"223.255.255.254",
"100.128.0.1",
"198.20.0.1",
"1.0.0.1",
"2606:2800:220:1::1",
"fe00::1",
] {
assert!(!is_internal(external.parse().unwrap()), "{external}");
}
}
#[test]
fn allowing_internal_addresses_admits_the_mapped_spelling_and_not_the_native_one() {
let allowed = PacPolicy::new()
.with_dns_resolution(true)
.with_internal_addresses(true);
let denied = PacPolicy::new().with_dns_resolution(true);
let mapped = "::ffff:10.0.0.1";
assert_eq!(
resolve_ipv4(mapped, &allowed),
Some(Ipv4Addr::new(10, 0, 0, 1))
);
assert_eq!(resolve_ipv4(mapped, &denied), None);
assert_eq!(resolve_ipv4("fd00::1", &allowed), None);
assert_eq!(resolve_ipv4("fd00::1", &denied), None);
}
fn strings(args: &[&str]) -> Vec<String> {
args.iter().map(|arg| (*arg).to_owned()).collect()
}
#[test]
fn weekday_ranges() {
let policy = pinned(); assert!(weekday_range(&strings(&["THU"]), &policy));
assert!(!weekday_range(&strings(&["FRI"]), &policy));
assert!(weekday_range(&strings(&["MON", "FRI"]), &policy));
assert!(weekday_range(&strings(&["MON", "FRI", "GMT"]), &policy));
assert!(!weekday_range(&strings(&["SAT", "SUN"]), &policy));
assert!(!weekday_range(&strings(&["FRI", "MON"]), &policy));
assert!(!weekday_range(&strings(&["NOPE"]), &policy));
assert!(!weekday_range(&[], &policy));
assert!(!weekday_range(&strings(&["MON", "FRI", "THU"]), &policy));
}
#[test]
fn date_ranges() {
let policy = pinned(); assert!(date_range(&strings(&["29"]), &policy));
assert!(!date_range(&strings(&["28"]), &policy));
assert!(date_range(&strings(&["FEB"]), &policy));
assert!(date_range(&strings(&["2024"]), &policy));
assert!(date_range(&strings(&["JAN", "MAR"]), &policy));
assert!(date_range(&strings(&["1", "31"]), &policy));
assert!(date_range(&strings(&["2020", "2030"]), &policy));
assert!(date_range(&strings(&["NOV", "FEB"]), &policy));
assert!(!date_range(&strings(&["NOV", "JAN"]), &policy));
assert!(date_range(&strings(&["25", "5"]), &policy));
assert!(!date_range(&strings(&["1", "28"]), &policy));
assert!(date_range(&strings(&["1", "FEB", "5", "MAR"]), &policy));
assert!(!date_range(&strings(&["1", "MAR", "5", "APR"]), &policy));
assert!(date_range(
&strings(&["JAN", "2024", "DEC", "2024"]),
&policy
));
assert!(date_range(
&strings(&["1", "JAN", "2024", "31", "DEC", "2024"]),
&policy
));
assert!(!date_range(
&strings(&["1", "JAN", "2025", "31", "DEC", "2025"]),
&policy
));
assert!(date_range(
&strings(&["NOV", "2023", "MAR", "2024"]),
&policy
));
assert!(!date_range(
&strings(&["NOV", "2024", "MAR", "2025"]),
&policy
));
assert!(date_range(
&strings(&["1", "NOV", "2023", "1", "MAR", "2024"]),
&policy
));
assert!(!date_range(
&strings(&["1", "NOV", "2024", "1", "MAR", "2025"]),
&policy
));
assert!(!date_range(&strings(&["bogus"]), &policy));
assert!(!date_range(&strings(&["1", "2", "3"]), &policy));
assert!(!date_range(&strings(&["99", "2024"]), &policy));
assert!(!date_range(&strings(&["999", "2024"]), &policy));
}
#[test]
fn a_range_ending_in_a_short_month_does_not_spill_into_the_next_one() {
let march = PacPolicy::new().with_now(UNIX_EPOCH + Duration::from_secs(1_709_337_600));
assert!(!date_range(&strings(&["JAN", "FEB"]), &march));
assert!(date_range(&strings(&["JAN", "FEB"]), &pinned()));
}
#[test]
fn date_range_overflowing_years_do_not_panic() {
let policy = pinned();
assert!(!date_range(
&strings(&[
"1",
"JAN",
"999999999999999999",
"31",
"DEC",
"999999999999999999"
]),
&policy
));
assert!(!date_range(
&strings(&["JAN", "999999999999999999", "DEC", "999999999999999999"]),
&policy
));
}
#[test]
fn time_ranges() {
let policy = pinned(); assert!(time_range(&strings(&["13"]), &policy));
assert!(!time_range(&strings(&["14"]), &policy));
assert!(time_range(&strings(&["8", "17"]), &policy));
assert!(!time_range(&strings(&["18", "20"]), &policy));
assert!(time_range(&strings(&["13", "0", "13", "59"]), &policy));
assert!(!time_range(&strings(&["13", "0", "13", "30"]), &policy));
assert!(time_range(&strings(&["13", "0", "13", "45"]), &policy));
assert!(time_range(
&strings(&["13", "45", "0", "13", "45", "10"]),
&policy
));
assert!(!time_range(
&strings(&["13", "45", "0", "13", "45", "5"]),
&policy
));
assert!(!time_range(&strings(&["22", "0", "6", "0"]), &policy));
assert!(time_range(&strings(&["12", "0", "6", "0"]), &policy));
assert!(!time_range(&strings(&["nope"]), &policy));
assert!(!time_range(&strings(&["13", "0", "14"]), &policy));
assert!(!time_range(&strings(&["13", "0", "0", "14", "0"]), &policy));
}
#[test]
fn time_range_two_argument_form_never_wraps_midnight() {
let policy = PacPolicy::new().with_now(UNIX_EPOCH + Duration::from_secs(1_709_249_400));
assert!(!time_range(&strings(&["22", "6"]), &policy));
assert!(time_range(&strings(&["22", "0", "6", "0"]), &policy));
}
#[test]
fn time_range_overflowing_hours_do_not_panic() {
let policy = pinned();
assert!(!time_range(
&strings(&["999999999999999999", "0", "999999999999999999", "0"]),
&policy
));
assert!(!time_range(
&strings(&[
"999999999999999999",
"0",
"0",
"999999999999999999",
"0",
"0"
]),
&policy
));
}
#[test]
fn the_local_offset_shifts_the_clock() {
let policy = pinned().with_local_utc_offset(9 * 3600);
assert!(time_range(&strings(&["22"]), &policy));
assert!(time_range(&strings(&["13", "GMT"]), &policy));
}
#[test]
fn the_default_policy_reads_the_clock_as_gmt() {
let policy = pinned();
assert_eq!(
time_range(&strings(&["13"]), &policy),
time_range(&strings(&["13", "GMT"]), &policy)
);
assert!(time_range(&strings(&["13", "GMT"]), &policy));
}
#[test]
fn alert_never_fails_whatever_it_is_handed() {
alert("this goes nowhere");
alert("newlines\nand\rcontrol\u{7}characters");
alert(&"x".repeat(100_000));
}
}