#pragma once #include #include // path parser and utility class for Wii U paths // optimized to be allocation-free for common path lengths class FSCPath { struct PathNode { PathNode(uint16 offset, uint16 len) : offset(offset), len(len) {}; uint16 offset; uint16 len; }; boost::container::small_vector m_nodes; boost::container::small_vector m_names; bool m_isAbsolute{}; inline bool isSlash(char c) { return c == '\\' || c == '/'; } void appendNode(const char* name, uint16 nameLen) { if (m_names.size() > 0xFFFF) return; m_nodes.emplace_back((uint16)m_names.size(), nameLen); m_names.insert(m_names.end(), name, name + nameLen); } public: FSCPath(std::string_view path) { if (path.empty()) return; if (isSlash(path.front())) { m_isAbsolute = true; path.remove_prefix(1); // skip any additional leading slashes while (!path.empty() && isSlash(path.front())) path.remove_prefix(1); } // parse nodes size_t n = 0; size_t nodeNameStartIndex = 0; while (n < path.size()) { if (isSlash(path[n])) { size_t nodeNameLen = n - nodeNameStartIndex; if (nodeNameLen > 0xFFFF) nodeNameLen = 0xFFFF; // truncate suspiciously long node names cemu_assert_debug(nodeNameLen > 0); appendNode(path.data() + nodeNameStartIndex, (uint16)nodeNameLen); // skip any repeating slashes while (n < path.size() && isSlash(path[n])) n++; nodeNameStartIndex = n; continue; } n++; } if (nodeNameStartIndex < n) { size_t nodeNameLen = n - nodeNameStartIndex; if (nodeNameLen > 0xFFFF) nodeNameLen = 0xFFFF; // truncate suspiciously long node names appendNode(path.data() + nodeNameStartIndex, (uint16)nodeNameLen); } } size_t GetNodeCount() const { return m_nodes.size(); } std::string_view GetNodeName(size_t index) const { if (index < 0 || index >= m_nodes.size()) return std::basic_string_view(); return std::basic_string_view(m_names.data() + m_nodes[index].offset, m_nodes[index].len); } // returns true if the node names match according to FSA case-insensitivity rules bool MatchNode(sint32 index, std::string_view name) const { if (index < 0 || index >= (sint32)m_nodes.size()) return false; auto nodeName = GetNodeName(index); if (nodeName.size() != name.size()) return false; for (size_t i = 0; i < nodeName.size(); i++) { char c1 = nodeName[i]; char c2 = name[i]; if (c1 >= 'A' && c1 <= 'Z') c1 += ('a' - 'A'); if (c2 >= 'A' && c2 <= 'Z') c2 += ('a' - 'A'); if (c1 != c2) return false; } return true; } }; class parsedPathW // todo - replaces this with FSCPath (using ascii/utf8 strings instead of wchar) { static const int MAX_NODES = 32; public: parsedPathW(std::wstring_view path) { // init vars this->numNodes = 0; // init parsed path data if (path.front() == '/') path.remove_prefix(1); pathData.assign(path.begin(), path.end()); pathData.push_back('\0'); // start parsing sint32 offset = 0; sint32 startOffset = 0; if (offset < pathData.size()-1) { this->nodeOffset[this->numNodes] = offset; this->numNodes++; } while (offset < pathData.size() - 1) { if (this->pathData[offset] == '/' || this->pathData[offset] == '\\') { this->pathData[offset] = '\0'; offset++; // double slashes are ignored and instead are handled like a single slash if (this->pathData[offset] == '/' || this->pathData[offset] == '\\') { // if we're in the beginning and having a \\ it's a network path if (offset != 1) { this->pathData[offset] = '\0'; offset++; } } // start new node if (this->numNodes < MAX_NODES) { if (offset < pathData.size() - 1) { this->nodeOffset[this->numNodes] = offset; this->numNodes++; } } continue; } offset++; } // handle special nodes like '.' or '..' sint32 nodeIndex = 0; while (nodeIndex < this->numNodes) { if (compareNodeName(nodeIndex, L"..")) { assert(false); } else if (compareNodeName(nodeIndex, L".")) { removeNode(nodeIndex); continue; } nodeIndex++; } } // returns true if the names match (case sensitive) bool compareNodeName(sint32 index, std::wstring_view name) { if (index < 0 || index >= this->numNodes) return false; const wchar_t* nodeName = this->pathData.data() + this->nodeOffset[index]; if (boost::iequals(nodeName, name)) return true; return false; } static bool compareNodeName(std::wstring_view name1, std::wstring_view name2) { if (boost::iequals(name1, name2)) return true; return false; } static bool compareNodeNameCaseInsensitive(std::wstring_view name1, std::wstring_view name2) { if (boost::iequals(name1, name2)) return true; return false; } const wchar_t* getNodeName(sint32 index) { if (index < 0 || index >= this->numNodes) return nullptr; return this->pathData.data() + this->nodeOffset[index]; } void removeNode(sint32 index) { if (index < 0 || index >= numNodes) return; numNodes--; for (sint32 i = 0; i < numNodes; i++) { nodeOffset[i] = nodeOffset[i + 1]; } // remove empty space if (numNodes > 0) updateOffsets(nodeOffset[0]); } void prependNode(wchar_t* newNode) { if (numNodes >= MAX_NODES) return; sint32 len = (sint32)wcslen(newNode); updateOffsets(-(len + 1)); numNodes++; for (sint32 i = numNodes - 1; i >= 1; i--) { nodeOffset[i] = nodeOffset[i - 1]; } nodeOffset[0] = 0; memcpy(pathData.data() + 0, newNode, (len + 1) * sizeof(wchar_t)); } void buildPathString(std::wstring& pathStr, bool appendSlash = false) { pathStr.resize(0); for (sint32 i = 0; i < numNodes; i++) { if (numNodes > 1) pathStr.append(L"/"); pathStr.append(pathData.data() + nodeOffset[i]); } if(appendSlash) pathStr.append(L"/"); } private: void updateOffsets(sint32 newBaseOffset) { if (numNodes == 0 || newBaseOffset == 0) return; cemu_assert_debug(newBaseOffset <= nodeOffset[0]); if (newBaseOffset > 0) { // decrease size memmove(pathData.data(), pathData.data() + newBaseOffset, (pathData.size() - newBaseOffset) * sizeof(wchar_t)); pathData.resize(pathData.size() - newBaseOffset); // update node offsets for (sint32 i = 0; i < numNodes; i++) { nodeOffset[i] -= newBaseOffset; } } else { // increase size newBaseOffset = -newBaseOffset; pathData.resize(pathData.size() + newBaseOffset); memmove(pathData.data() + newBaseOffset, pathData.data(), (pathData.size() - newBaseOffset) * sizeof(wchar_t)); // update node offsets for (sint32 i = 0; i < numNodes; i++) { nodeOffset[i] += newBaseOffset; } } } private: //std::wstring pathData; std::vector pathData; sint32 nodeOffset[MAX_NODES + 1]; public: sint32 numNodes; }; template class FileTree { public: private: enum NODETYPE : uint8 { NODETYPE_DIRECTORY, NODETYPE_FILE, }; typedef struct _node_t { std::wstring name; std::vector<_node_t*> subnodes; F* custom; NODETYPE type; }node_t; node_t* getByNodePath(parsedPathW& p, sint32 numNodes, bool createAsDirectories) { node_t* currentNode = &rootNode; for (sint32 i = 0; i < numNodes; i++) { // find subnode by path node_t* foundSubnode = getSubnode(currentNode, p.getNodeName(i)); if (foundSubnode == nullptr) { // no subnode found -> create new directory node (if requested) if (createAsDirectories == false) return nullptr; // path not found currentNode = newNode(currentNode, NODETYPE_DIRECTORY, p.getNodeName(i)); } else { currentNode = foundSubnode; } } return currentNode; } node_t* getSubnode(node_t* parentNode, std::wstring_view name) { for (auto& sn : parentNode->subnodes) { if constexpr (isCaseSensitive) { if (parsedPathW::compareNodeName(sn->name.c_str(), name)) return sn; } else { if (parsedPathW::compareNodeNameCaseInsensitive(sn->name.c_str(), name)) return sn; } } return nullptr; } node_t* newNode(node_t* parentNode, NODETYPE type, std::wstring_view name) { node_t* newNode = new node_t; newNode->name = std::wstring(name); newNode->type = type; newNode->custom = nullptr; parentNode->subnodes.push_back(newNode); return newNode; } public: FileTree() { rootNode.type = NODETYPE_DIRECTORY; } bool addFile(const wchar_t* path, F* custom) { parsedPathW p(path); if (p.numNodes == 0) return false; node_t* directoryNode = getByNodePath(p, p.numNodes - 1, true); // check if a node with same name already exists if (getSubnode(directoryNode, p.getNodeName(p.numNodes - 1)) != nullptr) return false; // node already exists // add file node node_t* fileNode = newNode(directoryNode, NODETYPE_FILE, p.getNodeName(p.numNodes - 1)); fileNode->custom = custom; return true; } bool getFile(std::wstring_view path, F* &custom) { parsedPathW p(path); if (p.numNodes == 0) return false; node_t* node = getByNodePath(p, p.numNodes, false); if (node == nullptr) return false; if (node->type != NODETYPE_FILE) return false; custom = node->custom; return true; } bool removeFile(std::wstring_view path) { parsedPathW p(path); if (p.numNodes == 0) return false; node_t* directoryNode = getByNodePath(p, p.numNodes - 1, false); if (directoryNode == nullptr) return false; // find node node_t* fileNode = getSubnode(directoryNode, p.getNodeName(p.numNodes - 1)); if (fileNode == nullptr) return false; if (fileNode->type != NODETYPE_FILE) return false; if (fileNode->subnodes.empty() == false) { // files shouldn't have subnodes assert(false); } // remove node from parent directoryNode->subnodes.erase(std::remove(directoryNode->subnodes.begin(), directoryNode->subnodes.end(), fileNode), directoryNode->subnodes.end()); // delete node delete fileNode; return true; } template bool listDirectory(const wchar_t* path, TFunc fn) { parsedPathW p(path); node_t* node = getByNodePath(p, p.numNodes, false); if (node == nullptr) return false; if (node->type != NODETYPE_DIRECTORY) return false; for (auto& it : node->subnodes) { if (it->type == NODETYPE_DIRECTORY) { fn(it->name, true, it->custom); } else if (it->type == NODETYPE_FILE) { fn(it->name, false, it->custom); } } return true; } private: node_t rootNode; }; static void FSTPathUnitTest() { // test 1 FSCPath p1("/vol/content"); cemu_assert_debug(p1.GetNodeCount() == 2); cemu_assert_debug(p1.MatchNode(0, "tst") == false); cemu_assert_debug(p1.MatchNode(0, "vol")); cemu_assert_debug(p1.MatchNode(1, "CONTENT")); // test 2 FSCPath p2("/vol/content/"); cemu_assert_debug(p2.GetNodeCount() == 2); cemu_assert_debug(p2.MatchNode(0, "vol")); cemu_assert_debug(p2.MatchNode(1, "content")); // test 3 FSCPath p3("/vol//content/\\/"); cemu_assert_debug(p3.GetNodeCount() == 2); cemu_assert_debug(p3.MatchNode(0, "vol")); cemu_assert_debug(p3.MatchNode(1, "content")); // test 4 FSCPath p4("vol/content/"); cemu_assert_debug(p4.GetNodeCount() == 2); // test 5 FSCPath p5("/vol/content/test.bin"); cemu_assert_debug(p5.GetNodeCount() == 3); cemu_assert_debug(p5.MatchNode(0, "vol")); cemu_assert_debug(p5.MatchNode(1, "content")); cemu_assert_debug(p5.MatchNode(2, "TEST.BIN")); // test 6 - empty paths FSCPath p6(""); cemu_assert_debug(p6.GetNodeCount() == 0); p6 = FSCPath("/////////////"); cemu_assert_debug(p6.GetNodeCount() == 0); }