| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| #include "platform/globals.h" | |
| #if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID) | |
| #include "bin/directory.h" | |
| #include <dirent.h> // NOLINT | |
| #include <errno.h> // NOLINT | |
| #include <fcntl.h> // NOLINT | |
| #include <stdlib.h> // NOLINT | |
| #include <string.h> // NOLINT | |
| #include <sys/param.h> // NOLINT | |
| #include <sys/stat.h> // NOLINT | |
| #include <unistd.h> // NOLINT | |
| #include "bin/crypto.h" | |
| #include "bin/dartutils.h" | |
| #include "bin/fdutils.h" | |
| #include "bin/file.h" | |
| #include "bin/namespace.h" | |
| #include "bin/platform.h" | |
| #include "platform/memory_sanitizer.h" | |
| #include "platform/signal_blocker.h" | |
| namespace dart { | |
| namespace bin { | |
| static int fstatat64_fixed(int dirfd, | |
| const char* __restrict pathname, | |
| struct stat64* __restrict statbuf, | |
| int flags) { | |
| int result = ::fstatat64(dirfd, pathname, statbuf, flags); | |
| if (result == 0) { | |
| // MSAN only intercepts the old symbol name. | |
| MSAN_UNPOISON(statbuf, sizeof(*statbuf)); | |
| } | |
| return result; | |
| } | |
| static int fstatat64(int dirfd, | |
| const char* __restrict pathname, | |
| struct stat64* __restrict statbuf, | |
| int flags) = delete; | |
| PathBuffer::PathBuffer() : length_(0) { | |
| data_ = calloc(PATH_MAX + 1, sizeof(char)); // NOLINT | |
| } | |
| PathBuffer::~PathBuffer() { | |
| free(data_); | |
| } | |
| bool PathBuffer::AddW(const wchar_t* name) { | |
| UNREACHABLE(); | |
| return false; | |
| } | |
| char* PathBuffer::AsString() const { | |
| return reinterpret_cast<char*>(data_); | |
| } | |
| wchar_t* PathBuffer::AsStringW() const { | |
| UNREACHABLE(); | |
| return nullptr; | |
| } | |
| const char* PathBuffer::AsScopedString() const { | |
| return DartUtils::ScopedCopyCString(AsString()); | |
| } | |
| bool PathBuffer::Add(const char* name) { | |
| char* data = AsString(); | |
| int written = snprintf(data + length_, PATH_MAX - length_, "%s", name); | |
| data[PATH_MAX] = '\0'; | |
| if ((written <= PATH_MAX - length_) && (written >= 0) && | |
| (static_cast<size_t>(written) == strnlen(name, PATH_MAX + 1))) { | |
| length_ += written; | |
| return true; | |
| } else { | |
| errno = ENAMETOOLONG; | |
| return false; | |
| } | |
| } | |
| void PathBuffer::Reset(intptr_t new_length) { | |
| length_ = new_length; | |
| AsString()[length_] = '\0'; | |
| } | |
| // A linked list of symbolic links, with their unique file system identifiers. | |
| // These are scanned to detect loops while doing a recursive directory listing. | |
| struct LinkList { | |
| decltype(stat64::st_dev) dev; | |
| ino64_t ino; | |
| LinkList* next; | |
| }; | |
| ListType DirectoryListingEntry::Next(DirectoryListing* listing) { | |
| if (done_) { | |
| return kListDone; | |
| } | |
| if (fd_ == -1) { | |
| ASSERT(lister_ == 0); | |
| NamespaceScope ns(listing->namespc(), listing->path_buffer().AsString()); | |
| const int listingfd = | |
| TEMP_FAILURE_RETRY(openat64(ns.fd(), ns.path(), O_DIRECTORY)); | |
| if (listingfd < 0) { | |
| done_ = true; | |
| return kListError; | |
| } | |
| fd_ = listingfd; | |
| } | |
| if (lister_ == 0) { | |
| do { | |
| lister_ = reinterpret_cast<intptr_t>(fdopendir(fd_)); | |
| } while ((lister_ == 0) && (errno == EINTR)); | |
| if (lister_ == 0) { | |
| done_ = true; | |
| return kListError; | |
| } | |
| if (parent_ != nullptr) { | |
| if (!listing->path_buffer().Add(File::PathSeparator())) { | |
| return kListError; | |
| } | |
| } | |
| path_length_ = listing->path_buffer().length(); | |
| } | |
| // Reset. | |
| listing->path_buffer().Reset(path_length_); | |
| ResetLink(); | |
| // Iterate the directory and post the directories and files to the | |
| // ports. | |
| errno = 0; | |
| dirent* entry = readdir(reinterpret_cast<DIR*>(lister_)); | |
| if (entry != nullptr) { | |
| if (!listing->path_buffer().Add(entry->d_name)) { | |
| done_ = true; | |
| return kListError; | |
| } | |
| switch (entry->d_type) { | |
| case DT_DIR: | |
| if ((strcmp(entry->d_name, ".") == 0) || | |
| (strcmp(entry->d_name, "..") == 0)) { | |
| return Next(listing); | |
| } | |
| return kListDirectory; | |
| case DT_BLK: | |
| case DT_CHR: | |
| case DT_FIFO: | |
| case DT_SOCK: | |
| case DT_REG: | |
| return kListFile; | |
| case DT_LNK: | |
| if (!listing->follow_links()) { | |
| return kListLink; | |
| } | |
| // Else fall through to next case. | |
| FALL_THROUGH; | |
| case DT_UNKNOWN: { | |
| // On some file systems the entry type is not determined by | |
| // readdir. For those and for links we use stat to determine | |
| // the actual entry type. Notice that stat returns the type of | |
| // the file pointed to. | |
| NamespaceScope ns(listing->namespc(), | |
| listing->path_buffer().AsString()); | |
| struct stat64 entry_info; | |
| int stat_success; | |
| stat_success = TEMP_FAILURE_RETRY(fstatat64_fixed( | |
| ns.fd(), ns.path(), &entry_info, AT_SYMLINK_NOFOLLOW)); | |
| if (stat_success == -1) { | |
| return kListError; | |
| } | |
| if (listing->follow_links() && S_ISLNK(entry_info.st_mode)) { | |
| // Check to see if we are in a loop created by a symbolic link. | |
| LinkList current_link = {entry_info.st_dev, entry_info.st_ino, link_}; | |
| LinkList* previous = link_; | |
| while (previous != nullptr) { | |
| if ((previous->dev == current_link.dev) && | |
| (previous->ino == current_link.ino)) { | |
| // Report the looping link as a link, rather than following it. | |
| return kListLink; | |
| } | |
| previous = previous->next; | |
| } | |
| stat_success = TEMP_FAILURE_RETRY( | |
| fstatat64_fixed(ns.fd(), ns.path(), &entry_info, 0)); | |
| if (stat_success == -1 || (S_IFMT & entry_info.st_mode) == 0) { | |
| // Report a broken link as a link, even if follow_links is true. | |
| // A symbolic link can potentially point to an anon_inode. For | |
| // example, an epoll file descriptor will have a symbolic link whose | |
| // content is the string anon_inode:[eventpoll]. In this case, the | |
| // target doesn't belong to any regular file category. | |
| return kListLink; | |
| } | |
| if (S_ISDIR(entry_info.st_mode)) { | |
| // Recurse into the subdirectory with current_link added to the | |
| // linked list of seen file system links. | |
| link_ = new LinkList(current_link); | |
| if ((strcmp(entry->d_name, ".") == 0) || | |
| (strcmp(entry->d_name, "..") == 0)) { | |
| return Next(listing); | |
| } | |
| return kListDirectory; | |
| } | |
| } | |
| if (S_ISDIR(entry_info.st_mode)) { | |
| if ((strcmp(entry->d_name, ".") == 0) || | |
| (strcmp(entry->d_name, "..") == 0)) { | |
| return Next(listing); | |
| } | |
| return kListDirectory; | |
| } else if (S_ISLNK(entry_info.st_mode)) { | |
| return kListLink; | |
| } else { | |
| // Regular files, character devices, block devices, fifos, sockets and | |
| // unknown types are all considered as files. | |
| return kListFile; | |
| } | |
| } | |
| default: | |
| // We should have covered all the bases. If not, let's get an error. | |
| FATAL("Unexpected d_type: %d\n", entry->d_type); | |
| return kListError; | |
| } | |
| } | |
| done_ = true; | |
| if (errno != 0) { | |
| return kListError; | |
| } | |
| return kListDone; | |
| } | |
| DirectoryListingEntry::~DirectoryListingEntry() { | |
| ResetLink(); | |
| if (lister_ != 0) { | |
| // This also closes fd_. | |
| VOID_NO_RETRY_EXPECTED(closedir(reinterpret_cast<DIR*>(lister_))); | |
| } | |
| } | |
| void DirectoryListingEntry::ResetLink() { | |
| if ((link_ != nullptr) && | |
| ((parent_ == nullptr) || (parent_->link_ != link_))) { | |
| delete link_; | |
| link_ = nullptr; | |
| } | |
| if (parent_ != nullptr) { | |
| link_ = parent_->link_; | |
| } | |
| } | |
| static bool DeleteRecursively(int dirfd, | |
| PathBuffer* path, | |
| bool fail_on_missing = true); | |
| static bool DeleteFile(int dirfd, char* file_name, PathBuffer* path) { | |
| if (!path->Add(file_name)) { | |
| return false; | |
| } | |
| if (NO_RETRY_EXPECTED(unlinkat(dirfd, path->AsString(), 0)) == 0) { | |
| return true; | |
| } | |
| return (errno == ENOENT); | |
| } | |
| static bool DeleteDir(int dirfd, char* dir_name, PathBuffer* path) { | |
| if ((strcmp(dir_name, ".") == 0) || (strcmp(dir_name, "..") == 0)) { | |
| return true; | |
| } | |
| return path->Add(dir_name) && | |
| DeleteRecursively(dirfd, path, /*fail_on_missing=*/false); | |
| } | |
| static bool DeleteRecursively(int dirfd, | |
| PathBuffer* path, | |
| bool fail_on_missing) { | |
| // Do not recurse into links for deletion. Instead delete the link. | |
| // If it's a file, delete it. | |
| struct stat64 st; | |
| if (TEMP_FAILURE_RETRY(fstatat64_fixed(dirfd, path->AsString(), &st, | |
| AT_SYMLINK_NOFOLLOW)) == -1) { | |
| return !fail_on_missing && (errno == ENOENT); | |
| } else if (!S_ISDIR(st.st_mode)) { | |
| return (NO_RETRY_EXPECTED(unlinkat(dirfd, path->AsString(), 0)) == 0) || | |
| (!fail_on_missing && (errno == ENOENT)); | |
| } | |
| if (!path->Add(File::PathSeparator())) { | |
| return false; | |
| } | |
| // Not a link. Attempt to open as a directory and recurse into the | |
| // directory. | |
| const int fd = | |
| TEMP_FAILURE_RETRY(openat64(dirfd, path->AsString(), O_DIRECTORY)); | |
| if (fd < 0) { | |
| return !fail_on_missing && (errno == ENOENT); | |
| } | |
| DIR* dir_pointer; | |
| do { | |
| dir_pointer = fdopendir(fd); | |
| } while ((dir_pointer == nullptr) && (errno == EINTR)); | |
| if (dir_pointer == nullptr) { | |
| FDUtils::SaveErrorAndClose(fd); | |
| return !fail_on_missing && (errno == ENOENT); | |
| } | |
| // Iterate the directory and delete all files and directories. | |
| int path_length = path->length(); | |
| while (true) { | |
| // In case `readdir()` returns `nullptr` we distinguish between | |
| // end-of-stream and error by looking if `errno` was updated. | |
| errno = 0; | |
| // In glibc 2.24+, readdir_r is deprecated. | |
| // According to the man page for readdir: | |
| // "readdir(3) is not required to be thread-safe. However, in modern | |
| // implementations (including the glibc implementation), concurrent calls to | |
| // readdir(3) that specify different directory streams are thread-safe." | |
| dirent* entry = readdir(dir_pointer); | |
| if (entry == nullptr) { | |
| // Failed to read next directory entry. | |
| if (errno != 0) { | |
| break; | |
| } | |
| // End of directory. | |
| int status = NO_RETRY_EXPECTED(closedir(dir_pointer)); | |
| if (status != 0) { | |
| return false; | |
| } | |
| status = | |
| NO_RETRY_EXPECTED(unlinkat(dirfd, path->AsString(), AT_REMOVEDIR)); | |
| return (status == 0) || (!fail_on_missing && (errno == ENOENT)); | |
| } | |
| bool ok = false; | |
| switch (entry->d_type) { | |
| case DT_DIR: | |
| ok = DeleteDir(dirfd, entry->d_name, path); | |
| break; | |
| case DT_BLK: | |
| case DT_CHR: | |
| case DT_FIFO: | |
| case DT_SOCK: | |
| case DT_REG: | |
| case DT_LNK: | |
| // Treat all links as files. This will delete the link which | |
| // is what we want no matter if the link target is a file or a | |
| // directory. | |
| ok = DeleteFile(dirfd, entry->d_name, path); | |
| break; | |
| case DT_UNKNOWN: { | |
| if (!path->Add(entry->d_name)) { | |
| break; | |
| } | |
| // On some file systems the entry type is not determined by | |
| // readdir. For those we use lstat to determine the entry | |
| // type. | |
| struct stat64 entry_info; | |
| if (TEMP_FAILURE_RETRY(fstatat64_fixed( | |
| dirfd, path->AsString(), &entry_info, AT_SYMLINK_NOFOLLOW)) == | |
| -1) { | |
| if (errno == ENOENT) { | |
| ok = true; | |
| } | |
| break; | |
| } | |
| path->Reset(path_length); | |
| if (S_ISDIR(entry_info.st_mode)) { | |
| ok = DeleteDir(dirfd, entry->d_name, path); | |
| } else { | |
| // Treat links as files. This will delete the link which is | |
| // what we want no matter if the link target is a file or a | |
| // directory. | |
| ok = DeleteFile(dirfd, entry->d_name, path); | |
| } | |
| break; | |
| } | |
| default: | |
| // We should have covered all the bases. If not, let's get an error. | |
| FATAL("Unexpected d_type: %d\n", entry->d_type); | |
| break; | |
| } | |
| if (!ok) { | |
| break; | |
| } | |
| path->Reset(path_length); | |
| } | |
| // Only happens if an error. | |
| ASSERT(errno != 0); | |
| int err = errno; | |
| VOID_NO_RETRY_EXPECTED(closedir(dir_pointer)); | |
| errno = err; | |
| return false; | |
| } | |
| Directory::ExistsResult Directory::Exists(Namespace* namespc, | |
| const char* dir_name) { | |
| NamespaceScope ns(namespc, dir_name); | |
| struct stat64 entry_info; | |
| int success = | |
| TEMP_FAILURE_RETRY(fstatat64_fixed(ns.fd(), ns.path(), &entry_info, 0)); | |
| if (success == 0) { | |
| if (S_ISDIR(entry_info.st_mode)) { | |
| return EXISTS; | |
| } else { | |
| // An OSError may be constructed based on the return value of this | |
| // function, so set errno to something that makes sense. | |
| errno = ENOTDIR; | |
| return DOES_NOT_EXIST; | |
| } | |
| } else { | |
| if ((errno == EACCES) || (errno == EBADF) || (errno == EFAULT) || | |
| (errno == ENOMEM) || (errno == EOVERFLOW)) { | |
| // Search permissions denied for one of the directories in the | |
| // path or a low level error occurred. We do not know if the | |
| // directory exists. | |
| return UNKNOWN; | |
| } | |
| ASSERT((errno == ELOOP) || (errno == ENAMETOOLONG) || (errno == ENOENT) || | |
| (errno == ENOTDIR)); | |
| return DOES_NOT_EXIST; | |
| } | |
| } | |
| char* Directory::CurrentNoScope() { | |
| return getcwd(nullptr, 0); | |
| } | |
| bool Directory::Create(Namespace* namespc, const char* dir_name) { | |
| NamespaceScope ns(namespc, dir_name); | |
| // Create the directory with the permissions specified by the | |
| // process umask. | |
| const int result = NO_RETRY_EXPECTED(mkdirat(ns.fd(), ns.path(), 0777)); | |
| // If the directory already exists, treat it as a success. | |
| if ((result == -1) && (errno == EEXIST)) { | |
| return (Exists(namespc, dir_name) == EXISTS); | |
| } | |
| return (result == 0); | |
| } | |
| const char* Directory::SystemTemp(Namespace* namespc) { | |
| if (Directory::system_temp_path_override_ != nullptr) { | |
| return DartUtils::ScopedCopyCString(Directory::system_temp_path_override_); | |
| } | |
| PathBuffer path; | |
| const char* temp_dir = getenv("TMPDIR"); | |
| if (temp_dir == nullptr) { | |
| temp_dir = getenv("TMP"); | |
| } | |
| if (temp_dir == nullptr) { | |
| #if defined(DART_HOST_OS_ANDROID) | |
| temp_dir = "/data/local/tmp"; | |
| #else | |
| temp_dir = "/tmp"; | |
| #endif | |
| } | |
| NamespaceScope ns(namespc, temp_dir); | |
| if (!path.Add(ns.path())) { | |
| return nullptr; | |
| } | |
| // Remove any trailing slash. | |
| char* result = path.AsString(); | |
| int length = strlen(result); | |
| if ((length > 1) && (result[length - 1] == '/')) { | |
| result[length - 1] = '\0'; | |
| } | |
| return path.AsScopedString(); | |
| } | |
| // Returns a new, unused directory name, adding characters to the end | |
| // of prefix. Creates the directory with the permissions specified | |
| // by the process umask. | |
| // The return value is Dart_ScopeAllocated. | |
| const char* Directory::CreateTemp(Namespace* namespc, const char* prefix) { | |
| PathBuffer path; | |
| const int firstchar = 'A'; | |
| const int numchars = 'Z' - 'A' + 1; | |
| uint8_t random_bytes[7]; | |
| // mkdtemp doesn't have an "at" variant, so we have to simulate it. | |
| if (!path.Add(prefix)) { | |
| return nullptr; | |
| } | |
| intptr_t prefix_length = path.length(); | |
| while (true) { | |
| Crypto::GetRandomBytes(6, random_bytes); | |
| for (intptr_t i = 0; i < 6; i++) { | |
| random_bytes[i] = (random_bytes[i] % numchars) + firstchar; | |
| } | |
| random_bytes[6] = '\0'; | |
| if (!path.Add(reinterpret_cast<char*>(random_bytes))) { | |
| return nullptr; | |
| } | |
| NamespaceScope ns(namespc, path.AsString()); | |
| // Only the owner may access the directory, matching mkdtemp(). | |
| const int result = NO_RETRY_EXPECTED(mkdirat(ns.fd(), ns.path(), 0700)); | |
| if (result == 0) { | |
| return path.AsScopedString(); | |
| } else if (errno == EEXIST) { | |
| path.Reset(prefix_length); | |
| } else { | |
| return nullptr; | |
| } | |
| } | |
| } | |
| bool Directory::Delete(Namespace* namespc, | |
| const char* dir_name, | |
| bool recursive) { | |
| NamespaceScope ns(namespc, dir_name); | |
| if (!recursive) { | |
| if ((File::GetType(namespc, dir_name, false) == File::kIsLink) && | |
| (File::GetType(namespc, dir_name, true) == File::kIsDirectory)) { | |
| return NO_RETRY_EXPECTED(unlinkat(ns.fd(), ns.path(), 0)) == 0; | |
| } | |
| return NO_RETRY_EXPECTED(unlinkat(ns.fd(), ns.path(), AT_REMOVEDIR)) == 0; | |
| } else { | |
| PathBuffer path; | |
| if (!path.Add(ns.path())) { | |
| return false; | |
| } | |
| return DeleteRecursively(ns.fd(), &path); | |
| } | |
| } | |
| bool Directory::Rename(Namespace* namespc, | |
| const char* old_path, | |
| const char* new_path) { | |
| ExistsResult exists = Exists(namespc, old_path); | |
| if (exists != EXISTS) { | |
| return false; | |
| } | |
| NamespaceScope oldns(namespc, old_path); | |
| NamespaceScope newns(namespc, new_path); | |
| return (NO_RETRY_EXPECTED(renameat(oldns.fd(), oldns.path(), newns.fd(), | |
| newns.path())) == 0); | |
| } | |
| } // namespace bin | |
| } // namespace dart | |
| #endif // defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID) |