use std::collections::{BTreeMap, BTreeSet};
use std::io::Read;
use std::path::{Path, PathBuf};
use super::{MessageId, Severity};
use crate::{Config, ObjectFormat, ObjectId};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Scope {
Repository,
Fetch,
Receive,
}
impl Scope {
fn key(self) -> (&'static str, Option<&'static str>) {
match self {
Self::Repository => ("fsck", None),
Self::Fetch => ("fetch", Some("fsck")),
Self::Receive => ("receive", Some("fsck")),
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct PolicyLimits {
pub max_bytes: usize,
pub max_entries: usize,
}
impl Default for PolicyLimits {
fn default() -> Self {
Self {
max_bytes: 16 << 20,
max_entries: 1 << 20,
}
}
}
#[derive(Clone, Debug)]
pub struct Policy {
overrides: BTreeMap<MessageId, Severity>,
skipped: BTreeSet<ObjectId>,
unknown: Vec<Vec<u8>>,
large_pathname: usize,
}
impl Default for Policy {
fn default() -> Self {
Self {
overrides: BTreeMap::new(),
skipped: BTreeSet::new(),
unknown: Vec::new(),
large_pathname: 4096,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PolicyError {
#[error("unknown fsck message ID: {0:?}")]
UnknownMessage(Vec<u8>),
#[error("invalid fsck severity: {0:?}")]
Severity(Vec<u8>),
#[error("cannot override fatal fsck message {0:?}")]
Fatal(MessageId),
#[error("invalid fsck skip-list record at line {0}")]
SkipRecord(usize),
#[error("reading fsck skip list {path}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("fsck policy limit exceeded: {0}")]
Limit(&'static str),
#[error("unsupported fsck skip-list path")]
Path,
}
impl Policy {
pub fn from_config(
config: &Config,
scope: Scope,
format: ObjectFormat,
base: &Path,
limits: PolicyLimits,
) -> Result<Self, PolicyError> {
let mut result = Self::default();
let (section, subsection) = scope.key();
for entry in config.entries().iter().filter(|entry| {
entry.section.eq_ignore_ascii_case(section.as_bytes())
&& entry.subsection.as_deref() == subsection.map(str::as_bytes)
}) {
if entry.name.eq_ignore_ascii_case(b"skiplist") {
continue;
}
let Some(id) = MessageId::from_config_name(&entry.name) else {
if scope == Scope::Repository {
return Err(PolicyError::UnknownMessage(entry.name.clone()));
}
result.unknown.push(entry.name.clone());
continue;
};
let raw_value = entry.value.as_deref().unwrap_or_default();
let value = if id == MessageId::LargePathname {
if let Some((severity, maximum)) = crate::bytes::split_once(raw_value, b':') {
result.large_pathname = std::str::from_utf8(maximum)
.ok()
.and_then(|value| value.parse().ok())
.ok_or_else(|| PolicyError::Severity(raw_value.to_vec()))?;
severity
} else {
raw_value
}
} else {
raw_value
};
let severity = if value.eq_ignore_ascii_case(b"error") {
Severity::Error
} else if value.eq_ignore_ascii_case(b"warn") {
Severity::Warning
} else if value.eq_ignore_ascii_case(b"ignore") {
Severity::Ignore
} else {
return Err(PolicyError::Severity(value.to_vec()));
};
if id.default_severity() == Severity::Fatal {
if severity != Severity::Error {
return Err(PolicyError::Fatal(id));
}
continue;
}
result.overrides.insert(id, severity);
}
if let Some(raw) = config.value(section, subsection.map(str::as_bytes), "skiplist") {
let raw = raw.ok_or(PolicyError::Path)?;
if raw.is_empty() || raw.starts_with(b"~") || raw.contains(&0) {
return Err(PolicyError::Path);
}
let path = native_path(raw)?;
let path = if path.is_absolute() {
path
} else {
base.join(path)
};
let file = std::fs::File::open(&path).map_err(|source| PolicyError::Io {
path: path.clone(),
source,
})?;
let mut bytes = Vec::new();
file.take(limits.max_bytes.saturating_add(1) as u64)
.read_to_end(&mut bytes)
.map_err(|source| PolicyError::Io { path, source })?;
result.add_skip_list(format, &bytes, limits)?;
}
Ok(result)
}
pub fn add_skip_list(
&mut self,
format: ObjectFormat,
bytes: &[u8],
limits: PolicyLimits,
) -> Result<(), PolicyError> {
if bytes.len() > limits.max_bytes {
return Err(PolicyError::Limit("skip-list bytes"));
}
let mut ids = BTreeSet::new();
let mut records = 0usize;
for (line, value) in bytes.split(|&byte| byte == b'\n').enumerate() {
let value = value
.split(|&byte| byte == b'#')
.next()
.unwrap_or_default()
.trim_ascii();
if value.is_empty() {
continue;
}
records += 1;
if records > limits.max_entries {
return Err(PolicyError::Limit("skip-list records"));
}
let id = std::str::from_utf8(value)
.ok()
.and_then(|value| ObjectId::from_hex(format, value).ok())
.ok_or(PolicyError::SkipRecord(line + 1))?;
ids.insert(id);
}
self.skipped.extend(ids);
Ok(())
}
pub fn severity(&self, id: ObjectId, message: MessageId, strict: bool) -> Severity {
if message.default_severity() == Severity::Fatal {
return Severity::Fatal;
}
if self.skipped.contains(&id) {
return Severity::Ignore;
}
self.overrides
.get(&message)
.copied()
.unwrap_or_else(|| message.severity(strict))
}
pub fn check_object(
&self,
id: ObjectId,
kind: crate::ObjectKind,
data: &[u8],
strict: bool,
) -> Vec<super::Finding> {
super::check_payload(id.format(), kind, data, self.large_pathname, strict)
.into_iter()
.filter_map(|mut finding| {
finding.severity = self.severity(id, finding.message_id, strict);
(finding.severity != Severity::Ignore).then_some(finding)
})
.collect()
}
pub fn unknown_messages(&self) -> &[Vec<u8>] {
&self.unknown
}
}
fn native_path(bytes: &[u8]) -> Result<PathBuf, PolicyError> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(PathBuf::from(std::ffi::OsStr::from_bytes(bytes)))
}
#[cfg(not(unix))]
{
std::str::from_utf8(bytes)
.map(PathBuf::from)
.map_err(|_| PolicyError::Path)
}
}