diff --git a/thirdparty/libgc/libgc-linux-amd64.a b/thirdparty/libgc/libgc-linux-amd64.a index 01b489f6..7f926757 100644 Binary files a/thirdparty/libgc/libgc-linux-amd64.a and b/thirdparty/libgc/libgc-linux-amd64.a differ diff --git a/y/fs/dir.sp b/y/fs/dir.sp index 3d36633b..edf592e9 100644 --- a/y/fs/dir.sp +++ b/y/fs/dir.sp @@ -1,5 +1,31 @@ module fs +import strings + +pub struct UnableToRemoveFilesError { + files_problems []UnableToRemoveFileInfo +} + +pub fn (err UnableToRemoveFilesError) msg() -> string { + mut message_builder := strings.new_builder(err.files_problems.len) + message_builder.write_string("Unable to remove files:").unwrap() + + for info in err.files_problems { + message_builder.write_string("\n\t${info.path}: ${info.reason}").unwrap() + } + + return message_builder.str() +} + +pub fn (err UnableToRemoveFilesError) source() -> ?Error { + return none +} + +pub struct UnableToRemoveFileInfo { + path string + reason string +} + // mkdir_all will create a valid full path of all directories given in `path`. pub fn mkdir_all(opath string, mode u32) -> ! { if exists(opath) { @@ -38,3 +64,58 @@ pub fn mkdir_all(opath string, mode u32) -> ! { pub fn read_dir(path string) -> ![]string { return read_dir_iter(path)!.collect(abs_paths: false) } + +// TODO: implement tests and logic to make sure this function is thread-safe. + +// remove_dir_recursively removes the directory, specified by path, and all its content. +// +// Function returns an error when: +// - `path` is empty +// - `path` does not exist +// - `path` is not a directory +// - when access to `path` is denied +// - when `force` is `false` and `path` is not empty +// +// The function will try to remove all files, and if any of them fails - +// function will be aborted, and special error [`UnableToRemoveFilesError`] will be returned. +// This error contains list of all files and their reasons why they can't be removed. +// +// If `path` is directory symlink, only it will be removed. +// Original directory will be kept intact. +// +// ATTENTION: there are not confirmation that this function is thread-safe. +// You must make sure that two or more threads don't work on the same `path` at the same time. +// +// Example: +// ``` +// fs.remove_dir_recursively('/tmp', true).unwrap() +// ``` +// +// To simply remove empty directory use [`remove_empty_dir`]. +pub fn remove_dir_recursively(path string) -> ! { + if is_link(path) { + remove(path)! + return + } + + files_in_dir := read_dir_iter(path)! + mut files_problems := []UnableToRemoveFileInfo{} + + for file in files_in_dir { + abs_file_path := file.path() + + if is_dir(abs_file_path) { + remove_dir_recursively(abs_file_path)! + } else { + remove(abs_file_path) or { + files_problems.push(UnableToRemoveFileInfo{ path: abs_file_path, reason: err.msg() }) + } + } + } + + if files_problems.len > 0 { + return error(UnableToRemoveFilesError{ files_problems: files_problems }) + } + + remove_empty_dir(path)! +} diff --git a/y/fs/dir_test.sp b/y/fs/dir_test.sp index c36a9956..3b8df9d7 100644 --- a/y/fs/dir_test.sp +++ b/y/fs/dir_test.sp @@ -2,6 +2,7 @@ module main import fs import pathlib +import os #[run_if(!windows)] // symbolic links are not supported by `is_dir` yet test "is_dir function" { @@ -127,3 +128,129 @@ test "mkdir_all several directories when middle one is already created" { fs.remove(path).unwrap() fs.remove(pathlib.dir(path)).unwrap() } + +test "remove_dir_recursively returns error when path is empty" { + fs.remove_dir_recursively("") or { + return + } + + t.fail('remove_dir_recursively should fail') +} + +test "remove_dir_recursively returns error when path does not exist" { + fs.remove_dir_recursively("/unknown_path") or { + return + } + + t.fail('remove_dir_recursively should fail') +} + +test "remove_dir_recursively returns error when path is not a directory" { + defer fn () { + fs.remove("./test.txt").unwrap() + }() + + fs.write_file("./test.txt", "").unwrap() + + fs.remove_dir_recursively("./test.txt") or { + t.assert_eq(fs.exists("./test.txt"), true, 'test.txt must exist') + return + } + + t.fail('remove_dir_recursively should fail') +} + +test "remove_dir_recursively returns error when access denied" { + defer fn () { + fs.chmod("./TEST", 0o755).unwrap() + fs.remove("./TEST").unwrap() + }() + + fs.mkdir("./TEST", 0o000).unwrap() + + fs.remove_dir_recursively("./TEST") or { + t.assert_eq(fs.exists("./TEST"), true, './TEST must exist') + return + } + + t.fail('remove_dir_recursively should fail') +} + +#[run_if(!windows)] // symbolic links are not supported by `is_dir` yet +test "remove_dir_recursively doesn't remove content of original directory if symlink is removed" { + defer fn () { + fs.remove("./TEST/test.txt").unwrap() + fs.remove("./TEST").unwrap() + }() + + fs.mkdir("./TEST", 0o755).unwrap() + fs.write_file("./TEST/test.txt", "").unwrap() + fs.symlink("./TEST", "./TEST_SYMLINKED").unwrap() + + fs.remove_dir_recursively("./TEST_SYMLINKED").unwrap() + + t.assert_eq(fs.exists("./TEST"), true, 'TEST folder must exist') + t.assert_eq(fs.exists("./TEST/test.txt"), true, 'TEST/test.txt must exist') +} + +#[run_if(!windows)] // `chattr` only works on Linux +#[run_if(!macos)] // `chattr` only works on Linux + +// TODO: this test requires `sudo`, which is a bad idea for unit tests, because password is required. +// Also, this tests can't be just run as root, because root can remove any files. +// This test requires either reimplementation, or a smart trick to be able to run without `sudo`. +#[skip] +test "remove_dir_recursively returns correct error when can't remove inner file" { + defer fn () { + os.exec("sudo chattr -i ./TEST/test.txt").unwrap() + fs.remove("./TEST/test.txt").unwrap() + fs.remove("./TEST").unwrap() + }() + + fs.mkdir("./TEST", 0o755).unwrap() + fs.write_file("./TEST/test.txt", "").unwrap() + os.exec("sudo chattr +i ./TEST/test.txt").unwrap() + + fs.remove_dir_recursively("./TEST") or { + remove_error := err as fs.UnableToRemoveFilesError + + t.assert_eq(remove_error.files_problems.len, 1, 'only one error must exist') + t.assert_eq(remove_error.files_problems[0].path, 'TEST/test.txt', 'path must be correct') + t.assert_eq(remove_error.files_problems[0].reason, 'Cannot remove "TEST/test.txt": Operation not permitted', 'reason must be correct') + + t.assert_eq(err.msg(), 'Unable to remove files:\n\tTEST/test.txt: Cannot remove "TEST/test.txt": Operation not permitted', 'correct error must be returned') + + t.assert_eq(fs.exists("./TEST"), true, 'TEST folder must exist') + t.assert_eq(fs.exists("./TEST/test.txt"), true, 'TEST/test.txt must exist') + + return + } + + t.fail('remove_dir_recursively should fail') +} + +test "remove_dir_recursively removes empty directory" { + fs.mkdir("./TEST", 0o755).unwrap() + fs.remove_dir_recursively("./TEST").unwrap() + t.assert_eq(fs.exists("./TEST"), false, 'TEST folder must not exist') +} + +test "remove_dir_recursively removes directory with files" { + fs.mkdir("./TEST", 0o755).unwrap() + fs.write_file("./TEST/test.txt", "").unwrap() + fs.remove_dir_recursively("./TEST").unwrap() + + t.assert_eq(fs.exists("./TEST"), false, 'TEST folder must not exist') + t.assert_eq(fs.exists("./TEST/test.txt"), false, 'TEST/test.txt must not exist') +} + +test "remove_dir_recursively removes directory with inner directories with files" { + fs.mkdir("./TEST", 0o755).unwrap() + fs.mkdir("./TEST/inner", 0o755).unwrap() + fs.write_file("./TEST/inner/test.txt", "").unwrap() + fs.remove_dir_recursively("./TEST").unwrap() + + t.assert_eq(fs.exists("./TEST"), false, 'TEST folder must not exist') + t.assert_eq(fs.exists("./TEST/inner"), false, 'TEST/inner folder must not exist') + t.assert_eq(fs.exists("./TEST/inner/test.txt"), false, 'TEST/inner/test.txt must not exist') +} diff --git a/y/fs/dir_unix.sp b/y/fs/dir_unix.sp index 518c0aec..462dc4ee 100644 --- a/y/fs/dir_unix.sp +++ b/y/fs/dir_unix.sp @@ -24,7 +24,10 @@ pub fn mkdir(path string, mode u32) -> ! { FsError.throw(libc.mkdir(apath.c_str(), mode) != -1, 'Could not create folder "${path}": ')! } -// rmdir removes the directory specified by path. -pub fn rmdir(path string) -> ! { +// remove_empty_dir removes the empty directory specified by path. +// +// Function returns an error when the directory is not empty. +// To remove non-empty directory use [`remove_dir_recursively`]. +pub fn remove_empty_dir(path string) -> ! { FsError.throw(libc.rmdir(path.c_str()) != -1, 'Could not remove folder "${path}": ')! } diff --git a/y/fs/dir_windows.sp b/y/fs/dir_windows.sp index c8c55b42..0a0705d2 100644 --- a/y/fs/dir_windows.sp +++ b/y/fs/dir_windows.sp @@ -27,7 +27,10 @@ pub fn mkdir(path string, _ u32) -> ! { winapi.throw(winapi.CreateDirectoryW(apath.to_wide(), nil), "Failed to create directory '${path}'")! } -// rmdir removes the directory specified by path. -pub fn rmdir(path string) -> ! { +// remove_empty_dir removes the empty directory specified by path. +// +// Function returns an error when the directory is not empty. +// To remove non-empty directory use [`remove_dir_recursively`]. +pub fn remove_empty_dir(path string) -> ! { winapi.throw(winapi.RemoveDirectoryW(path.to_wide()), "Failed to remove directory '${path}'")! }