1 #include "PathUtil.h"
2 
3 #include <array>
4 #include <fstream>
5 
6 #include <glib.h>
7 #include <stdlib.h>
8 
9 #include "StringUtils.h"
10 #include "Util.h"
11 #include "XojMsgBox.h"
12 #include "i18n.h"
13 
14 #ifdef GHC_FILESYSTEM
15 // Fix of ghc::filesystem bug (path::operator/=() won't support string_views)
16 constexpr auto const* CONFIG_FOLDER_NAME = "xournalpp";
17 #else
18 using namespace std::string_view_literals;
19 constexpr auto CONFIG_FOLDER_NAME = "xournalpp"sv;
20 #endif
21 
22 #ifdef _WIN32
23 #include <windows.h>
24 
getLongPath(const fs::path & path)25 auto Util::getLongPath(const fs::path& path) -> fs::path {
26     DWORD wLongPathSz = GetLongPathNameW(path.c_str(), nullptr, 0);
27 
28     if (wLongPathSz == 0) {
29         return path;
30     }
31 
32     std::wstring wLongPath(wLongPathSz, L'\0');
33     GetLongPathNameW(path.c_str(), wLongPath.data(), static_cast<DWORD>(wLongPath.size()));
34     wLongPath.pop_back();
35     return fs::path(std::move(wLongPath));
36 }
37 #else
getLongPath(const fs::path & path)38 auto Util::getLongPath(const fs::path& path) -> fs::path { return path; }
39 #endif
40 
41 /**
42  * Read a file to a string
43  *
44  * @param path Path to read
45  * @param showErrorToUser Show an error to the user, if the file could not be read
46  *
47  * @return contents if the file was read, std::nullopt if not
48  */
readString(fs::path const & path,bool showErrorToUser)49 auto Util::readString(fs::path const& path, bool showErrorToUser) -> std::optional<std::string> {
50     try {
51         std::string s;
52         std::ifstream ifs{path};
53         s.resize(fs::file_size(path));
54         ifs.read(s.data(), s.size());
55         return {std::move(s)};
56     } catch (fs::filesystem_error const& e) {
57         if (showErrorToUser) {
58             XojMsgBox::showErrorToUser(nullptr, e.what());
59         }
60     }
61     return std::nullopt;
62 }
63 
getEscapedPath(const fs::path & path)64 auto Util::getEscapedPath(const fs::path& path) -> string {
65     string escaped = path.string();
66     StringUtils::replaceAllChars(escaped, {replace_pair('\\', "\\\\"), replace_pair('\"', "\\\"")});
67     return escaped;
68 }
69 
hasXournalFileExt(const fs::path & path)70 auto Util::hasXournalFileExt(const fs::path& path) -> bool {
71     auto extension = path.extension();
72     return extension == ".xoj" || extension == ".xopp";
73 }
74 
clearExtensions(fs::path & path,const std::string & ext)75 auto Util::clearExtensions(fs::path& path, const std::string& ext) -> void {
76     auto rm_ext = [&path](const std::string ext) {
77         if (StringUtils::toLowerCase(path.extension().string()) == StringUtils::toLowerCase(ext)) {
78             path.replace_extension("");
79         }
80     };
81 
82     rm_ext(".xoj");
83     rm_ext(".xopp");
84     if (!ext.empty()) {
85         rm_ext(ext);
86     }
87 }
88 
89 // Uri must be ASCII-encoded!
fromUri(const std::string & uri)90 auto Util::fromUri(const std::string& uri) -> std::optional<fs::path> {
91     if (!StringUtils::startsWith(uri, "file://")) {
92         return std::nullopt;
93     }
94 
95     gchar* filename = g_filename_from_uri(uri.c_str(), nullptr, nullptr);
96     if (filename == nullptr) {
97         return std::nullopt;
98     }
99     auto p = fs::u8path(filename);
100     g_free(filename);
101 
102     return {std::move(p)};
103 }
104 
toUri(const fs::path & path)105 auto Util::toUri(const fs::path& path) -> std::optional<std::string> {
106     GError* error{};
107     char* uri = [&] {
108         if (path.is_absolute()) {
109             return g_filename_to_uri(path.u8string().c_str(), nullptr, &error);
110         }
111         return g_filename_to_uri(fs::absolute(path).u8string().c_str(), nullptr, &error);
112     }();
113 
114     if (error != nullptr) {
115         g_warning("Util::toUri: could not parse path to URI, error: %s\n", error->message);
116         g_error_free(error);
117         return std::nullopt;
118     }
119 
120     if (!uri) {
121         g_warning("Util::toUri: path results in empty URI");
122         return std::nullopt;
123     }
124 
125     string uriString(uri);
126     g_free(uri);
127     return {std::move(uriString)};
128 }
129 
fromGFile(GFile * file)130 auto Util::fromGFile(GFile* file) -> fs::path {
131     char* p = g_file_get_path(file);
132     auto ret = p ? fs::u8path(p) : fs::path{};
133     g_free(p);
134     return ret;
135 }
136 
toGFile(fs::path const & path)137 auto Util::toGFile(fs::path const& path) -> GFile* { return g_file_new_for_path(path.u8string().c_str()); }
138 
139 
openFileWithDefaultApplication(const fs::path & filename)140 void Util::openFileWithDefaultApplication(const fs::path& filename) {
141 #ifdef __APPLE__
142     constexpr auto const OPEN_PATTERN = "open \"{1}\"";
143 #elif _WIN32  // note the underscore: without it, it's not msdn official!
144     constexpr auto const OPEN_PATTERN = "start \"{1}\"";
145 #else         // linux, unix, ...
146     constexpr auto const OPEN_PATTERN = "xdg-open \"{1}\"";
147 #endif
148 
149     string command = FS(FORMAT_STR(OPEN_PATTERN) % Util::getEscapedPath(filename));
150     if (system(command.c_str()) != 0) {
151         string msg = FS(_F("File couldn't be opened. You have to do it manually:\n"
152                            "URL: {1}") %
153                         filename.u8string());
154         XojMsgBox::showErrorToUser(nullptr, msg);
155     }
156 }
157 
openFileWithFilebrowser(const fs::path & filename)158 void Util::openFileWithFilebrowser(const fs::path& filename) {
159 #ifdef __APPLE__
160     constexpr auto const OPEN_PATTERN = "open \"{1}\"";
161 #elif _WIN32
162     constexpr auto const OPEN_PATTERN = "explorer.exe /n,/e,\"{1}\"";
163 #else  // linux, unix, ...
164     constexpr auto const OPEN_PATTERN = R"(nautilus "file://{1}" || dolphin "file://{1}" || konqueror "file://{1}" &)";
165 #endif
166     string command = FS(FORMAT_STR(OPEN_PATTERN) % Util::getEscapedPath(filename));
167     if (system(command.c_str()) != 0) {
168         string msg = FS(_F("File couldn't be opened. You have to do it manually:\n"
169                            "URL: {1}") %
170                         filename.u8string());
171         XojMsgBox::showErrorToUser(nullptr, msg);
172     }
173 }
174 
getGettextFilepath(const char * localeDir)175 auto Util::getGettextFilepath(const char* localeDir) -> fs::path {
176     const char* gettextEnv = g_getenv("TEXTDOMAINDIR");
177     // Only consider first path in environment variable
178     std::string directories;
179     if (gettextEnv) {
180         directories = std::string(gettextEnv);
181         size_t firstDot = directories.find(G_SEARCHPATH_SEPARATOR);
182         if (firstDot != std::string::npos) {
183             directories = directories.substr(0, firstDot);
184         }
185     }
186     const char* dir = (gettextEnv) ? directories.c_str() : localeDir;
187     g_message("TEXTDOMAINDIR = %s, PACKAGE_LOCALE_DIR = %s, chosen directory = %s", gettextEnv, localeDir, dir);
188     return fs::path(dir);
189 }
190 
getAutosaveFilepath()191 auto Util::getAutosaveFilepath() -> fs::path {
192     fs::path p(getConfigSubfolder("autosave"));
193     p /= std::to_string(getPid()) + ".xopp";
194     return p;
195 }
196 
getConfigFolder()197 auto Util::getConfigFolder() -> fs::path {
198     auto p = fs::u8path(g_get_user_config_dir());
199     return (p /= CONFIG_FOLDER_NAME);
200 }
201 
getConfigSubfolder(const fs::path & subfolder)202 auto Util::getConfigSubfolder(const fs::path& subfolder) -> fs::path {
203     fs::path p = getConfigFolder();
204     p /= subfolder;
205 
206     return Util::ensureFolderExists(p);
207 }
208 
getCacheSubfolder(const fs::path & subfolder)209 auto Util::getCacheSubfolder(const fs::path& subfolder) -> fs::path {
210     auto p = fs::u8path(g_get_user_cache_dir());
211     p /= CONFIG_FOLDER_NAME;
212     p /= subfolder;
213 
214     return Util::ensureFolderExists(p);
215 }
216 
getDataSubfolder(const fs::path & subfolder)217 auto Util::getDataSubfolder(const fs::path& subfolder) -> fs::path {
218     auto p = fs::u8path(g_get_user_data_dir());
219     p /= CONFIG_FOLDER_NAME;
220     p /= subfolder;
221 
222     return Util::ensureFolderExists(p);
223 }
224 
getConfigFile(const fs::path & relativeFileName)225 auto Util::getConfigFile(const fs::path& relativeFileName) -> fs::path {
226     fs::path p = getConfigSubfolder(relativeFileName.parent_path());
227     p /= relativeFileName.filename();
228     return p;
229 }
230 
getCacheFile(const fs::path & relativeFileName)231 auto Util::getCacheFile(const fs::path& relativeFileName) -> fs::path {
232     fs::path p = getCacheSubfolder(relativeFileName.parent_path());
233     p /= relativeFileName.filename();
234     return p;
235 }
236 
getTmpDirSubfolder(const fs::path & subfolder)237 auto Util::getTmpDirSubfolder(const fs::path& subfolder) -> fs::path {
238     auto p = fs::u8path(g_get_tmp_dir());
239     p /= FS(_F("xournalpp-{1}") % Util::getPid());
240     p /= subfolder;
241     return Util::ensureFolderExists(p);
242 }
243 
ensureFolderExists(const fs::path & p)244 auto Util::ensureFolderExists(const fs::path& p) -> fs::path {
245     try {
246         fs::create_directories(p);
247     } catch (fs::filesystem_error const& fe) {
248         Util::execInUiThread([=]() {
249             string msg = FS(_F("Could not create folder: {1}\nFailed with error: {2}") % p.u8string() % fe.what());
250             g_warning("%s %s", msg.c_str(), fe.what());
251             XojMsgBox::showErrorToUser(nullptr, msg);
252         });
253     }
254     return p;
255 }
256 
isChildOrEquivalent(fs::path const & path,fs::path const & base)257 auto Util::isChildOrEquivalent(fs::path const& path, fs::path const& base) -> bool {
258     auto safeCanonical = [](fs::path const& p) {
259         try {
260             return fs::weakly_canonical(p);
261         } catch (fs::filesystem_error const& fe) {
262             g_warning("Util::isChildOrEquivalent: Error resolving paths, failed with %s.\nFalling back to "
263                       "lexicographical path",
264                       fe.what());
265             return p;
266         }
267     };
268     auto relativePath = safeCanonical(path).lexically_relative(safeCanonical(base));
269     return !relativePath.empty() && *std::begin(relativePath) != "..";
270 }
271 
safeRenameFile(fs::path const & from,fs::path const & to)272 bool Util::safeRenameFile(fs::path const& from, fs::path const& to) {
273     if (!fs::is_regular_file(from)) {
274         return false;
275     }
276 
277     // Due to https://github.com/xournalpp/xournalpp/issues/1122,
278     // we first attempt to move the file with fs::rename.
279     // If this fails, we then copy and delete the source, as
280     // discussed in the issue
281     // Use target default perms; the source partition may have different file
282     // system attributes than the target, and we don't want anything bad in the
283     // autosave directory
284 
285     // Attempt move
286     try {
287         fs::remove(to);
288         fs::rename(from, to);
289     } catch (fs::filesystem_error const& fe) {
290         // Attempt copy and delete
291         g_warning("Renaming file %s to %s failed with %s. This may happen when source and target are on different "
292                   "filesystems. Attempt to copy the file.",
293                   fe.path1().string().c_str(), fe.path2().string().c_str(), fe.what());
294         fs::copy_file(from, to, fs::copy_options::overwrite_existing);
295         fs::remove(from);
296     }
297     return true;
298 }
299