diff --git a/include/tools/pathTools.h b/include/tools/pathTools.h index 01bc485d9..19c843321 100644 --- a/include/tools/pathTools.h +++ b/include/tools/pathTools.h @@ -25,9 +25,7 @@ bool isRelativePath(const std::string& path); std::string computeAbsolutePath(const std::string& path, const std::string& relativePath); std::string computeRelativePath(const std::string& path, const std::string& absolutePath); -std::string removeLastPathElement(const std::string& path, - const bool removePreSeparator = false, - const bool removePostSeparator = false); +std::string removeLastPathElement(const std::string& path); std::string appendToDirectory(const std::string& directoryPath, const std::string& filename); unsigned int getFileSize(const std::string& path); diff --git a/include/tools/stringTools.h b/include/tools/stringTools.h index bfb073c27..02868f7c6 100644 --- a/include/tools/stringTools.h +++ b/include/tools/stringTools.h @@ -43,10 +43,7 @@ void loadICUExternalTables(); std::string urlEncode(const std::string& value, bool encodeReserved = false); std::string urlDecode(const std::string& value, bool component = false); -std::vector split(const std::string&, const std::string&); -std::vector split(const char*, const char*); -std::vector split(const std::string&, const char*); -std::vector split(const char*, const std::string&); +std::vector split(const std::string&, const std::string&, bool trimEmpty = true); std::string join(const std::vector& list, const std::string& sep); std::string ucAll(const std::string& word); diff --git a/src/aria2.cpp b/src/aria2.cpp index a6e081d7d..0cef601b7 100644 --- a/src/aria2.cpp +++ b/src/aria2.cpp @@ -49,7 +49,7 @@ Aria2::Aria2(): m_secret = "token:"+m_secret; std::string aria2cmd = appendToDirectory( - removeLastPathElement(getExecutablePath(true), true, true), + removeLastPathElement(getExecutablePath(true)), ARIA2_CMD); if (fileExists(aria2cmd)) { // A local aria2c exe exists (packaged with kiwix-desktop), use it. diff --git a/src/kiwixserve.cpp b/src/kiwixserve.cpp index f8e08d780..db44f16a5 100644 --- a/src/kiwixserve.cpp +++ b/src/kiwixserve.cpp @@ -36,7 +36,7 @@ void KiwixServe::run() std::vector callCmd; std::string kiwixServeCmd = appendToDirectory( - removeLastPathElement(getExecutablePath(true), true, true), + removeLastPathElement(getExecutablePath(true)), KIWIXSERVE_CMD); if (fileExists(kiwixServeCmd)) { // A local kiwix-serve exe exists (packaged with kiwix-desktop), use it. diff --git a/src/library.cpp b/src/library.cpp index 6b2a1939d..446b5508f 100644 --- a/src/library.cpp +++ b/src/library.cpp @@ -115,7 +115,7 @@ unsigned int Library::getBookCount(const bool localBooks, bool Library::writeToFile(const std::string& path) { - auto baseDir = removeLastPathElement(path, true, false); + auto baseDir = removeLastPathElement(path); LibXMLDumper dumper(this); dumper.setBaseDir(baseDir); return writeTextFile(path, dumper.dumpLibXMLContent(getBooksIds())); diff --git a/src/manager.cpp b/src/manager.cpp index 2f97ffddc..7ff386d7c 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -59,7 +59,7 @@ bool Manager::parseXmlDom(const pugi::xml_document& doc, book.setReadOnly(readOnly); book.updateFromXml(bookNode, - removeLastPathElement(libraryPath, true, false)); + removeLastPathElement(libraryPath)); /* Update the book properties with the new importer */ if (libraryVersion.empty() @@ -177,7 +177,7 @@ std::string Manager::addBookFromPathAndGetId(const std::string& pathToOpen, if (pathToSave != pathToOpen) { book.setPath(isRelativePath(pathToSave) ? computeAbsolutePath( - removeLastPathElement(writableLibraryPath, true, false), + removeLastPathElement(writableLibraryPath), pathToSave) : pathToSave); } diff --git a/src/tools/pathTools.cpp b/src/tools/pathTools.cpp index 9c46fc01c..33c28ecde 100644 --- a/src/tools/pathTools.cpp +++ b/src/tools/pathTools.cpp @@ -39,11 +39,12 @@ #include #include #include +#include #ifdef _WIN32 -const std::string SEPARATOR("\\"); + #define SEPARATOR "\\" #else -const std::string SEPARATOR("/"); + #define SEPARATOR "/" #include #endif @@ -62,11 +63,63 @@ bool isRelativePath(const std::string& path) #endif } +std::vector normalizeParts(std::vector parts, bool absolute) +{ + std::vector ret; +#ifdef _WIN32 + //Special case if we have a drive directory not at first. + //Starts from there. + auto it = find_if(parts.rbegin(), parts.rend(), + [](const std::string& p) ->bool + { return p.length() == 2 && p[1] == ':'; }); + if (it != parts.rend()) { + parts.erase(parts.begin(), it.base()-1); + } +#endif + + for (auto& part: parts) { + if (part == "..") { + if (absolute) { + // We try to remove as far as possible. + if (ret.size() > 1) { + ret.pop_back(); + } + } else { + // We remove only if we can remove it. + // Else we add it. + if (!ret.empty() && ret.back() != "..") { + ret.pop_back(); + } else { + ret.push_back(".."); + } + } + continue; + } + if (part == "") { +#ifndef _WIN32 + if (ret.empty()) { + ret.push_back(""); + } +#endif + continue; + } + if (part == ".") { + continue; + } + ret.push_back(part); + } +#ifndef _WIN32 + if (absolute && ret.size() == 1 && ret.back() == "") { + ret.push_back(""); + } +#endif + return ret; +} + std::string computeRelativePath(const std::string& path, const std::string& absolutePath) { - std::vector pathParts = kiwix::split(path, SEPARATOR); - std::vector absolutePathParts - = kiwix::split(absolutePath, SEPARATOR); + auto pathParts = normalizeParts(kiwix::split(path, SEPARATOR, false), false); + auto absolutePathParts = kiwix::split(absolutePath, SEPARATOR, false); unsigned int commonCount = 0; while (commonCount < pathParts.size() @@ -75,108 +128,63 @@ std::string computeRelativePath(const std::string& path, const std::string& abso commonCount++; } - std::string relativePath; -#ifdef _WIN32 - /* On Windows you have a token more because the root is represented - by a letter */ - if (commonCount == 0) { - relativePath = ".." + SEPARATOR; - } -#endif - + std::vector relativeParts; for (unsigned int i = commonCount; i < pathParts.size(); i++) { - relativePath += ".." + SEPARATOR; + relativeParts.push_back(".."); } for (unsigned int i = commonCount; i < absolutePathParts.size(); i++) { - relativePath += absolutePathParts[i]; - relativePath += i + 1 < absolutePathParts.size() ? SEPARATOR : ""; + relativeParts.push_back(absolutePathParts[i]); } - return relativePath; + return kiwix::join(normalizeParts(relativeParts, false), SEPARATOR); } -#ifdef _WIN32 -# define STRTOK strtok_s -#else -# define STRTOK strtok_r -#endif - -/* Warning: the relative path must be with slashes */ std::string computeAbsolutePath(const std::string& path, const std::string& relativePath) { - std::string absolutePath; - + std::string absolutePath = path; if (path.empty()) { - char* path = NULL; - size_t size = 0; - + char* cpath; #ifdef _WIN32 - path = _getcwd(path, size); + cpath = _getcwd(NULL, 0); #else - path = getcwd(path, size); + cpath = getcwd(NULL, 0); #endif - - absolutePath = std::string(path) + SEPARATOR; - } else { - absolutePath = path.substr(path.length() - 1, 1) == SEPARATOR - ? path - : path + SEPARATOR; + absolutePath = cpath; + free(cpath); } -#if _WIN32 - char* cRelativePath = _strdup(relativePath.c_str()); -#else - char* cRelativePath = strdup(relativePath.c_str()); -#endif - char* saveptr = nullptr; - char* token = STRTOK(cRelativePath, "/", &saveptr); + auto absoluteParts = normalizeParts(kiwix::split(absolutePath, SEPARATOR, false), true); + auto relativeParts = kiwix::split(relativePath, SEPARATOR, false); - while (token != NULL) { - if (std::string(token) == "..") { - absolutePath = removeLastPathElement(absolutePath, true, false); - token = STRTOK(NULL, "/", &saveptr); - } else if (strcmp(token, ".") && strcmp(token, "")) { - absolutePath += std::string(token); - token = STRTOK(NULL, "/", &saveptr); - if (token != NULL) { - absolutePath += SEPARATOR; - } - } else { - token = STRTOK(NULL, "/", &saveptr); - } - } - free(cRelativePath); - - return absolutePath; + absoluteParts.insert(absoluteParts.end(), relativeParts.begin(), relativeParts.end()); + return kiwix::join(normalizeParts(absoluteParts, true), SEPARATOR); } -std::string removeLastPathElement(const std::string& path, - const bool removePreSeparator, - const bool removePostSeparator) +std::string removeLastPathElement(const std::string& path) { - std::string newPath = path; - size_t offset = newPath.find_last_of(SEPARATOR); - if (removePreSeparator && -#ifndef _WIN32 - offset != newPath.find_first_of(SEPARATOR) && -#endif - offset == newPath.length() - 1) { - newPath = newPath.substr(0, offset); - offset = newPath.find_last_of(SEPARATOR); + auto parts = normalizeParts(kiwix::split(path, SEPARATOR, false), false); + if (!parts.empty()) { + parts.pop_back(); } - newPath = removePostSeparator ? newPath.substr(0, offset) - : newPath.substr(0, offset + 1); - return newPath; + return kiwix::join(parts, SEPARATOR); } std::string appendToDirectory(const std::string& directoryPath, const std::string& filename) { - std::string newPath = directoryPath + SEPARATOR + filename; + std::string newPath = directoryPath; + if (!directoryPath.empty() && directoryPath.back() != SEPARATOR[0]) { + newPath += SEPARATOR; + } + newPath += filename; return newPath; } std::string getLastPathElement(const std::string& path) { - return path.substr(path.find_last_of(SEPARATOR) + 1); + auto parts = normalizeParts(kiwix::split(path, SEPARATOR), false); + if (parts.empty()) { + return ""; + } + return parts.back(); } unsigned int getFileSize(const std::string& path) diff --git a/src/tools/stringTools.cpp b/src/tools/stringTools.cpp index ef596f1e1..e050442ba 100644 --- a/src/tools/stringTools.cpp +++ b/src/tools/stringTools.cpp @@ -267,37 +267,28 @@ std::string kiwix::urlDecode(const std::string& value, bool component) /* Split string in a token array */ std::vector kiwix::split(const std::string& str, - const std::string& delims = " *-") + const std::string& delims, + bool trimEmpty) { - std::string::size_type lastPos = str.find_first_not_of(delims, 0); - std::string::size_type pos = str.find_first_of(delims, lastPos); + std::string::size_type lastPos = 0; + std::string::size_type pos = 0; std::vector tokens; - - while (std::string::npos != pos || std::string::npos != lastPos) { - tokens.push_back(str.substr(lastPos, pos - lastPos)); - lastPos = str.find_first_not_of(delims, pos); - pos = str.find_first_of(delims, lastPos); + while( (pos = str.find_first_of(delims, lastPos)) < str.length() ) + { + auto token = str.substr(lastPos, pos - lastPos); + if (!trimEmpty || !token.empty()) { + tokens.push_back(token); + } + lastPos = pos + 1; } + auto token = str.substr(lastPos); + if (!trimEmpty || !token.empty()) { + tokens.push_back(token); + } return tokens; } -std::vector kiwix::split(const char* lhs, const char* rhs) -{ - const std::string m1(lhs), m2(rhs); - return split(m1, m2); -} - -std::vector kiwix::split(const char* lhs, const std::string& rhs) -{ - return split(lhs, rhs.c_str()); -} - -std::vector kiwix::split(const std::string& lhs, const char* rhs) -{ - return split(lhs.c_str(), rhs); -} - std::string kiwix::join(const std::vector& list, const std::string& sep) { std::stringstream ss; diff --git a/test/meson.build b/test/meson.build index 437f5b528..360c66d93 100644 --- a/test/meson.build +++ b/test/meson.build @@ -5,7 +5,8 @@ tests = [ 'library', 'regex', 'tagParsing', - 'stringTools' + 'stringTools', + 'pathTools' ] diff --git a/test/pathTools.cpp b/test/pathTools.cpp new file mode 100644 index 000000000..6b3b48329 --- /dev/null +++ b/test/pathTools.cpp @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2019 Matthieu Gautier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "gtest/gtest.h" +#include +#include +#include "../include/tools/pathTools.h" + +#ifdef _WIN32 +# define S "\\" +# define AS "c:" +#else +# define S "/" +# define AS "" +#endif + +#define P2(a, b) a S b +#define P3(a, b, c) P2(P2(a, b), c) +#define P4(a, b, c, d) P2(P3(a, b, c), d) +#define P5(a, b, c, d, e) P2(P4(a, b, c, d), e) +#define P6(a, b, c, d, e, f) P2(P5(a, b, c ,d, e), f) + +#define A1(a) P2(AS,a) +#define A2(a, b) A1(P2(a, b)) +#define A3(a, b, c) A1(P3(a, b, c)) +#define A4(a, b, c, d) A1(P4(a, b, c, d)) +#define A5(a, b, c, d, e) A1(P5(a, b, c, d, e)) + +std::vector normalizeParts(std::vector parts, bool absolute); + +namespace +{ + +#define V std::vector +TEST(pathTools, normalizePartsAbsolute) +{ +#define N(...) normalizeParts(__VA_ARGS__, true) + ASSERT_EQ(N({}), V({})); +#ifdef _WIN32 + ASSERT_EQ(N({"c:"}), V({"c:"})); +#else + ASSERT_EQ(N({""}), V({"", ""})); +#endif + ASSERT_EQ(N({AS, "a"}), V({AS, "a"})); + ASSERT_EQ(N({AS, "a", "b"}), V({AS, "a", "b"})); + ASSERT_EQ(N({AS, "a", "b", ".."}), V({AS, "a"})); +#ifdef _WIN32 + ASSERT_EQ(N({AS, "a", "b", "..", ".."}), V({AS})); +#else + ASSERT_EQ(N({AS, "a", "b", "..", ".."}), V({AS, ""})); +#endif + ASSERT_EQ(N({AS, "a", "b", "..", "..", "..", "foo"}), V({AS, "foo"})); + ASSERT_EQ(N({AS, "..", "..", "c", "d", ".", "..", "foo"}), V({AS, "c", "foo"})); + ASSERT_EQ(N({AS, "a", "b", ".", "c", "d", "..", "foo"}), V({AS, "a", "b", "c", "foo"})); + +#ifdef _WIN32 + ASSERT_EQ(N({"c:", "a", "b", ".", "c", "d:", "..", "foo"}), V({"d:", "foo"})); +#endif +#undef N +} + +TEST(pathTools, normalizePartsRelative) +{ +#define N(...) normalizeParts(__VA_ARGS__, false) + ASSERT_EQ(N({}), V({})); + ASSERT_EQ(N({""}), V({""})); + ASSERT_EQ(N({"a"}), V({"a"})); + ASSERT_EQ(N({"a", "b"}), V({"a", "b"})); + ASSERT_EQ(N({"a", "b", ".."}), V({"a"})); + ASSERT_EQ(N({"a", "b", "..", ".."}), V({})); + ASSERT_EQ(N({"a", "b", "..", "..", "..", "foo"}), V({"..", "foo"})); + ASSERT_EQ(N({"..", "..", "c", "d", ".", "..", "foo"}), V({"..", "..", "c", "foo"})); + ASSERT_EQ(N({"a", "b", ".", "c", "d", "..", "foo"}), V({"a", "b", "c", "foo"})); +#undef N +} + +TEST(pathTools, isRelativePath) +{ + ASSERT_TRUE(isRelativePath("foo")); + ASSERT_TRUE(isRelativePath(P2("foo","bar"))); + ASSERT_TRUE(isRelativePath(P3(".","foo","bar"))); + ASSERT_TRUE(isRelativePath(P2("..","foo"))); + ASSERT_TRUE(isRelativePath(P4("foo","","bar",""))); + ASSERT_FALSE(isRelativePath(A1("foo"))); + ASSERT_FALSE(isRelativePath(A2("foo", "bar"))); +} + +TEST(pathTools, computeAbsolutePath) +{ + ASSERT_EQ(computeAbsolutePath(A2("a","b"), "foo"), + A3("a","b","foo")); + ASSERT_EQ(computeAbsolutePath(A3("a","b",""), "foo"), + A3("a","b","foo")); + ASSERT_EQ(computeAbsolutePath(A2("a","b"), P2(".","foo")), + A3("a","b","foo")); + ASSERT_EQ(computeAbsolutePath(A2("a","b"), P2("..","foo")), + A2("a","foo")); + ASSERT_EQ(computeAbsolutePath(A3("a","b",""), P2("..","foo")), + A2("a","foo")); + ASSERT_EQ(computeAbsolutePath(A5("a","b","c","d","e"), P2("..","foo")), + A5("a","b","c","d","foo")); + ASSERT_EQ(computeAbsolutePath(A5("a","b","c","d","e"), P5("..","..","..","g","foo")), + A4("a","b","g","foo")); +} + +TEST(pathTools, computeRelativePath) +{ + ASSERT_EQ(computeRelativePath(A2("a","b"), A3("a","b","foo")), + "foo"); + ASSERT_EQ(computeRelativePath(A3("a","b",""), A3("a","b","foo")), + "foo"); + ASSERT_EQ(computeRelativePath(A2("a","b"), A2("a","foo")), + P2("..","foo")); + ASSERT_EQ(computeRelativePath(A3("a","b",""), A2("a","foo")), + P2("..","foo")); + ASSERT_EQ(computeRelativePath(A5("a","b","c","d","e"), A5("a","b","c","d","foo")), + P2("..","foo")); + ASSERT_EQ(computeRelativePath(A5("a","b","c","d","e"), A4("a","b","g","foo")), + P5("..","..","..","g","foo")); +} + +TEST(pathTools, removeLastPathElement) +{ + ASSERT_EQ(removeLastPathElement(P3("a","b","c")), + P2("a","b")); + ASSERT_EQ(removeLastPathElement(A3("a","b","c")), + A2("a","b")); + ASSERT_EQ(removeLastPathElement(P4("a","b","c","")), + P2("a","b")); + ASSERT_EQ(removeLastPathElement(A4("a","b","c","")), + A2("a","b")); +} + +TEST(pathTools, appendToDirectory) +{ + ASSERT_EQ(appendToDirectory(P3("a","b","c"), "foo.xml"), + P4("a","b","c","foo.xml")); + ASSERT_EQ(appendToDirectory(P4("a","b","c",""), "foo.xml"), + P4("a","b","c","foo.xml")); + ASSERT_EQ(appendToDirectory(P3("a","b","c"), P2("d","foo.xml")), + P5("a","b","c","d","foo.xml")); + ASSERT_EQ(appendToDirectory(P4("a","b","c",""), P2("d","foo.xml")), + P5("a","b","c","d","foo.xml")); + ASSERT_EQ(appendToDirectory(P3("a","b","c"), P2(".","foo.xml")), + P5("a","b","c",".","foo.xml")); + ASSERT_EQ(appendToDirectory(P4("a","b","c",""), P2(".","foo.xml")), + P5("a","b","c",".","foo.xml")); +} + + +TEST(pathTools, goUp) +{ + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), ".."), + A2("a", "b")); + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P2("..","..")), + A1("a")); +#ifdef _WIN32 + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","..")), + "c:"); + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")), + "c:"); +#else + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","..")), + "/"); + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")), + "/"); +#endif + + + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P2("..", "foo")), + A3("a", "b","foo")); + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","foo")), + A2("a","foo")); + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","foo")), + A1("foo")); + ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P5("..","..","..","..","foo")), + A1("foo")); +} + + +#ifdef _WIN32 +TEST(pathTools, dirChange) +{ + std::string p1("c:\\a\\b\\c"); + std::string p2("d:\\d\\e\\foo.xml"); + std::string relative_path = computeRelativePath(p1, p2); + ASSERT_EQ(relative_path, "d:\\d\\e\\foo.xml"); + std::string abs_path = computeAbsolutePath(p1, relative_path); + ASSERT_EQ(abs_path, p2); + ASSERT_EQ(computeAbsolutePath(p1, "..\\..\\..\\..\\..\\d:\\d\\e\\foo.xml"), p2); +} +#endif + + +}; +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/stringTools.cpp b/test/stringTools.cpp index c632f0b7c..28e717f53 100644 --- a/test/stringTools.cpp +++ b/test/stringTools.cpp @@ -23,6 +23,7 @@ namespace kiwix { std::string join(const std::vector& list, const std::string& sep); +std::vector split(const std::string& base, const std::string& sep, bool trimEmpty); }; using namespace kiwix; @@ -36,6 +37,22 @@ TEST(stringTools, join) ASSERT_EQ(join(list, ";"), "a;b;c"); } +TEST(stringTools, split) +{ + std::vector list1 = { "a", "b", "c" }; + ASSERT_EQ(split("a;b;c", ";", false), list1); + ASSERT_EQ(split("a;b;c", ";", true), list1); + std::vector list2 = { "", "a", "b", "c" }; + ASSERT_EQ(split(";a;b;c", ";", false), list2); + ASSERT_EQ(split(";a;b;c", ";", true), list1); + std::vector list3 = { "", "a", "b", "c", ""}; + ASSERT_EQ(split(";a;b;c;", ";", false), list3); + ASSERT_EQ(split(";a;b;c;", ";", true), list1); + std::vector list4 = { "", "a", "b", "", "c", ""}; + ASSERT_EQ(split(";a;b;;c;", ";", false), list4); + ASSERT_EQ(split(";a;b;;c;", ";", true), list1); +} + }; int main(int argc, char** argv) {