#![deny(missing_docs)]
use crate::error::{Error, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) struct SizeCap {
context: &'static str,
limit: u64,
seen: u64,
}
impl SizeCap {
pub(crate) fn new(context: &'static str, limit: u64) -> Self {
Self {
context,
limit,
seen: 0,
}
}
pub(crate) fn check_declared(&self, declared: Option<u64>) -> Result<()> {
if let Some(n) = declared
&& n > self.limit
{
return Err(self.exceeded(Some(n)));
}
Ok(())
}
pub(crate) fn add(&mut self, n: u64) -> Result<()> {
self.seen = self.seen.saturating_add(n);
if self.seen > self.limit {
return Err(self.exceeded(Some(self.seen)));
}
Ok(())
}
fn exceeded(&self, actual: Option<u64>) -> Error {
Error::SizeLimitExceeded {
context: self.context.to_string(),
limit: self.limit,
actual,
}
}
}
pub(crate) fn io_context(context: impl std::fmt::Display, err: std::io::Error) -> Error {
Error::Io(std::io::Error::new(err.kind(), format!("{context}: {err}")))
}
pub(crate) fn full_name_prefix(s: &str) -> &str {
match s.match_indices('-').nth(1) {
Some((idx, _)) => &s[..idx],
None => s,
}
}
pub(crate) fn split_version(s: &str) -> Option<(&str, &str)> {
let name = full_name_prefix(s);
let version = s.get(name.len() + 1..)?;
(!version.is_empty()).then_some((name, version))
}
pub(crate) fn same_file(source: &Path, dest: &Path) -> bool {
let canonical_dest = fs::canonicalize(dest).or_else(|_| canonicalize_missing(dest));
match (fs::canonicalize(source), canonical_dest) {
(Ok(canonical_source), Ok(canonical_dest)) => canonical_source == canonical_dest,
_ => source == dest,
}
}
pub(crate) fn canonicalize_missing(path: &Path) -> std::io::Result<PathBuf> {
let parent = path
.parent()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
let file_name = path
.file_name()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
fs::canonicalize(parent).map(|canonical_parent| canonical_parent.join(file_name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_declared_accepts_none_and_within_limit() {
let cap = SizeCap::new("body", 100);
assert!(cap.check_declared(None).is_ok());
assert!(cap.check_declared(Some(100)).is_ok());
}
#[test]
fn check_declared_rejects_over_limit() {
let cap = SizeCap::new("body", 100);
let err = cap.check_declared(Some(101)).unwrap_err();
assert!(matches!(
err,
Error::SizeLimitExceeded {
limit: 100,
actual: Some(101),
..
}
));
}
#[test]
fn add_accumulates_and_rejects_when_total_exceeds() {
let mut cap = SizeCap::new("body", 10);
assert!(cap.add(6).is_ok());
let err = cap.add(5).unwrap_err();
assert!(matches!(
err,
Error::SizeLimitExceeded {
limit: 10,
actual: Some(11),
..
}
));
}
#[test]
fn add_accepts_exactly_at_limit() {
let mut cap = SizeCap::new("body", 10);
assert!(cap.add(10).is_ok());
}
#[test]
fn io_context_prefixes_message_and_preserves_kind() {
let base = std::io::Error::new(std::io::ErrorKind::NotFound, "nope");
let wrapped = io_context("reading /x", base);
match wrapped {
Error::Io(e) => {
assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
assert!(e.to_string().contains("reading /x"));
assert!(e.to_string().contains("nope"));
}
other => panic!("unexpected error: {other}"),
}
}
#[test]
fn full_name_prefix_variants() {
assert_eq!(full_name_prefix("Owner-Name"), "Owner-Name");
assert_eq!(full_name_prefix("Owner-Name-1.2.3"), "Owner-Name");
assert_eq!(
full_name_prefix("Owner-Multi-Word-Name-1.0.0"),
"Owner-Multi"
);
assert_eq!(full_name_prefix("NoHyphen"), "NoHyphen");
assert_eq!(full_name_prefix(""), "");
}
#[test]
fn split_version_variants() {
assert_eq!(
split_version("Owner-Name-1.2.3"),
Some(("Owner-Name", "1.2.3"))
);
assert_eq!(split_version("Owner-Name"), None);
assert_eq!(split_version("Owner-Name-"), None);
assert_eq!(split_version("NoHyphen"), None);
assert_eq!(split_version(""), None);
}
}