// Portable path helpers used by filesystem search and public callers.
///|
/// Converts Windows separators to `/`, removes duplicate separators, and
/// removes a leading `./` from a relative path.
pub fn normalize_path(path : String) -> String {
let builder = StringBuilder::new()
let mut previous_separator = false
let mut i = 0
while i < path.length() {
let c = path[i].to_int().unsafe_to_char()
if c == '/' || c == '\\' {
if !previous_separator {
builder.write_char('/')
}
previous_separator = true
} else {
builder.write_char(c)
previous_separator = false
}
i = i + 1
}
let result = builder.to_string()
if result.has_prefix("./") {
result[2:].to_owned()
} else if result == "." {
""
} else {
result
}
}
///|
/// Returns the number of non-empty path components.
pub fn path_depth(path : String) -> Int {
let normalized = normalize_path(path)
if normalized.is_empty() || normalized == "/" {
0
} else {
let mut depth = 1
let mut i = 0
while i < normalized.length() {
if normalized[i].to_int().unsafe_to_char() == '/' {
depth = depth + 1
}
i = i + 1
}
depth
}
}
///|
/// Returns the final component of a path.
pub fn basename(path : String) -> String {
let normalized = normalize_path(path)
let trimmed = if normalized.has_suffix("/") {
normalized[:normalized.length() - 1].to_owned()
} else {
normalized
}
let mut last_separator = -1
let mut i = 0
while i < trimmed.length() {
if trimmed[i].to_int().unsafe_to_char() == '/' {
last_separator = i
}
i = i + 1
}
if last_separator < 0 {
trimmed
} else {
trimmed[last_separator + 1:].to_owned()
}
}
///|
/// Returns the parent path, or `.` when the path has no parent.
pub fn dirname(path : String) -> String {
let normalized = normalize_path(path)
let trimmed = if normalized.has_suffix("/") {
normalized[:normalized.length() - 1].to_owned()
} else {
normalized
}
let mut last_separator = -1
let mut i = 0
while i < trimmed.length() {
if trimmed[i].to_int().unsafe_to_char() == '/' {
last_separator = i
}
i = i + 1
}
if last_separator < 0 {
"."
} else if last_separator == 0 {
"/"
} else {
trimmed[:last_separator].to_owned()
}
}
///|
/// Returns the final extension without the leading dot.
pub fn extension(path : String) -> String? {
let name = basename(path)
let mut last_dot = -1
let mut i = 0
while i < name.length() {
if name[i].to_int().unsafe_to_char() == '.' {
last_dot = i
}
i = i + 1
}
if last_dot <= 0 || last_dot + 1 >= name.length() {
None
} else {
Some(name[last_dot + 1:].to_owned())
}
}
///|
/// Returns true when any path component starts with a dot.
pub fn is_hidden_path(path : String) -> Bool {
let normalized = normalize_path(path)
let mut component_start = 0
let mut i = 0
while i <= normalized.length() {
if i == normalized.length() ||
normalized[i].to_int().unsafe_to_char() == '/' {
if i > component_start &&
normalized[component_start].to_int().unsafe_to_char() == '.' {
return true
}
component_start = i + 1
}
i = i + 1
}
false
}