From 2c049ae06dbbdfff7542c23ca4d748879f4beb71 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Thu, 18 Jan 2024 23:08:37 +0100 Subject: fs: Add path class --- src/core/file_sys/fs_path.h | 570 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 src/core/file_sys/fs_path.h (limited to 'src/core/file_sys/fs_path.h') 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(m_length_and_is_normalized & 1); + } + + constexpr void SetNormalized() { + m_length_and_is_normalized |= static_cast(1); + } + + constexpr void SetNotNormalized() { + m_length_and_is_normalized &= ~static_cast(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(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(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(dst, old_write_buffer.Get(), cur_len + 1); + } + + /* Add separator. */ + dst[cur_len] = '/'; + + /* Copy the child path. */ + const size_t copied = Strlcpy(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(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(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 -- cgit v1.2.3