///|
pub(all) enum AuditSeverity {
AuditInfo
AuditWarning
AuditError
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) enum SnapshotAuditKind {
InvalidRoot
InvalidCreationTime
InvalidEntryPath
DuplicateEntryPath
CaseInsensitiveCollision
NegativeEntrySize
NonFileHasSize
MissingParentEntry
NonDirectoryParent
EntryOrderMismatch
InvalidErrorPath
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct SnapshotAuditIssue {
kind : SnapshotAuditKind
severity : AuditSeverity
path : String
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct SnapshotAudit {
issues : Array[SnapshotAuditIssue]
errors : Int
warnings : Int
information : Int
} derive(Eq, Debug, ToJson, FromJson)
///|
fn audit_add(
issues : Array[SnapshotAuditIssue],
kind : SnapshotAuditKind,
severity : AuditSeverity,
path : String,
message : String,
) -> Unit {
issues.push(SnapshotAuditIssue::{ kind, severity, path, message, })
}
///|
fn audit_entry_paths(
snapshot : Snapshot,
issues : Array[SnapshotAuditIssue],
) -> Unit {
let exact : Map[String, EntryKind] = Map([])
let folded : Map[String, String] = Map([])
let mut previous = ""
let mut first = true
for entry in snapshot.entries {
let normalized = normalize_relative_path(entry.path)
if entry.path == "" ||
normalized != entry.path ||
entry.path.has_prefix("/") ||
entry.path.contains(":") {
audit_add(
issues,
InvalidEntryPath,
AuditError,
entry.path,
"Entry paths must be non-empty normalized paths relative to the scan root.",
)
}
if exact.get(entry.path) is Some(_) {
audit_add(
issues,
DuplicateEntryPath,
AuditError,
entry.path,
"The snapshot contains this exact path more than once.",
)
} else {
exact[entry.path] = entry.kind
}
let lower = entry.path.to_lower()
match folded.get(lower) {
Some(existing) if existing != entry.path =>
audit_add(
issues,
CaseInsensitiveCollision,
AuditWarning,
entry.path,
"This path differs only by letter case from " + existing + ".",
)
None => folded[lower] = entry.path
_ => ()
}
if !first && entry.path < previous {
audit_add(
issues,
EntryOrderMismatch,
AuditInfo,
entry.path,
"Entries are not in deterministic path order.",
)
}
previous = entry.path
first = false
if entry.size < 0L {
audit_add(
issues,
NegativeEntrySize,
AuditError,
entry.path,
"Entry size cannot be negative.",
)
} else if !entry.is_file() && entry.size != 0L {
audit_add(
issues,
NonFileHasSize,
AuditWarning,
entry.path,
"Directory, symbolic-link, and other entries should store zero logical bytes.",
)
}
}
for entry in snapshot.entries {
let parent = entry.parent()
if parent == "" {
continue
}
match exact.get(parent) {
None =>
audit_add(
issues,
MissingParentEntry,
AuditInfo,
entry.path,
"The parent directory " + parent + " is not recorded in the snapshot.",
)
Some(Directory) => ()
Some(_) =>
audit_add(
issues,
NonDirectoryParent,
AuditError,
entry.path,
"The recorded parent " + parent + " is not a directory.",
)
}
}
}
///|
pub fn audit_snapshot(snapshot : Snapshot) -> SnapshotAudit {
let issues : Array[SnapshotAuditIssue] = []
if snapshot.root.trim().is_empty() {
audit_add(
issues,
InvalidRoot,
AuditError,
"",
"Snapshot root cannot be empty.",
)
}
if snapshot.created_at < 0L {
audit_add(
issues,
InvalidCreationTime,
AuditError,
"",
"Snapshot creation time cannot be negative.",
)
}
audit_entry_paths(snapshot, issues)
for error in snapshot.errors {
if error.path != normalize_relative_path(error.path) {
audit_add(
issues,
InvalidErrorPath,
AuditWarning,
error.path,
"Scan error paths should use normalized relative separators.",
)
}
}
let mut errors = 0
let mut warnings = 0
let mut information = 0
for issue in issues {
match issue.severity {
AuditError => errors += 1
AuditWarning => warnings += 1
AuditInfo => information += 1
}
}
SnapshotAudit::{ issues, errors, warnings, information, }
}
///|
pub fn SnapshotAudit::is_usable(self : SnapshotAudit) -> Bool {
self.errors == 0
}
///|
pub fn audit_severity_name(severity : AuditSeverity) -> String {
match severity {
AuditInfo => "info"
AuditWarning => "warning"
AuditError => "error"
}
}
///|
pub fn render_snapshot_audit(audit : SnapshotAudit) -> String {
let lines : Array[String] = [
"MoonDisk snapshot audit",
"Errors: " + audit.errors.to_string(),
"Warnings: " + audit.warnings.to_string(),
"Information: " + audit.information.to_string(),
]
if audit.issues.is_empty() {
lines.push("Result: snapshot structure is consistent.")
} else {
if audit.is_usable() {
lines.push("Result: snapshot structure is consistent.")
} else {
lines.push("Result: snapshot has structural errors.")
}
lines.push("")
for issue in audit.issues {
let location = if issue.path == "" { "" } else { " [" + issue.path + "]" }
lines.push(
"[" +
audit_severity_name(issue.severity) +
"] " +
issue.message +
location,
)
}
}
lines.join("\n")
}
///|
pub fn snapshot_audit_to_json(audit : SnapshotAudit) -> String {
ToJson::to_json(audit).stringify(indent=2)
}