use std::path::PathBuf;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileIdentity {
pub file_system: u64,
pub file: u64,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FileVersion {
#[cfg_attr(feature = "serde", serde(default))]
pub modified_ns: Option<u128>,
#[cfg_attr(feature = "serde", serde(default))]
pub changed_ns: Option<u128>,
#[cfg_attr(feature = "serde", serde(default))]
pub identity: Option<FileIdentity>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScannedFile {
#[cfg_attr(feature = "serde", serde(with = "crate::path_serde"))]
pub absolute: PathBuf,
pub relative: String,
pub bytes: u64,
pub content_hash: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
pub content_fingerprint: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
pub version: FileVersion,
#[cfg_attr(feature = "serde", serde(default))]
pub binary_checked: bool,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompactScannedFile {
pub relative: Box<str>,
pub bytes: u64,
#[cfg_attr(feature = "serde", serde(default))]
pub content: Option<Box<CompactContentEvidence>>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompactContentEvidence {
pub content_hash: Option<Box<str>>,
#[cfg_attr(feature = "serde", serde(default))]
pub content_fingerprint: Option<Box<str>>,
#[cfg_attr(feature = "serde", serde(default))]
pub version: FileVersion,
#[cfg_attr(feature = "serde", serde(default))]
pub binary_checked: bool,
}
impl CompactScannedFile {
#[must_use]
pub fn content_hash(&self) -> Option<&str> {
self.content
.as_deref()
.and_then(|content| content.content_hash.as_deref())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ScanCacheStats {
pub reused_hashes: u64,
pub content_reads: u64,
#[cfg_attr(feature = "serde", serde(default))]
pub fingerprint_reads: u64,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SkipKind {
Binary,
FileSystemBoundary,
Extension,
Ignored,
IoError,
MaxDepth,
Oversized,
PathEscape,
StandardDirectory,
Hidden,
Override,
Symlink,
SymlinkLoop,
ScanLimit,
ConcurrentModification,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedEntry {
pub relative: String,
pub kind: SkipKind,
pub detail: Option<String>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScanWarning {
pub relative: Option<String>,
pub message: String,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum IgnoreSourceKind {
GitGlobal,
GitExclude,
GitIgnore,
DotIgnore,
Custom,
Explicit,
Override,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IgnoreSourceEvidence {
pub kind: IgnoreSourceKind,
pub location: String,
pub content_hash: String,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanTermination {
MaxEntries,
MaxTotalBytes,
Timeout,
Cancelled,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScanReport {
#[cfg_attr(feature = "serde", serde(with = "crate::path_serde"))]
pub root: PathBuf,
pub files: Vec<ScannedFile>,
pub skipped: Vec<SkippedEntry>,
pub warnings: Vec<ScanWarning>,
#[cfg_attr(feature = "serde", serde(default))]
pub ignore_sources: Vec<IgnoreSourceEvidence>,
pub revision: String,
#[cfg_attr(feature = "serde", serde(default = "default_complete"))]
pub complete: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub termination: Option<ScanTermination>,
#[cfg_attr(feature = "serde", serde(default = "default_portable"))]
pub portable: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub cache: ScanCacheStats,
#[cfg_attr(feature = "serde", serde(skip, default = "default_record_skipped"))]
record_skipped: bool,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompactScanReport {
#[cfg_attr(feature = "serde", serde(with = "crate::path_serde"))]
pub root: PathBuf,
pub files: Vec<CompactScannedFile>,
pub skipped: Vec<SkippedEntry>,
pub warnings: Vec<ScanWarning>,
#[cfg_attr(feature = "serde", serde(default))]
pub ignore_sources: Vec<IgnoreSourceEvidence>,
pub revision: String,
#[cfg_attr(feature = "serde", serde(default = "default_complete"))]
pub complete: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub termination: Option<ScanTermination>,
#[cfg_attr(feature = "serde", serde(default = "default_portable"))]
pub portable: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub cache: ScanCacheStats,
}
impl CompactScanReport {
#[must_use]
pub fn absolute_path(&self, file: &CompactScannedFile) -> PathBuf {
self.root.join(file.relative.as_ref())
}
#[must_use]
pub fn into_scan_report(self) -> ScanReport {
let root = self.root;
let files = self
.files
.into_iter()
.map(|file| {
let content = file.content.map(|content| *content);
ScannedFile {
absolute: root.join(file.relative.as_ref()),
relative: file.relative.into(),
bytes: file.bytes,
content_hash: content
.as_ref()
.and_then(|value| value.content_hash.as_deref())
.map(str::to_owned),
content_fingerprint: content
.as_ref()
.and_then(|value| value.content_fingerprint.as_deref())
.map(str::to_owned),
version: content
.as_ref()
.map_or_else(FileVersion::default, |value| value.version),
binary_checked: content.is_some_and(|value| value.binary_checked),
}
})
.collect();
ScanReport {
root,
files,
skipped: self.skipped,
warnings: self.warnings,
ignore_sources: self.ignore_sources,
revision: self.revision,
complete: self.complete,
termination: self.termination,
portable: self.portable,
cache: self.cache,
record_skipped: true,
}
}
}
#[cfg(feature = "serde")]
const fn default_complete() -> bool {
true
}
#[cfg(feature = "serde")]
const fn default_record_skipped() -> bool {
true
}
#[cfg(feature = "serde")]
const fn default_portable() -> bool {
true
}
impl ScanReport {
#[must_use]
pub fn delta_from(&self, previous: &Self) -> crate::ScanDelta {
crate::ScanDelta::between(previous, self)
}
pub(crate) fn new(root: PathBuf, record_skipped: bool) -> Self {
Self {
root,
files: Vec::new(),
skipped: Vec::new(),
warnings: Vec::new(),
ignore_sources: Vec::new(),
revision: String::new(),
complete: true,
termination: None,
portable: true,
cache: ScanCacheStats::default(),
record_skipped,
}
}
pub(crate) fn skip(&mut self, relative: String, kind: SkipKind, detail: Option<String>) {
if self.record_skipped {
self.skipped.push(SkippedEntry {
relative,
kind,
detail,
});
}
}
pub(crate) fn skip_borrowed(&mut self, relative: &str, kind: SkipKind, detail: Option<String>) {
if self.record_skipped {
self.skipped.push(SkippedEntry {
relative: relative.to_owned(),
kind,
detail,
});
}
}
pub(crate) fn warn(&mut self, relative: Option<String>, message: impl Into<String>) {
self.complete = false;
self.warnings.push(ScanWarning {
relative,
message: message.into(),
});
}
pub(crate) fn terminate(&mut self, reason: ScanTermination) {
self.complete = false;
self.termination.get_or_insert(reason);
}
pub(crate) fn finish_recording(&mut self) {
self.record_skipped = true;
}
}