use std::fmt;
use std::str::FromStr;
pub(crate) const LINE_RANGE_EOF: u32 = 999_999;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LineRange {
pub(crate) start: u32,
pub(crate) end: u32,
}
impl LineRange {
#[must_use]
pub(crate) fn new(start: u32, end: u32) -> Self {
Self {
start: start.max(1),
end,
}
}
#[must_use]
pub(crate) fn to_eof(start: u32) -> Self {
Self::new(start, LINE_RANGE_EOF)
}
}
impl fmt::Display for LineRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{}", self.start, self.end)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ReadMode {
Full,
FullCompact,
Anchored,
Raw,
Signatures,
Map,
Aggressive,
Entropy,
Task,
Reference,
Auto,
Diff,
Lines(LineRange),
Density(f64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ParseModeError {
Unknown(String),
Malformed(String),
}
impl fmt::Display for ParseModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseModeError::Unknown(s) => write!(f, "unknown read mode '{s}'"),
ParseModeError::Malformed(s) => write!(f, "malformed read mode '{s}'"),
}
}
}
impl std::error::Error for ParseModeError {}
fn parse_line_range(payload: &str) -> Result<LineRange, ParseModeError> {
let malformed = || ParseModeError::Malformed(format!("lines:{payload}"));
if let Some((a, b)) = payload.split_once('-') {
let start = a.trim().parse::<u32>().map_err(|_| malformed())?;
let end = b.trim().parse::<u32>().map_err(|_| malformed())?;
Ok(LineRange::new(start, end))
} else {
let start = payload.trim().parse::<u32>().map_err(|_| malformed())?;
Ok(LineRange::to_eof(start))
}
}
impl FromStr for ReadMode {
type Err = ParseModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"full" => ReadMode::Full,
"full-compact" => ReadMode::FullCompact,
"anchored" => ReadMode::Anchored,
"raw" => ReadMode::Raw,
"signatures" => ReadMode::Signatures,
"map" => ReadMode::Map,
"aggressive" => ReadMode::Aggressive,
"entropy" => ReadMode::Entropy,
"task" => ReadMode::Task,
"reference" => ReadMode::Reference,
"auto" => ReadMode::Auto,
"diff" => ReadMode::Diff,
other => {
if let Some(payload) = other.strip_prefix("lines:") {
ReadMode::Lines(parse_line_range(payload)?)
} else if let Some(payload) = other.strip_prefix("density:") {
let target = payload
.trim()
.parse::<f64>()
.map_err(|_| ParseModeError::Malformed(other.to_string()))?;
ReadMode::Density(target)
} else {
return Err(ParseModeError::Unknown(other.to_string()));
}
}
})
}
}
impl fmt::Display for ReadMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let keyword = match self {
ReadMode::Full => "full",
ReadMode::FullCompact => "full-compact",
ReadMode::Anchored => "anchored",
ReadMode::Raw => "raw",
ReadMode::Signatures => "signatures",
ReadMode::Map => "map",
ReadMode::Aggressive => "aggressive",
ReadMode::Entropy => "entropy",
ReadMode::Task => "task",
ReadMode::Reference => "reference",
ReadMode::Auto => "auto",
ReadMode::Diff => "diff",
ReadMode::Lines(range) => return write!(f, "lines:{range}"),
ReadMode::Density(target) => return write!(f, "density:{target:.2}"),
};
f.write_str(keyword)
}
}
impl ReadMode {
#[must_use]
pub(crate) fn is_compressed_cacheable(&self) -> bool {
matches!(self, ReadMode::Map | ReadMode::Signatures)
}
#[must_use]
pub(crate) fn allows_raw_cap(&self) -> bool {
!matches!(
self,
ReadMode::Lines(_)
| ReadMode::Reference
| ReadMode::Diff
| ReadMode::Raw
| ReadMode::Anchored
)
}
#[must_use]
pub(crate) fn is_lossy_summary(&self) -> bool {
matches!(
self,
ReadMode::Map
| ReadMode::Signatures
| ReadMode::Aggressive
| ReadMode::Entropy
| ReadMode::Task
)
}
#[must_use]
pub(crate) fn counts_as_compressed(&self) -> bool {
!matches!(
self,
ReadMode::Full | ReadMode::FullCompact | ReadMode::Diff | ReadMode::Anchored
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const CANONICAL: &[&str] = &[
"full",
"full-compact",
"anchored",
"raw",
"signatures",
"map",
"aggressive",
"entropy",
"task",
"reference",
"auto",
"diff",
"lines:5-10",
"lines:5-999999",
"density:0.40",
];
fn legacy_is_cacheable(mode: &str) -> bool {
["map", "signatures"].contains(&mode)
}
fn legacy_allows_raw_cap(mode: &str) -> bool {
!(mode.starts_with("lines:") || matches!(mode, "reference" | "diff" | "raw" | "anchored"))
}
fn legacy_is_lossy_summary(mode: &str) -> bool {
matches!(
mode,
"map" | "signatures" | "aggressive" | "entropy" | "task"
)
}
fn legacy_counts_as_compressed(mode: &str) -> bool {
!matches!(
mode,
"full" | "full-compact" | "diff" | "lines" | "anchored"
)
}
#[test]
fn round_trips_every_canonical_mode() {
for mode in CANONICAL {
let parsed: ReadMode = mode.parse().expect("canonical mode parses");
assert_eq!(
parsed.to_string(),
*mode,
"Display must round-trip '{mode}' byte-identically"
);
}
}
#[test]
fn classification_matches_legacy_predicates() {
for mode in CANONICAL {
let parsed: ReadMode = mode.parse().expect("canonical mode parses");
assert_eq!(
parsed.is_compressed_cacheable(),
legacy_is_cacheable(mode),
"is_compressed_cacheable diverged for '{mode}'"
);
assert_eq!(
parsed.allows_raw_cap(),
legacy_allows_raw_cap(mode),
"allows_raw_cap diverged for '{mode}'"
);
assert_eq!(
parsed.is_lossy_summary(),
legacy_is_lossy_summary(mode),
"is_lossy_summary diverged for '{mode}'"
);
assert_eq!(
parsed.counts_as_compressed(),
legacy_counts_as_compressed(mode),
"counts_as_compressed diverged for '{mode}'"
);
}
}
#[test]
fn unknown_mode_is_rejected_by_from_str() {
assert_eq!(
"wat".parse::<ReadMode>(),
Err(ParseModeError::Unknown("wat".to_string()))
);
assert_eq!(
"".parse::<ReadMode>(),
Err(ParseModeError::Unknown(String::new()))
);
}
#[test]
fn malformed_parameterized_modes_are_rejected() {
assert_eq!(
"lines:abc".parse::<ReadMode>(),
Err(ParseModeError::Malformed("lines:abc".to_string()))
);
assert_eq!(
"lines:5-x".parse::<ReadMode>(),
Err(ParseModeError::Malformed("lines:5-x".to_string()))
);
assert_eq!(
"density:nope".parse::<ReadMode>(),
Err(ParseModeError::Malformed("density:nope".to_string()))
);
}
#[test]
fn line_range_parses_bounded_unbounded_and_bare() {
assert_eq!(
"lines:5-10".parse::<ReadMode>().unwrap(),
ReadMode::Lines(LineRange::new(5, 10))
);
assert_eq!(
"lines:5-999999".parse::<ReadMode>().unwrap(),
ReadMode::Lines(LineRange::to_eof(5))
);
assert_eq!(
"lines:5".parse::<ReadMode>().unwrap(),
ReadMode::Lines(LineRange::to_eof(5))
);
}
#[test]
fn line_range_clamps_start_to_one() {
assert_eq!(LineRange::new(0, 10).start, 1);
}
#[test]
fn density_display_normalizes_to_two_decimals() {
let parsed: ReadMode = "density:0.5".parse().unwrap();
assert_eq!(parsed, ReadMode::Density(0.5));
assert_eq!(parsed.to_string(), "density:0.50");
}
}