// 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_WINDOWS) #include #include #include // clang-format off #include // NOLINT #include // NOLINT #include // NOLINT #include // NOLINT #include // NOLINT #undef StrDup // defined in Shlwapi.h as StrDupW #include // NOLINT #include // NOLINT #include // NOLINT #include // NOLINT // clang-format on #include "bin/builtin.h" #include "bin/crypto.h" #include "bin/directory.h" #include "bin/file.h" #include "bin/file_win.h" #include "bin/namespace.h" #include "bin/utils.h" #include "bin/utils_win.h" #include "platform/syslog.h" #include "platform/utils.h" namespace dart { namespace bin { class FileHandle { public: explicit FileHandle(int fd) : fd_(fd) {} ~FileHandle() {} int fd() const { return fd_; } void set_fd(int fd) { fd_ = fd; } private: int fd_; DISALLOW_COPY_AND_ASSIGN(FileHandle); }; File::~File() { if (!IsClosed() && handle_->fd() != _fileno(stdout) && handle_->fd() != _fileno(stderr)) { Close(); } delete handle_; } void File::Close() { ASSERT(handle_->fd() >= 0); int closing_fd = handle_->fd(); if ((closing_fd == _fileno(stdout)) || (closing_fd == _fileno(stderr))) { int fd = _open("NUL", _O_WRONLY); ASSERT(fd >= 0); _dup2(fd, closing_fd); Utils::Close(fd); } else { int err = Utils::Close(closing_fd); if (err != 0) { Syslog::PrintErr("%s\n", strerror(errno)); } } handle_->set_fd(kClosedFd); } intptr_t File::GetFD() { return handle_->fd(); } bool File::IsClosed() { return handle_->fd() == kClosedFd; } MappedMemory* File::Map(File::MapType type, int64_t position, int64_t length, void* start) { DWORD prot = PAGE_NOACCESS; switch (type) { case File::kReadOnly: prot = PAGE_READONLY; break; case File::kReadExecute: prot = PAGE_EXECUTE_READ; break; case File::kReadWrite: prot = PAGE_READWRITE; break; } void* addr = start; if (addr == nullptr) { addr = VirtualAlloc(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (addr == nullptr) { int error = GetLastError(); char buffer[1024]; Syslog::PrintErr("VirtualAlloc failed %d (%s)\n", error, Utils::StrError(error, buffer, sizeof(buffer))); return nullptr; } } const int64_t remaining_length = Length() - position; SetPosition(position); if (!ReadFully(addr, Utils::Minimum(length, remaining_length))) { int error = GetLastError(); char buffer[1024]; Syslog::PrintErr("ReadFully failed %d (%s)\n", error, Utils::StrError(error, buffer, sizeof(buffer))); if (start == nullptr) { VirtualFree(addr, 0, MEM_RELEASE); } return nullptr; } // If the requested mapping is larger than the file size, we should fill the // extra memory with zeros. if (length > remaining_length) { memset(reinterpret_cast(addr) + remaining_length, 0, length - remaining_length); } DWORD old_prot; bool result = VirtualProtect(addr, length, prot, &old_prot); if (!result) { int error = GetLastError(); char buffer[1024]; Syslog::PrintErr("VirtualProtect failed %d (%s)\n", error, Utils::StrError(error, buffer, sizeof(buffer))); if (start == nullptr) { VirtualFree(addr, 0, MEM_RELEASE); } return nullptr; } return new MappedMemory(addr, length, /*should_unmap=*/start == nullptr); } void MappedMemory::Unmap() { BOOL result = VirtualFree(address_, 0, MEM_RELEASE); ASSERT(result); address_ = nullptr; size_ = 0; } int64_t File::Read(void* buffer, int64_t num_bytes) { ASSERT(handle_->fd() >= 0); return Utils::Read(handle_->fd(), buffer, num_bytes); } int64_t File::Write(const void* buffer, int64_t num_bytes) { int fd = handle_->fd(); // Avoid narrowing conversion ASSERT(fd >= 0 && num_bytes <= MAXDWORD && num_bytes >= 0); HANDLE handle = reinterpret_cast(_get_osfhandle(fd)); DWORD written = 0; BOOL result = ::WriteFile(handle, buffer, num_bytes, &written, nullptr); if (!result) { return -1; } DWORD mode; int64_t bytes_written = written; if (GetConsoleMode(handle, &mode)) { // If `handle` is for a console, then `written` may refer to the number of // characters printed to the screen rather than the number of bytes of the // buffer that were actually consumed. To compute the number of bytes that // were actually consumed, we convert the buffer to a wchar_t using the // console's current code page, filling as many characters as were // printed, and then convert that many characters back to the encoding for // the code page, which gives the number of bytes of `buffer` used to // generate the characters that were printed. wchar_t* wide = new wchar_t[written]; int cp = GetConsoleOutputCP(); MultiByteToWideChar(cp, 0, reinterpret_cast(buffer), -1, wide, written); int buffer_len = WideCharToMultiByte(cp, 0, wide, written, nullptr, 0, nullptr, nullptr); delete[] wide; bytes_written = buffer_len; } return bytes_written; } bool File::VPrint(const char* format, va_list args) { // Measure. va_list measure_args; va_copy(measure_args, args); intptr_t len = _vscprintf(format, measure_args); va_end(measure_args); char* buffer = reinterpret_cast(malloc(len + 1)); // Print. va_list print_args; va_copy(print_args, args); _vsnprintf(buffer, len + 1, format, print_args); va_end(print_args); bool result = WriteFully(buffer, len); free(buffer); return result; } int64_t File::Position() { ASSERT(handle_->fd() >= 0); HANDLE handle = reinterpret_cast(_get_osfhandle(handle_->fd())); LARGE_INTEGER zero_offset; zero_offset.QuadPart = 0; LARGE_INTEGER position; if (!SetFilePointerEx(handle, zero_offset, &position, FILE_CURRENT)) { return -1L; } return position.QuadPart; } bool File::SetPosition(int64_t position) { ASSERT(handle_->fd() >= 0); HANDLE handle = reinterpret_cast(_get_osfhandle(handle_->fd())); LARGE_INTEGER requested_position; requested_position.QuadPart = position; return SetFilePointerEx(handle, requested_position, /*lpNewFilePointer=*/nullptr, FILE_BEGIN); } bool File::Truncate(int64_t length) { if (!SetPosition(length)) { return false; } HANDLE handle = reinterpret_cast(_get_osfhandle(handle_->fd())); return SetEndOfFile(handle); } bool File::Flush() { ASSERT(handle_->fd()); HANDLE handle = reinterpret_cast(_get_osfhandle(handle_->fd())); return FlushFileBuffers(handle); } bool File::Lock(File::LockType lock, int64_t start, int64_t end) { ASSERT(handle_->fd() >= 0); ASSERT((end == -1) || (end > start)); HANDLE handle = reinterpret_cast(_get_osfhandle(handle_->fd())); OVERLAPPED overlapped; ZeroMemory(&overlapped, sizeof(OVERLAPPED)); overlapped.Offset = Utils::Low32Bits(start); overlapped.OffsetHigh = Utils::High32Bits(start); int64_t length = end == -1 ? 0 : end - start; if (length == 0) { length = kMaxInt64; } int32_t length_low = Utils::Low32Bits(length); int32_t length_high = Utils::High32Bits(length); BOOL rc; switch (lock) { case File::kLockUnlock: rc = UnlockFileEx(handle, 0, length_low, length_high, &overlapped); break; case File::kLockShared: case File::kLockExclusive: case File::kLockBlockingShared: case File::kLockBlockingExclusive: { DWORD flags = 0; if ((lock == File::kLockShared) || (lock == File::kLockExclusive)) { flags |= LOCKFILE_FAIL_IMMEDIATELY; } if ((lock == File::kLockExclusive) || (lock == File::kLockBlockingExclusive)) { flags |= LOCKFILE_EXCLUSIVE_LOCK; } rc = LockFileEx(handle, flags, 0, length_low, length_high, &overlapped); break; } default: UNREACHABLE(); } return rc; } int64_t File::Length() { ASSERT(handle_->fd() >= 0); struct __stat64 st; if (_fstat64(handle_->fd(), &st) == 0) { return st.st_size; } return -1; } File* File::FileOpenW(const wchar_t* system_name, FileOpenMode mode) { int flags = O_RDONLY | O_BINARY | O_NOINHERIT; if ((mode & kWrite) != 0) { ASSERT((mode & kWriteOnly) == 0); flags = (O_RDWR | O_CREAT | O_BINARY | O_NOINHERIT); } if ((mode & kWriteOnly) != 0) { ASSERT((mode & kWrite) == 0); flags = (O_WRONLY | O_CREAT | O_BINARY | O_NOINHERIT); } if ((mode & kTruncate) != 0) { flags = flags | O_TRUNC; } int fd = _wopen(system_name, flags, 0666); if (fd < 0) { return nullptr; } if ((((mode & kWrite) != 0) && ((mode & kTruncate) == 0)) || (((mode & kWriteOnly) != 0) && ((mode & kTruncate) == 0))) { int64_t position = _lseeki64(fd, 0, SEEK_END); if (position < 0) { return nullptr; } } return OpenFD(fd); } File* File::OpenFD(int fd) { return new File(new FileHandle(fd)); } static std::unique_ptr ConvertToAbsolutePath( const std::unique_ptr& path) { // Initial buffer size is selected to avoid overallocating too much // memory. int buffer_size = 1024; do { auto buffer = std::make_unique(buffer_size); int full_path_length = GetFullPathNameW(path.get(), buffer_size, buffer.get(), /*lpFilePart=*/nullptr); if (full_path_length == 0) { return nullptr; } // Note: when sucessful full_path_length does *not* include terminating // NUL character, but on failure it *does* include it when returning // the size of buffer which we need. Hence comparison here is `<`, rather // than `<=`. if (full_path_length < buffer_size) { return buffer; } buffer_size = full_path_length; } while (true); } static bool IsAbsolutePath(const wchar_t* pathname) { if (pathname == nullptr) return false; char first = pathname[0]; char second = pathname[1]; if (first == L'\\' && second == L'\\') return true; if (second != L':') return false; first |= 0x20; char third = pathname[2]; return (first >= L'a') && (first <= L'z') && (third == L'\\' || third == L'/'); } const wchar_t* kLongPathPrefix = L"\\\\?\\"; const int kLongPathPrefixLength = 4; // `\\.\` is a device namespace prefix somewhat similar to `\\?\`. // We should preserve it at the start of the file names. const wchar_t* kDeviceNamespacePrefix = L"\\\\.\\"; const int kDeviceNamespacePrefixLength = 4; static bool IsLongPathPrefixed(const std::unique_ptr& path) { return wcsncmp(path.get(), kLongPathPrefix, kLongPathPrefixLength) == 0; } static bool IsDeviceNamespacePrefixed(const std::unique_ptr& path) { return wcsncmp(path.get(), kDeviceNamespacePrefix, kDeviceNamespacePrefixLength) == 0; } // Converts the given UTF8 path to wide char '\\?\'-prefix absolute path. // // Note that some WinAPI functions (like SetCurrentDirectoryW) are always // limited to MAX_PATH long paths and converting to `\\?\`-prefixed form does // not remove this limitation. Always check Win API documentation. std::unique_ptr ToWinAPIPath(const char* utf8_path) { auto path = Utf8ToWideChar(utf8_path); // Among other things ConvertToAbsolutePath replaces '/' with '\', // which PathAllocCanonicalize won't do. auto abs_path = ConvertToAbsolutePath(path); if (abs_path.get() == nullptr) { return std::unique_ptr(nullptr); } PWSTR canonical_path; if (PathAllocCanonicalize(abs_path.get(), PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, &canonical_path) != S_OK) { return std::unique_ptr(nullptr); } auto result = std::unique_ptr(wcsdup(canonical_path)); LocalFree(canonical_path); return result; } File* File::Open(Namespace* namespc, const char* name, FileOpenMode mode, bool executable) { const auto path = ToWinAPIPath(name); if (path.get() == nullptr) { SetLastError(ERROR_INVALID_NAME); return nullptr; } File* file = FileOpenW(path.get(), mode); return file; } CStringUniquePtr File::UriToPath(const char* uri) { UriDecoder uri_decoder(uri); if (uri_decoder.decoded() == nullptr) { SetLastError(ERROR_INVALID_NAME); return CStringUniquePtr(nullptr); } const auto uri_w = Utf8ToWideChar(uri_decoder.decoded()); if (!UrlIsFileUrlW(uri_w.get())) { return CStringUniquePtr(Utils::StrDup(uri_decoder.decoded())); } wchar_t filename_w[MAX_PATH]; DWORD filename_len = MAX_PATH; HRESULT result = PathCreateFromUrlW(uri_w.get(), filename_w, &filename_len, /* dwFlags= */ 0); if (result != S_OK) { return CStringUniquePtr(nullptr); } WideToUtf8Scope utf8_path(filename_w); return utf8_path.release(); } File* File::OpenUri(Namespace* namespc, const char* uri, FileOpenMode mode, bool executable) { auto path = UriToPath(uri); if (path == nullptr) { return nullptr; } return Open(namespc, path.get(), mode, executable); } File* File::OpenStdio(int fd) { int stdio_fd = -1; switch (fd) { case 1: stdio_fd = _fileno(stdout); break; case 2: stdio_fd = _fileno(stderr); break; default: UNREACHABLE(); } _setmode(stdio_fd, _O_BINARY); return new File(new FileHandle(stdio_fd)); } static bool StatHelper(const wchar_t* path, struct __stat64* st) { int stat_status = _wstat64(path, st); if (stat_status != 0) { return false; } if ((st->st_mode & S_IFMT) != S_IFREG) { SetLastError(ERROR_NOT_SUPPORTED); return false; } return true; } static bool FileExists(const wchar_t* path) { struct __stat64 st; return StatHelper(path, &st); } bool File::Exists(Namespace* namespc, const char* name) { const auto path = ToWinAPIPath(name); if (path.get() == nullptr) { return false; } return FileExists(path.get()); } bool File::ExistsUri(Namespace* namespc, const char* uri) { UriDecoder uri_decoder(uri); if (uri_decoder.decoded() == nullptr) { SetLastError(ERROR_INVALID_NAME); return false; } return File::Exists(namespc, uri_decoder.decoded()); } bool File::Create(Namespace* namespc, const char* name, bool exclusive) { const auto path = ToWinAPIPath(name); if (path.get() == nullptr) { return false; } int flags = O_RDONLY | O_CREAT; if (exclusive) { flags |= O_EXCL; } int fd = _wopen(path.get(), flags, 0666); if (fd < 0) { return false; } return (Utils::Close(fd) == 0); } // This structure is needed for creating and reading Junctions. typedef struct _REPARSE_DATA_BUFFER { ULONG ReparseTag; USHORT ReparseDataLength; USHORT Reserved; union { struct { USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; ULONG Flags; WCHAR PathBuffer[1]; } SymbolicLinkReparseBuffer; struct { USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; WCHAR PathBuffer[1]; } MountPointReparseBuffer; struct { UCHAR DataBuffer[1]; } GenericReparseBuffer; }; } REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER; bool File::CreateLink(Namespace* namespc, const char* utf8_name, const char* utf8_target) { const auto name = ToWinAPIPath(utf8_name); if (name.get() == nullptr) { SetLastError(ERROR_INVALID_NAME); return false; } std::unique_ptr target; bool target_is_directory; if (File::IsAbsolutePath(utf8_target)) { target = ToWinAPIPath(utf8_target); if (target.get() == nullptr) { SetLastError(ERROR_INVALID_NAME); return false; } target_is_directory = File::GetType(target.get(), /*follow_links=*/true) == kIsDirectory; } else { // The path of `target` is relative to `name`. // // To determine if `target` is a file or directory, we need to calculate // either its absolute path or its path relative to the current working // directory. // // For example: // // name= C:\A\B\Link ..\..\Link ..\..\Link // target= MyFile MyFile ..\Dir\MyFile // -------------------------------------------------------------------- // target_path= C:\A\B\MyFile ..\..\MyFile ..\..\..\Dir\MyFile // // The transformation steps are: // 1. target_path := name ..\..\Link // 2. target_path := remove_file(target_path) ..\..\ // 3. target_path := combine(target_path, target) ..\..\..\Dir\MyFile target = Utf8ToWideChar(utf8_target); // 1. target_path := name intptr_t target_path_max_length = wcslen(name.get()) + wcslen(target.get()) + 2; auto target_path = std::make_unique(target_path_max_length); wcscpy_s(target_path.get(), target_path_max_length, name.get()); // 2. target_path := remove_file(target_path) HRESULT remove_result = PathCchRemoveFileSpec(target_path.get(), target_path_max_length); if (remove_result == S_FALSE) { // If the file component could not be removed, then `name` is // top-level, like "C:\" or "/". Attempts to create files at those paths // will fail with ERROR_ACCESS_DENIED. SetLastError(ERROR_ACCESS_DENIED); return false; } else if (remove_result != S_OK) { SetLastError(remove_result); return false; } // 3. target_path := combine(target_path, target) HRESULT combine_result = PathCchCombineEx( target_path.get(), target_path_max_length, target_path.get(), target.get(), PATHCCH_ALLOW_LONG_PATHS); if (combine_result != S_OK) { SetLastError(combine_result); return false; } target_is_directory = File::GetType(target_path.get(), /*follow_links=*/true) == kIsDirectory; } DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; if (target_is_directory) { flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; } int create_status = CreateSymbolicLinkW(name.get(), target.get(), flags); // If running on a Windows 10 build older than 14972, an invalid parameter // error will be returned when trying to use the // SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag. Retry without the flag. if ((create_status == 0) && (GetLastError() == ERROR_INVALID_PARAMETER)) { flags &= ~SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; create_status = CreateSymbolicLinkW(name.get(), target.get(), flags); } return (create_status != 0); } bool File::CreatePipe(Namespace* namespc, File** readPipe, File** writePipe) { int pipe_fds[2]; int status = _pipe(pipe_fds, 4096, _O_BINARY); if (status != 0) { return false; } *readPipe = OpenFD(pipe_fds[0]); *writePipe = OpenFD(pipe_fds[1]); return true; } bool File::Delete(Namespace* namespc, const char* name) { const auto path = ToWinAPIPath(name); if (path.get() == nullptr) { return false; } int status = _wremove(path.get()); return status != -1; } static bool DeleteLinkHelper(const wchar_t* path) { bool result = false; DWORD attributes = GetFileAttributesW(path); if ((attributes == INVALID_FILE_ATTRIBUTES) || ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)) { SetLastError(ERROR_NOT_A_REPARSE_POINT); return false; } if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { // It's a junction, which is a special type of directory, or a symbolic // link to a directory. Remove the directory. result = (RemoveDirectoryW(path) != 0); } else { // Symbolic link to a file. Remove the file. result = (DeleteFileW(path) != 0); } return result; } bool File::DeleteLink(Namespace* namespc, const char* name) { const auto path = ToWinAPIPath(name); if (path.get() == nullptr) { return false; } return DeleteLinkHelper(path.get()); } static bool RenameHelper(File::Type expected, const char* old_name, const char* new_name) { const auto old_path = ToWinAPIPath(old_name); if (old_path.get() == nullptr) { SetLastError(ERROR_INVALID_NAME); return false; } File::Type type = File::GetType(old_path.get(), /*follow_links=*/false); if (type != expected) { SetLastError(ERROR_FILE_NOT_FOUND); return false; } const auto new_path = ToWinAPIPath(new_name); if (new_path.get() == nullptr) { SetLastError(ERROR_INVALID_NAME); return false; } DWORD flags = MOVEFILE_WRITE_THROUGH | MOVEFILE_REPLACE_EXISTING; // Symbolic links (e.g. produced by Link.create) to directories on Windows // appear as special directories. MoveFileExW's MOVEFILE_REPLACE_EXISTING // does not allow for replacement of directories, so we need to remove it // before renaming. if ((Directory::Exists(new_path.get()) == Directory::EXISTS) && (File::GetType(new_path.get(), /*follow_links=*/false) == File::kIsLink)) { // Bail out if the DeleteLink call fails. if (!DeleteLinkHelper(new_path.get())) { return false; } } int move_status = MoveFileExW(old_path.get(), new_path.get(), flags); return (move_status != 0); } bool File::Rename(Namespace* namespc, const char* old_name, const char* new_name) { return RenameHelper(File::kIsFile, old_name, new_name); } bool File::RenameLink(Namespace* namespc, const char* old_name, const char* new_name) { return RenameHelper(File::kIsLink, old_name, new_name); } static std::unique_ptr GetDirectoryPath( const std::unique_ptr& path) { for (intptr_t i = wcslen(path.get()) - 1; i >= 0; --i) { if (path.get()[i] == '\\' || path.get()[i] == '/') { // Note: we need to copy the trailing directory separator so we need to // copy i + 1 characters (plus trailing '\0'). auto result = std::make_unique(i + 2); wcsncpy(result.get(), path.get(), i + 1); return result; } } return nullptr; } static void FreeUUID(wchar_t* ptr) { RpcStringFreeW(&ptr); } static std::unique_ptr GenerateUUIDString() { UUID uuid; RPC_STATUS status = UuidCreateSequential(&uuid); if ((status != RPC_S_OK) && (status != RPC_S_UUID_LOCAL_ONLY)) { return {nullptr, nullptr}; } wchar_t* uuid_string; status = UuidToStringW(&uuid, &uuid_string); if (status != RPC_S_OK) { return {nullptr, nullptr}; } return {uuid_string, &FreeUUID}; } // This function will copy the |src| file to a temporary file in the // directory where |dest| resides and returns the path of temp file. static std::unique_ptr CopyIntoTempFile( const std::unique_ptr& src, const std::unique_ptr& dest) { const auto dir = GetDirectoryPath(dest); if (dir == nullptr) { return nullptr; } uint32_t suffix_bytes = 0; const int kSuffixSize = sizeof(suffix_bytes); if (Crypto::GetRandomBytes(kSuffixSize, reinterpret_cast(&suffix_bytes))) { const size_t file_path_buf_size = wcslen(dir.get()) + 8 + 1; auto file_path = std::make_unique(file_path_buf_size); swprintf(file_path.get(), file_path_buf_size, L"%s%x", dir.get(), suffix_bytes); if (CopyFileExW(src.get(), file_path.get(), nullptr, nullptr, nullptr, 0) != 0) { return file_path; } // If CopyFileExW() fails to copy to a temp file with random hex, fall // back to copy to a uuid temp file. } const auto uuid_str = GenerateUUIDString(); if (uuid_str == nullptr) { return nullptr; } const size_t file_path_buf_size = wcslen(dir.get()) + wcslen(uuid_str.get()) + 1; auto file_path = std::make_unique(file_path_buf_size); swprintf(file_path.get(), file_path_buf_size, L"%s%s", dir.get(), uuid_str.get()); if (CopyFileExW(src.get(), file_path.get(), nullptr, nullptr, nullptr, 0) != 0) { return file_path; } return nullptr; } bool File::Copy(Namespace* namespc, const char* old_name, const char* new_name) { // We are going to concatenate new path with temporary file names in // CopyIntoTempFile so we force long prefix no matter what. const auto old_path = ToWinAPIPath(old_name); const auto new_path = ToWinAPIPath(new_name); if (new_path.get() == nullptr || old_path.get() == nullptr) { SetLastError(ERROR_INVALID_NAME); return false; } File::Type type = GetType(old_path.get(), /*follow_links=*/false); if (type != kIsFile) { SetLastError(ERROR_FILE_NOT_FOUND); return false; } const auto temp_file = CopyIntoTempFile(old_path, new_path); if (temp_file == nullptr) { // If temp file creation fails, fall back on doing a direct copy. return CopyFileExW(old_path.get(), new_path.get(), nullptr, nullptr, nullptr, 0) != 0; } // Remove the existing file. Otherwise, renaming will fail. if (FileExists(new_path.get())) { DeleteFileW(new_path.get()); } if (!MoveFileW(temp_file.get(), new_path.get())) { DWORD error = GetLastError(); DeleteFileW(temp_file.get()); SetLastError(error); return false; } return true; } int64_t File::LengthFromPath(Namespace* namespc, const char* name) { struct __stat64 st; const auto path = ToWinAPIPath(name); if (path.get() == nullptr || !StatHelper(path.get(), &st)) { return -1; } return st.st_size; } const char* File::LinkTarget(Namespace* namespc, const char* pathname, char* dest, int dest_size) { const auto path = ToWinAPIPath(pathname); if (path.get() == nullptr) { return nullptr; } HANDLE dir_handle = CreateFileW( path.get(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr); if (dir_handle == INVALID_HANDLE_VALUE) { return nullptr; } // Allocate a buffer for regular paths (smaller than MAX_PATH). If buffer is // too small for a long path, allocate a bigger buffer and try again. int buffer_size = sizeof(REPARSE_DATA_BUFFER) + (MAX_PATH + 1) * sizeof(WCHAR); REPARSE_DATA_BUFFER* buffer = reinterpret_cast(Dart_ScopeAllocate(buffer_size)); DWORD received_bytes; // Value is not used. int result = DeviceIoControl(dir_handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, buffer, buffer_size, &received_bytes, nullptr); if (result == 0) { DWORD error = GetLastError(); // If ERROR_MORE_DATA is thrown, the target path exceeds the size limit. A // bigger buffer will be required. if (error == ERROR_MORE_DATA) { // Allocate a bigger buffer with MAX_LONG_PATH buffer_size = sizeof(REPARSE_DATA_BUFFER) + (MAX_LONG_PATH + 1) * sizeof(WCHAR); buffer = reinterpret_cast( Dart_ScopeAllocate(buffer_size)); result = DeviceIoControl(dir_handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, buffer, buffer_size, &received_bytes, nullptr); if (result == 0) { // Overwrite the ERROR_MORE_DATA. error = GetLastError(); } } if (result == 0) { CloseHandle(dir_handle); SetLastError(error); return nullptr; } } if (CloseHandle(dir_handle) == 0) { return nullptr; } wchar_t* target; size_t target_offset; size_t target_length; if (buffer->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { target = buffer->MountPointReparseBuffer.PathBuffer; target_offset = buffer->MountPointReparseBuffer.SubstituteNameOffset; target_length = buffer->MountPointReparseBuffer.SubstituteNameLength; } else if (buffer->ReparseTag == IO_REPARSE_TAG_SYMLINK) { target = buffer->SymbolicLinkReparseBuffer.PathBuffer; target_offset = buffer->SymbolicLinkReparseBuffer.SubstituteNameOffset; target_length = buffer->SymbolicLinkReparseBuffer.SubstituteNameLength; } else { // Not a junction or a symbolic link. SetLastError(ERROR_NOT_A_REPARSE_POINT); return nullptr; } target_offset /= sizeof(wchar_t); // Offset and length are in bytes. target_length /= sizeof(wchar_t); target += target_offset; // Remove "\??\" from beginning of target. if ((target_length > 4) && (wcsncmp(L"\\??\\", target, 4) == 0)) { target += 4; target_length -= 4; } int utf8_length = WideCharToMultiByte(CP_UTF8, 0, target, target_length, nullptr, 0, nullptr, nullptr); if (dest_size > 0 && dest_size <= utf8_length) { return nullptr; } if (dest == nullptr) { dest = DartUtils::ScopedCString(utf8_length + 1); } if (0 == WideCharToMultiByte(CP_UTF8, 0, target, target_length, dest, utf8_length, nullptr, nullptr)) { return nullptr; } dest[utf8_length] = '\0'; return dest; } void File::Stat(Namespace* namespc, const char* name, int64_t* data) { const auto path = ToWinAPIPath(name); if (path.get() == nullptr) { data[kType] = File::kDoesNotExist; return; } File::Type type = GetType(path.get(), /*follow_links=*/true); data[kType] = type; if (type != kDoesNotExist) { struct _stat64 st; int stat_status = _wstat64(path.get(), &st); if (stat_status == 0) { data[kCreatedTime] = st.st_ctime * 1000; data[kModifiedTime] = st.st_mtime * 1000; data[kAccessedTime] = st.st_atime * 1000; data[kMode] = st.st_mode; data[kSize] = st.st_size; } else { data[kType] = File::kDoesNotExist; } } } time_t File::LastAccessed(Namespace* namespc, const char* name) { struct __stat64 st; const auto path = ToWinAPIPath(name); if (path.get() == nullptr || !StatHelper(path.get(), &st)) { return -1; } return st.st_atime; } time_t File::LastModified(Namespace* namespc, const char* name) { struct __stat64 st; const auto path = ToWinAPIPath(name); if (path.get() == nullptr || !StatHelper(path.get(), &st)) { return -1; } return st.st_mtime; } bool File::SetLastAccessed(Namespace* namespc, const char* name, int64_t millis) { struct __stat64 st; const auto path = ToWinAPIPath(name); if (path.get() == nullptr || !StatHelper(path.get(), &st)) { return false; } // _utime and related functions set the access and modification times of the // affected file. Even if the specified modification time is not changed // from the current value, _utime will trigger a file modification event // (e.g. ReadDirectoryChangesW will report the file as modified). // // So set the file access time directly using SetFileTime. FILETIME at = GetFiletimeFromMillis(millis); HANDLE file_handle = CreateFileW(path.get(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (file_handle == INVALID_HANDLE_VALUE) { return false; } bool result = SetFileTime(file_handle, nullptr, &at, nullptr); CloseHandle(file_handle); return result; } bool File::SetLastModified(Namespace* namespc, const char* name, int64_t millis) { // First get the current times. struct __stat64 st; const auto path = ToWinAPIPath(name); if (path.get() == nullptr || !StatHelper(path.get(), &st)) { return false; } // Set the new time: struct __utimbuf64 times; times.actime = st.st_atime; times.modtime = millis / kMillisecondsPerSecond; return _wutime64(path.get(), ×) == 0; } // Keep this function synchronized with the behavior // of `FileSystemEntity.isAbsolute` in file_system_entity.dart. bool File::IsAbsolutePath(const char* pathname) { if (pathname == nullptr) return false; char first = pathname[0]; char second = pathname[1]; if (first == '\\' && second == '\\') return true; if (second != ':') return false; first |= 0x20; char third = pathname[2]; return (first >= 'a') && (first <= 'z') && (third == '\\' || third == '/'); } const char* File::GetCanonicalPath(Namespace* namespc, const char* pathname, char* dest, int dest_size) { const auto path = ToWinAPIPath(pathname); if (path.get() == nullptr) { SetLastError(ERROR_INVALID_NAME); return nullptr; } HANDLE file_handle = CreateFileW(path.get(), 0, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (file_handle == INVALID_HANDLE_VALUE) { return nullptr; } wchar_t dummy_buffer[1]; int required_size = GetFinalPathNameByHandle(file_handle, dummy_buffer, 0, VOLUME_NAME_DOS); if (required_size == 0) { DWORD error = GetLastError(); CloseHandle(file_handle); SetLastError(error); return nullptr; } const auto canonical_path = std::make_unique(required_size); int result_size = GetFinalPathNameByHandle(file_handle, canonical_path.get(), required_size, VOLUME_NAME_DOS); ASSERT(result_size <= required_size - 1); CloseHandle(file_handle); // Remove leading \\?\ since it is only to overcome MAX_PATH limitation. // Leave it if input used it though. int offset = 0; if ((result_size > 4) && (wcsncmp(canonical_path.get(), L"\\\\?\\", 4) == 0) && (strncmp(pathname, "\\\\?\\", 4) != 0)) { if ((result_size > 8) && (wcsncmp(canonical_path.get(), L"\\\\?\\UNC\\", 8) == 0)) { // Leave '\\?\UNC\' prefix intact - stripping it makes invalid UNC name. } else { offset = 4; } } int utf8_size = WideCharToMultiByte(CP_UTF8, 0, canonical_path.get() + offset, -1, nullptr, 0, nullptr, nullptr); if (dest == nullptr) { dest = DartUtils::ScopedCString(utf8_size); dest_size = utf8_size; } if (dest_size != 0) { ASSERT(utf8_size <= dest_size); } if (0 == WideCharToMultiByte(CP_UTF8, 0, canonical_path.get() + offset, -1, dest, dest_size, nullptr, nullptr)) { return nullptr; } return dest; } const char* File::PathSeparator() { // This is already UTF-8 encoded. return "\\"; } const char* File::StringEscapedPathSeparator() { // This is already UTF-8 encoded. return "\\\\"; } File::StdioHandleType File::GetStdioHandleType(int fd) { // Treat all stdio handles as pipes. The Windows event handler and // socket code will handle the different handle types. return kPipe; } static BOOL GetReparsePointTag(const wchar_t* path, DWORD* tag) { WIN32_FIND_DATA data; HANDLE result = FindFirstFileW(path, &data); if (result == INVALID_HANDLE_VALUE) { return FALSE; } FindClose(result); if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { // The meaning of |dwReserved0| is documented in // https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags *tag = data.dwReserved0; return TRUE; } return FALSE; } File::Type File::GetType(const wchar_t* path, bool follow_links) { DWORD attributes = GetFileAttributesW(path); if (attributes == INVALID_FILE_ATTRIBUTES) { return File::kDoesNotExist; } else if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { DWORD tag; if (!GetReparsePointTag(path, &tag)) { return File::kDoesNotExist; } // We treat only Unix domain sockets specially - all other reparse points // we treat as links, even though there are many other types of them. See: // https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags if (tag == IO_REPARSE_TAG_AF_UNIX) { return File::kIsSock; } if (follow_links) { HANDLE target_handle = CreateFileW( path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (target_handle == INVALID_HANDLE_VALUE) { return File::kDoesNotExist; } else { BY_HANDLE_FILE_INFORMATION info; if (!GetFileInformationByHandle(target_handle, &info)) { CloseHandle(target_handle); return File::kDoesNotExist; } CloseHandle(target_handle); return ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) ? File::kIsDirectory : File::kIsFile; } } else { return File::kIsLink; } } else if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { return File::kIsDirectory; } return File::kIsFile; } File::Type File::GetType(Namespace* namespc, const char* name, bool follow_links) { const auto path = ToWinAPIPath(name); if (path.get() == nullptr) { return File::kDoesNotExist; } return GetType(path.get(), follow_links); } File::Identical File::AreIdentical(Namespace* namespc_1, const char* file_1, Namespace* namespc_2, const char* file_2) { USE(namespc_1); USE(namespc_2); BY_HANDLE_FILE_INFORMATION file_info[2]; const std::unique_ptr file_names[2] = {ToWinAPIPath(file_1), ToWinAPIPath(file_2)}; if (file_names[0].get() == nullptr || file_names[1].get() == nullptr) { return File::kError; } for (int i = 0; i < 2; ++i) { HANDLE file_handle = CreateFileW( file_names[i].get(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr); if (file_handle == INVALID_HANDLE_VALUE) { return File::kError; } int result = GetFileInformationByHandle(file_handle, &file_info[i]); if (result == 0) { DWORD error = GetLastError(); CloseHandle(file_handle); SetLastError(error); return File::kError; } if (CloseHandle(file_handle) == 0) { return File::kError; } } if ((file_info[0].dwVolumeSerialNumber == file_info[1].dwVolumeSerialNumber) && (file_info[0].nFileIndexHigh == file_info[1].nFileIndexHigh) && (file_info[0].nFileIndexLow == file_info[1].nFileIndexLow)) { return kIdentical; } else { return kDifferent; } } } // namespace bin } // namespace dart #endif // defined(DART_HOST_OS_WINDOWS)