///|
pub(all) struct ScanConfig {
format_version : Int
options : ScanOptions
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) enum ScanConfigIssueKind {
NegativeDepth
InvalidEntryLimit
InvalidConcurrency
EmptyIgnorePattern
DuplicateIgnorePattern
AbsoluteIgnorePattern
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct ScanConfigIssue {
kind : ScanConfigIssueKind
value : String
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn validate_scan_config(config : ScanConfig) -> Array[ScanConfigIssue] {
let issues : Array[ScanConfigIssue] = []
if config.options.max_depth < 0 {
issues.push(ScanConfigIssue::{
kind: NegativeDepth,
value: config.options.max_depth.to_string(),
message: "Maximum scan depth cannot be negative.",
})
}
if config.options.max_entries < 1 {
issues.push(ScanConfigIssue::{
kind: InvalidEntryLimit,
value: config.options.max_entries.to_string(),
message: "Maximum entry count must be at least one.",
})
}
if config.options.max_concurrency < 1 || config.options.max_concurrency > 256 {
issues.push(ScanConfigIssue::{
kind: InvalidConcurrency,
value: config.options.max_concurrency.to_string(),
message: "Scan concurrency must be between 1 and 256.",
})
}
let patterns : Map[String, Bool] = Map([])
for pattern in config.options.ignore_patterns {
let normalized = normalize_relative_path(pattern)
if pattern.trim().is_empty() {
issues.push(ScanConfigIssue::{
kind: EmptyIgnorePattern,
value: pattern,
message: "Ignore patterns cannot be empty.",
})
} else if pattern.has_prefix("/") ||
pattern.has_prefix("\\") ||
pattern.contains(":") {
issues.push(ScanConfigIssue::{
kind: AbsoluteIgnorePattern,
value: pattern,
message: "Ignore patterns must be relative to the scan root.",
})
} else if patterns.get(normalized) is Some(_) {
issues.push(ScanConfigIssue::{
kind: DuplicateIgnorePattern,
value: pattern,
message: "This normalized ignore pattern appears more than once.",
})
} else {
patterns[normalized] = true
}
}
issues
}
///|
pub fn default_scan_config() -> ScanConfig {
ScanConfig::{ format_version: 1, options: ScanOptions::default(), }
}
///|
pub fn developer_scan_config() -> ScanConfig {
ScanConfig::{
format_version: 1,
options: {
..ScanOptions::default(),
max_entries: 2000000,
max_concurrency: 24,
ignore_patterns: [
".git", ".git/*", ".moondisk-quarantine", ".moondisk-quarantine/*",
],
},
}
}
///|
pub fn quick_scan_config() -> ScanConfig {
ScanConfig::{
format_version: 1,
options: {
..ScanOptions::default(),
include_hidden: false,
max_depth: 8,
max_entries: 250000,
max_concurrency: 32,
ignore_patterns: [
".git", ".git/*", "_build", "_build/*", "*/_build", "*/_build/*", "node_modules",
"*/node_modules", "*/node_modules/*",
],
},
}
}
///|
pub fn scan_config_for_profile(profile : String) -> ScanConfig raise {
match profile.to_lower() {
"default" => default_scan_config()
"developer" => developer_scan_config()
"quick" => quick_scan_config()
name => raise Failure("Unknown scan profile: " + name)
}
}
///|
pub fn scan_config_to_json(
config : ScanConfig,
pretty? : Bool = true,
) -> String {
let json = ToJson::to_json(config)
if pretty {
json.stringify(indent=2)
} else {
json.stringify()
}
}
///|
pub fn scan_config_from_json(source : String) -> ScanConfig raise {
let config : ScanConfig = @json.from_json(@json.parse(source))
if config.format_version != 1 {
raise Failure("Unsupported scan configuration version")
}
let issues = validate_scan_config(config)
if !issues.is_empty() {
raise Failure("Invalid scan configuration: " + issues[0].message)
}
config
}
///|
pub async fn write_scan_config(path : String, config : ScanConfig) -> Unit {
@fs.write_file(path, scan_config_to_json(config))
}
///|
pub async fn read_scan_config(path : String) -> ScanConfig {
scan_config_from_json(@fs.read_file(path).text())
}