Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #ifdef HAVE_CONFIG_H
19 : #include "config.h"
20 : #endif
21 :
22 : #include "fileutils.h"
23 : #include "logger.h"
24 : #include "archiver.h"
25 : #include "compiler_intrinsics.h"
26 : #include "base64.h"
27 : #include "string_utils.h"
28 :
29 : #include <opendht/crypto.h>
30 :
31 : #ifdef __APPLE__
32 : #include <TargetConditionals.h>
33 : #endif
34 :
35 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
36 : #include "client/jami_signal.h"
37 : #endif
38 :
39 : #ifdef _WIN32
40 : #include <windows.h>
41 : #include "string_utils.h"
42 : #endif
43 :
44 : #include <sys/types.h>
45 : #include <sys/stat.h>
46 :
47 : #ifndef _MSC_VER
48 : #include <libgen.h>
49 : #endif
50 :
51 : #ifdef _MSC_VER
52 : #include "windirent.h"
53 : #else
54 : #include <dirent.h>
55 : #endif
56 :
57 : #include <signal.h>
58 : #include <unistd.h>
59 : #include <fcntl.h>
60 : #ifndef _WIN32
61 : #include <pwd.h>
62 : #else
63 : #include <shlobj.h>
64 : #define NAME_MAX 255
65 : #endif
66 : #if !defined __ANDROID__ && !defined _WIN32
67 : #include <wordexp.h>
68 : #endif
69 :
70 : #include <nettle/sha3.h>
71 :
72 : #include <sstream>
73 : #include <fstream>
74 : #include <iostream>
75 : #include <stdexcept>
76 : #include <limits>
77 : #include <array>
78 :
79 : #include <cstdlib>
80 : #include <cstring>
81 : #include <cerrno>
82 : #include <cstddef>
83 :
84 : #include <pj/ctype.h>
85 : #include <pjlib-util/md5.h>
86 :
87 : #ifndef _MSC_VER
88 : #define PROTECTED_GETENV(str) \
89 : ({ \
90 : char* envvar_ = getenv((str)); \
91 : envvar_ ? envvar_ : ""; \
92 : })
93 :
94 : #define XDG_DATA_HOME (PROTECTED_GETENV("XDG_DATA_HOME"))
95 : #define XDG_CONFIG_HOME (PROTECTED_GETENV("XDG_CONFIG_HOME"))
96 : #define XDG_CACHE_HOME (PROTECTED_GETENV("XDG_CACHE_HOME"))
97 : #else
98 : const wchar_t*
99 : winGetEnv(const wchar_t* name)
100 : {
101 : const DWORD buffSize = 65535;
102 : static wchar_t buffer[buffSize];
103 : if (GetEnvironmentVariable(name, buffer, buffSize)) {
104 : return buffer;
105 : } else {
106 : return L"";
107 : }
108 : }
109 :
110 : #define PROTECTED_GETENV(str) winGetEnv(str)
111 :
112 : #define JAMI_DATA_HOME PROTECTED_GETENV(L"JAMI_DATA_HOME")
113 : #define JAMI_CONFIG_HOME PROTECTED_GETENV(L"JAMI_CONFIG_HOME")
114 : #define JAMI_CACHE_HOME PROTECTED_GETENV(L"JAMI_CACHE_HOME")
115 : #endif
116 :
117 : #define PIDFILE ".ring.pid"
118 : #define ERASE_BLOCK 4096
119 :
120 : namespace jami {
121 : namespace fileutils {
122 :
123 : static std::filesystem::path resource_dir_path;
124 :
125 : void
126 0 : set_resource_dir_path(const std::filesystem::path& resourceDirPath)
127 : {
128 0 : resource_dir_path = resourceDirPath;
129 0 : }
130 :
131 : const std::filesystem::path&
132 815 : get_resource_dir_path()
133 : {
134 815 : static const std::filesystem::path jami_default_data_dir(JAMI_DATADIR);
135 815 : return resource_dir_path.empty() ? jami_default_data_dir : resource_dir_path;
136 : }
137 :
138 : std::string
139 0 : expand_path(const std::string& path)
140 : {
141 : #if defined __ANDROID__ || defined _MSC_VER || defined WIN32 || defined __APPLE__
142 : JAMI_ERR("Path expansion not implemented, returning original");
143 : return path;
144 : #else
145 :
146 0 : std::string result;
147 :
148 : wordexp_t p;
149 0 : int ret = wordexp(path.c_str(), &p, 0);
150 :
151 0 : switch (ret) {
152 0 : case WRDE_BADCHAR:
153 0 : JAMI_ERR("Illegal occurrence of newline or one of |, &, ;, <, >, "
154 : "(, ), {, }.");
155 0 : return result;
156 0 : case WRDE_BADVAL:
157 0 : JAMI_ERR("An undefined shell variable was referenced");
158 0 : return result;
159 0 : case WRDE_CMDSUB:
160 0 : JAMI_ERR("Command substitution occurred");
161 0 : return result;
162 0 : case WRDE_SYNTAX:
163 0 : JAMI_ERR("Shell syntax error");
164 0 : return result;
165 0 : case WRDE_NOSPACE:
166 0 : JAMI_ERR("Out of memory.");
167 : // This is the only error where we must call wordfree
168 0 : break;
169 0 : default:
170 0 : if (p.we_wordc > 0)
171 0 : result = std::string(p.we_wordv[0]);
172 0 : break;
173 : }
174 :
175 0 : wordfree(&p);
176 :
177 0 : return result;
178 : #endif
179 0 : }
180 :
181 : bool
182 2 : isDirectoryWritable(const std::string& directory)
183 : {
184 2 : return accessFile(directory, W_OK) == 0;
185 : }
186 :
187 : bool
188 13 : createSymlink(const std::filesystem::path& linkFile, const std::filesystem::path& target)
189 : {
190 13 : std::error_code ec;
191 13 : std::filesystem::create_symlink(target, linkFile, ec);
192 13 : if (ec) {
193 0 : JAMI_WARNING("Unable to create soft link from {} to {}: {}", linkFile, target, ec.message());
194 0 : return false;
195 : } else {
196 52 : JAMI_LOG("Created soft link from {} to {}", linkFile, target);
197 : }
198 13 : return true;
199 : }
200 :
201 : bool
202 14 : createHardlink(const std::filesystem::path& linkFile, const std::filesystem::path& target)
203 : {
204 14 : std::error_code ec;
205 14 : std::filesystem::create_hard_link(target, linkFile, ec);
206 14 : if (ec) {
207 0 : JAMI_WARNING("Unable to create hard link from {} to {}: {}", linkFile, target, ec.message());
208 0 : return false;
209 : } else {
210 56 : JAMI_LOG("Created hard link from {} to {}", linkFile, target);
211 : }
212 14 : return true;
213 : }
214 :
215 : bool
216 29 : createFileLink(const std::filesystem::path& linkFile, const std::filesystem::path& target, bool hard)
217 : {
218 29 : if (linkFile == target)
219 0 : return true;
220 29 : std::error_code ec;
221 : // Use symlink_status() because exists() could return false for broken symlinks
222 29 : auto status = std::filesystem::symlink_status(linkFile, ec);
223 29 : if (status.type() != std::filesystem::file_type::not_found) {
224 6 : if (status.type() == std::filesystem::file_type::symlink
225 3 : && std::filesystem::read_symlink(linkFile, ec) == target) {
226 8 : JAMI_DEBUG("createFileLink: {} symlink already points to target {}", linkFile, target);
227 2 : return true;
228 : }
229 : // Remove any existing file or symlink before creating a new one, as create_symlink()
230 : // will fail with "File exists" error if the linkFile path already exists.
231 1 : if (status.type() == std::filesystem::file_type::regular
232 1 : || status.type() == std::filesystem::file_type::symlink) {
233 1 : std::filesystem::remove(linkFile, ec);
234 : }
235 : }
236 :
237 : // Try to create a hard link if requested; fall back to symlink on failure
238 27 : if (not hard or not createHardlink(linkFile, target))
239 13 : return createSymlink(linkFile, target);
240 14 : return true;
241 : }
242 :
243 : std::string_view
244 86 : getFileExtension(std::string_view filename)
245 : {
246 86 : std::string_view result;
247 86 : auto sep = filename.find_last_of('.');
248 86 : if (sep != std::string_view::npos && sep != filename.size() - 1)
249 4 : result = filename.substr(sep + 1);
250 86 : if (result.size() >= 8)
251 0 : return {};
252 86 : return result;
253 : }
254 :
255 : bool
256 9196 : isPathRelative(const std::filesystem::path& path)
257 : {
258 9196 : return not path.empty() and path.is_relative();
259 : }
260 :
261 : std::string
262 10231 : getCleanPath(const std::string& base, const std::string& path)
263 : {
264 10231 : if (base.empty() or path.size() < base.size())
265 10224 : return path;
266 7 : auto base_sep = base + DIR_SEPARATOR_STR;
267 7 : if (path.compare(0, base_sep.size(), base_sep) == 0)
268 7 : return path.substr(base_sep.size());
269 : else
270 0 : return path;
271 7 : }
272 :
273 : std::filesystem::path
274 14452 : getFullPath(const std::filesystem::path& base, const std::filesystem::path& path)
275 : {
276 14452 : bool isRelative {not base.empty() and isPathRelative(path)};
277 14452 : return isRelative ? base / path : path;
278 : }
279 :
280 : std::vector<uint8_t>
281 6845 : loadFile(const std::filesystem::path& path, const std::filesystem::path& default_dir)
282 : {
283 11010 : return dhtnet::fileutils::loadFile(getFullPath(default_dir, path));
284 : }
285 :
286 : std::string
287 9 : loadTextFile(const std::filesystem::path& path, const std::filesystem::path& default_dir)
288 : {
289 9 : std::string buffer;
290 9 : auto fullPath = getFullPath(default_dir, path);
291 :
292 : // Open with explicit share mode to allow reading even if file is opened elsewhere
293 : #ifdef _WIN32
294 : std::ifstream file(fullPath, std::ios::in | std::ios::binary, _SH_DENYNO);
295 : #else
296 9 : std::ifstream file(fullPath);
297 : #endif
298 :
299 9 : if (!file)
300 3 : throw std::runtime_error("Unable to read file: " + path.string());
301 :
302 6 : file.seekg(0, std::ios::end);
303 6 : auto size = file.tellg();
304 6 : if (size > std::numeric_limits<unsigned>::max())
305 0 : throw std::runtime_error("File is too big: " + path.string());
306 6 : buffer.resize(size);
307 6 : file.seekg(0, std::ios::beg);
308 6 : if (!file.read((char*) buffer.data(), size))
309 0 : throw std::runtime_error("Unable to load file: " + path.string());
310 12 : return buffer;
311 15 : }
312 :
313 : void
314 1571 : saveFile(const std::filesystem::path& path, const uint8_t* data, size_t data_size, mode_t UNUSED mode)
315 : {
316 1571 : std::ofstream file(path, std::ios::trunc | std::ios::binary);
317 1571 : if (!file.is_open()) {
318 0 : JAMI_ERROR("Unable to write data to {}", path);
319 0 : return;
320 : }
321 1571 : file.write((char*) data, data_size);
322 : #ifndef _WIN32
323 1571 : file.close();
324 1571 : if (chmod(path.c_str(), mode) < 0)
325 0 : JAMI_WARNING("fileutils::saveFile(): chmod() failed on {}, {}", path, strerror(errno));
326 : #endif
327 1571 : }
328 :
329 : std::vector<uint8_t>
330 0 : loadCacheFile(const std::filesystem::path& path, std::chrono::system_clock::duration maxAge)
331 : {
332 : // last_write_time throws exception if file doesn't exist
333 0 : std::error_code ec;
334 0 : auto writeTime = std::filesystem::last_write_time(path, ec);
335 0 : if (ec)
336 0 : throw std::runtime_error("unable to get last write time of file");
337 0 : auto now = decltype(writeTime)::clock::now();
338 0 : if (now - writeTime > maxAge)
339 0 : throw std::runtime_error("file too old " + dht::print_time_relative(now, writeTime));
340 :
341 0 : JAMI_LOG("Loading cache file '{}'", path);
342 0 : return dhtnet::fileutils::loadFile(path);
343 : }
344 :
345 : std::string
346 0 : loadCacheTextFile(const std::filesystem::path& path, std::chrono::system_clock::duration maxAge)
347 : {
348 : // last_write_time throws exception if file doesn't exist
349 0 : std::error_code ec;
350 0 : auto writeTime = std::filesystem::last_write_time(path, ec);
351 0 : if (ec)
352 0 : throw std::runtime_error("unable to get last write time of file");
353 0 : auto now = decltype(writeTime)::clock::now();
354 0 : if (now - writeTime > maxAge)
355 0 : throw std::runtime_error("file too old " + dht::print_time_relative(now, writeTime));
356 :
357 0 : JAMI_LOG("Loading cache file '{}'", path);
358 0 : return loadTextFile(path);
359 : }
360 :
361 : ArchiveStorageData
362 107 : readArchive(const std::filesystem::path& path, std::string_view scheme, const std::string& pwd)
363 : {
364 428 : JAMI_LOG("Reading archive from {} with scheme '{}'", path, scheme);
365 :
366 199 : auto isUnencryptedGzip = [](const std::vector<uint8_t>& data) {
367 : // NOTE: some webserver modify gzip files and this can end with a gunzip in a gunzip
368 : // file. So, to make the readArchive more robust, we can support this case by detecting
369 : // gzip header via 1f8b 08
370 : // We don't need to support more than 2 level, else somebody may be able to send
371 : // gunzip in loops and abuse.
372 199 : return data.size() > 3 && data[0] == 0x1f && data[1] == 0x8b && data[2] == 0x08;
373 : };
374 :
375 104 : auto decompress = [](std::vector<uint8_t>& data) {
376 : try {
377 104 : data = archiver::decompress(data);
378 0 : } catch (const std::exception& e) {
379 0 : JAMI_ERROR("Error decrypting archive: {}", e.what());
380 0 : throw e;
381 0 : }
382 104 : };
383 :
384 107 : std::vector<uint8_t> fileContent;
385 :
386 : // Read file
387 : try {
388 107 : fileContent = dhtnet::fileutils::loadFile(path);
389 0 : } catch (const std::exception& e) {
390 0 : JAMI_ERROR("Error loading archive: {}", e.what());
391 0 : throw;
392 0 : }
393 :
394 107 : if (isUnencryptedGzip(fileContent)) {
395 93 : if (!pwd.empty())
396 8 : JAMI_WARNING("A gunzip in a gunzip is detected. A webserver may have a bad config");
397 93 : decompress(fileContent);
398 : }
399 :
400 107 : ArchiveStorageData ret;
401 : // ret.data = {fileContent.data(), fileContent.data()+fileContent.size()};
402 :
403 107 : if (!pwd.empty()) {
404 : // Decrypt
405 15 : if (scheme == ARCHIVE_AUTH_SCHEME_KEY) {
406 : try {
407 0 : ret.salt = dht::crypto::aesGetSalt(fileContent);
408 0 : fileContent = dht::crypto::aesDecrypt(dht::crypto::aesGetEncrypted(fileContent), base64::decode(pwd));
409 0 : } catch (const std::exception& e) {
410 0 : JAMI_ERROR("Error decrypting archive: {}", e.what());
411 0 : throw;
412 0 : }
413 15 : } else if (scheme == ARCHIVE_AUTH_SCHEME_PASSWORD) {
414 : try {
415 15 : ret.salt = dht::crypto::aesGetSalt(fileContent);
416 15 : fileContent = dht::crypto::aesDecrypt(fileContent, pwd);
417 5 : } catch (const std::exception& e) {
418 20 : JAMI_ERROR("Error decrypting archive: {}", e.what());
419 5 : throw;
420 5 : }
421 : }
422 10 : decompress(fileContent);
423 92 : } else if (isUnencryptedGzip(fileContent)) {
424 4 : JAMI_WARNING("A gunzip in a gunzip is detected. A webserver may have a bad config");
425 1 : decompress(fileContent);
426 : }
427 102 : ret.data = {fileContent.data(), fileContent.data() + fileContent.size()};
428 204 : return ret;
429 112 : }
430 :
431 : bool
432 820 : writeArchive(const std::string& archive_str,
433 : const std::filesystem::path& path,
434 : std::string_view scheme,
435 : const std::string& password,
436 : const std::vector<uint8_t>& password_salt)
437 : {
438 3280 : JAMI_LOG("Writing archive to {} using scheme '{}'", path, scheme);
439 :
440 820 : if (scheme == ARCHIVE_AUTH_SCHEME_KEY) {
441 : // Encrypt using provided key
442 : try {
443 0 : auto key = base64::decode(password);
444 0 : auto newArchive = dht::crypto::aesEncrypt(archiver::compress(archive_str), key);
445 0 : saveFile(path, dht::crypto::aesBuildEncrypted(newArchive, password_salt));
446 0 : } catch (const std::runtime_error& ex) {
447 0 : JAMI_ERROR("Export failed: {}", ex.what());
448 0 : return false;
449 0 : }
450 820 : } else if (scheme == ARCHIVE_AUTH_SCHEME_PASSWORD and not password.empty()) {
451 : // Encrypt using provided password
452 : try {
453 18 : saveFile(path, dht::crypto::aesEncrypt(archiver::compress(archive_str), password, password_salt));
454 0 : } catch (const std::runtime_error& ex) {
455 0 : JAMI_ERROR("Export failed: {}", ex.what());
456 0 : return false;
457 0 : }
458 802 : } else if (scheme == ARCHIVE_AUTH_SCHEME_NONE || (scheme == ARCHIVE_AUTH_SCHEME_PASSWORD && password.empty())) {
459 3208 : JAMI_WARNING("Unsecured archiving (no password)");
460 802 : archiver::compressGzip(archive_str, path.string());
461 : } else {
462 0 : JAMI_ERROR("Unsupported scheme: {}", scheme);
463 0 : return false;
464 : }
465 820 : return true;
466 : }
467 :
468 : std::filesystem::path
469 64 : get_cache_dir([[maybe_unused]] const char* pkg)
470 : {
471 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
472 : std::vector<std::string> paths;
473 : paths.reserve(1);
474 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("cache", &paths);
475 : if (not paths.empty())
476 : return paths[0];
477 : return {};
478 : #elif defined(__APPLE__)
479 : return get_home_dir() / "Library" / "Caches" / pkg;
480 : #else
481 : #ifdef _WIN32
482 : const std::wstring cache_home(JAMI_CACHE_HOME);
483 : if (not cache_home.empty())
484 : return jami::to_string(cache_home);
485 : #else
486 64 : const std::string cache_home(XDG_CACHE_HOME);
487 64 : if (not cache_home.empty())
488 0 : return cache_home;
489 : #endif
490 128 : return get_home_dir() / ".cache" / pkg;
491 : #endif
492 64 : }
493 :
494 : const std::filesystem::path&
495 3280 : get_cache_dir()
496 : {
497 3280 : static const std::filesystem::path cache_dir = get_cache_dir(PACKAGE);
498 3280 : return cache_dir;
499 : }
500 :
501 : std::filesystem::path
502 33 : get_home_dir_impl()
503 : {
504 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
505 : std::vector<std::string> paths;
506 : paths.reserve(1);
507 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("files", &paths);
508 : if (not paths.empty())
509 : return paths[0];
510 : return {};
511 : #elif defined _WIN32
512 : TCHAR path[MAX_PATH];
513 : if (SUCCEEDED(SHGetFolderPath(nullptr, CSIDL_PROFILE, nullptr, 0, path))) {
514 : return jami::to_string(path);
515 : }
516 : return {};
517 : #else
518 :
519 : // 1) try getting user's home directory from the environment
520 33 : std::string home(PROTECTED_GETENV("HOME"));
521 33 : if (not home.empty())
522 33 : return home;
523 :
524 : // 2) try getting it from getpwuid_r (i.e. /etc/passwd)
525 0 : const long max = sysconf(_SC_GETPW_R_SIZE_MAX);
526 0 : if (max != -1) {
527 0 : char buf[max];
528 : struct passwd pwbuf, *pw;
529 0 : if (getpwuid_r(getuid(), &pwbuf, buf, sizeof(buf), &pw) == 0 and pw != NULL)
530 0 : return pw->pw_dir;
531 0 : }
532 :
533 0 : return {};
534 : #endif
535 33 : }
536 :
537 : const std::filesystem::path&
538 195 : get_home_dir()
539 : {
540 195 : static const std::filesystem::path home_dir = get_home_dir_impl();
541 195 : return home_dir;
542 : }
543 :
544 : std::filesystem::path
545 64 : get_data_dir([[maybe_unused]] const char* pkg)
546 : {
547 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
548 : std::vector<std::string> paths;
549 : paths.reserve(1);
550 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("files", &paths);
551 : if (not paths.empty())
552 : return paths[0];
553 : return {};
554 : #elif defined(__APPLE__)
555 : return get_home_dir() / "Library" / "Application Support" / pkg;
556 : #elif defined(_WIN32)
557 : std::wstring data_home(JAMI_DATA_HOME);
558 : if (not data_home.empty())
559 : return std::filesystem::path(data_home) / pkg;
560 :
561 : if (!strcmp(pkg, "ring")) {
562 : return get_home_dir() / ".local" / "share" / pkg;
563 : } else {
564 : return get_home_dir() / "AppData" / "Local" / pkg;
565 : }
566 : #else
567 64 : std::string_view data_home(XDG_DATA_HOME);
568 64 : if (not data_home.empty())
569 0 : return std::filesystem::path(data_home) / pkg;
570 : // "If $XDG_DATA_HOME is either not set or empty, a default equal to
571 : // $HOME/.local/share should be used."
572 128 : return get_home_dir() / ".local" / "share" / pkg;
573 : #endif
574 : }
575 :
576 : const std::filesystem::path&
577 60287 : get_data_dir()
578 : {
579 60287 : static const std::filesystem::path data_dir = get_data_dir(PACKAGE);
580 60285 : return data_dir;
581 : }
582 :
583 : std::filesystem::path
584 65 : get_config_dir([[maybe_unused]] const char* pkg)
585 : {
586 65 : std::filesystem::path configdir;
587 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
588 : std::vector<std::string> paths;
589 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("config", &paths);
590 : if (not paths.empty())
591 : configdir = std::filesystem::path(paths[0]);
592 : #elif defined(__APPLE__)
593 : configdir = fileutils::get_home_dir() / "Library" / "Application Support" / pkg;
594 : #elif defined(_WIN32)
595 : std::wstring xdg_env(JAMI_CONFIG_HOME);
596 : if (not xdg_env.empty()) {
597 : configdir = std::filesystem::path(xdg_env) / pkg;
598 : } else if (!strcmp(pkg, "ring")) {
599 : configdir = fileutils::get_home_dir() / ".config" / pkg;
600 : } else {
601 : configdir = fileutils::get_home_dir() / "AppData" / "Local" / pkg;
602 : }
603 : #else
604 65 : std::string xdg_env(XDG_CONFIG_HOME);
605 65 : if (not xdg_env.empty())
606 0 : configdir = std::filesystem::path(xdg_env) / pkg;
607 : else
608 65 : configdir = fileutils::get_home_dir() / ".config" / pkg;
609 : #endif
610 65 : if (!dhtnet::fileutils::recursive_mkdir(configdir, 0700)) {
611 : // If directory creation failed
612 0 : if (errno != EEXIST)
613 0 : JAMI_DBG("Unable to create directory: %s!", configdir.c_str());
614 : }
615 130 : return configdir;
616 65 : }
617 :
618 : const std::filesystem::path&
619 258 : get_config_dir()
620 : {
621 258 : static const std::filesystem::path config_dir = get_config_dir(PACKAGE);
622 258 : return config_dir;
623 : }
624 :
625 : #ifdef _WIN32
626 : bool
627 : eraseFile_win32(const std::string& path, bool dosync)
628 : {
629 : // Note: from
630 : // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilea#remarks To
631 : // delete a read-only file, first you must remove the read-only attribute.
632 : SetFileAttributesA(path.c_str(), GetFileAttributesA(path.c_str()) & ~FILE_ATTRIBUTE_READONLY);
633 : HANDLE h = CreateFileA(path.c_str(), GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
634 : if (h == INVALID_HANDLE_VALUE) {
635 : JAMI_WARN("Unable to open file %s for erasing.", path.c_str());
636 : return false;
637 : }
638 :
639 : LARGE_INTEGER size;
640 : if (!GetFileSizeEx(h, &size)) {
641 : JAMI_WARN("Unable to erase file %s: GetFileSizeEx() failed.", path.c_str());
642 : CloseHandle(h);
643 : return false;
644 : }
645 : if (size.QuadPart == 0) {
646 : CloseHandle(h);
647 : return false;
648 : }
649 :
650 : uint64_t size_blocks = size.QuadPart / ERASE_BLOCK;
651 : if (size.QuadPart % ERASE_BLOCK)
652 : size_blocks++;
653 :
654 : char* buffer;
655 : try {
656 : buffer = new char[ERASE_BLOCK];
657 : } catch (std::bad_alloc& ba) {
658 : JAMI_WARN("Unable to allocate buffer for erasing %s.", path.c_str());
659 : CloseHandle(h);
660 : return false;
661 : }
662 : memset(buffer, 0x00, ERASE_BLOCK);
663 :
664 : OVERLAPPED ovlp;
665 : if (size.QuadPart < (1024 - 42)) { // a small file can be stored in the MFT record
666 : ovlp.Offset = 0;
667 : ovlp.OffsetHigh = 0;
668 : WriteFile(h, buffer, (DWORD) size.QuadPart, 0, &ovlp);
669 : FlushFileBuffers(h);
670 : }
671 : for (uint64_t i = 0; i < size_blocks; i++) {
672 : uint64_t offset = i * ERASE_BLOCK;
673 : ovlp.Offset = offset & 0x00000000FFFFFFFF;
674 : ovlp.OffsetHigh = offset >> 32;
675 : WriteFile(h, buffer, ERASE_BLOCK, 0, &ovlp);
676 : }
677 :
678 : delete[] buffer;
679 :
680 : if (dosync)
681 : FlushFileBuffers(h);
682 :
683 : CloseHandle(h);
684 : return true;
685 : }
686 :
687 : #else
688 :
689 : bool
690 0 : eraseFile_posix(const std::string& path, bool dosync)
691 : {
692 : struct stat st;
693 0 : if (stat(path.c_str(), &st) == -1) {
694 0 : JAMI_WARN("Unable to erase file %s: fstat() failed.", path.c_str());
695 0 : return false;
696 : }
697 : // Remove read-only flag if possible
698 0 : chmod(path.c_str(), st.st_mode | (S_IWGRP + S_IWUSR));
699 :
700 0 : int fd = open(path.c_str(), O_WRONLY);
701 0 : if (fd == -1) {
702 0 : JAMI_WARN("Unable to open file %s for erasing.", path.c_str());
703 0 : return false;
704 : }
705 :
706 0 : if (st.st_size == 0) {
707 0 : close(fd);
708 0 : return false;
709 : }
710 :
711 0 : lseek(fd, 0, SEEK_SET);
712 :
713 : std::array<char, ERASE_BLOCK> buffer;
714 0 : buffer.fill(0);
715 0 : decltype(st.st_size) written(0);
716 0 : while (written < st.st_size) {
717 0 : auto ret = write(fd, buffer.data(), buffer.size());
718 0 : if (ret < 0) {
719 0 : JAMI_WARNING("Error while overriding file with zeros.");
720 0 : break;
721 : } else
722 0 : written += ret;
723 : }
724 :
725 0 : if (dosync)
726 0 : fsync(fd);
727 :
728 0 : close(fd);
729 0 : return written >= st.st_size;
730 : }
731 : #endif
732 :
733 : bool
734 0 : eraseFile(const std::string& path, bool dosync)
735 : {
736 : #ifdef _WIN32
737 : return eraseFile_win32(path, dosync);
738 : #else
739 0 : return eraseFile_posix(path, dosync);
740 : #endif
741 : }
742 :
743 : int
744 0 : remove(const std::filesystem::path& path, bool erase)
745 : {
746 0 : if (erase and dhtnet::fileutils::isFile(path, false) and !dhtnet::fileutils::hasHardLink(path))
747 0 : eraseFile(path.string(), true);
748 :
749 : #ifdef _WIN32
750 : // use Win32 api since std::remove will not unlink directory in use
751 : if (std::filesystem::is_directory(path))
752 : return !RemoveDirectory(path.c_str());
753 : #endif
754 :
755 0 : return std::remove(path.string().c_str());
756 : }
757 :
758 : std::string
759 58 : sha3File(const std::filesystem::path& path)
760 : {
761 : sha3_512_ctx ctx;
762 58 : sha3_512_init(&ctx);
763 :
764 : try {
765 58 : if (not std::filesystem::is_regular_file(path)) {
766 8 : JAMI_ERROR("Unable to compute sha3sum of {}: not a regular file", path);
767 2 : return {};
768 : }
769 56 : std::ifstream file(path, std::ios::binary | std::ios::in);
770 56 : if (!file) {
771 0 : JAMI_ERROR("Unable to compute sha3sum of {}: failed to open file", path);
772 0 : return {};
773 : }
774 56 : constexpr size_t BUFFER_SIZE = 64 * 1024ul;
775 56 : std::vector<char> buffer(BUFFER_SIZE);
776 940 : while (file) {
777 886 : file.read(buffer.data(), BUFFER_SIZE);
778 886 : const auto bytesRead = file.gcount();
779 886 : if (bytesRead == 0)
780 2 : break;
781 884 : sha3_512_update(&ctx, static_cast<size_t>(bytesRead), (const uint8_t*) buffer.data());
782 : }
783 56 : } catch (const std::exception& e) {
784 0 : JAMI_ERROR("Unable to compute sha3sum of {}: {}", path, e.what());
785 0 : return {};
786 0 : }
787 :
788 : unsigned char digest[SHA3_512_DIGEST_SIZE];
789 56 : sha3_512_digest(&ctx, SHA3_512_DIGEST_SIZE, digest);
790 56 : return dht::toHex(digest, SHA3_512_DIGEST_SIZE);
791 : }
792 :
793 : std::string
794 6 : sha3sum(const std::vector<uint8_t>& buffer)
795 : {
796 : sha3_512_ctx ctx;
797 6 : sha3_512_init(&ctx);
798 6 : sha3_512_update(&ctx, buffer.size(), buffer.data());
799 : unsigned char digest[SHA3_512_DIGEST_SIZE];
800 6 : sha3_512_digest(&ctx, SHA3_512_DIGEST_SIZE, digest);
801 12 : return dht::toHex(digest, SHA3_512_DIGEST_SIZE);
802 : }
803 :
804 : int
805 2 : accessFile(const std::string& file, int mode)
806 : {
807 : #ifdef _WIN32
808 : return _waccess(jami::to_wstring(file).c_str(), mode);
809 : #else
810 2 : return access(file.c_str(), mode);
811 : #endif
812 : }
813 :
814 : uint64_t
815 19 : lastWriteTimeInSeconds(const std::filesystem::path& filePath)
816 : {
817 19 : std::error_code ec;
818 19 : auto lastWrite = std::filesystem::last_write_time(filePath, ec);
819 19 : if (ec) {
820 8 : JAMI_WARNING("Unable to get last write time of {}: {}", filePath, ec.message());
821 2 : return 0;
822 : }
823 17 : return std::chrono::duration_cast<std::chrono::seconds>(lastWrite.time_since_epoch()).count();
824 : }
825 :
826 : std::string
827 32 : getOrCreateLocalDeviceId()
828 : {
829 32 : const auto& localDir = get_data_dir(); // ~/.local/share/jami/
830 32 : auto fullIdPath = localDir / "local_device_id";
831 32 : std::string localDeviceId;
832 :
833 32 : if (std::filesystem::exists(fullIdPath)) {
834 : // Get the id inside the file
835 0 : std::ifstream inStream(fullIdPath);
836 0 : std::getline(inStream, localDeviceId);
837 0 : inStream.close();
838 0 : if (!localDeviceId.empty())
839 0 : return localDeviceId;
840 0 : }
841 :
842 : // Generate a random hex string
843 : {
844 32 : std::random_device randomDevice;
845 32 : localDeviceId = to_hex_string(std::uniform_int_distribution<uint64_t>()(randomDevice));
846 32 : }
847 :
848 : // Create a new file and write the id in it
849 32 : std::error_code ec;
850 32 : std::filesystem::create_directories(localDir, ec);
851 32 : std::ofstream outStream(fullIdPath);
852 32 : if (outStream) {
853 32 : outStream << localDeviceId << '\n';
854 32 : outStream.close();
855 : } else {
856 0 : JAMI_ERROR("Unable to create local device id file: {}", fullIdPath);
857 : }
858 32 : return localDeviceId;
859 32 : }
860 :
861 : } // namespace fileutils
862 : } // namespace jami
|