diff options
author | FearlessTobi <thm.frey@gmail.com> | 2024-01-18 23:08:37 +0100 |
---|---|---|
committer | Liam <byteslice@airmail.cc> | 2024-01-25 22:42:06 +0100 |
commit | 2c049ae06dbbdfff7542c23ca4d748879f4beb71 (patch) | |
tree | f93c8be5b6d1781a69d8c28e6aa3500d34c8b55b /src/core/file_sys | |
parent | result: Make fully constexpr, add ON_RESULT_INCLUDED (diff) | |
download | yuzu-2c049ae06dbbdfff7542c23ca4d748879f4beb71.tar yuzu-2c049ae06dbbdfff7542c23ca4d748879f4beb71.tar.gz yuzu-2c049ae06dbbdfff7542c23ca4d748879f4beb71.tar.bz2 yuzu-2c049ae06dbbdfff7542c23ca4d748879f4beb71.tar.lz yuzu-2c049ae06dbbdfff7542c23ca4d748879f4beb71.tar.xz yuzu-2c049ae06dbbdfff7542c23ca4d748879f4beb71.tar.zst yuzu-2c049ae06dbbdfff7542c23ca4d748879f4beb71.zip |
Diffstat (limited to 'src/core/file_sys')
-rw-r--r-- | src/core/file_sys/errors.h | 11 | ||||
-rw-r--r-- | src/core/file_sys/fs_file.h | 63 | ||||
-rw-r--r-- | src/core/file_sys/fs_memory_management.h | 48 | ||||
-rw-r--r-- | src/core/file_sys/fs_operate_range.h | 20 | ||||
-rw-r--r-- | src/core/file_sys/fs_path.h | 570 | ||||
-rw-r--r-- | src/core/file_sys/fs_path_utility.h | 1240 | ||||
-rw-r--r-- | src/core/file_sys/fs_string_util.h | 241 |
7 files changed, 2193 insertions, 0 deletions
diff --git a/src/core/file_sys/errors.h b/src/core/file_sys/errors.h index 7134445cf..d4e0eb6f4 100644 --- a/src/core/file_sys/errors.h +++ b/src/core/file_sys/errors.h @@ -73,10 +73,21 @@ constexpr Result ResultUnexpectedInCompressedStorageA{ErrorModule::FS, 5324}; constexpr Result ResultUnexpectedInCompressedStorageB{ErrorModule::FS, 5325}; constexpr Result ResultUnexpectedInCompressedStorageC{ErrorModule::FS, 5326}; constexpr Result ResultUnexpectedInCompressedStorageD{ErrorModule::FS, 5327}; +constexpr Result ResultUnexpectedInPathA{ErrorModule::FS, 5328}; constexpr Result ResultInvalidArgument{ErrorModule::FS, 6001}; +constexpr Result ResultInvalidPath{ErrorModule::FS, 6002}; +constexpr Result ResultTooLongPath{ErrorModule::FS, 6003}; +constexpr Result ResultInvalidCharacter{ErrorModule::FS, 6004}; +constexpr Result ResultInvalidPathFormat{ErrorModule::FS, 6005}; +constexpr Result ResultDirectoryUnobtainable{ErrorModule::FS, 6006}; +constexpr Result ResultNotNormalized{ErrorModule::FS, 6007}; constexpr Result ResultInvalidOffset{ErrorModule::FS, 6061}; constexpr Result ResultInvalidSize{ErrorModule::FS, 6062}; constexpr Result ResultNullptrArgument{ErrorModule::FS, 6063}; +constexpr Result ResultInvalidOpenMode{ErrorModule::FS, 6072}; +constexpr Result ResultFileExtensionWithoutOpenModeAllowAppend{ErrorModule::FS, 6201}; +constexpr Result ResultReadNotPermitted{ErrorModule::FS, 6202}; +constexpr Result ResultWriteNotPermitted{ErrorModule::FS, 6203}; constexpr Result ResultUnsupportedSetSizeForIndirectStorage{ErrorModule::FS, 6325}; constexpr Result ResultUnsupportedWriteForCompressedStorage{ErrorModule::FS, 6387}; constexpr Result ResultUnsupportedOperateRangeForCompressedStorage{ErrorModule::FS, 6388}; diff --git a/src/core/file_sys/fs_file.h b/src/core/file_sys/fs_file.h new file mode 100644 index 000000000..35f66b959 --- /dev/null +++ b/src/core/file_sys/fs_file.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +namespace FileSys { + +struct ReadOption { + u32 _value; + + static const ReadOption None; +}; + +enum ReadOptionFlag : u32 { + ReadOptionFlag_None = (0 << 0), +}; + +inline constexpr const ReadOption ReadOption::None = {ReadOptionFlag_None}; + +inline constexpr bool operator==(const ReadOption& lhs, const ReadOption& rhs) { + return lhs._value == rhs._value; +} + +inline constexpr bool operator!=(const ReadOption& lhs, const ReadOption& rhs) { + return !(lhs == rhs); +} + +static_assert(sizeof(ReadOption) == sizeof(u32)); + +enum WriteOptionFlag : u32 { + WriteOptionFlag_None = (0 << 0), + WriteOptionFlag_Flush = (1 << 0), +}; + +struct WriteOption { + u32 _value; + + constexpr inline bool HasFlushFlag() const { + return _value & WriteOptionFlag_Flush; + } + + static const WriteOption None; + static const WriteOption Flush; +}; + +inline constexpr const WriteOption WriteOption::None = {WriteOptionFlag_None}; +inline constexpr const WriteOption WriteOption::Flush = {WriteOptionFlag_Flush}; + +inline constexpr bool operator==(const WriteOption& lhs, const WriteOption& rhs) { + return lhs._value == rhs._value; +} + +inline constexpr bool operator!=(const WriteOption& lhs, const WriteOption& rhs) { + return !(lhs == rhs); +} + +static_assert(sizeof(WriteOption) == sizeof(u32)); + +struct FileHandle { + void* handle; +}; + +} // namespace FileSys diff --git a/src/core/file_sys/fs_memory_management.h b/src/core/file_sys/fs_memory_management.h new file mode 100644 index 000000000..596143ba9 --- /dev/null +++ b/src/core/file_sys/fs_memory_management.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <mutex> +#include "common/alignment.h" + +namespace FileSys { + +std::mutex g_mutex; + +constexpr size_t RequiredAlignment = alignof(u64); + +void* AllocateUnsafe(size_t size) { + /* Allocate. */ + void* const ptr = ::operator new(size, std::align_val_t{RequiredAlignment}); + + /* Check alignment. */ + ASSERT(Common::IsAligned(reinterpret_cast<uintptr_t>(ptr), RequiredAlignment)); + + /* Return allocated pointer. */ + return ptr; +} + +void DeallocateUnsafe(void* ptr, size_t size) { + /* Deallocate the pointer. */ + ::operator delete(ptr, std::align_val_t{RequiredAlignment}); +} + +void* Allocate(size_t size) { + /* Lock the allocator. */ + std::scoped_lock lk(g_mutex); + + return AllocateUnsafe(size); +} + +void Deallocate(void* ptr, size_t size) { + /* If the pointer is non-null, deallocate it. */ + if (ptr != nullptr) { + /* Lock the allocator. */ + std::scoped_lock lk(g_mutex); + + DeallocateUnsafe(ptr, size); + } +} + +} // namespace FileSys diff --git a/src/core/file_sys/fs_operate_range.h b/src/core/file_sys/fs_operate_range.h new file mode 100644 index 000000000..750999907 --- /dev/null +++ b/src/core/file_sys/fs_operate_range.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +namespace FileSys { + +enum class OperationId : s64 { + FillZero = 0, + DestroySignature = 1, + Invalidate = 2, + QueryRange = 3, + QueryUnpreparedRange = 4, + QueryLazyLoadCompletionRate = 5, + SetLazyLoadPriority = 6, + + ReadLazyLoadFileForciblyForDebug = 10001, +}; + +} // namespace FileSys diff --git a/src/core/file_sys/fs_path.h b/src/core/file_sys/fs_path.h new file mode 100644 index 000000000..9ea452644 --- /dev/null +++ b/src/core/file_sys/fs_path.h @@ -0,0 +1,570 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/alignment.h" +#include "common/common_funcs.h" +#include "core/file_sys/errors.h" +#include "core/file_sys/fs_memory_management.h" +#include "core/file_sys/fs_path_utility.h" +#include "core/file_sys/fs_string_util.h" +#include "core/hle/result.h" + +namespace FileSys { +class DirectoryPathParser; + +class Path { + YUZU_NON_COPYABLE(Path); + YUZU_NON_MOVEABLE(Path); + +private: + static constexpr const char* EmptyPath = ""; + static constexpr size_t WriteBufferAlignmentLength = 8; + +private: + friend class DirectoryPathParser; + +public: + class WriteBuffer { + YUZU_NON_COPYABLE(WriteBuffer); + + private: + char* m_buffer; + size_t m_length_and_is_normalized; + + public: + constexpr WriteBuffer() : m_buffer(nullptr), m_length_and_is_normalized(0) { /* ... */ + } + + constexpr ~WriteBuffer() { + if (m_buffer != nullptr) { + Deallocate(m_buffer, this->GetLength()); + this->ResetBuffer(); + } + } + + constexpr WriteBuffer(WriteBuffer&& rhs) + : m_buffer(rhs.m_buffer), m_length_and_is_normalized(rhs.m_length_and_is_normalized) { + rhs.ResetBuffer(); + } + + constexpr WriteBuffer& operator=(WriteBuffer&& rhs) { + if (m_buffer != nullptr) { + Deallocate(m_buffer, this->GetLength()); + } + + m_buffer = rhs.m_buffer; + m_length_and_is_normalized = rhs.m_length_and_is_normalized; + + rhs.ResetBuffer(); + + return *this; + } + + constexpr void ResetBuffer() { + m_buffer = nullptr; + this->SetLength(0); + } + + constexpr char* Get() const { + return m_buffer; + } + + constexpr size_t GetLength() const { + return m_length_and_is_normalized >> 1; + } + + constexpr bool IsNormalized() const { + return static_cast<bool>(m_length_and_is_normalized & 1); + } + + constexpr void SetNormalized() { + m_length_and_is_normalized |= static_cast<size_t>(1); + } + + constexpr void SetNotNormalized() { + m_length_and_is_normalized &= ~static_cast<size_t>(1); + } + + private: + constexpr WriteBuffer(char* buffer, size_t length) + : m_buffer(buffer), m_length_and_is_normalized(0) { + this->SetLength(length); + } + + public: + static WriteBuffer Make(size_t length) { + if (void* alloc = Allocate(length); alloc != nullptr) { + return WriteBuffer(static_cast<char*>(alloc), length); + } else { + return WriteBuffer(); + } + } + + private: + constexpr void SetLength(size_t size) { + m_length_and_is_normalized = (m_length_and_is_normalized & 1) | (size << 1); + } + }; + +private: + const char* m_str; + WriteBuffer m_write_buffer; + +public: + constexpr Path() : m_str(EmptyPath), m_write_buffer() { + /* ... */ + } + + constexpr Path(const char* s) : m_str(s), m_write_buffer() { + m_write_buffer.SetNormalized(); + } + + constexpr ~Path() { /* ... */ + } + + constexpr Result SetShallowBuffer(const char* buffer) { + /* Check pre-conditions. */ + ASSERT(m_write_buffer.GetLength() == 0); + + /* Check the buffer is valid. */ + R_UNLESS(buffer != nullptr, ResultNullptrArgument); + + /* Set buffer. */ + this->SetReadOnlyBuffer(buffer); + + /* Note that we're normalized. */ + this->SetNormalized(); + + R_SUCCEED(); + } + + constexpr const char* GetString() const { + /* Check pre-conditions. */ + ASSERT(this->IsNormalized()); + + return m_str; + } + + constexpr size_t GetLength() const { + if (std::is_constant_evaluated()) { + return Strlen(this->GetString()); + } else { + return std::strlen(this->GetString()); + } + } + + constexpr bool IsEmpty() const { + return *m_str == '\x00'; + } + + constexpr bool IsMatchHead(const char* p, size_t len) const { + return Strncmp(this->GetString(), p, len) == 0; + } + + Result Initialize(const Path& rhs) { + /* Check the other path is normalized. */ + const bool normalized = rhs.IsNormalized(); + R_UNLESS(normalized, ResultNotNormalized); + + /* Allocate buffer for our path. */ + const auto len = rhs.GetLength(); + R_TRY(this->Preallocate(len + 1)); + + /* Copy the path. */ + const size_t copied = Strlcpy<char>(m_write_buffer.Get(), rhs.GetString(), len + 1); + R_UNLESS(copied == len, ResultUnexpectedInPathA); + + /* Set normalized. */ + this->SetNormalized(); + R_SUCCEED(); + } + + Result Initialize(const char* path, size_t len) { + /* Check the path is valid. */ + R_UNLESS(path != nullptr, ResultNullptrArgument); + + /* Initialize. */ + R_TRY(this->InitializeImpl(path, len)); + + /* Set not normalized. */ + this->SetNotNormalized(); + + R_SUCCEED(); + } + + Result Initialize(const char* path) { + /* Check the path is valid. */ + R_UNLESS(path != nullptr, ResultNullptrArgument); + + R_RETURN(this->Initialize(path, std::strlen(path))); + } + + Result InitializeWithReplaceBackslash(const char* path) { + /* Check the path is valid. */ + R_UNLESS(path != nullptr, ResultNullptrArgument); + + /* Initialize. */ + R_TRY(this->InitializeImpl(path, std::strlen(path))); + + /* Replace slashes as desired. */ + if (const auto write_buffer_length = m_write_buffer.GetLength(); write_buffer_length > 1) { + Replace(m_write_buffer.Get(), write_buffer_length - 1, '\\', '/'); + } + + /* Set not normalized. */ + this->SetNotNormalized(); + + R_SUCCEED(); + } + + Result InitializeWithReplaceForwardSlashes(const char* path) { + /* Check the path is valid. */ + R_UNLESS(path != nullptr, ResultNullptrArgument); + + /* Initialize. */ + R_TRY(this->InitializeImpl(path, std::strlen(path))); + + /* Replace slashes as desired. */ + if (m_write_buffer.GetLength() > 1) { + if (auto* p = m_write_buffer.Get(); p[0] == '/' && p[1] == '/') { + p[0] = '\\'; + p[1] = '\\'; + } + } + + /* Set not normalized. */ + this->SetNotNormalized(); + + R_SUCCEED(); + } + + Result InitializeWithNormalization(const char* path, size_t size) { + /* Check the path is valid. */ + R_UNLESS(path != nullptr, ResultNullptrArgument); + + /* Initialize. */ + R_TRY(this->InitializeImpl(path, size)); + + /* Set not normalized. */ + this->SetNotNormalized(); + + /* Perform normalization. */ + PathFlags path_flags; + if (IsPathRelative(m_str)) { + path_flags.AllowRelativePath(); + } else if (IsWindowsPath(m_str, true)) { + path_flags.AllowWindowsPath(); + } else { + /* NOTE: In this case, Nintendo checks is normalized, then sets is normalized, then + * returns success. */ + /* This seems like a bug. */ + size_t dummy; + bool normalized; + R_TRY(PathFormatter::IsNormalized(std::addressof(normalized), std::addressof(dummy), + m_str)); + + this->SetNormalized(); + R_SUCCEED(); + } + + /* Normalize. */ + R_TRY(this->Normalize(path_flags)); + + this->SetNormalized(); + R_SUCCEED(); + } + + Result InitializeWithNormalization(const char* path) { + /* Check the path is valid. */ + R_UNLESS(path != nullptr, ResultNullptrArgument); + + R_RETURN(this->InitializeWithNormalization(path, std::strlen(path))); + } + + Result InitializeAsEmpty() { + /* Clear our buffer. */ + this->ClearBuffer(); + + /* Set normalized. */ + this->SetNormalized(); + + R_SUCCEED(); + } + + Result AppendChild(const char* child) { + /* Check the path is valid. */ + R_UNLESS(child != nullptr, ResultNullptrArgument); + + /* Basic checks. If we hvea a path and the child is empty, we have nothing to do. */ + const char* c = child; + if (m_str[0]) { + /* Skip an early separator. */ + if (*c == '/') { + ++c; + } + + R_SUCCEED_IF(*c == '\x00'); + } + + /* If we don't have a string, we can just initialize. */ + auto cur_len = std::strlen(m_str); + if (cur_len == 0) { + R_RETURN(this->Initialize(child)); + } + + /* Remove a trailing separator. */ + if (m_str[cur_len - 1] == '/' || m_str[cur_len - 1] == '\\') { + --cur_len; + } + + /* Get the child path's length. */ + auto child_len = std::strlen(c); + + /* Reset our write buffer. */ + WriteBuffer old_write_buffer; + if (m_write_buffer.Get() != nullptr) { + old_write_buffer = std::move(m_write_buffer); + this->ClearBuffer(); + } + + /* Pre-allocate the new buffer. */ + R_TRY(this->Preallocate(cur_len + 1 + child_len + 1)); + + /* Get our write buffer. */ + auto* dst = m_write_buffer.Get(); + if (old_write_buffer.Get() != nullptr && cur_len > 0) { + Strlcpy<char>(dst, old_write_buffer.Get(), cur_len + 1); + } + + /* Add separator. */ + dst[cur_len] = '/'; + + /* Copy the child path. */ + const size_t copied = Strlcpy<char>(dst + cur_len + 1, c, child_len + 1); + R_UNLESS(copied == child_len, ResultUnexpectedInPathA); + + R_SUCCEED(); + } + + Result AppendChild(const Path& rhs) { + R_RETURN(this->AppendChild(rhs.GetString())); + } + + Result Combine(const Path& parent, const Path& child) { + /* Get the lengths. */ + const auto p_len = parent.GetLength(); + const auto c_len = child.GetLength(); + + /* Allocate our buffer. */ + R_TRY(this->Preallocate(p_len + c_len + 1)); + + /* Initialize as parent. */ + R_TRY(this->Initialize(parent)); + + /* If we're empty, we can just initialize as child. */ + if (this->IsEmpty()) { + R_TRY(this->Initialize(child)); + } else { + /* Otherwise, we should append the child. */ + R_TRY(this->AppendChild(child)); + } + + R_SUCCEED(); + } + + Result RemoveChild() { + /* If we don't have a write-buffer, ensure that we have one. */ + if (m_write_buffer.Get() == nullptr) { + if (const auto len = std::strlen(m_str); len > 0) { + R_TRY(this->Preallocate(len)); + Strlcpy<char>(m_write_buffer.Get(), m_str, len + 1); + } + } + + /* Check that it's possible for us to remove a child. */ + auto* p = m_write_buffer.Get(); + s32 len = std::strlen(p); + R_UNLESS(len != 1 || (p[0] != '/' && p[0] != '.'), ResultNotImplemented); + + /* Handle a trailing separator. */ + if (len > 0 && (p[len - 1] == '\\' || p[len - 1] == '/')) { + --len; + } + + /* Remove the child path segment. */ + while ((--len) >= 0 && p[len]) { + if (p[len] == '/' || p[len] == '\\') { + if (len > 0) { + p[len] = 0; + } else { + p[1] = 0; + len = 1; + } + break; + } + } + + /* Check that length remains > 0. */ + R_UNLESS(len > 0, ResultNotImplemented); + + R_SUCCEED(); + } + + Result Normalize(const PathFlags& flags) { + /* If we're already normalized, nothing to do. */ + R_SUCCEED_IF(this->IsNormalized()); + + /* Check if we're normalized. */ + bool normalized; + size_t dummy; + R_TRY(PathFormatter::IsNormalized(std::addressof(normalized), std::addressof(dummy), m_str, + flags)); + + /* If we're not normalized, normalize. */ + if (!normalized) { + /* Determine necessary buffer length. */ + auto len = m_write_buffer.GetLength(); + if (flags.IsRelativePathAllowed() && IsPathRelative(m_str)) { + len += 2; + } + if (flags.IsWindowsPathAllowed() && IsWindowsPath(m_str, true)) { + len += 1; + } + + /* Allocate a new buffer. */ + const size_t size = Common::AlignUp(len, WriteBufferAlignmentLength); + auto buf = WriteBuffer::Make(size); + R_UNLESS(buf.Get() != nullptr, ResultAllocationMemoryFailedMakeUnique); + + /* Normalize into it. */ + R_TRY(PathFormatter::Normalize(buf.Get(), size, m_write_buffer.Get(), + m_write_buffer.GetLength(), flags)); + + /* Set the normalized buffer as our buffer. */ + this->SetModifiableBuffer(std::move(buf)); + } + + /* Set normalized. */ + this->SetNormalized(); + R_SUCCEED(); + } + +private: + void ClearBuffer() { + m_write_buffer.ResetBuffer(); + m_str = EmptyPath; + } + + void SetModifiableBuffer(WriteBuffer&& buffer) { + /* Check pre-conditions. */ + ASSERT(buffer.Get() != nullptr); + ASSERT(buffer.GetLength() > 0); + ASSERT(Common::IsAligned(buffer.GetLength(), WriteBufferAlignmentLength)); + + /* Get whether we're normalized. */ + if (m_write_buffer.IsNormalized()) { + buffer.SetNormalized(); + } else { + buffer.SetNotNormalized(); + } + + /* Set write buffer. */ + m_write_buffer = std::move(buffer); + m_str = m_write_buffer.Get(); + } + + constexpr void SetReadOnlyBuffer(const char* buffer) { + m_str = buffer; + m_write_buffer.ResetBuffer(); + } + + Result Preallocate(size_t length) { + /* Allocate additional space, if needed. */ + if (length > m_write_buffer.GetLength()) { + /* Allocate buffer. */ + const size_t size = Common::AlignUp(length, WriteBufferAlignmentLength); + auto buf = WriteBuffer::Make(size); + R_UNLESS(buf.Get() != nullptr, ResultAllocationMemoryFailedMakeUnique); + + /* Set write buffer. */ + this->SetModifiableBuffer(std::move(buf)); + } + + R_SUCCEED(); + } + + Result InitializeImpl(const char* path, size_t size) { + if (size > 0 && path[0]) { + /* Pre allocate a buffer for the path. */ + R_TRY(this->Preallocate(size + 1)); + + /* Copy the path. */ + const size_t copied = Strlcpy<char>(m_write_buffer.Get(), path, size + 1); + R_UNLESS(copied >= size, ResultUnexpectedInPathA); + } else { + /* We can just clear the buffer. */ + this->ClearBuffer(); + } + + R_SUCCEED(); + } + + constexpr char* GetWriteBuffer() { + ASSERT(m_write_buffer.Get() != nullptr); + return m_write_buffer.Get(); + } + + constexpr size_t GetWriteBufferLength() const { + return m_write_buffer.GetLength(); + } + + constexpr bool IsNormalized() const { + return m_write_buffer.IsNormalized(); + } + + constexpr void SetNormalized() { + m_write_buffer.SetNormalized(); + } + + constexpr void SetNotNormalized() { + m_write_buffer.SetNotNormalized(); + } + +public: + bool operator==(const FileSys::Path& rhs) const { + return std::strcmp(this->GetString(), rhs.GetString()) == 0; + } + bool operator!=(const FileSys::Path& rhs) const { + return !(*this == rhs); + } + bool operator==(const char* p) const { + return std::strcmp(this->GetString(), p) == 0; + } + bool operator!=(const char* p) const { + return !(*this == p); + } +}; + +inline Result SetUpFixedPath(FileSys::Path* out, const char* s) { + /* Verify the path is normalized. */ + bool normalized; + size_t dummy; + R_TRY(PathNormalizer::IsNormalized(std::addressof(normalized), std::addressof(dummy), s)); + + R_UNLESS(normalized, ResultInvalidPathFormat); + + /* Set the fixed path. */ + R_RETURN(out->SetShallowBuffer(s)); +} + +constexpr inline bool IsWindowsDriveRootPath(const FileSys::Path& path) { + const char* const str = path.GetString(); + return IsWindowsDrive(str) && + (str[2] == StringTraits::DirectorySeparator || + str[2] == StringTraits::AlternateDirectorySeparator) && + str[3] == StringTraits::NullTerminator; +} + +} // namespace FileSys diff --git a/src/core/file_sys/fs_path_utility.h b/src/core/file_sys/fs_path_utility.h new file mode 100644 index 000000000..276445707 --- /dev/null +++ b/src/core/file_sys/fs_path_utility.h @@ -0,0 +1,1240 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/assert.h" +#include "common/common_types.h" +#include "common/scope_exit.h" +#include "core/file_sys/fs_directory.h" +#include "core/file_sys/fs_memory_management.h" +#include "core/file_sys/fs_string_util.h" +#include "core/hle/result.h" + +namespace FileSys { + +constexpr inline size_t MountNameLengthMax = 15; +using Result = ::Result; + +namespace StringTraits { + +constexpr inline char DirectorySeparator = '/'; +constexpr inline char DriveSeparator = ':'; +constexpr inline char Dot = '.'; +constexpr inline char NullTerminator = '\x00'; + +constexpr inline char AlternateDirectorySeparator = '\\'; + +constexpr inline const char InvalidCharacters[6] = {':', '*', '?', '<', '>', '|'}; +constexpr inline const char InvalidCharactersForHostName[6] = {':', '*', '<', '>', '|', '$'}; +constexpr inline const char InvalidCharactersForMountName[5] = {'*', '?', '<', '>', '|'}; + +namespace impl { + +template <const char* InvalidCharacterSet, size_t NumInvalidCharacters> +consteval u64 MakeInvalidCharacterMask(size_t n) { + u64 mask = 0; + for (size_t i = 0; i < NumInvalidCharacters; ++i) { + if ((static_cast<u64>(InvalidCharacterSet[i]) >> 6) == n) { + mask |= static_cast<u64>(1) << (static_cast<u64>(InvalidCharacterSet[i]) & 0x3F); + } + } + return mask; +} + +template <const char* InvalidCharacterSet, size_t NumInvalidCharacters> +constexpr bool IsInvalidCharacterImpl(char c) { + constexpr u64 Masks[4] = { + MakeInvalidCharacterMask<InvalidCharacterSet, NumInvalidCharacters>(0), + MakeInvalidCharacterMask<InvalidCharacterSet, NumInvalidCharacters>(1), + MakeInvalidCharacterMask<InvalidCharacterSet, NumInvalidCharacters>(2), + MakeInvalidCharacterMask<InvalidCharacterSet, NumInvalidCharacters>(3)}; + + return (Masks[static_cast<u64>(c) >> 6] & + (static_cast<u64>(1) << (static_cast<u64>(c) & 0x3F))) != 0; +} + +} // namespace impl + +constexpr bool IsInvalidCharacter(char c) { + return impl::IsInvalidCharacterImpl<InvalidCharacters, size(InvalidCharacters)>(c); +} +constexpr bool IsInvalidCharacterForHostName(char c) { + return impl::IsInvalidCharacterImpl<InvalidCharactersForHostName, + size(InvalidCharactersForHostName)>(c); +} +constexpr bool IsInvalidCharacterForMountName(char c) { + return impl::IsInvalidCharacterImpl<InvalidCharactersForMountName, + size(InvalidCharactersForMountName)>(c); +} + +} // namespace StringTraits + +constexpr inline size_t WindowsDriveLength = 2; +constexpr inline size_t UncPathPrefixLength = 2; +constexpr inline size_t DosDevicePathPrefixLength = 4; + +class PathFlags { +private: + static constexpr u32 WindowsPathFlag = (1 << 0); + static constexpr u32 RelativePathFlag = (1 << 1); + static constexpr u32 EmptyPathFlag = (1 << 2); + static constexpr u32 MountNameFlag = (1 << 3); + static constexpr u32 BackslashFlag = (1 << 4); + static constexpr u32 AllCharactersFlag = (1 << 5); + +private: + u32 m_value; + +public: + constexpr PathFlags() : m_value(0) { /* ... */ + } + +#define DECLARE_PATH_FLAG_HANDLER(__WHICH__) \ + constexpr bool Is##__WHICH__##Allowed() const { return (m_value & __WHICH__##Flag) != 0; } \ + constexpr void Allow##__WHICH__() { m_value |= __WHICH__##Flag; } + + DECLARE_PATH_FLAG_HANDLER(WindowsPath) + DECLARE_PATH_FLAG_HANDLER(RelativePath) + DECLARE_PATH_FLAG_HANDLER(EmptyPath) + DECLARE_PATH_FLAG_HANDLER(MountName) + DECLARE_PATH_FLAG_HANDLER(Backslash) + DECLARE_PATH_FLAG_HANDLER(AllCharacters) + +#undef DECLARE_PATH_FLAG_HANDLER +}; + +template <typename T> + requires(std::same_as<T, char> || std::same_as<T, wchar_t>) +constexpr inline bool IsDosDevicePath(const T* path) { + ASSERT(path != nullptr); + + using namespace StringTraits; + + return path[0] == AlternateDirectorySeparator && path[1] == AlternateDirectorySeparator && + (path[2] == Dot || path[2] == '?') && + (path[3] == DirectorySeparator || path[3] == AlternateDirectorySeparator); +} + +template <typename T> + requires(std::same_as<T, char> || std::same_as<T, wchar_t>) +constexpr inline bool IsUncPath(const T* path, bool allow_forward_slash = true, + bool allow_back_slash = true) { + ASSERT(path != nullptr); + + using namespace StringTraits; + + return (allow_forward_slash && path[0] == DirectorySeparator && + path[1] == DirectorySeparator) || + (allow_back_slash && path[0] == AlternateDirectorySeparator && + path[1] == AlternateDirectorySeparator); +} + +constexpr inline bool IsWindowsDrive(const char* path) { + ASSERT(path != nullptr); + + return (('a' <= path[0] && path[0] <= 'z') || ('A' <= path[0] && path[0] <= 'Z')) && + path[1] == StringTraits::DriveSeparator; +} + +constexpr inline bool IsWindowsPath(const char* path, bool allow_forward_slash_unc) { + return IsWindowsDrive(path) || IsDosDevicePath(path) || + IsUncPath(path, allow_forward_slash_unc, true); +} + +constexpr inline int GetWindowsSkipLength(const char* path) { + if (IsDosDevicePath(path)) { + return DosDevicePathPrefixLength; + } else if (IsWindowsDrive(path)) { + return WindowsDriveLength; + } else if (IsUncPath(path)) { + return UncPathPrefixLength; + } else { + return 0; + } +} + +constexpr inline bool IsPathAbsolute(const char* path) { + return IsWindowsPath(path, false) || path[0] == StringTraits::DirectorySeparator; +} + +constexpr inline bool IsPathRelative(const char* path) { + return path[0] && !IsPathAbsolute(path); +} + +constexpr inline bool IsCurrentDirectory(const char* path) { + return path[0] == StringTraits::Dot && + (path[1] == StringTraits::NullTerminator || path[1] == StringTraits::DirectorySeparator); +} + +constexpr inline bool IsParentDirectory(const char* path) { + return path[0] == StringTraits::Dot && path[1] == StringTraits::Dot && + (path[2] == StringTraits::NullTerminator || path[2] == StringTraits::DirectorySeparator); +} + +constexpr inline bool IsPathStartWithCurrentDirectory(const char* path) { + return IsCurrentDirectory(path) || IsParentDirectory(path); +} + +constexpr inline bool IsSubPath(const char* lhs, const char* rhs) { + /* Check pre-conditions. */ + ASSERT(lhs != nullptr); + ASSERT(rhs != nullptr); + + /* Import StringTraits names for current scope. */ + using namespace StringTraits; + + /* Special case certain paths. */ + if (IsUncPath(lhs) && !IsUncPath(rhs)) { + return false; + } + if (!IsUncPath(lhs) && IsUncPath(rhs)) { + return false; + } + + if (lhs[0] == DirectorySeparator && lhs[1] == NullTerminator && rhs[0] == DirectorySeparator && + rhs[1] != NullTerminator) { + return true; + } + if (rhs[0] == DirectorySeparator && rhs[1] == NullTerminator && lhs[0] == DirectorySeparator && + lhs[1] != NullTerminator) { + return true; + } + + /* Check subpath. */ + for (size_t i = 0; /* ... */; ++i) { + if (lhs[i] == NullTerminator) { + return rhs[i] == DirectorySeparator; + } else if (rhs[i] == NullTerminator) { + return lhs[i] == DirectorySeparator; + } else if (lhs[i] != rhs[i]) { + return false; + } + } +} + +/* Path utilities. */ +constexpr inline void Replace(char* dst, size_t dst_size, char old_char, char new_char) { + ASSERT(dst != nullptr); + for (char* cur = dst; cur < dst + dst_size && *cur; ++cur) { + if (*cur == old_char) { + *cur = new_char; + } + } +} + +constexpr inline Result CheckUtf8(const char* s) { + /* Check pre-conditions. */ + ASSERT(s != nullptr); + + /* Iterate, checking for utf8-validity. */ + while (*s) { + char utf8_buf[4] = {}; + + const auto pick_res = PickOutCharacterFromUtf8String(utf8_buf, std::addressof(s)); + R_UNLESS(pick_res == CharacterEncodingResult_Success, ResultInvalidPathFormat); + + u32 dummy; + const auto cvt_res = ConvertCharacterUtf8ToUtf32(std::addressof(dummy), utf8_buf); + R_UNLESS(cvt_res == CharacterEncodingResult_Success, ResultInvalidPathFormat); + } + + R_SUCCEED(); +} + +/* Path formatting. */ +class PathNormalizer { +private: + enum class PathState { + Start, + Normal, + FirstSeparator, + Separator, + CurrentDir, + ParentDir, + }; + +private: + static constexpr void ReplaceParentDirectoryPath(char* dst, const char* src) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Start with a dir-separator. */ + dst[0] = DirectorySeparator; + + auto i = 1; + while (src[i] != NullTerminator) { + if ((src[i - 1] == DirectorySeparator || src[i - 1] == AlternateDirectorySeparator) && + src[i + 0] == Dot && src[i + 1] == Dot && + (src[i + 2] == DirectorySeparator || src[i + 2] == AlternateDirectorySeparator)) { + dst[i - 1] = DirectorySeparator; + dst[i + 0] = Dot; + dst[i + 1] = Dot; + dst[i + 2] = DirectorySeparator; + i += 3; + } else { + if (src[i - 1] == AlternateDirectorySeparator && src[i + 0] == Dot && + src[i + 1] == Dot && src[i + 2] == NullTerminator) { + dst[i - 1] = DirectorySeparator; + dst[i + 0] = Dot; + dst[i + 1] = Dot; + i += 2; + break; + } + + dst[i] = src[i]; + ++i; + } + } + + dst[i] = StringTraits::NullTerminator; + } + +public: + static constexpr bool IsParentDirectoryPathReplacementNeeded(const char* path) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + if (path[0] != DirectorySeparator && path[0] != AlternateDirectorySeparator) { + return false; + } + + /* Check to find a parent reference using alternate separators. */ + if (path[0] != NullTerminator && path[1] != NullTerminator && path[2] != NullTerminator) { + size_t i; + for (i = 0; path[i + 3] != NullTerminator; ++path) { + if (path[i + 1] != Dot || path[i + 2] != Dot) { + continue; + } + + const char c0 = path[i + 0]; + const char c3 = path[i + 3]; + + if (c0 == AlternateDirectorySeparator && + (c3 == DirectorySeparator || c3 == AlternateDirectorySeparator || + c3 == NullTerminator)) { + return true; + } + + if (c3 == AlternateDirectorySeparator && + (c0 == DirectorySeparator || c0 == AlternateDirectorySeparator)) { + return true; + } + } + + if (path[i + 0] == AlternateDirectorySeparator && path[i + 1] == Dot && + path[i + 2] == Dot /* && path[i + 3] == NullTerminator */) { + return true; + } + } + + return false; + } + + static constexpr Result IsNormalized(bool* out, size_t* out_len, const char* path, + bool allow_all_characters = false) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Parse the path. */ + auto state = PathState::Start; + size_t len = 0; + while (path[len] != NullTerminator) { + /* Get the current character. */ + const char c = path[len++]; + + /* Check the current character is valid. */ + if (!allow_all_characters && state != PathState::Start) { + R_UNLESS(!IsInvalidCharacter(c), ResultInvalidCharacter); + } + + /* Process depending on current state. */ + switch (state) { + /* Import the PathState enums for convenience. */ + using enum PathState; + + case Start: + R_UNLESS(c == DirectorySeparator, ResultInvalidPathFormat); + state = FirstSeparator; + break; + case Normal: + if (c == DirectorySeparator) { + state = Separator; + } + break; + case FirstSeparator: + case Separator: + if (c == DirectorySeparator) { + *out = false; + R_SUCCEED(); + } + + if (c == Dot) { + state = CurrentDir; + } else { + state = Normal; + } + break; + case CurrentDir: + if (c == DirectorySeparator) { + *out = false; + R_SUCCEED(); + } + + if (c == Dot) { + state = ParentDir; + } else { + state = Normal; + } + break; + case ParentDir: + if (c == DirectorySeparator) { + *out = false; + R_SUCCEED(); + } + + state = Normal; + break; + default: + UNREACHABLE(); + break; + } + } + + /* Check the final state. */ + switch (state) { + /* Import the PathState enums for convenience. */ + using enum PathState; + case Start: + R_THROW(ResultInvalidPathFormat); + case Normal: + case FirstSeparator: + *out = true; + break; + case Separator: + case CurrentDir: + case ParentDir: + *out = false; + break; + default: + UNREACHABLE(); + break; + } + + /* Set the output length. */ + *out_len = len; + R_SUCCEED(); + } + + static Result Normalize(char* dst, size_t* out_len, const char* path, size_t max_out_size, + bool is_windows_path, bool is_drive_relative_path, + bool allow_all_characters = false) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Prepare to iterate. */ + const char* cur_path = path; + size_t total_len = 0; + + /* If path begins with a separator, check that we're not drive relative. */ + if (cur_path[0] != DirectorySeparator) { + R_UNLESS(is_drive_relative_path, ResultInvalidPathFormat); + + dst[total_len++] = DirectorySeparator; + } + + /* We're going to need to do path replacement, potentially. */ + char* replacement_path = nullptr; + size_t replacement_path_size = 0; + + SCOPE_EXIT({ + if (replacement_path != nullptr) { + if (std::is_constant_evaluated()) { + delete[] replacement_path; + } else { + Deallocate(replacement_path, replacement_path_size); + } + } + }); + + /* Perform path replacement, if necessary. */ + if (IsParentDirectoryPathReplacementNeeded(cur_path)) { + if (std::is_constant_evaluated()) { + replacement_path_size = EntryNameLengthMax + 1; + replacement_path = new char[replacement_path_size]; + } else { + replacement_path_size = EntryNameLengthMax + 1; + replacement_path = static_cast<char*>(Allocate(replacement_path_size)); + } + + ReplaceParentDirectoryPath(replacement_path, cur_path); + + cur_path = replacement_path; + } + + /* Iterate, normalizing path components. */ + bool skip_next_sep = false; + size_t i = 0; + + while (cur_path[i] != NullTerminator) { + /* Process a directory separator, if we run into one. */ + if (cur_path[i] == DirectorySeparator) { + /* Swallow separators. */ + do { + ++i; + } while (cur_path[i] == DirectorySeparator); + + /* Check if we hit end of string. */ + if (cur_path[i] == NullTerminator) { + break; + } + + /* If we aren't skipping the separator, write it, checking that we remain in bounds. + */ + if (!skip_next_sep) { + if (total_len + 1 == max_out_size) { + dst[total_len] = NullTerminator; + *out_len = total_len; + R_THROW(ResultTooLongPath); + } + + dst[total_len++] = DirectorySeparator; + } + + /* Don't skip the next separator. */ + skip_next_sep = false; + } + + /* Get the length of the current directory component. */ + size_t dir_len = 0; + while (cur_path[i + dir_len] != DirectorySeparator && + cur_path[i + dir_len] != NullTerminator) { + /* Check for validity. */ + if (!allow_all_characters) { + R_UNLESS(!IsInvalidCharacter(cur_path[i + dir_len]), ResultInvalidCharacter); + } + + ++dir_len; + } + + /* Handle the current dir component. */ + if (IsCurrentDirectory(cur_path + i)) { + skip_next_sep = true; + } else if (IsParentDirectory(cur_path + i)) { + /* We should have just written a separator. */ + ASSERT(dst[total_len - 1] == DirectorySeparator); + + /* We should have started with a separator, for non-windows paths. */ + if (!is_windows_path) { + ASSERT(dst[0] == DirectorySeparator); + } + + /* Remove the previous component. */ + if (total_len == 1) { + R_UNLESS(is_windows_path, ResultDirectoryUnobtainable); + + --total_len; + } else { + total_len -= 2; + + do { + if (dst[total_len] == DirectorySeparator) { + break; + } + } while ((--total_len) != 0); + } + + /* We should be pointing to a directory separator, for non-windows paths. */ + if (!is_windows_path) { + ASSERT(dst[total_len] == DirectorySeparator); + } + + /* We should remain in bounds. */ + ASSERT(total_len < max_out_size); + } else { + /* Copy, possibly truncating. */ + if (total_len + dir_len + 1 > max_out_size) { + const size_t copy_len = max_out_size - (total_len + 1); + + for (size_t j = 0; j < copy_len; ++j) { + dst[total_len++] = cur_path[i + j]; + } + + dst[total_len] = NullTerminator; + *out_len = total_len; + R_THROW(ResultTooLongPath); + } + + for (size_t j = 0; j < dir_len; ++j) { + dst[total_len++] = cur_path[i + j]; + } + } + + /* Advance past the current directory component. */ + i += dir_len; + } + + if (skip_next_sep) { + --total_len; + } + + if (total_len == 0 && max_out_size != 0) { + total_len = 1; + dst[0] = DirectorySeparator; + } + + /* NOTE: Probable nintendo bug, as max_out_size must be at least total_len + 1 for the null + * terminator. */ + R_UNLESS(max_out_size >= total_len - 1, ResultTooLongPath); + + dst[total_len] = NullTerminator; + + /* Check that the result path is normalized. */ + bool is_normalized; + size_t dummy; + R_TRY(IsNormalized(std::addressof(is_normalized), std::addressof(dummy), dst, + allow_all_characters)); + + /* Assert that the result path is normalized. */ + ASSERT(is_normalized); + + /* Set the output length. */ + *out_len = total_len; + R_SUCCEED(); + } +}; + +class PathFormatter { +private: + static constexpr Result CheckSharedName(const char* name, size_t len) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + if (len == 1) { + R_UNLESS(name[0] != Dot, ResultInvalidPathFormat); + } else if (len == 2) { + R_UNLESS(name[0] != Dot || name[1] != Dot, ResultInvalidPathFormat); + } + + for (size_t i = 0; i < len; ++i) { + R_UNLESS(!IsInvalidCharacter(name[i]), ResultInvalidCharacter); + } + + R_SUCCEED(); + } + + static constexpr Result CheckHostName(const char* name, size_t len) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + if (len == 2) { + R_UNLESS(name[0] != Dot || name[1] != Dot, ResultInvalidPathFormat); + } + + for (size_t i = 0; i < len; ++i) { + R_UNLESS(!IsInvalidCharacterForHostName(name[i]), ResultInvalidCharacter); + } + + R_SUCCEED(); + } + + static constexpr Result CheckInvalidBackslash(bool* out_contains_backslash, const char* path, + bool allow_backslash) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Default to no backslashes, so we can just write if we see one. */ + *out_contains_backslash = false; + + while (*path != NullTerminator) { + if (*(path++) == AlternateDirectorySeparator) { + *out_contains_backslash = true; + + R_UNLESS(allow_backslash, ResultInvalidCharacter); + } + } + + R_SUCCEED(); + } + +public: + static constexpr Result CheckPathFormat(const char* path, const PathFlags& flags) { + bool normalized; + size_t len; + R_RETURN(IsNormalized(std::addressof(normalized), std::addressof(len), path, flags)); + } + + static constexpr Result SkipMountName(const char** out, size_t* out_len, const char* path) { + R_RETURN(ParseMountName(out, out_len, nullptr, 0, path)); + } + + static constexpr Result ParseMountName(const char** out, size_t* out_len, char* out_mount_name, + size_t out_mount_name_buffer_size, const char* path) { + /* Check pre-conditions. */ + ASSERT(path != nullptr); + ASSERT(out_len != nullptr); + ASSERT(out != nullptr); + ASSERT((out_mount_name == nullptr) == (out_mount_name_buffer_size == 0)); + + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Determine max mount length. */ + const auto max_mount_len = + out_mount_name_buffer_size == 0 + ? MountNameLengthMax + 1 + : std::min(MountNameLengthMax + 1, out_mount_name_buffer_size); + + /* Parse the path until we see a drive separator. */ + size_t mount_len = 0; + for (/* ... */; mount_len < max_mount_len && path[mount_len]; ++mount_len) { + const char c = path[mount_len]; + + /* If we see a drive separator, advance, then we're done with the pre-drive separator + * part of the mount. */ + if (c == DriveSeparator) { + ++mount_len; + break; + } + + /* If we see a directory separator, we're not in a mount name. */ + if (c == DirectorySeparator || c == AlternateDirectorySeparator) { + *out = path; + *out_len = 0; + R_SUCCEED(); + } + } + + /* Check to be sure we're actually looking at a mount name. */ + if (mount_len <= 2 || path[mount_len - 1] != DriveSeparator) { + *out = path; + *out_len = 0; + R_SUCCEED(); + } + + /* Check that all characters in the mount name are allowable. */ + for (size_t i = 0; i < mount_len; ++i) { + R_UNLESS(!IsInvalidCharacterForMountName(path[i]), ResultInvalidCharacter); + } + + /* Copy out the mount name. */ + if (out_mount_name_buffer_size > 0) { + R_UNLESS(mount_len < out_mount_name_buffer_size, ResultTooLongPath); + + for (size_t i = 0; i < mount_len; ++i) { + out_mount_name[i] = path[i]; + } + out_mount_name[mount_len] = NullTerminator; + } + + /* Set the output. */ + *out = path + mount_len; + *out_len = mount_len; + R_SUCCEED(); + } + + static constexpr Result SkipRelativeDotPath(const char** out, size_t* out_len, + const char* path) { + R_RETURN(ParseRelativeDotPath(out, out_len, nullptr, 0, path)); + } + + static constexpr Result ParseRelativeDotPath(const char** out, size_t* out_len, + char* out_relative, + size_t out_relative_buffer_size, + const char* path) { + /* Check pre-conditions. */ + ASSERT(path != nullptr); + ASSERT(out_len != nullptr); + ASSERT(out != nullptr); + ASSERT((out_relative == nullptr) == (out_relative_buffer_size == 0)); + + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Initialize the output buffer, if we have one. */ + if (out_relative_buffer_size > 0) { + out_relative[0] = NullTerminator; + } + + /* Check if the path is relative. */ + if (path[0] == Dot && (path[1] == NullTerminator || path[1] == DirectorySeparator || + path[1] == AlternateDirectorySeparator)) { + if (out_relative_buffer_size > 0) { + R_UNLESS(out_relative_buffer_size >= 2, ResultTooLongPath); + + out_relative[0] = Dot; + out_relative[1] = NullTerminator; + } + + *out = path + 1; + *out_len = 1; + R_SUCCEED(); + } + + /* Ensure the path isn't a parent directory. */ + R_UNLESS(!(path[0] == Dot && path[1] == Dot), ResultDirectoryUnobtainable); + + /* There was no relative dot path. */ + *out = path; + *out_len = 0; + R_SUCCEED(); + } + + static constexpr Result SkipWindowsPath(const char** out, size_t* out_len, bool* out_normalized, + const char* path, bool has_mount_name) { + /* We're normalized if and only if the parsing doesn't throw ResultNotNormalized(). */ + *out_normalized = true; + + R_TRY_CATCH(ParseWindowsPath(out, out_len, nullptr, 0, path, has_mount_name)) { + R_CATCH(ResultNotNormalized) { + *out_normalized = false; + } + } + R_END_TRY_CATCH; + ON_RESULT_INCLUDED(ResultNotNormalized) { + *out_normalized = false; + }; + + R_SUCCEED(); + } + + static constexpr Result ParseWindowsPath(const char** out, size_t* out_len, char* out_win, + size_t out_win_buffer_size, const char* path, + bool has_mount_name) { + /* Check pre-conditions. */ + ASSERT(path != nullptr); + ASSERT(out_len != nullptr); + ASSERT(out != nullptr); + ASSERT((out_win == nullptr) == (out_win_buffer_size == 0)); + + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Initialize the output buffer, if we have one. */ + if (out_win_buffer_size > 0) { + out_win[0] = NullTerminator; + } + + /* Handle path start. */ + const char* cur_path = path; + if (has_mount_name && path[0] == DirectorySeparator) { + if (path[1] == AlternateDirectorySeparator && path[2] == AlternateDirectorySeparator) { + R_UNLESS(out_win_buffer_size > 0, ResultNotNormalized); + + ++cur_path; + } else if (IsWindowsDrive(path + 1)) { + R_UNLESS(out_win_buffer_size > 0, ResultNotNormalized); + + ++cur_path; + } + } + + /* Handle windows drive. */ + if (IsWindowsDrive(cur_path)) { + /* Parse up to separator. */ + size_t win_path_len = WindowsDriveLength; + for (/* ... */; cur_path[win_path_len] != NullTerminator; ++win_path_len) { + R_UNLESS(!IsInvalidCharacter(cur_path[win_path_len]), ResultInvalidCharacter); + + if (cur_path[win_path_len] == DirectorySeparator || + cur_path[win_path_len] == AlternateDirectorySeparator) { + break; + } + } + + /* Ensure that we're normalized, if we're required to be. */ + if (out_win_buffer_size == 0) { + for (size_t i = 0; i < win_path_len; ++i) { + R_UNLESS(cur_path[i] != AlternateDirectorySeparator, ResultNotNormalized); + } + } else { + /* Ensure we can copy into the normalized buffer. */ + R_UNLESS(win_path_len < out_win_buffer_size, ResultTooLongPath); + + for (size_t i = 0; i < win_path_len; ++i) { + out_win[i] = cur_path[i]; + } + out_win[win_path_len] = NullTerminator; + + Replace(out_win, win_path_len, AlternateDirectorySeparator, DirectorySeparator); + } + + *out = cur_path + win_path_len; + *out_len = win_path_len; + R_SUCCEED(); + } + + /* Handle DOS device. */ + if (IsDosDevicePath(cur_path)) { + size_t dos_prefix_len = DosDevicePathPrefixLength; + + if (IsWindowsDrive(cur_path + dos_prefix_len)) { + dos_prefix_len += WindowsDriveLength; + } else { + --dos_prefix_len; + } + + if (out_win_buffer_size > 0) { + /* Ensure we can copy into the normalized buffer. */ + R_UNLESS(dos_prefix_len < out_win_buffer_size, ResultTooLongPath); + + for (size_t i = 0; i < dos_prefix_len; ++i) { + out_win[i] = cur_path[i]; + } + out_win[dos_prefix_len] = NullTerminator; + + Replace(out_win, dos_prefix_len, DirectorySeparator, AlternateDirectorySeparator); + } + + *out = cur_path + dos_prefix_len; + *out_len = dos_prefix_len; + R_SUCCEED(); + } + + /* Handle UNC path. */ + if (IsUncPath(cur_path, false, true)) { + const char* final_path = cur_path; + + R_UNLESS(cur_path[UncPathPrefixLength] != DirectorySeparator, ResultInvalidPathFormat); + R_UNLESS(cur_path[UncPathPrefixLength] != AlternateDirectorySeparator, + ResultInvalidPathFormat); + + size_t cur_component_offset = 0; + size_t pos = UncPathPrefixLength; + for (/* ... */; cur_path[pos] != NullTerminator; ++pos) { + if (cur_path[pos] == DirectorySeparator || + cur_path[pos] == AlternateDirectorySeparator) { + if (cur_component_offset != 0) { + R_TRY(CheckSharedName(cur_path + cur_component_offset, + pos - cur_component_offset)); + + final_path = cur_path + pos; + break; + } + + R_UNLESS(cur_path[pos + 1] != DirectorySeparator, ResultInvalidPathFormat); + R_UNLESS(cur_path[pos + 1] != AlternateDirectorySeparator, + ResultInvalidPathFormat); + + R_TRY(CheckHostName(cur_path + 2, pos - 2)); + + cur_component_offset = pos + 1; + } + } + + R_UNLESS(cur_component_offset != pos, ResultInvalidPathFormat); + + if (cur_component_offset != 0 && final_path == cur_path) { + R_TRY(CheckSharedName(cur_path + cur_component_offset, pos - cur_component_offset)); + + final_path = cur_path + pos; + } + + size_t unc_prefix_len = final_path - cur_path; + + /* Ensure that we're normalized, if we're required to be. */ + if (out_win_buffer_size == 0) { + for (size_t i = 0; i < unc_prefix_len; ++i) { + R_UNLESS(cur_path[i] != DirectorySeparator, ResultNotNormalized); + } + } else { + /* Ensure we can copy into the normalized buffer. */ + R_UNLESS(unc_prefix_len < out_win_buffer_size, ResultTooLongPath); + + for (size_t i = 0; i < unc_prefix_len; ++i) { + out_win[i] = cur_path[i]; + } + out_win[unc_prefix_len] = NullTerminator; + + Replace(out_win, unc_prefix_len, DirectorySeparator, AlternateDirectorySeparator); + } + + *out = cur_path + unc_prefix_len; + *out_len = unc_prefix_len; + R_SUCCEED(); + } + + /* There's no windows path to parse. */ + *out = path; + *out_len = 0; + R_SUCCEED(); + } + + static constexpr Result IsNormalized(bool* out, size_t* out_len, const char* path, + const PathFlags& flags = {}) { + /* Ensure nothing is null. */ + R_UNLESS(out != nullptr, ResultNullptrArgument); + R_UNLESS(out_len != nullptr, ResultNullptrArgument); + R_UNLESS(path != nullptr, ResultNullptrArgument); + + /* Verify that the path is valid utf-8. */ + R_TRY(CheckUtf8(path)); + + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Handle the case where the path is empty. */ + if (path[0] == NullTerminator) { + R_UNLESS(flags.IsEmptyPathAllowed(), ResultInvalidPathFormat); + + *out = true; + *out_len = 0; + R_SUCCEED(); + } + + /* All normalized paths start with a directory separator...unless they're windows paths, + * relative paths, or have mount names. */ + if (path[0] != DirectorySeparator) { + R_UNLESS(flags.IsWindowsPathAllowed() || flags.IsRelativePathAllowed() || + flags.IsMountNameAllowed(), + ResultInvalidPathFormat); + } + + /* Check that the path is allowed to be a windows path, if it is. */ + if (IsWindowsPath(path, false)) { + R_UNLESS(flags.IsWindowsPathAllowed(), ResultInvalidPathFormat); + } + + /* Skip past the mount name, if one is present. */ + size_t total_len = 0; + size_t mount_name_len = 0; + R_TRY(SkipMountName(std::addressof(path), std::addressof(mount_name_len), path)); + + /* If we had a mount name, check that that was allowed. */ + if (mount_name_len > 0) { + R_UNLESS(flags.IsMountNameAllowed(), ResultInvalidPathFormat); + + total_len += mount_name_len; + } + + /* Check that the path starts as a normalized path should. */ + if (path[0] != DirectorySeparator && !IsPathStartWithCurrentDirectory(path) && + !IsWindowsPath(path, false)) { + R_UNLESS(flags.IsRelativePathAllowed(), ResultInvalidPathFormat); + R_UNLESS(!IsInvalidCharacter(path[0]), ResultInvalidPathFormat); + + *out = false; + R_SUCCEED(); + } + + /* Process relative path. */ + size_t relative_len = 0; + R_TRY(SkipRelativeDotPath(std::addressof(path), std::addressof(relative_len), path)); + + /* If we have a relative path, check that was allowed. */ + if (relative_len > 0) { + R_UNLESS(flags.IsRelativePathAllowed(), ResultInvalidPathFormat); + + total_len += relative_len; + + if (path[0] == NullTerminator) { + *out = true; + *out_len = total_len; + R_SUCCEED(); + } + } + + /* Process windows path. */ + size_t windows_len = 0; + bool normalized_win = false; + R_TRY(SkipWindowsPath(std::addressof(path), std::addressof(windows_len), + std::addressof(normalized_win), path, mount_name_len > 0)); + + /* If the windows path wasn't normalized, we're not normalized. */ + if (!normalized_win) { + R_UNLESS(flags.IsWindowsPathAllowed(), ResultInvalidPathFormat); + + *out = false; + R_SUCCEED(); + } + + /* If we had a windows path, check that was allowed. */ + if (windows_len > 0) { + R_UNLESS(flags.IsWindowsPathAllowed(), ResultInvalidPathFormat); + + total_len += windows_len; + + /* We can't have both a relative path and a windows path. */ + R_UNLESS(relative_len == 0, ResultInvalidPathFormat); + + /* A path ending in a windows path isn't normalized. */ + if (path[0] == NullTerminator) { + *out = false; + R_SUCCEED(); + } + + /* Check that there are no windows directory separators in the path. */ + for (size_t i = 0; path[i] != NullTerminator; ++i) { + if (path[i] == AlternateDirectorySeparator) { + *out = false; + R_SUCCEED(); + } + } + } + + /* Check that parent directory replacement is not needed if backslashes are allowed. */ + if (flags.IsBackslashAllowed() && + PathNormalizer::IsParentDirectoryPathReplacementNeeded(path)) { + *out = false; + R_SUCCEED(); + } + + /* Check that the backslash state is valid. */ + bool is_backslash_contained = false; + R_TRY(CheckInvalidBackslash(std::addressof(is_backslash_contained), path, + flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed())); + + /* Check that backslashes are contained only if allowed. */ + if (is_backslash_contained && !flags.IsBackslashAllowed()) { + *out = false; + R_SUCCEED(); + } + + /* Check that the final result path is normalized. */ + size_t normal_len = 0; + R_TRY(PathNormalizer::IsNormalized(out, std::addressof(normal_len), path, + flags.IsAllCharactersAllowed())); + + /* Add the normal length. */ + total_len += normal_len; + + /* Set the output length. */ + *out_len = total_len; + R_SUCCEED(); + } + + static Result Normalize(char* dst, size_t dst_size, const char* path, size_t path_len, + const PathFlags& flags) { + /* Use StringTraits names for remainder of scope. */ + using namespace StringTraits; + + /* Prepare to iterate. */ + const char* src = path; + size_t cur_pos = 0; + bool is_windows_path = false; + + /* Check if the path is empty. */ + if (src[0] == NullTerminator) { + if (dst_size != 0) { + dst[0] = NullTerminator; + } + + R_UNLESS(flags.IsEmptyPathAllowed(), ResultInvalidPathFormat); + + R_SUCCEED(); + } + + /* Handle a mount name. */ + size_t mount_name_len = 0; + if (flags.IsMountNameAllowed()) { + R_TRY(ParseMountName(std::addressof(src), std::addressof(mount_name_len), dst + cur_pos, + dst_size - cur_pos, src)); + + cur_pos += mount_name_len; + } + + /* Handle a drive-relative prefix. */ + bool is_drive_relative = false; + if (src[0] != DirectorySeparator && !IsPathStartWithCurrentDirectory(src) && + !IsWindowsPath(src, false)) { + R_UNLESS(flags.IsRelativePathAllowed(), ResultInvalidPathFormat); + R_UNLESS(!IsInvalidCharacter(src[0]), ResultInvalidPathFormat); + + dst[cur_pos++] = Dot; + is_drive_relative = true; + } + + size_t relative_len = 0; + if (flags.IsRelativePathAllowed()) { + R_UNLESS(cur_pos < dst_size, ResultTooLongPath); + + R_TRY(ParseRelativeDotPath(std::addressof(src), std::addressof(relative_len), + dst + cur_pos, dst_size - cur_pos, src)); + + cur_pos += relative_len; + + if (src[0] == NullTerminator) { + R_UNLESS(cur_pos < dst_size, ResultTooLongPath); + + dst[cur_pos] = NullTerminator; + R_SUCCEED(); + } + } + + /* Handle a windows path. */ + if (flags.IsWindowsPathAllowed()) { + const char* const orig = src; + + R_UNLESS(cur_pos < dst_size, ResultTooLongPath); + + size_t windows_len = 0; + R_TRY(ParseWindowsPath(std::addressof(src), std::addressof(windows_len), dst + cur_pos, + dst_size - cur_pos, src, mount_name_len != 0)); + + cur_pos += windows_len; + + if (src[0] == NullTerminator) { + /* NOTE: Bug in original code here repeated, should be checking cur_pos + 2. */ + R_UNLESS(cur_pos + 1 < dst_size, ResultTooLongPath); + + dst[cur_pos + 0] = DirectorySeparator; + dst[cur_pos + 1] = NullTerminator; + R_SUCCEED(); + } + + if ((src - orig) > 0) { + is_windows_path = true; + } + } + + /* Check for invalid backslash. */ + bool backslash_contained = false; + R_TRY(CheckInvalidBackslash(std::addressof(backslash_contained), src, + flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed())); + + /* Handle backslash replacement as necessary. */ + if (backslash_contained && flags.IsWindowsPathAllowed()) { + /* Create a temporary buffer holding a slash-replaced version of the path. */ + /* NOTE: Nintendo unnecessarily allocates and replaces here a fully copy of the path, + * despite having skipped some of it already. */ + const size_t replaced_src_len = path_len - (src - path); + + char* replaced_src = nullptr; + SCOPE_EXIT({ + if (replaced_src != nullptr) { + if (std::is_constant_evaluated()) { + delete[] replaced_src; + } else { + Deallocate(replaced_src, replaced_src_len); + } + } + }); + + if (std::is_constant_evaluated()) { + replaced_src = new char[replaced_src_len]; + } else { + replaced_src = static_cast<char*>(Allocate(replaced_src_len)); + } + + Strlcpy<char>(replaced_src, src, replaced_src_len); + + Replace(replaced_src, replaced_src_len, AlternateDirectorySeparator, + DirectorySeparator); + + size_t dummy; + R_TRY(PathNormalizer::Normalize(dst + cur_pos, std::addressof(dummy), replaced_src, + dst_size - cur_pos, is_windows_path, is_drive_relative, + flags.IsAllCharactersAllowed())); + } else { + /* We can just do normalization. */ + size_t dummy; + R_TRY(PathNormalizer::Normalize(dst + cur_pos, std::addressof(dummy), src, + dst_size - cur_pos, is_windows_path, is_drive_relative, + flags.IsAllCharactersAllowed())); + } + + R_SUCCEED(); + } +}; + +} // namespace FileSys diff --git a/src/core/file_sys/fs_string_util.h b/src/core/file_sys/fs_string_util.h new file mode 100644 index 000000000..68114e72c --- /dev/null +++ b/src/core/file_sys/fs_string_util.h @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/assert.h" + +namespace FileSys { + +template <typename T> +constexpr int Strlen(const T* str) { + ASSERT(str != nullptr); + + int length = 0; + while (*str++) { + ++length; + } + + return length; +} + +template <typename T> +constexpr int Strnlen(const T* str, int count) { + ASSERT(str != nullptr); + ASSERT(count >= 0); + + int length = 0; + while (count-- && *str++) { + ++length; + } + + return length; +} + +template <typename T> +constexpr int Strncmp(const T* lhs, const T* rhs, int count) { + ASSERT(lhs != nullptr); + ASSERT(rhs != nullptr); + ASSERT(count >= 0); + + if (count == 0) { + return 0; + } + + T l, r; + do { + l = *(lhs++); + r = *(rhs++); + } while (l && (l == r) && (--count)); + + return l - r; +} + +template <typename T> +static constexpr int Strlcpy(T* dst, const T* src, int count) { + ASSERT(dst != nullptr); + ASSERT(src != nullptr); + + const T* cur = src; + if (count > 0) { + while ((--count) && *cur) { + *(dst++) = *(cur++); + } + *dst = 0; + } + + while (*cur) { + cur++; + } + + return static_cast<int>(cur - src); +} + +/* std::size() does not support zero-size C arrays. We're fixing that. */ +template <class C> +constexpr auto size(const C& c) -> decltype(c.size()) { + return std::size(c); +} + +template <class C> +constexpr std::size_t size(const C& c) { + if constexpr (sizeof(C) == 0) { + return 0; + } else { + return std::size(c); + } +} + +enum CharacterEncodingResult { + CharacterEncodingResult_Success = 0, + CharacterEncodingResult_InsufficientLength = 1, + CharacterEncodingResult_InvalidFormat = 2, +}; + +namespace impl { + +class CharacterEncodingHelper { +public: + static constexpr int8_t Utf8NBytesInnerTable[0x100 + 1] = { + -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8, + }; + + static constexpr char GetUtf8NBytes(size_t i) { + return static_cast<char>(Utf8NBytesInnerTable[1 + i]); + } +}; + +} // namespace impl + +constexpr inline CharacterEncodingResult ConvertCharacterUtf8ToUtf32(u32* dst, const char* src) { + /* Check pre-conditions. */ + ASSERT(dst != nullptr); + ASSERT(src != nullptr); + + /* Perform the conversion. */ + const auto* p = src; + switch (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[0]))) { + case 1: + *dst = static_cast<u32>(p[0]); + return CharacterEncodingResult_Success; + case 2: + if ((static_cast<u32>(p[0]) & 0x1E) != 0) { + if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == + 0) { + *dst = (static_cast<u32>(p[0] & 0x1F) << 6) | (static_cast<u32>(p[1] & 0x3F) << 0); + return CharacterEncodingResult_Success; + } + } + break; + case 3: + if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 && + impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0) { + const u32 c = (static_cast<u32>(p[0] & 0xF) << 12) | + (static_cast<u32>(p[1] & 0x3F) << 6) | + (static_cast<u32>(p[2] & 0x3F) << 0); + if ((c & 0xF800) != 0 && (c & 0xF800) != 0xD800) { + *dst = c; + return CharacterEncodingResult_Success; + } + } + return CharacterEncodingResult_InvalidFormat; + case 4: + if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 && + impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0 && + impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[3])) == 0) { + const u32 c = + (static_cast<u32>(p[0] & 0x7) << 18) | (static_cast<u32>(p[1] & 0x3F) << 12) | + (static_cast<u32>(p[2] & 0x3F) << 6) | (static_cast<u32>(p[3] & 0x3F) << 0); + if (c >= 0x10000 && c < 0x110000) { + *dst = c; + return CharacterEncodingResult_Success; + } + } + return CharacterEncodingResult_InvalidFormat; + default: + break; + } + + /* We failed to convert. */ + return CharacterEncodingResult_InvalidFormat; +} + +constexpr inline CharacterEncodingResult PickOutCharacterFromUtf8String(char* dst, + const char** str) { + /* Check pre-conditions. */ + ASSERT(dst != nullptr); + ASSERT(str != nullptr); + ASSERT(*str != nullptr); + + /* Clear the output. */ + dst[0] = 0; + dst[1] = 0; + dst[2] = 0; + dst[3] = 0; + + /* Perform the conversion. */ + const auto* p = *str; + u32 c = static_cast<u32>(*p); + switch (impl::CharacterEncodingHelper::GetUtf8NBytes(c)) { + case 1: + dst[0] = (*str)[0]; + ++(*str); + break; + case 2: + if ((p[0] & 0x1E) != 0) { + if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == + 0) { + c = (static_cast<u32>(p[0] & 0x1F) << 6) | (static_cast<u32>(p[1] & 0x3F) << 0); + dst[0] = (*str)[0]; + dst[1] = (*str)[1]; + (*str) += 2; + break; + } + } + return CharacterEncodingResult_InvalidFormat; + case 3: + if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 && + impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0) { + c = (static_cast<u32>(p[0] & 0xF) << 12) | (static_cast<u32>(p[1] & 0x3F) << 6) | + (static_cast<u32>(p[2] & 0x3F) << 0); + if ((c & 0xF800) != 0 && (c & 0xF800) != 0xD800) { + dst[0] = (*str)[0]; + dst[1] = (*str)[1]; + dst[2] = (*str)[2]; + (*str) += 3; + break; + } + } + return CharacterEncodingResult_InvalidFormat; + case 4: + if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 && + impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0 && + impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[3])) == 0) { + c = (static_cast<u32>(p[0] & 0x7) << 18) | (static_cast<u32>(p[1] & 0x3F) << 12) | + (static_cast<u32>(p[2] & 0x3F) << 6) | (static_cast<u32>(p[3] & 0x3F) << 0); + if (c >= 0x10000 && c < 0x110000) { + dst[0] = (*str)[0]; + dst[1] = (*str)[1]; + dst[2] = (*str)[2]; + dst[3] = (*str)[3]; + (*str) += 4; + break; + } + } + return CharacterEncodingResult_InvalidFormat; + default: + return CharacterEncodingResult_InvalidFormat; + } + + return CharacterEncodingResult_Success; +} + +} // namespace FileSys |