///|
pub(all) enum OperationAuditKind {
UnsafeOperationPath
DuplicateOperationSource
DuplicateOperationDestination
QuarantineItemMissing
OriginalPathOccupied
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct OperationAuditIssue {
kind : OperationAuditKind
source : String
destination : String
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct OperationAudit {
restorable_items : Int
blocked_items : Int
ignored_items : Int
issues : Array[OperationAuditIssue]
} derive(Eq, Debug, ToJson, FromJson)
///|
fn operation_path_is_safe(path : String) -> Bool {
path != "" &&
normalize_relative_path(path) == path &&
!path.has_prefix("/") &&
!path.has_prefix("\\") &&
!path.contains(":")
}
///|
pub async fn audit_operation(operation : OperationManifest) -> OperationAudit {
let issues : Array[OperationAuditIssue] = []
let sources : Map[String, Bool] = Map([])
let destinations : Map[String, Bool] = Map([])
let blocked : Map[String, Bool] = Map([])
let mut ignored = 0
for item in operation.items {
if item.status != Moved {
ignored += 1
continue
}
if !operation_path_is_safe(item.source) ||
!operation_path_is_safe(item.destination) {
issues.push(OperationAuditIssue::{
kind: UnsafeOperationPath,
source: item.source,
destination: item.destination,
message: "Operation paths must remain normalized and relative.",
})
blocked[item.source] = true
}
if sources.get(item.source) is Some(_) {
issues.push(OperationAuditIssue::{
kind: DuplicateOperationSource,
source: item.source,
destination: item.destination,
message: "The operation source appears more than once.",
})
blocked[item.source] = true
} else {
sources[item.source] = true
}
if destinations.get(item.destination) is Some(_) {
issues.push(OperationAuditIssue::{
kind: DuplicateOperationDestination,
source: item.source,
destination: item.destination,
message: "The quarantine destination appears more than once.",
})
blocked[item.source] = true
} else {
destinations[item.destination] = true
}
let quarantined = native_join(operation.quarantine_root, item.destination)
let original = native_join(operation.root, item.source)
if !@fs.exists(quarantined) {
issues.push(OperationAuditIssue::{
kind: QuarantineItemMissing,
source: item.source,
destination: item.destination,
message: "The item is no longer present in quarantine.",
})
blocked[item.source] = true
}
if @fs.exists(original) {
issues.push(OperationAuditIssue::{
kind: OriginalPathOccupied,
source: item.source,
destination: item.destination,
message: "The original path is occupied and cannot be restored safely.",
})
blocked[item.source] = true
}
}
let moved = operation.items.filter(fn(item) { item.status == Moved }).length()
OperationAudit::{
restorable_items: moved - blocked.length(),
blocked_items: blocked.length(),
ignored_items: ignored,
issues,
}
}
///|
pub fn render_operation_audit(audit : OperationAudit) -> String {
let lines : Array[String] = [
"MoonDisk operation audit",
"Restorable items: " + audit.restorable_items.to_string(),
"Blocked items: " + audit.blocked_items.to_string(),
"Ignored items: " + audit.ignored_items.to_string(),
]
if audit.issues.is_empty() {
lines.push("Result: every moved item is ready for restoration.")
} else {
lines.push("")
for issue in audit.issues {
lines.push("- " + issue.source + ": " + issue.message)
}
}
lines.join("\n")
}
///|
pub fn operation_audit_to_json(audit : OperationAudit) -> String {
ToJson::to_json(audit).stringify(indent=2)
}