///|
fn normalized_path(path : String) -> String {
path.replace_all(old="\\", new="/")
}
///|
fn path_is_absolute(path : String) -> Bool {
let path = normalized_path(path)
path.has_prefix("/") ||
(path.length() >= 3 && path[1] == ':' && path[2] == '/')
}
///|
fn join_path(root : String, child : String) -> String {
if root == "" || path_is_absolute(child) {
child
} else if root.has_suffix("/") || root.has_suffix("\\") {
root + child
} else {
root + "/" + child
}
}
///|
fn parent_dir(path : String) -> String {
let path = normalized_path(path)
let mut slash = -1
for index in 0..<path.length() {
if path[index] == '/' {
slash = index
}
}
if slash < 0 {
"."
} else if slash == 0 {
"/"
} else {
path[:slash].to_owned()
}
}
///|
fn base_name(path : String) -> String {
let path = normalized_path(path)
let trimmed = if path.length() > 1 && path.has_suffix("/") {
path[:path.length() - 1].to_owned()
} else {
path
}
let mut slash = -1
for index in 0..<trimmed.length() {
if trimmed[index] == '/' {
slash = index
}
}
if slash < 0 {
trimmed
} else {
trimmed[slash + 1:].to_owned()
}
}
///|
fn basename(path : String) -> String {
base_name(path)
}
///|
fn read_file_string_optional(path : String) -> String? {
let text = @fs.read_file_to_string(path) catch { _ => return None }
Some(text)
}
///|
fn resolve_path(root : String, path : String) -> String {
if path_is_absolute(path) {
path
} else {
join_path(root, path)
}
}
///|
fn moon_workspace_root(project_root : String) -> String? {
let current = Ref(normalized_path(project_root))
for _ in 0..<64 {
let marker = join_path(current.val, "moon.work")
let has_marker = @fs.is_file(marker) catch { _ => false }
if has_marker {
return Some(current.val)
}
let drive_root = current.val.length() == 2 && current.val[1] == ':'
if current.val == "/" || drive_root {
return None
}
let parent = parent_dir(current.val)
if parent == current.val {
return None
}
current.val = parent
}
None
}
///|
fn project_search_roots(project_root : String) -> Array[String] {
let project_root = normalized_path(project_root)
let roots = [project_root]
match moon_workspace_root(project_root) {
Some(root) if root != project_root => roots.push(root)
_ => ()
}
roots
}
///|
fn validate_relative_path(path : String) -> Result[Unit, String] {
let normalized = normalized_path(path)
if normalized == "" || path_is_absolute(normalized) {
return Err("generated file path must be a non-empty relative path: " + path)
}
for part in normalized.split("/") {
if part == "" || part == "." || part == ".." {
return Err("generated file path contains an unsafe segment: " + path)
}
}
Ok(())
}
///|
fn create_dir_if_missing(path : String) -> Result[Unit, String] {
if path == "" || path == "." || @fs.path_exists(path) {
return if path == "" ||
path == "." ||
(@fs.is_dir(path) catch { _ => false }) {
Ok(())
} else {
Err("path exists and is not a directory: " + path)
}
}
@fs.create_dir(path) catch {
_ => return Err("could not create directory: " + path)
}
Ok(())
}
///|
fn mkdir_p(path : String) -> Result[Unit, String] {
let normalized = normalized_path(path)
if normalized == "" || normalized == "." || @fs.path_exists(normalized) {
return create_dir_if_missing(normalized)
}
let rooted = normalized.has_prefix("/")
let has_drive = normalized.length() >= 3 &&
normalized[1] == ':' &&
normalized[2] == '/'
let current = Ref(
if rooted {
"/"
} else if has_drive {
normalized[:2].to_owned()
} else {
""
},
)
for part_index, part_view in normalized.split("/") {
let part = part_view.to_owned()
if part == "" || (has_drive && part_index == 0) {
continue
}
current.val = if current.val == "" {
part
} else if current.val == "/" || current.val.has_suffix(":") {
current.val + (if current.val.has_suffix(":") { "/" } else { "" }) + part
} else {
current.val + "/" + part
}
match create_dir_if_missing(current.val) {
Ok(_) => ()
Err(message) => return Err(message)
}
}
Ok(())
}
///|
fn nul_terminated(value : String) -> Bytes {
let source = @utf8.encode(value[:], bom=false)
source + b"\x00"
}
///|
#borrow(source, destination)
extern "C" fn rename_path_native(source : Bytes, destination : Bytes) -> Int = "moui_cli_rename_path"
///|
#borrow(source, destination)
extern "C" fn replace_path_native(source : Bytes, destination : Bytes) -> Int = "moui_cli_replace_path"
///|
#borrow(path)
extern "C" fn path_is_executable_native(path : Bytes) -> Int = "moui_cli_path_is_executable"
///|
#borrow(path)
extern "C" fn path_is_symlink_native(path : Bytes) -> Int = "moui_cli_path_is_symlink"
///|
#borrow(path)
extern "C" fn canonical_path_native(path : Bytes) -> Bytes = "moui_cli_canonical_path"
///|
fn path_is_executable(path : String) -> Bool {
path_is_executable_native(nul_terminated(path)) == 1
}
///|
fn path_is_symlink(path : String) -> Bool {
path_is_symlink_native(nul_terminated(path)) == 1
}
///|
fn canonical_path(path : String) -> String? {
let bytes = canonical_path_native(nul_terminated(path))
if bytes.is_empty() {
None
} else {
Some(@utf8.decode_lossy(bytes))
}
}
///|
fn rename_path(source : String, destination : String) -> Result[Unit, String] {
if rename_path_native(nul_terminated(source), nul_terminated(destination)) ==
0 {
Ok(())
} else {
Err("could not move generated directory into place: " + destination)
}
}
///|
fn remove_tree(path : String) -> Unit {
if !@fs.path_exists(path) {
return
}
if path_is_symlink(path) {
@fs.remove_file(path) catch {
_ => ()
}
return
}
let is_directory = @fs.is_dir(path) catch { _ => false }
if is_directory {
let entries = @fs.read_dir(path) catch { _ => return }
for entry in entries {
if entry == "." || entry == ".." {
continue
}
remove_tree(join_path(path, entry))
}
@fs.remove_dir(path) catch {
_ => ()
}
} else {
@fs.remove_file(path) catch {
_ => ()
}
}
}
///|
fn replace_file_atomic(path : String, bytes : Bytes) -> Result[Unit, String] {
let temp = path + ".moui-tmp-" + @env.now().to_string()
if @fs.path_exists(temp) {
return Err("temporary config path already exists: " + temp)
}
@fs.write_bytes_to_file(temp, bytes) catch {
_ => return Err("could not write temporary config file: " + temp)
}
if replace_path_native(nul_terminated(temp), nul_terminated(path)) == 0 {
Ok(())
} else {
@fs.remove_file(temp) catch {
_ => ()
}
Err("could not atomically update config file: " + path)
}
}
///|
fn validate_file_plan(files : Array[PlannedFile]) -> Result[Unit, String] {
let paths : Map[String, Bool] = Map([])
for file in files {
match validate_relative_path(file.path) {
Ok(_) => ()
Err(message) => return Err(message)
}
if paths.contains(file.path) {
return Err("duplicate generated file path: " + file.path)
}
paths[file.path] = true
}
Ok(())
}
///|
fn stage_file_plan(
target : String,
files : Array[PlannedFile],
) -> Result[String, String] {
if @fs.path_exists(target) {
return Err("target already exists: " + target)
}
match validate_file_plan(files) {
Ok(_) => ()
Err(message) => return Err(message)
}
match mkdir_p(parent_dir(target)) {
Ok(_) => ()
Err(message) => return Err(message)
}
let temp = target + ".moui-tmp-" + @env.now().to_string()
if @fs.path_exists(temp) {
return Err("temporary generation path already exists: " + temp)
}
match mkdir_p(temp) {
Ok(_) => ()
Err(message) => return Err(message)
}
let sorted = files.copy()
sorted.sort_by((left, right) => left.path.lexical_compare(right.path))
for file in sorted {
let destination = join_path(temp, file.path)
match mkdir_p(parent_dir(destination)) {
Ok(_) => ()
Err(message) => {
remove_tree(temp)
return Err(message)
}
}
@fs.write_bytes_to_file(destination, file.bytes) catch {
_ => {
remove_tree(temp)
return Err("could not write generated file: " + file.path)
}
}
}
Ok(temp)
}
///|
fn commit_staged_file_plan(
temp : String,
target : String,
) -> Result[Unit, String] {
match rename_path(temp, target) {
Ok(_) => Ok(())
Err(message) => {
remove_tree(temp)
Err(message)
}
}
}
///|
fn write_file_plan_atomic(
target : String,
files : Array[PlannedFile],
) -> Result[Unit, String] {
let temp = match stage_file_plan(target, files) {
Ok(value) => value
Err(message) => return Err(message)
}
commit_staged_file_plan(temp, target)
}
///|
fn text_file(path : String, text : String) -> PlannedFile {
{
path,
bytes: @utf8.encode(
(if text.has_suffix("\n") { text } else { text + "\n" })[:],
bom=false,
),
}
}
///|