From 919492945e76e0778b94d11b9e5a5bed34255074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C8=9Bca=20Dumitru?= Date: Sun, 21 Feb 2021 02:51:55 +0200 Subject: [PATCH] LibCore: Add file management helpers to reduce code duplication FileManager, cp, mv, rm had some duplicated code, implementing basic file management operations. This patch creates adds functions that try to provide an interface suited for all these programs, but this patch does not make them be used throughout the Userland. They are added to Core::File following the example of functions such as read_link/real_path_for. --- Userland/Libraries/LibCore/File.cpp | 203 ++++++++++++++++++++++++++++ Userland/Libraries/LibCore/File.h | 36 ++++- 2 files changed, 238 insertions(+), 1 deletion(-) diff --git a/Userland/Libraries/LibCore/File.cpp b/Userland/Libraries/LibCore/File.cpp index 38da00fb81..f5522ed14d 100644 --- a/Userland/Libraries/LibCore/File.cpp +++ b/Userland/Libraries/LibCore/File.cpp @@ -27,7 +27,9 @@ #ifdef __serenity__ # include #endif +#include #include +#include #include #include #include @@ -37,6 +39,11 @@ #include #include +// On Linux distros that use glibc `basename` is defined as a macro that expands to `__xpg_basename`, so we undefine it +#if defined(__linux__) && defined(basename) +# undef basename +#endif + namespace Core { Result, String> File::open(const String& filename, IODevice::OpenMode mode, mode_t permissions) @@ -259,4 +266,200 @@ NonnullRefPtr File::standard_error() } return *stderr_file; } + +static String get_duplicate_name(const String& path, int duplicate_count) +{ + if (duplicate_count == 0) { + return path; + } + LexicalPath lexical_path(path); + StringBuilder duplicated_name; + duplicated_name.append('/'); + for (size_t i = 0; i < lexical_path.parts().size() - 1; ++i) { + duplicated_name.appendff("{}/", lexical_path.parts()[i]); + } + auto prev_duplicate_tag = String::formatted("({})", duplicate_count); + auto title = lexical_path.title(); + if (title.ends_with(prev_duplicate_tag)) { + // remove the previous duplicate tag "(n)" so we can add a new tag. + title = title.substring(0, title.length() - prev_duplicate_tag.length()); + } + duplicated_name.appendff("{} ({})", lexical_path.title(), duplicate_count); + if (!lexical_path.extension().is_empty()) { + duplicated_name.appendff(".{}", lexical_path.extension()); + } + return duplicated_name.build(); +} + +Result File::copy_file_or_directory(const String& dst_path, const String& src_path, RecursionMode recursion_mode, LinkMode link_mode, AddDuplicateFileMarker add_duplicate_file_marker) +{ + if (add_duplicate_file_marker == AddDuplicateFileMarker::Yes) { + int duplicate_count = 0; + while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) { + ++duplicate_count; + } + if (duplicate_count != 0) { + return copy_file_or_directory(get_duplicate_name(dst_path, duplicate_count), src_path); + } + } + + auto source_or_error = File::open(src_path, IODevice::ReadOnly); + if (source_or_error.is_error()) + return CopyError { OSError(errno), false }; + + auto& source = *source_or_error.value(); + + struct stat src_stat; + if (fstat(source.fd(), &src_stat) < 0) + return CopyError { OSError(errno), false }; + + if (source.is_directory()) { + if (recursion_mode == RecursionMode::Disallowed) + return CopyError { OSError(errno), true }; + return copy_directory(dst_path, src_path, src_stat); + } + + if (link_mode == LinkMode::Allowed) { + if (link(src_path.characters(), dst_path.characters()) < 0) + return CopyError { OSError(errno), false }; + + return {}; + } + + return copy_file(dst_path, src_stat, source); +} + +Result File::copy_file(const String& dst_path, const struct stat& src_stat, File& source) +{ + + int dst_fd = creat(dst_path.characters(), 0666); + if (dst_fd < 0) { + if (errno != EISDIR) + return CopyError { OSError(errno), false }; + + auto dst_dir_path = String::formatted("{}/{}", dst_path, LexicalPath(source.filename()).basename()); + dst_fd = creat(dst_dir_path.characters(), 0666); + if (dst_fd < 0) + return CopyError { OSError(errno), false }; + } + + ScopeGuard close_fd_guard([dst_fd]() { ::close(dst_fd); }); + + if (src_stat.st_size > 0) { + if (ftruncate(dst_fd, src_stat.st_size) < 0) + return CopyError { OSError(errno), false }; + } + + for (;;) { + char buffer[32768]; + ssize_t nread = ::read(source.fd(), buffer, sizeof(buffer)); + if (nread < 0) { + return CopyError { OSError(errno), false }; + } + if (nread == 0) + break; + ssize_t remaining_to_write = nread; + char* bufptr = buffer; + while (remaining_to_write) { + ssize_t nwritten = ::write(dst_fd, bufptr, remaining_to_write); + if (nwritten < 0) + return CopyError { OSError(errno), false }; + + ASSERT(nwritten > 0); + remaining_to_write -= nwritten; + bufptr += nwritten; + } + } + + // NOTE: We don't copy the set-uid and set-gid bits. + auto my_umask = umask(0); + umask(my_umask); + if (fchmod(dst_fd, (src_stat.st_mode & ~my_umask) & ~06000) < 0) + return CopyError { OSError(errno), false }; + + return {}; +} + +Result File::copy_directory(const String& dst_path, const String& src_path, const struct stat& src_stat, LinkMode link) +{ + if (mkdir(dst_path.characters(), 0755) < 0) + return CopyError { OSError(errno), false }; + + String src_rp = File::real_path_for(src_path); + src_rp = String::formatted("{}/", src_rp); + String dst_rp = File::real_path_for(dst_path); + dst_rp = String::formatted("{}/", dst_rp); + + if (!dst_rp.is_empty() && dst_rp.starts_with(src_rp)) + return CopyError { OSError(errno), false }; + + DirIterator di(src_path, DirIterator::SkipDots); + if (di.has_error()) + return CopyError { OSError(errno), false }; + + while (di.has_next()) { + String filename = di.next_path(); + auto result = copy_file_or_directory( + String::formatted("{}/{}", dst_path, filename), + String::formatted("{}/{}", src_path, filename), + RecursionMode::Allowed, link); + if (result.is_error()) + return result.error(); + } + + auto my_umask = umask(0); + umask(my_umask); + if (chmod(dst_path.characters(), src_stat.st_mode & ~my_umask) < 0) + return CopyError { OSError(errno), false }; + + return {}; +} + +Result File::link_file(const String& dst_path, const String& src_path) +{ + int duplicate_count = 0; + while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) { + ++duplicate_count; + } + if (duplicate_count != 0) { + return link_file(src_path, get_duplicate_name(dst_path, duplicate_count)); + } + int rc = symlink(src_path.characters(), dst_path.characters()); + if (rc < 0) { + return OSError(errno); + } + + return {}; +} + +Result File::remove(const String& path, RecursionMode mode, bool force) +{ + struct stat path_stat; + if (lstat(path.characters(), &path_stat) < 0) { + if (!force) + return RemoveError { path, OSError(errno) }; + return {}; + } + + if (S_ISDIR(path_stat.st_mode) && mode == RecursionMode::Allowed) { + auto di = DirIterator(path, DirIterator::SkipParentAndBaseDir); + if (di.has_error()) + return RemoveError { path, OSError(di.error()) }; + + while (di.has_next()) { + auto result = remove(di.next_full_path(), RecursionMode::Allowed, true); + if (result.is_error()) + return result.error(); + } + + if (rmdir(path.characters()) < 0 && !force) + return RemoveError { path, OSError(errno) }; + } else { + if (unlink(path.characters()) < 0 && !force) + return RemoveError { path, OSError(errno) }; + } + + return {}; +} + } diff --git a/Userland/Libraries/LibCore/File.h b/Userland/Libraries/LibCore/File.h index 040a2813b7..d85856e6b1 100644 --- a/Userland/Libraries/LibCore/File.h +++ b/Userland/Libraries/LibCore/File.h @@ -26,9 +26,11 @@ #pragma once +#include #include #include #include +#include namespace Core { @@ -46,9 +48,41 @@ public: static bool is_directory(const String& filename); static bool exists(const String& filename); + static bool ensure_parent_directories(const String& path); + + enum class RecursionMode { + Allowed, + Disallowed + }; + + enum class LinkMode { + Allowed, + Disallowed + }; + + enum class AddDuplicateFileMarker { + Yes, + No, + }; + + struct CopyError { + OSError error_code; + bool tried_recursing; + }; + + static Result copy_file(const String& dst_path, const struct stat& src_stat, File& source); + static Result copy_directory(const String& dst_path, const String& src_path, const struct stat& src_stat, LinkMode = LinkMode::Disallowed); + static Result copy_file_or_directory(const String& dst_path, const String& src_path, RecursionMode = RecursionMode::Allowed, LinkMode = LinkMode::Disallowed, AddDuplicateFileMarker = AddDuplicateFileMarker::Yes); + static String real_path_for(const String& filename); static String read_link(const StringView& link_path); - static bool ensure_parent_directories(const String& path); + static Result link_file(const String& dst_path, const String& src_path); + + struct RemoveError { + String file; + OSError error_code; + }; + static Result remove(const String& path, RecursionMode, bool force); virtual bool open(IODevice::OpenMode) override;