use std::path::Path;
use crate::engine_contract::{EntryKind, Error, Result};
use crate::query::query_glob::Pattern;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SizeMetric {
Apparent,
Allocated,
}
impl SizeMetric {
pub const fn label(self) -> &'static str {
match self {
Self::Apparent => "apparent",
Self::Allocated => "allocated",
}
}
pub(crate) const fn of(self, attrs: &crate::Attrs) -> u64 {
match self {
Self::Apparent => attrs.size,
Self::Allocated => attrs.allocated,
}
}
}
impl Default for SizeMetric {
fn default() -> Self {
crate::query::Request::DEFAULTS.size
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum SortKey {
#[default]
Size,
Count,
Mtime,
Name,
Metric(&'static str),
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ModifiedWindow {
pub since: Option<i64>,
pub before: Option<i64>,
}
impl ModifiedWindow {
pub fn contains(&self, mtime_ns: i64) -> bool {
self.since.is_none_or(|since| mtime_ns >= since)
&& self.before.is_none_or(|before| mtime_ns < before)
}
pub fn is_unbounded(&self) -> bool {
self.since.is_none() && self.before.is_none()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Hash)]
pub enum IgnoredEntries {
#[default]
Include,
Exclude,
Only,
}
impl IgnoredEntries {
pub const fn admits(self, ignored: bool) -> bool {
match self {
Self::Include => true,
Self::Exclude => !ignored,
Self::Only => ignored,
}
}
pub fn parse(value: &str) -> std::result::Result<Self, String> {
match value.trim().to_ascii_lowercase().as_str() {
"include" => Ok(Self::Include),
"exclude" => Ok(Self::Exclude),
"only" => Ok(Self::Only),
_ => Err("expected one of include, exclude, only".to_string()),
}
}
pub const fn label(self) -> &'static str {
match self {
Self::Include => "include",
Self::Exclude => "exclude",
Self::Only => "only",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Bound {
#[default]
All,
Limit(usize),
}
impl Bound {
pub fn admits(self, index: usize) -> bool {
match self {
Self::All => true,
Self::Limit(limit) => index < limit,
}
}
pub fn limit(self) -> Option<usize> {
match self {
Self::All => None,
Self::Limit(limit) => Some(limit),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShareThreshold {
whole: u8,
fractional: Vec<u8>,
}
impl ShareThreshold {
pub fn one_percent() -> Self {
Self { whole: 1, fractional: Vec::new() }
}
pub fn parse(value: &str) -> Option<Self> {
let digits = value.trim().strip_suffix('%')?;
let (whole, fraction) = digits.split_once('.').unwrap_or((digits, ""));
if whole.is_empty()
|| !whole.bytes().all(|byte| byte.is_ascii_digit())
|| !fraction.bytes().all(|byte| byte.is_ascii_digit())
|| (digits.contains('.') && fraction.is_empty())
{
return None;
}
let whole: u8 = whole.parse().ok()?;
if whole > 100 || (whole == 100 && fraction.bytes().any(|digit| digit != b'0')) {
return None;
}
Some(Self { whole, fractional: fraction.bytes().map(|digit| digit - b'0').collect() })
}
pub fn admits(&self, part: u64, whole: u64) -> bool {
if self.whole == 0 && self.fractional.iter().all(|digit| *digit == 0) {
return true;
}
if whole == 0 {
return false;
}
let numerator = u128::from(part) * 100;
let denominator = u128::from(whole);
let integral = numerator / denominator;
if integral != u128::from(self.whole) {
return integral > u128::from(self.whole);
}
let mut remainder = numerator % denominator;
for digit in &self.fractional {
remainder *= 10;
let actual = remainder / denominator;
if actual != u128::from(*digit) {
return actual > u128::from(*digit);
}
remainder %= denominator;
}
true
}
pub(crate) fn admitted_parts_bound(&self) -> Option<u64> {
const PLACES: usize = 18;
let places = self.fractional.len().min(PLACES);
let scale = 10_u128.pow(u32::try_from(places).expect("at most 18 places"));
let spelled = self.fractional[..places]
.iter()
.fold(u128::from(self.whole), |value, digit| value * 10 + u128::from(*digit));
if spelled == 0 {
return None;
}
u64::try_from((100 * scale).div_ceil(spelled)).ok()
}
pub fn label(&self) -> String {
let mut out = self.whole.to_string();
if !self.fractional.is_empty() {
out.push('.');
for digit in &self.fractional {
out.push(char::from(b'0' + *digit));
}
}
out.push('%');
out
}
}
#[derive(Clone, Debug, Default)]
pub struct Selection {
pub include: Vec<Pattern>,
pub exclude: Vec<Pattern>,
pub min_size: Option<u64>,
pub kinds: Vec<EntryKind>,
pub modified: ModifiedWindow,
pub ignored: IgnoredEntries,
pub depth: Option<Bound>,
pub min_share: Option<ShareThreshold>,
pub breadth: Option<Bound>,
pub limit: Option<Bound>,
pub sort: Option<SortKey>,
pub reverse: bool,
pub size: SizeMetric,
}
#[derive(Clone, Copy, Debug)]
pub struct Candidate<'a> {
pub relative: &'a Path,
pub name: &'a str,
pub kind: EntryKind,
pub bytes: u64,
pub allocated: u64,
pub mtime_ns: i64,
pub ignored: bool,
}
#[derive(Clone, Debug, Default)]
pub struct EntrySelection {
pub query: Selection,
pub max_size: Option<u64>,
pub exclude_ignored: bool,
pub logical_extensions: Vec<String>,
pub exact_names: Vec<String>,
pub terminal_extensions: Vec<String>,
pub ancestor_names: Vec<String>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum NameIdentity {
Native,
Portable,
}
impl From<Selection> for EntrySelection {
fn from(query: Selection) -> Self {
Self { query, ..Self::default() }
}
}
impl Selection {
pub(crate) fn retained_heap_bytes(&self) -> usize {
let pattern_bytes =
self.include.iter().chain(&self.exclude).fold(0_usize, |total, pattern| {
total.saturating_add(pattern.retained_heap_bytes())
});
self.include
.capacity()
.saturating_add(self.exclude.capacity())
.saturating_mul(std::mem::size_of::<Pattern>())
.saturating_add(pattern_bytes)
.saturating_add(self.kinds.capacity().saturating_mul(std::mem::size_of::<EntryKind>()))
}
pub fn is_unfiltered(&self) -> bool {
self.include.is_empty()
&& self.exclude.is_empty()
&& self.min_size.is_none()
&& self.kinds.is_empty()
&& self.modified.is_unbounded()
&& self.ignored == IgnoredEntries::Include
}
pub fn admits(&self, candidate: &Candidate<'_>) -> bool {
if !self.kinds.is_empty() && !self.kinds.contains(&candidate.kind) {
return false;
}
if !self.ignored.admits(candidate.ignored) {
return false;
}
if let Some(min_size) = self.min_size {
if self.size_of(candidate) < min_size {
return false;
}
}
if !self.modified.contains(candidate.mtime_ns) {
return false;
}
if self.exclude.iter().any(|p| p.matches(candidate.relative, candidate.name)) {
return false;
}
if self.include.is_empty() {
return true;
}
self.include.iter().any(|p| p.matches(candidate.relative, candidate.name))
}
pub fn size_of(&self, candidate: &Candidate<'_>) -> u64 {
match self.size {
SizeMetric::Apparent => candidate.bytes,
SizeMetric::Allocated => candidate.allocated,
}
}
}
impl EntrySelection {
pub fn admit_terminal_extension(&mut self, value: impl Into<String>) -> Result<()> {
let value = value.into();
if self.terminal_extensions.contains(&value) {
return Err(refusal(TERMINAL_KIND, &value, TERMINAL_UNIQUE));
}
check_value(TERMINAL_KIND, &value, TERMINAL_RULES)?;
self.terminal_extensions.push(value);
Ok(())
}
pub fn admit_ancestor_name(&mut self, value: impl Into<String>) -> Result<()> {
let value = value.into();
if self.ancestor_names.contains(&value) {
return Err(refusal(ANCESTOR_KIND, &value, ANCESTOR_UNIQUE));
}
check_value(ANCESTOR_KIND, &value, ANCESTOR_RULES)?;
self.ancestor_names.push(value);
Ok(())
}
pub fn validate(&self) -> Result<()> {
check_list(TERMINAL_KIND, &self.terminal_extensions, TERMINAL_UNIQUE, TERMINAL_RULES)?;
check_list(ANCESTOR_KIND, &self.ancestor_names, ANCESTOR_UNIQUE, ANCESTOR_RULES)
}
pub(crate) fn retained_heap_bytes(&self) -> usize {
self.query
.retained_heap_bytes()
.saturating_add(retained_strings(
&self.logical_extensions,
self.logical_extensions.capacity(),
))
.saturating_add(retained_strings(&self.exact_names, self.exact_names.capacity()))
.saturating_add(retained_strings(
&self.terminal_extensions,
self.terminal_extensions.capacity(),
))
.saturating_add(retained_strings(&self.ancestor_names, self.ancestor_names.capacity()))
}
pub fn is_unfiltered(&self) -> bool {
self.query.is_unfiltered()
&& self.max_size.is_none()
&& !self.exclude_ignored
&& self.logical_extensions.is_empty()
&& self.exact_names.is_empty()
&& self.terminal_extensions.is_empty()
&& self.ancestor_names.is_empty()
}
pub fn admits(&self, candidate: &Candidate<'_>) -> bool {
if !self.query.admits(candidate) {
return false;
}
if let Some(max_size) = self.max_size {
if self.query.size_of(candidate) > max_size {
return false;
}
}
if self.exclude_ignored && candidate.ignored {
return false;
}
if !self.logical_extensions.is_empty() || !self.exact_names.is_empty() {
if candidate.kind != EntryKind::File {
return false;
}
let extension_matches = crate::classify::logical_ext(candidate.name.as_ref())
.is_some_and(|extension| {
self.logical_extensions
.iter()
.any(|expected| extension.eq_ignore_ascii_case(expected))
});
let name_matches = self
.exact_names
.iter()
.any(|expected| candidate.name.eq_ignore_ascii_case(expected));
if !extension_matches && !name_matches {
return false;
}
}
if !self.terminal_extensions.is_empty() {
if candidate.kind != EntryKind::File {
return false;
}
let Some(suffix) = terminal_suffix(candidate.name) else {
return false;
};
if !self
.terminal_extensions
.iter()
.any(|expected| suffix.eq_ignore_ascii_case(expected))
{
return false;
}
}
if !self.ancestor_names.is_empty()
&& !candidate.relative.parent().is_some_and(|parent| {
parent.components().any(|component| {
let std::path::Component::Normal(name) = component else {
return false;
};
self.ancestor_names.iter().any(|expected| name == expected.as_str())
})
})
{
return false;
}
true
}
}
const TERMINAL_KIND: &str = "terminal extension";
const ANCESTOR_KIND: &str = "ancestor name";
const TERMINAL_UNIQUE: &str = "terminal_extensions entries must be unique";
const ANCESTOR_UNIQUE: &str = "ancestor_names entries must be unique";
type Rule = (fn(&str) -> bool, &'static str);
const TERMINAL_RULES: &[Rule] = &[
(undotted, "terminal_extensions entries must start with a dot"),
(not_lowercase, "terminal_extensions entries must be lowercase"),
(not_terminal_suffix, "terminal_extensions entries must be canonical terminal suffixes"),
];
const ANCESTOR_RULES: &[Rule] =
&[(not_path_component, "ancestor_names entries must be exact path-component names")];
fn undotted(value: &str) -> bool {
!value.starts_with('.')
}
fn not_lowercase(value: &str) -> bool {
value.to_lowercase() != value
}
fn not_terminal_suffix(value: &str) -> bool {
value.chars().count() < 2 || value.contains(['/', '\\']) || value[1..].contains('.')
}
fn not_path_component(value: &str) -> bool {
value.is_empty() || value == "." || value == ".." || value.contains(['/', '\\'])
}
fn refusal(kind: &'static str, value: &str, hint: &str) -> Error {
Error::InvalidValue { kind, value: value.to_owned(), hint: hint.to_owned() }
}
fn check_value(kind: &'static str, value: &str, rules: &[Rule]) -> Result<()> {
match rules.iter().find(|(breaks, _)| breaks(value)) {
Some((_, hint)) => Err(refusal(kind, value, hint)),
None => Ok(()),
}
}
fn check_list(kind: &'static str, values: &[String], unique: &str, rules: &[Rule]) -> Result<()> {
let mut seen = std::collections::HashSet::with_capacity(values.len());
if let Some(repeated) = values.iter().find(|value| !seen.insert(value.as_str())) {
return Err(refusal(kind, repeated, unique));
}
for (breaks, hint) in rules {
if let Some(value) = values.iter().find(|value| breaks(value)) {
return Err(refusal(kind, value, hint));
}
}
Ok(())
}
fn retained_strings(values: &[String], capacity: usize) -> usize {
capacity.saturating_mul(std::mem::size_of::<String>()).saturating_add(
values.iter().fold(0_usize, |total, value| total.saturating_add(value.capacity())),
)
}
fn terminal_suffix(name: &str) -> Option<&str> {
let dot = name.rfind('.')?;
(dot > 0 && dot + 1 < name.len()).then_some(&name[dot..])
}
#[cfg(test)]
mod tests {
#[test]
fn decimal_shares_compare_exactly_at_and_below_a_boundary() {
use super::ShareThreshold;
let share = ShareThreshold::parse("1%").expect("percentage");
assert!(share.admits(1, 100));
assert!(!share.admits(1, 101));
assert!(share.admits(u64::MAX / 100, u64::MAX / 100));
assert!(!share.admits(0, 0));
assert!(ShareThreshold::parse("0%").expect("zero").admits(0, 0));
assert!(
ShareThreshold::parse("0.0000000000000000001%")
.expect("fine precision")
.admits(1, u64::MAX)
);
for invalid in ["-1%", "101%", "100.1%", "NaN%", "1", "1.%", "1e1%"] {
assert!(ShareThreshold::parse(invalid).is_none(), "{invalid}");
}
}
#[test]
fn no_partition_admits_more_parts_than_the_share_bounds() {
use super::ShareThreshold;
fn partitions(whole: u64, largest: u64, prefix: &mut Vec<u64>, out: &mut Vec<Vec<u64>>) {
out.push(prefix.clone());
if prefix.len() == 12 {
return;
}
for part in (0..=largest.min(whole)).rev() {
prefix.push(part);
partitions(whole - part, part, prefix, out);
prefix.pop();
}
}
let bound =
|share: &str| ShareThreshold::parse(share).expect("percentage").admitted_parts_bound();
for (share, expected) in [
("100%", Some(1)),
("10%", Some(10)),
("3%", Some(34)),
("1%", Some(100)),
("1.0%", Some(100)),
("0.5%", Some(200)),
("0.01%", Some(10_000)),
("0.00152587890625%", Some(65_536)),
("0.0015258%", Some(65_540)),
("0%", None),
("0.000%", None),
("0.0000000000000000001%", None),
] {
assert_eq!(bound(share), expected, "{share}");
}
assert_eq!(bound("0.0010000000000000000001%"), Some(100_000));
for share in ["100%", "50%", "33%", "33.4%", "25%", "12.5%", "10%", "9.99%"] {
let threshold = ShareThreshold::parse(share).expect("percentage");
let bound = threshold.admitted_parts_bound().expect("a positive share bounds parts");
for whole in 0..=12 {
let mut all = Vec::new();
partitions(whole, whole, &mut Vec::new(), &mut all);
for parts in all {
let total = parts.iter().sum::<u64>();
let admitted =
parts.iter().filter(|part| threshold.admits(**part, total)).count();
assert!(
u64::try_from(admitted).expect("small") <= bound,
"{share}: {parts:?} admits {admitted} > {bound}"
);
}
}
}
}
use super::*;
use std::path::PathBuf;
fn candidate(path: &str, kind: EntryKind, bytes: u64, mtime_ns: i64) -> (PathBuf, String) {
let relative = PathBuf::from(path);
let name = relative
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
let _ = (kind, bytes, mtime_ns);
(relative, name)
}
fn entry_admits(
selection: &EntrySelection,
path: &str,
kind: EntryKind,
bytes: u64,
mtime: i64,
ignored: bool,
) -> bool {
let (relative, name) = candidate(path, kind, bytes, mtime);
selection.admits(&Candidate {
relative: &relative,
name: &name,
kind,
bytes,
allocated: bytes.div_ceil(512) * 512,
mtime_ns: mtime,
ignored,
})
}
fn admits(selection: &Selection, path: &str, kind: EntryKind, bytes: u64, mtime: i64) -> bool {
classified_admits(selection, path, kind, bytes, mtime, false)
}
fn classified_admits(
selection: &Selection,
path: &str,
kind: EntryKind,
bytes: u64,
mtime: i64,
ignored: bool,
) -> bool {
let (relative, name) = candidate(path, kind, bytes, mtime);
selection.admits(&Candidate {
relative: &relative,
name: &name,
kind,
bytes,
allocated: bytes.div_ceil(512) * 512,
mtime_ns: mtime,
ignored,
})
}
fn pattern(source: &str) -> Pattern {
Pattern::parse(source).expect("pattern compiles")
}
#[test]
fn a_default_selection_admits_everything_and_reads_the_fast_tier() {
let selection = Selection::default();
assert!(selection.is_unfiltered());
assert!(admits(&selection, "src/main.rs", EntryKind::File, 10, 5));
assert!(admits(&selection, "src", EntryKind::Dir, 0, 5));
}
#[test]
fn include_patterns_narrow_and_exclude_patterns_win() {
let mut selection = Selection { include: vec![pattern("*.rs")], ..Selection::default() };
assert!(!selection.is_unfiltered());
assert!(admits(&selection, "src/main.rs", EntryKind::File, 10, 5));
assert!(!admits(&selection, "src/main.toml", EntryKind::File, 10, 5));
selection.exclude.push(pattern("**/generated/**"));
assert!(!admits(&selection, "src/generated/api.rs", EntryKind::File, 10, 5));
assert!(admits(&selection, "src/hand/api.rs", EntryKind::File, 10, 5));
}
#[test]
fn min_size_follows_the_selected_metric() {
let apparent = Selection { min_size: Some(600), ..Selection::default() };
assert!(!admits(&apparent, "a.bin", EntryKind::File, 100, 0));
let allocated =
Selection { min_size: Some(600), size: SizeMetric::Allocated, ..Selection::default() };
assert!(admits(&allocated, "a.bin", EntryKind::File, 600, 0));
assert!(!admits(&allocated, "b.bin", EntryKind::File, 100, 0));
}
#[test]
fn the_modified_window_is_half_open() {
let selection = Selection {
modified: ModifiedWindow { since: Some(100), before: Some(200) },
..Selection::default()
};
assert!(admits(&selection, "a", EntryKind::File, 1, 100));
assert!(admits(&selection, "b", EntryKind::File, 1, 150));
assert!(!admits(&selection, "c", EntryKind::File, 1, 200));
assert!(!admits(&selection, "d", EntryKind::File, 1, 99));
}
#[test]
fn kinds_filter_and_an_empty_list_means_every_kind() {
let files = Selection { kinds: vec![EntryKind::File], ..Selection::default() };
assert!(admits(&files, "a.rs", EntryKind::File, 1, 0));
assert!(!admits(&files, "src", EntryKind::Dir, 0, 0));
let both =
Selection { kinds: vec![EntryKind::File, EntryKind::Dir], ..Selection::default() };
assert!(admits(&both, "src", EntryKind::Dir, 0, 0));
assert!(!admits(&both, "link", EntryKind::Symlink, 0, 0));
}
#[test]
fn ignored_entries_select_one_partition_or_both() {
let include = Selection::default();
let exclude = Selection { ignored: IgnoredEntries::Exclude, ..Selection::default() };
let only = Selection { ignored: IgnoredEntries::Only, ..Selection::default() };
assert!(!exclude.is_unfiltered() && !only.is_unfiltered());
for (selection, admits_unignored, admits_ignored) in
[(&include, true, true), (&exclude, true, false), (&only, false, true)]
{
assert_eq!(
classified_admits(selection, "src/lib.rs", EntryKind::File, 1, 0, false),
admits_unignored,
"{:?} on an unignored entry",
selection.ignored
);
assert_eq!(
classified_admits(selection, "dist", EntryKind::Dir, 0, 0, true),
admits_ignored,
"{:?} on an ignored entry",
selection.ignored
);
}
for mode in [IgnoredEntries::Include, IgnoredEntries::Exclude, IgnoredEntries::Only] {
assert_eq!(IgnoredEntries::parse(mode.label()), Ok(mode));
}
assert_eq!(
IgnoredEntries::parse("some"),
Err("expected one of include, exclude, only".to_string())
);
}
#[test]
fn portable_catalog_predicates_compose_without_client_side_filtering() {
let selection = EntrySelection {
query: Selection { size: SizeMetric::Apparent, ..Selection::default() },
max_size: Some(10),
exclude_ignored: true,
terminal_extensions: vec![".rs".to_string(), ".md".to_string()],
ancestor_names: vec!["src".to_string(), "docs".to_string()],
..EntrySelection::default()
};
assert!(entry_admits(&selection, "src/lib.rs", EntryKind::File, 10, 0, false));
assert!(entry_admits(&selection, "docs/readme.md", EntryKind::File, 9, 0, false));
assert!(!entry_admits(&selection, "src/lib.RS", EntryKind::File, 11, 0, false));
assert!(!entry_admits(&selection, "tests/lib.rs", EntryKind::File, 9, 0, false));
assert!(!entry_admits(&selection, "src/lib.rs", EntryKind::File, 9, 0, true));
assert!(!entry_admits(&selection, "src/.gitignore", EntryKind::File, 1, 0, false));
}
#[test]
fn logical_extensions_and_exact_names_form_one_name_identity_filter() {
let selection = EntrySelection {
logical_extensions: vec![".v2.zip".to_string()],
exact_names: vec!["makefile".to_string()],
..EntrySelection::default()
};
assert!(entry_admits(&selection, "release.v2.zip", EntryKind::File, 1, 0, false));
assert!(entry_admits(&selection, "Makefile", EntryKind::File, 1, 0, false));
assert!(!entry_admits(&selection, "plain.zip", EntryKind::File, 1, 0, false));
assert!(!entry_admits(&selection, "README", EntryKind::File, 1, 0, false));
}
#[test]
fn terminal_extensions_and_ancestor_names_refuse_what_could_never_match() {
let refused = |hint: &str, outcome: Result<()>| match outcome {
Err(Error::InvalidValue { hint: actual, .. }) => {
assert!(actual.contains(hint), "{actual:?} names {hint:?}");
}
other => panic!("expected a refusal naming {hint:?}, got {other:?}"),
};
for (value, hint) in [
("rs", "start with a dot"),
(".RS", "lowercase"),
(".Ée", "lowercase"),
(".", "canonical terminal suffixes"),
(".tar.gz", "canonical terminal suffixes"),
("..", "canonical terminal suffixes"),
(".a/b", "canonical terminal suffixes"),
(".a\\b", "canonical terminal suffixes"),
] {
let mut selection = EntrySelection::default();
refused(hint, selection.admit_terminal_extension(value));
assert!(selection.terminal_extensions.is_empty(), "{value:?} was not added");
let written = EntrySelection {
terminal_extensions: vec![value.to_string()],
..Default::default()
};
refused(hint, written.validate());
}
for value in ["", ".", "..", "a/b", "a\\b"] {
let mut selection = EntrySelection::default();
refused("exact path-component names", selection.admit_ancestor_name(value));
let written =
EntrySelection { ancestor_names: vec![value.to_string()], ..Default::default() };
refused("exact path-component names", written.validate());
}
let mut selection = EntrySelection::default();
selection.admit_terminal_extension(".rs").expect("a canonical suffix");
selection.admit_terminal_extension(".c++").expect("a non-alphanumeric suffix");
refused("unique", selection.admit_terminal_extension(".rs"));
selection.admit_ancestor_name("src").expect("a component");
selection.admit_ancestor_name("x%FF").expect("an escaped component");
selection.admit_ancestor_name("..foo").expect("dots inside a name");
refused("unique", selection.admit_ancestor_name("src"));
assert_eq!(selection.terminal_extensions, [".rs", ".c++"]);
assert_eq!(selection.ancestor_names, ["src", "x%FF", "..foo"]);
selection.validate().expect("admitted values validate");
refused(
"unique",
EntrySelection {
terminal_extensions: vec![".md".to_string(), ".md".to_string()],
..Default::default()
}
.validate(),
);
refused(
"unique",
EntrySelection {
ancestor_names: vec!["docs".to_string(), "docs".to_string()],
..Default::default()
}
.validate(),
);
}
#[test]
fn a_list_wrong_twice_is_refused_in_the_catalog_query_order() {
let hint_of = |selection: EntrySelection| match selection.validate() {
Err(Error::InvalidValue { hint, .. }) => hint,
other => panic!("expected a refusal, got {other:?}"),
};
let terminal = |values: &[&str]| EntrySelection {
terminal_extensions: values.iter().map(ToString::to_string).collect(),
..Default::default()
};
let ancestors = |values: &[&str]| EntrySelection {
ancestor_names: values.iter().map(ToString::to_string).collect(),
..Default::default()
};
for (selection, hint) in [
(terminal(&["rs", "rs"]), TERMINAL_UNIQUE),
(terminal(&[".RS", ".RS"]), TERMINAL_UNIQUE),
(terminal(&[".RS", "rs"]), "terminal_extensions entries must start with a dot"),
(terminal(&[".tar.gz", ".RS"]), "terminal_extensions entries must be lowercase"),
(ancestors(&["..", ".."]), ANCESTOR_UNIQUE),
(
EntrySelection {
terminal_extensions: vec!["rs".to_string()],
ancestor_names: vec!["src".to_string(), "src".to_string()],
..Default::default()
},
"terminal_extensions entries must start with a dot",
),
] {
assert_eq!(hint_of(selection), hint);
}
}
#[test]
fn bounds_admit_by_index_and_report_their_limit() {
assert!(Bound::All.admits(1_000_000));
assert_eq!(Bound::All.limit(), None);
assert!(Bound::Limit(2).admits(0));
assert!(Bound::Limit(2).admits(1));
assert!(!Bound::Limit(2).admits(2));
assert_eq!(Bound::Limit(2).limit(), Some(2));
assert!(!Bound::Limit(0).admits(0));
}
#[test]
fn an_unbounded_window_does_not_constrain() {
assert!(ModifiedWindow::default().is_unbounded());
assert!(ModifiedWindow::default().contains(i64::MIN));
assert!(ModifiedWindow::default().contains(i64::MAX));
}
}