///|
fn collapse_path(path : String) -> String {
let slash_path = path.replace_all(old="\\", new="/")
let unix_absolute = slash_path.has_prefix("/")
let drive_absolute = slash_path.length() >= 3 &&
slash_path[1] == ':' &&
slash_path[2] == '/'
let absolute = unix_absolute || drive_absolute
let protected_segments = if drive_absolute { 1 } else { 0 }
let segments : Array[String] = []
for view in slash_path.split("/") {
let segment = "\{view}"
match segment {
"" | "." => ()
".." =>
if segments.length() > protected_segments &&
segments[segments.length() - 1] != ".." {
ignore(segments.pop())
} else if !absolute {
segments.push(segment)
}
_ => segments.push(segment)
}
}
let body = segments.join("/")
if unix_absolute {
if body == "" {
"/"
} else {
"/\{body}"
}
} else if drive_absolute && segments.length() == 1 {
"\{body}/"
} else {
body
}
}
///|
fn remove_normalized_prefix(path : String, prefix : String) -> String {
if prefix == "" || prefix == "." {
return path
}
if path == prefix {
return ""
}
let boundary = if prefix.has_suffix("/") { prefix } else { "\{prefix}/" }
if path.has_prefix(boundary) {
let remainder = path[boundary.length():]
"\{remainder}"
} else {
path
}
}
///|
/// Normalize separators and dot segments in a coverage source path.
///
/// If `strip_prefix` is supplied, it is normalized first and removed only at
/// a complete path-segment boundary. The function intentionally preserves
/// letter case because case sensitivity belongs to the caller's filesystem.
///
/// # Example
/// ```mbt check
/// test {
/// inspect(
/// normalize_path(
/// "D:\\work\\pkg\\.\\src\\..\\src\\lib.mbt",
/// strip_prefix="D:/work/pkg",
/// ),
/// content="src/lib.mbt",
/// )
/// }
/// ```
pub fn normalize_path(path : String, strip_prefix? : String = "") -> String {
let normalized = collapse_path(path)
remove_normalized_prefix(normalized, collapse_path(strip_prefix))
}
///|
fn glob_chars(
pattern : Array[Char],
pattern_index : Int,
value : Array[Char],
value_index : Int,
) -> Bool {
if pattern_index == pattern.length() {
return value_index == value.length()
}
match pattern[pattern_index] {
'*' => {
let is_double = pattern_index + 1 < pattern.length() &&
pattern[pattern_index + 1] == '*'
let next_pattern = if is_double {
pattern_index + 2
} else {
pattern_index + 1
}
if is_double &&
next_pattern < pattern.length() &&
pattern[next_pattern] == '/' &&
glob_chars(pattern, next_pattern + 1, value, value_index) {
return true
}
if glob_chars(pattern, next_pattern, value, value_index) {
return true
}
value_index < value.length() &&
(is_double || value[value_index] != '/') &&
glob_chars(pattern, pattern_index, value, value_index + 1)
}
'?' =>
value_index < value.length() &&
value[value_index] != '/' &&
glob_chars(pattern, pattern_index + 1, value, value_index + 1)
expected =>
value_index < value.length() &&
value[value_index] == expected &&
glob_chars(pattern, pattern_index + 1, value, value_index + 1)
}
}
///|
/// Match a normalized path against a small, portable glob dialect.
///
/// `*` matches within one segment, `?` matches one non-separator character,
/// and `**` can cross separators. Backslashes in both inputs are normalized.
pub fn glob_match(pattern : String, path : String) -> Bool {
let normalized_pattern = pattern.replace_all(old="\\", new="/")
let normalized_path = normalize_path(path)
glob_chars(normalized_pattern.to_array(), 0, normalized_path.to_array(), 0)
}