diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4a9c6fd2fe..ae9e8dcea4 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -24,6 +24,7 @@ set(SRCS file_sys/archive_systemsavedata.cpp file_sys/disk_archive.cpp file_sys/ivfc_archive.cpp + file_sys/path_parser.cpp gdbstub/gdbstub.cpp hle/config_mem.cpp hle/hle.cpp @@ -168,6 +169,7 @@ set(HEADERS file_sys/disk_archive.h file_sys/file_backend.h file_sys/ivfc_archive.h + file_sys/path_parser.h gdbstub/gdbstub.h hle/config_mem.h hle/function_wrappers.h diff --git a/src/core/file_sys/path_parser.cpp b/src/core/file_sys/path_parser.cpp new file mode 100644 index 0000000000..5a89b02b8d --- /dev/null +++ b/src/core/file_sys/path_parser.cpp @@ -0,0 +1,98 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/file_util.h" +#include "common/string_util.h" +#include "core/file_sys/path_parser.h" + +namespace FileSys { + +PathParser::PathParser(const Path& path) { + if (path.GetType() != LowPathType::Char && path.GetType() != LowPathType::Wchar) { + is_valid = false; + return; + } + + auto path_string = path.AsString(); + if (path_string.size() == 0 || path_string[0] != '/') { + is_valid = false; + return; + } + + // Filter out invalid characters for the host system. + // Although some of these characters are valid on 3DS, they are unlikely to be used by games. + if (std::find_if(path_string.begin(), path_string.end(), [](char c) { + static const std::set invalid_chars{'<', '>', '\\', '|', ':', '\"', '*', '?'}; + return invalid_chars.find(c) != invalid_chars.end(); + }) != path_string.end()) { + is_valid = false; + return; + } + + Common::SplitString(path_string, '/', path_sequence); + + auto begin = path_sequence.begin(); + auto end = path_sequence.end(); + end = std::remove_if(begin, end, [](std::string& str) { return str == "" || str == "."; }); + path_sequence = std::vector(begin, end); + + // checks if the path is out of bounds. + int level = 0; + for (auto& node : path_sequence) { + if (node == "..") { + --level; + if (level < 0) { + is_valid = false; + return; + } + } else { + ++level; + } + } + + is_valid = true; + is_root = level == 0; +} + +PathParser::HostStatus PathParser::GetHostStatus(const std::string& mount_point) const { + auto path = mount_point; + if (!FileUtil::IsDirectory(path)) + return InvalidMountPoint; + if (path_sequence.empty()) { + return DirectoryFound; + } + + for (auto iter = path_sequence.begin(); iter != path_sequence.end() - 1; iter++) { + if (path.back() != '/') + path += '/'; + path += *iter; + + if (!FileUtil::Exists(path)) + return PathNotFound; + if (FileUtil::IsDirectory(path)) + continue; + return FileInPath; + } + + path += "/" + path_sequence.back(); + if (!FileUtil::Exists(path)) + return NotFound; + if (FileUtil::IsDirectory(path)) + return DirectoryFound; + return FileFound; +} + +std::string PathParser::BuildHostPath(const std::string& mount_point) const { + std::string path = mount_point; + for (auto& node : path_sequence) { + if (path.back() != '/') + path += '/'; + path += node; + } + return path; +} + +} // namespace FileSys diff --git a/src/core/file_sys/path_parser.h b/src/core/file_sys/path_parser.h new file mode 100644 index 0000000000..9908025797 --- /dev/null +++ b/src/core/file_sys/path_parser.h @@ -0,0 +1,61 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "core/file_sys/archive_backend.h" + +namespace FileSys { + +/** + * A helper class parsing and verifying a string-type Path. + * Every archives with a sub file system should use this class to parse the path argument and check + * the status of the file / directory in question on the host file system. + */ +class PathParser { +public: + PathParser(const Path& path); + + /** + * Checks if the Path is valid. + * This function should be called once a PathParser is constructed. + * A Path is valid if: + * - it is a string path (with type LowPathType::Char or LowPathType::Wchar), + * - it starts with "/" (this seems a hard requirement in real 3DS), + * - it doesn't contain invalid characters, and + * - it doesn't go out of the root directory using "..". + */ + bool IsValid() const { + return is_valid; + } + + /// Checks if the Path represents the root directory. + bool IsRootDirectory() const { + return is_root; + } + + enum HostStatus { + InvalidMountPoint, + PathNotFound, // "/a/b/c" when "a" doesn't exist + FileInPath, // "/a/b/c" when "a" is a file + FileFound, // "/a/b/c" when "c" is a file + DirectoryFound, // "/a/b/c" when "c" is a directory + NotFound // "/a/b/c" when "a/b/" exists but "c" doesn't exist + }; + + /// Checks the status of the specified file / directory by the Path on the host file system. + HostStatus GetHostStatus(const std::string& mount_point) const; + + /// Builds a full path on the host file system. + std::string BuildHostPath(const std::string& mount_point) const; + +private: + std::vector path_sequence; + bool is_valid{}; + bool is_root{}; +}; + +} // namespace FileSys diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 457c55571c..47799e1c26 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,5 +1,6 @@ set(SRCS tests.cpp + core/file_sys/path_parser.cpp ) set(HEADERS diff --git a/src/tests/core/file_sys/path_parser.cpp b/src/tests/core/file_sys/path_parser.cpp new file mode 100644 index 0000000000..2b543e438a --- /dev/null +++ b/src/tests/core/file_sys/path_parser.cpp @@ -0,0 +1,38 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "common/file_util.h" +#include "core/file_sys/path_parser.h" + +namespace FileSys { + +TEST_CASE("PathParser", "[core][file_sys]") { + REQUIRE(!PathParser(Path(std::vector{})).IsValid()); + REQUIRE(!PathParser(Path("a")).IsValid()); + REQUIRE(!PathParser(Path("/|")).IsValid()); + REQUIRE(PathParser(Path("/a")).IsValid()); + REQUIRE(!PathParser(Path("/a/b/../../c/../../d")).IsValid()); + REQUIRE(PathParser(Path("/a/b/../c/../../d")).IsValid()); + REQUIRE(PathParser(Path("/")).IsRootDirectory()); + REQUIRE(!PathParser(Path("/a")).IsRootDirectory()); + REQUIRE(PathParser(Path("/a/..")).IsRootDirectory()); +} + +TEST_CASE("PathParser - Host file system", "[core][file_sys]") { + std::string test_dir = "./test"; + FileUtil::CreateDir(test_dir); + FileUtil::CreateDir(test_dir + "/z"); + FileUtil::CreateEmptyFile(test_dir + "/a"); + + REQUIRE(PathParser(Path("/a")).GetHostStatus(test_dir) == PathParser::FileFound); + REQUIRE(PathParser(Path("/b")).GetHostStatus(test_dir) == PathParser::NotFound); + REQUIRE(PathParser(Path("/z")).GetHostStatus(test_dir) == PathParser::DirectoryFound); + REQUIRE(PathParser(Path("/a/c")).GetHostStatus(test_dir) == PathParser::FileInPath); + REQUIRE(PathParser(Path("/b/c")).GetHostStatus(test_dir) == PathParser::PathNotFound); + + FileUtil::DeleteDirRecursively(test_dir); +} + +} // namespace FileSys