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