// Copyright 2026 PaiGack
// Licensed under the Apache License, Version 2.0.
// Ported from jlaffaye/ftp (ISC License), see LICENSE-THIRD-PARTY.
// Layer: pure logic — no IO, no `moonbitlang/async` dependency.
///|
/// Join two remote path fragments the way Go's `path.Join` does: the result
/// is always cleaned (`.` dropped, `..` resolved, no empty segment, no
/// trailing slash), using `/` as separator. A leading `/` in the inputs is
/// significant only for the first non-empty segment, matching `path.Join`.
///
/// ```text
/// join("root/", "lo") == "root/lo"
/// join("root", "a") == "root/a"
/// join("root", "..") == "."
/// join("", "a") == "a"
/// ```
pub fn join(base : String, name : String) -> String {
join_all([base, name])
}
///|
/// Whether the joined path should keep a leading slash, i.e. the first
/// non-empty part started with `/`.
fn starts_absolute(parts : Array[String]) -> Bool {
for part in parts {
guard part != "" else { continue }
return part.has_prefix("/")
}
false
}
///|
/// Variadic form of `join`, equivalent to `path.Join(parts...)`.
pub fn join_all(parts : Array[String]) -> String {
let segments : Array[String] = []
for i = 0; i < parts.length(); i = i + 1 {
for segment in split_slashes(parts[i]) {
match segment {
"" | "." => ()
".." =>
// `..` pops the previous real segment, or is kept when the result
// would escape the root — same rule as Go's path.Clean.
if segments.length() > 0 && segments[segments.length() - 1] != ".." {
ignore(segments.pop())
} else {
segments.push("..")
}
_ => segments.push(segment)
}
}
}
let joined = match segments {
[] => "."
_ => segments.join("/")
}
if joined != "." && starts_absolute(parts) {
"/" + joined
} else {
joined
}
}
///|
/// Split on `/` keeping empty segments, so that `a//b` collapses correctly
/// inside `join_all` (empty segments are simply skipped there).
fn split_slashes(src : String) -> Array[String] {
let parts : Array[String] = []
let current = StringBuilder()
for ch in src {
if ch == '/' {
parts.push(current.to_string())
current.reset()
} else {
current.write_char(ch)
}
}
parts.push(current.to_string())
parts
}