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