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;