From 3c489c3f514c376787af35d448d75a766fe5c32c Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 10 May 2024 13:12:23 +0300 Subject: [PATCH 01/99] Update the duplicate finder --- src/duplicateFinder/duplicateFinder.cppm | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index 11d733c..42cbbaa 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -23,7 +23,6 @@ module; #include #include #include -#include #include #include #include @@ -33,13 +32,13 @@ import utils; namespace fs = std::filesystem; -constexpr std::size_t CHUNK_SIZE = 4096; // Read and process files in chunks of 4 kB +constexpr std::size_t CHUNK_SIZE = 4096; ///< Read and process files in chunks of 4 kB /// \brief Represents a file by its path (canonical) and hash. struct FileInfo { - std::string path; // the path to the file. - std::string hash; // the file's BLAKE3 hash + std::string path; ///< the path to the file. + std::string hash; ///< the file's BLAKE3 hash }; /// \brief Calculates the 256-bit BLAKE3 hash of a file. @@ -100,7 +99,7 @@ inline void handleAccessError(const std::string_view filename) { case EROFS: // Read-only file system errMsg = "the file system is read-only"; break; - default: // Success (most likely) + default: [[likely]] // Success (most likely) return; } @@ -164,7 +163,7 @@ std::size_t findDuplicates(const std::string_view directoryPath) { std::vector files; traverseDirectory(directoryPath, files); const std::size_t filesProcessed = files.size(); - if (filesProcessed < 1) return 0; + if (filesProcessed < 2) return 0; // Number of threads to use const unsigned int n{std::jthread::hardware_concurrency()}; @@ -207,8 +206,8 @@ std::size_t findDuplicates(const std::string_view directoryPath) { printColor(":", 'c', true); for (const auto &filePath: duplicates) { - std::cout << " " << filePath << std::endl; ++numDuplicates; + std::cout << " " << filePath << std::endl; } } } From 5ee4826deb472e3b94e2d71695da1ce4c7081422 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 10 May 2024 13:15:12 +0300 Subject: [PATCH 02/99] Ignore ide config files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a91432d..b664571 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ # .idea/modules # *.iml # *.ipr +.idea/**/*copilot* # CMake cmake-build-*/ From f3e5b42712ba707de52b3d965ff2ed62bd5be28a Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 10 May 2024 17:00:21 +0300 Subject: [PATCH 03/99] Improve the performance of the encryption module --- src/encryption/encryptDecrypt.cpp | 13 ++-- src/encryption/encryptFiles.cpp | 102 ++++++++++++++++++------------ 2 files changed, 66 insertions(+), 49 deletions(-) diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index ccdaa7d..1bf9e90 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -97,7 +97,7 @@ constexpr struct { /// \throws std::invalid_argument if \p mode is invalid. /// \throws std::runtime_error if the input file does not exist, is a directory, /// is not a regular file, or is not readable. -inline void checkInputFile(const fs::path &inFile, const OperationMode &mode) { +void checkInputFile(const fs::path &inFile, const OperationMode &mode) { if (mode != OperationMode::Encryption && mode != OperationMode::Decryption) throw std::invalid_argument("Invalid mode of operation."); @@ -126,17 +126,15 @@ inline void checkInputFile(const fs::path &inFile, const OperationMode &mode) { /// \brief Creates non-existing parent directories for a file. /// \param filePath The file path for which the directory path needs to be created. /// \return True if the directory path is created successfully or already exists, false otherwise. -inline bool createPath(const fs::path &filePath) noexcept { +bool createPath(const fs::path &filePath) noexcept { if (filePath.string().empty()) return false; // Can't create empty paths std::error_code ec; - auto absolutePath = weakly_canonical(filePath, ec); if (ec) { absolutePath = filePath; ec.clear(); } - if (absolutePath.has_filename()) absolutePath.remove_filename(); @@ -370,7 +368,6 @@ void encryptDecrypt() { printColor("Invalid choice!", 'r', true, std::cerr); continue; } - const auto it = algoChoice.find(algo); auto cipher = it != algoChoice.end() ? it->second : Algorithms::AES; @@ -383,13 +380,11 @@ void encryptDecrypt() { printColor("Please avoid empty or weak passwords. Please try again.", 'r', true, std::cerr); password = getSensitiveInfo("Enter the password: "); } - if (tries >= 3) throw std::runtime_error("Empty encryption password."); - const privacy::string password2{getSensitiveInfo("Enter the password again: ")}; - - if (!verifyPassword(password2, hashPassword(password, crypto_pwhash_OPSLIMIT_INTERACTIVE, + if (const privacy::string password2{getSensitiveInfo("Enter the password again: ")}; + !verifyPassword(password2, hashPassword(password, crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE))) { printColor("Passwords do not match.", 'r', true, std::cerr); continue; diff --git a/src/encryption/encryptFiles.cpp b/src/encryption/encryptFiles.cpp index 49fcadf..972c707 100644 --- a/src/encryption/encryptFiles.cpp +++ b/src/encryption/encryptFiles.cpp @@ -33,9 +33,9 @@ import secureAllocator; module encryption; -constexpr int MAX_KEY_SIZE = EVP_MAX_KEY_LENGTH; // For bounds checking -constexpr std::size_t CHUNK_SIZE = 4096; // Read/Write files in chunks of 4 kB -constexpr unsigned int PBKDF2_ITERATIONS = 100'000; // Iterations for PBKDF2 key derivation +constexpr int MAX_KEY_SIZE = EVP_MAX_KEY_LENGTH; ///< Maximum length of a key +constexpr std::streamsize CHUNK_SIZE = 4096; ///< Read/Write files in chunks of 4 kB +constexpr unsigned int PBKDF2_ITERATIONS = 100'000; ///< Iterations for PBKDF2 key derivation /// \brief Generates random bytes using a CSPRNG. @@ -184,33 +184,40 @@ void encryptFile(const std::string &inputFile, const std::string &outputFile, co outFile.write(reinterpret_cast(salt.data()), static_cast(salt.size())); outFile.write(reinterpret_cast(iv.data()), static_cast(iv.size())); - // Encrypt the file - std::vector inBuf(CHUNK_SIZE); - std::vector outBuf(CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH); - int bytesRead, bytesWritten; + // Buffers for file processing + unsigned char inBuf[CHUNK_SIZE]; + unsigned char outBuf[CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH]; + + // Lock the buffers + sodium_mlock(inBuf, CHUNK_SIZE); + sodium_mlock(outBuf, CHUNK_SIZE); - while (true) { + int bytesRead, bytesWritten; + // The encryption loop + while (!inFile.eof()) { // Read data from the file in chunks - inFile.read(reinterpret_cast(inBuf.data()), static_cast(inBuf.size())); + inFile.read(reinterpret_cast(inBuf), CHUNK_SIZE); bytesRead = static_cast(inFile.gcount()); - if (bytesRead <= 0) - break; - // Encrypt the data - if (EVP_EncryptUpdate(cipher.getCtx(), outBuf.data(), &bytesWritten, inBuf.data(), bytesRead) != 1) + // Encrypt the chunk + if (EVP_EncryptUpdate(cipher.getCtx(), outBuf, &bytesWritten, inBuf, bytesRead) != 1) throw std::runtime_error("Failed to encrypt the data."); // Write the ciphertext (the encrypted data) to the output file - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); - outFile.flush(); // Ensure data is written immediately + outFile.write(reinterpret_cast(outBuf), bytesWritten); + // outFile.flush(); // Ensure data is written immediately } // Finalize the encryption operation - if (EVP_EncryptFinal_ex(cipher.getCtx(), outBuf.data(), &bytesWritten) != 1) + if (EVP_EncryptFinal_ex(cipher.getCtx(), outBuf, &bytesWritten) != 1) throw std::runtime_error("Failed to finalize encryption."); // Write the last chunk - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); + outFile.write(reinterpret_cast(outBuf), bytesWritten); + + // Unlock and zeroize the buffers + sodium_munlock(inBuf, CHUNK_SIZE); + sodium_munlock(outBuf, CHUNK_SIZE); } /// \brief Decrypts a file encrypted by encryptFile() function. @@ -278,33 +285,38 @@ void decryptFile(const std::string &inputFile, const std::string &outputFile, co // Set automatic padding handling EVP_CIPHER_CTX_set_padding(cipher.getCtx(), EVP_PADDING_PKCS7); - // Decrypt the file - privacy::vector inBuf(CHUNK_SIZE); - privacy::vector outBuf(CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH); + // Buffers for file processing + unsigned char inBuf[CHUNK_SIZE]; + unsigned char outBuf[CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH]; + + // Lock the buffers + sodium_mlock(inBuf, CHUNK_SIZE); + sodium_mlock(outBuf, CHUNK_SIZE); int bytesRead, bytesWritten; - while (true) { + while (!inFile.eof()) { // Read the data in chunks - inFile.read(reinterpret_cast(inBuf.data()), static_cast(inBuf.size())); + inFile.read(reinterpret_cast(inBuf), CHUNK_SIZE); bytesRead = static_cast(inFile.gcount()); - if (bytesRead <= 0) - break; - // Decrypt the data - if (EVP_DecryptUpdate(cipher.getCtx(), outBuf.data(), &bytesWritten, inBuf.data(), bytesRead) != 1) + // Decrypt the chunk + if (EVP_DecryptUpdate(cipher.getCtx(), outBuf, &bytesWritten, inBuf, bytesRead) != 1) throw std::runtime_error("Failed to decrypt the data."); // Write the decrypted data to the output file - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); - outFile.flush(); + outFile.write(reinterpret_cast(outBuf), bytesWritten); + // outFile.flush(); } // Finalize the decryption operation - if (EVP_DecryptFinal_ex(cipher.getCtx(), outBuf.data(), &bytesWritten) != 1) + if (EVP_DecryptFinal_ex(cipher.getCtx(), outBuf, &bytesWritten) != 1) throw std::runtime_error("Failed to finalize decryption."); - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); - outFile.flush(); + outFile.write(reinterpret_cast(outBuf), bytesWritten); + + // Unlock and zeroize the buffers + sodium_munlock(inBuf, CHUNK_SIZE); + sodium_munlock(outBuf, CHUNK_SIZE); } /// \brief Throws a thread-safe Gcrypt error. @@ -383,22 +395,27 @@ encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o outputFile.write(reinterpret_cast(salt.data()), static_cast(salt.size())); outputFile.write(reinterpret_cast(ctr.data()), static_cast(ctr.size())); + unsigned char buffer[CHUNK_SIZE]; + // Lock the buffer + sodium_mlock(buffer, CHUNK_SIZE); + // Encrypt the file in chunks - privacy::vector buffer(CHUNK_SIZE); while (!inputFile.eof()) { - inputFile.read(reinterpret_cast(buffer.data()), CHUNK_SIZE); + inputFile.read(reinterpret_cast(buffer), CHUNK_SIZE); const auto bytesRead = inputFile.gcount(); // Encrypt the chunk - err = gcry_cipher_encrypt(cipherHandle, buffer.data(), buffer.size(), nullptr, 0); + err = gcry_cipher_encrypt(cipherHandle, buffer, CHUNK_SIZE, nullptr, 0); if (err) throwSafeError(err, "Failed to encrypt file"); // Write the encrypted chunk to the output file - outputFile.write(reinterpret_cast(buffer.data()), bytesRead); + outputFile.write(reinterpret_cast(buffer), bytesRead); } - // Clean up + // Release the handle gcry_cipher_close(cipherHandle); + // Unlock the buffer + sodium_munlock(buffer, CHUNK_SIZE); } /// \brief Decrypts a file encrypted by encryptFileWithMoreRounds() function. @@ -466,20 +483,25 @@ decryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o if (err) throwSafeError(err, "Failed to set the decryption counter"); + unsigned char buffer[CHUNK_SIZE]; + // Lock the buffer + sodium_mlock(buffer, CHUNK_SIZE); + // Decrypt the file in chunks - privacy::vector buffer(CHUNK_SIZE); while (!inputFile.eof()) { - inputFile.read(reinterpret_cast(buffer.data()), CHUNK_SIZE); + inputFile.read(reinterpret_cast(buffer), CHUNK_SIZE); const auto bytesRead = inputFile.gcount(); // Decrypt the chunk in place - err = gcry_cipher_decrypt(cipherHandle, buffer.data(), buffer.size(), nullptr, 0); + err = gcry_cipher_decrypt(cipherHandle, buffer, CHUNK_SIZE, nullptr, 0); if (err) throwSafeError(err, "Failed to decrypt the ciphertext"); // Write the decrypted chunk to the output file - outputFile.write(reinterpret_cast(buffer.data()), bytesRead); + outputFile.write(reinterpret_cast(buffer), bytesRead); } // Release resources gcry_cipher_close(cipherHandle); + // Unlock the buffer + sodium_munlock(buffer, CHUNK_SIZE); } From 4da6d3dd3bcdd39e1d25dfc50ad3184facfcfc41 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 10 May 2024 17:27:18 +0300 Subject: [PATCH 04/99] Refactor the file shredder --- src/fileShredder/fileShredder.cppm | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/fileShredder/fileShredder.cppm b/src/fileShredder/fileShredder.cppm index 2541985..23bdf33 100644 --- a/src/fileShredder/fileShredder.cppm +++ b/src/fileShredder/fileShredder.cppm @@ -213,7 +213,6 @@ inline void wipeClusterTips(const std::string &fileName) { if (clusterTipSize >= fileInformation.fileStat.st_size) { clusterTipSize = 0; } - // Seek to the end of the file if (lseek(fileDescriptor.fd, 0, SEEK_END) == -1) { throw std::runtime_error(std::format("Failed to seek to end of file: ({})", std::strerror(errno))); @@ -321,10 +320,10 @@ void dod5220Shred(const std::string &filename, const int &nPasses = 3, const boo /// \enum ShredOptions /// \brief Represents the different shredding options. enum class ShredOptions : std::uint_fast8_t { - Simple = 1 << 0, // Simple overwrite with random bytes - Dod5220 = 1 << 1, // DoD 5220.22-M Standard algorithm - Dod5220_7 = 1 << 2, // DoD 5220.22-M Standard algorithm with 7 passes - WipeClusterTips = 1 << 3 // Wiping of the cluster tips + Simple = 1 << 0, ///< Simple overwrite with random bytes + Dod5220 = 1 << 1, ///< DoD 5220.22-M Standard algorithm + Dod5220_7 = 1 << 2, ///< DoD 5220.22-M Standard algorithm with 7 passes + WipeClusterTips = 1 << 3 ///< Wiping of the cluster tips }; /// \brief Adds write and write permissions to a file, if the user has authority. @@ -332,14 +331,14 @@ enum class ShredOptions : std::uint_fast8_t { /// \return True if the operation succeeds, else false. /// /// \details The actions of this function are similar to the unix command: -/// \code chmod ugo+rw fileName \endcode or \code chmod a+rw fileName \endcode +/// \code chmod ugo+rw fileName \endcode or \code chmod a+rw fileName \endcode. \n /// The read/write permissions are added for everyone. /// \note This function is meant for the file shredder ONLY, which might /// need to modify a file's permissions (if and only if it has to) to successfully shred it. /// /// \warning Modifying file permissions unnecessarily is a serious security risk, /// and this program doesn't take that for granted. -inline bool addReadWritePermissions(const std::string_view fileName) noexcept { +static inline bool addReadWritePermissions(const std::string_view fileName) noexcept { std::error_code ec; permissions(fileName, fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write | fs::perms::others_read | fs::perms::others_write, @@ -472,11 +471,10 @@ export void fileShredder() { preferences |= std::to_underlying(ShredOptions::Simple) | wipeTips; } else if (moreChoices1 == 2) { // Configure shredding options - const int alg = getResponseInt("\nChoose a shredding algorithm:\n" + if (const int alg = getResponseInt("\nChoose a shredding algorithm:\n" "1. Overwrite with random bytes (default)\n" "2. 3-pass DoD 5220.22-M Standard algorithm\n" - "3. 7-pass DoD 5220.22-M Standard algorithm"); - if (alg == 1) { + "3. 7-pass DoD 5220.22-M Standard algorithm"); alg == 1) { preferences |= std::to_underlying(ShredOptions::Simple) | wipeTips; do { From b6d2bb2c3f7270fc2ce6bed6b774f50ef2514ec0 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 10 May 2024 19:31:41 +0300 Subject: [PATCH 05/99] Improve the password manager performance --- src/passwordManager/passwordManager.cpp | 98 +++++++++++-------------- src/passwordManager/passwords.cpp | 20 +++-- 2 files changed, 50 insertions(+), 68 deletions(-) diff --git a/src/passwordManager/passwordManager.cpp b/src/passwordManager/passwordManager.cpp index ec55bc3..577727b 100644 --- a/src/passwordManager/passwordManager.cpp +++ b/src/passwordManager/passwordManager.cpp @@ -44,11 +44,7 @@ const string DefaultPasswordFile = getHomeDir() + "/.privacyShield/passwords"; /// \param rhs another record to be compared with lhs. /// \return true if lhs is less than (i.e. is ordered before) rhs, else false. bool constexpr comparator -// Avoid a compiler error on ignored scoped attribute directives (-Werror=attributes is enabled in debug config), -// while still encouraging both Clang and GCC compilers to inline the function. -#if __clang__ -[[clang::always_inline]] -#elif __GNUC__ +#if __clang__ || __GNUC__ [[gnu::always_inline]] #endif (const auto &lhs, const auto &rhs) noexcept { @@ -89,9 +85,7 @@ constexpr void printPasswordDetails(const auto &pw, const bool &isStrong = false /// /// \note This function is always inlined by the compiler. constexpr void computeStrengths -#if __clang__ -[[clang::always_inline]] -#elif __GNUC__ +#if __clang__ || __GNUC__ [[gnu::always_inline]] #endif (const privacy::vector &passwords, std::vector &pwStrengths) { @@ -102,7 +96,7 @@ constexpr void computeStrengths } /// \brief Adds a new password to the saved records. -inline void addPassword(privacy::vector &passwords, std::vector &strengths) { +void addPassword(privacy::vector &passwords, std::vector &strengths) { privacy::string site{getResponseStr("Enter the name of the site/app: ")}; // The site name must be non-empty if (site.empty()) { @@ -166,7 +160,7 @@ inline void addPassword(privacy::vector &passwords, std::vecto } /// \brief Generates a random password. -inline void generatePassword(privacy::vector &, std::vector &) { +void generatePassword(privacy::vector &, std::vector &) { int length = getResponseInt("Enter the length of the password to generate: "); int tries{0}; @@ -190,9 +184,9 @@ inline void generatePassword(privacy::vector &, std::vector &passwords, std::vector &strengths) { +void viewAllPasswords(privacy::vector &passwords, std::vector &strengths) { // Check if there are any passwords saved - if (auto &&constPasswordsRef = std::as_const(passwords); constPasswordsRef.empty()) { + if ( auto &&constPasswordsView = std::ranges::views::as_const(passwords); constPasswordsView.empty()) { printColor("You haven't saved any password yet.", 'r', true); } else { std::cout << "All passwords: ("; @@ -204,8 +198,8 @@ inline void viewAllPasswords(privacy::vector &passwords, std:: printColor("-----------------------------------------------------", 'w', true); // Print all the passwords - for (std::size_t i = 0; i < constPasswordsRef.size(); ++i) { - printPasswordDetails(constPasswordsRef[i], strengths[i]); + for (std::size_t i = 0; i < constPasswordsView.size(); ++i) { + printPasswordDetails(constPasswordsView[i], strengths[i]); printColor("-----------------------------------------------------", 'w', true); } } @@ -244,7 +238,7 @@ void checkFuzzyMatches(auto &iter, privacy::vector &records, p } /// \brief Updates a password record. -inline void updatePassword(privacy::vector &passwords, std::vector &strengths) { +void updatePassword(privacy::vector &passwords, std::vector &strengths) { if (passwords.empty()) [[unlikely]] { // There is nothing to update printColor("No passwords saved yet.", 'r', true, std::cerr); @@ -267,13 +261,11 @@ inline void updatePassword(privacy::vector &passwords, std::ve checkFuzzyMatches(it, passwords, site); // Extract all the accounts under the site - auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), - [](const auto &lhs, const auto &rhs) { - // this is consistent with the comparator() used to find the lower bound - return std::get<0>(lhs) < std::get<0>(rhs); - }); - - if (!matches.empty()) { + if (auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), + [](const auto &lhs, const auto &rhs) { + // this is consistent with the comparator() used to find the lower bound + return std::get<0>(lhs) < std::get<0>(rhs); + }); !matches.empty()) { // site found if (matches.size() > 1) { // there are multiple accounts under the site @@ -317,7 +309,6 @@ inline void updatePassword(privacy::vector &passwords, std::ve } } } - const privacy::string newPassword{ getSensitiveInfo("Enter the new password (Leave blank to keep the current one): ") }; @@ -353,7 +344,7 @@ inline void updatePassword(privacy::vector &passwords, std::ve } /// \brief Deletes a password record. -inline void deletePassword(privacy::vector &passwords, std::vector &strengths) { +void deletePassword(privacy::vector &passwords, std::vector &strengths) { if (passwords.empty()) { printColor("No passwords saved yet.", 'r', true, std::cerr); return; @@ -375,12 +366,10 @@ inline void deletePassword(privacy::vector &passwords, std::ve checkFuzzyMatches(it, passwords, site); // Extract all the accounts under the site - auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), - [](const auto &lhs, const auto &rhs) { - return std::get<0>(lhs) < std::get<0>(rhs); - }); - - if (!matches.empty()) { + if (auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), + [](const auto &lhs, const auto &rhs) { + return std::get<0>(lhs) < std::get<0>(rhs); + }); !matches.empty()) { // site found if (matches.size() > 1) { std::cout << "Found the following usernames for " << std::quoted(site) << ":\n"; @@ -442,7 +431,7 @@ inline void deletePassword(privacy::vector &passwords, std::ve } /// \brief Finds a password record. -inline void searchPasswords(privacy::vector &passwords, std::vector &) { +void searchPasswords(privacy::vector &passwords, std::vector &) { if (passwords.empty()) [[unlikely]] { // There is nothing to search printColor("No passwords saved yet.", 'r', true, std::cerr); @@ -457,27 +446,25 @@ inline void searchPasswords(privacy::vector &passwords, std::v } // Use a const reference to protect the passwords from accidental modifications - auto &&constPasswordsRef = std::as_const(passwords); + auto &&constPasswordsView = std::ranges::views::as_const(passwords); // Look for partial and exact matches - auto matches = constPasswordsRef | std::ranges::views::filter([&query](const auto &vec) -> bool { + if (auto matches = constPasswordsView | std::ranges::views::filter([&query](const auto &vec) -> bool { return std::get<0>(vec).contains(query); - }); - - // Print all the matches - if (!matches.empty()) [[likely]] { + }); !matches.empty()) [[likely]] { + // Print all the matches std::cout << "All the matches:" << std::endl; printColor("------------------------------------------------------", 'm', true); for (const auto &el: matches) { - printPasswordDetails(el); + printPasswordDetails(el, isPasswordStrong(std::get<2>(el))); printColor("------------------------------------------------------", 'm', true); } } else { printColor(std::format("No matches found for '{}'", query), 'r', true); // Fuzzy-match the query against the site names - const FuzzyMatcher matcher(constPasswordsRef | std::ranges::views::elements<0>); + const FuzzyMatcher matcher(constPasswordsView | std::ranges::views::elements<0>); // If there is a single match, ask the user if they want to view it if (const auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; fuzzyMatched.size() == 1) { @@ -488,15 +475,14 @@ inline void searchPasswords(privacy::vector &passwords, std::v printColor("'? (y/n):", 'c'); if (validateYesNo()) { - auto matched = std::ranges::equal_range(constPasswordsRef, std::tie(match), - [](const auto &lhs, const auto &rhs) noexcept -> bool { - return std::get<0>(lhs) < std::get<0>(rhs); - }); // print all the records under the match - if (!matched.empty()) [[likely]] { + if (auto matched = std::ranges::equal_range(constPasswordsView, std::tie(match), + [](const auto &lhs, const auto &rhs) noexcept -> bool { + return std::get<0>(lhs) < std::get<0>(rhs); + }); !matched.empty()) [[likely]] { printColor("-----------------------------------------------------", 'w', true); for (const auto &pass: matched) { - printPasswordDetails(pass); + printPasswordDetails(pass, isPasswordStrong(std::get<2>(pass))); printColor("-----------------------------------------------------", 'w', true); } } @@ -514,7 +500,7 @@ inline void searchPasswords(privacy::vector &passwords, std::v } /// \brief Imports passwords from a csv file. -inline void importPasswords(privacy::vector &passwords, std::vector &strengths) { +void importPasswords(privacy::vector &passwords, std::vector &strengths) { const string fileName = getResponseStr("Enter the path to the csv file: "); privacy::vector imports{importCsv(fileName)}; @@ -531,7 +517,7 @@ inline void importPasswords(privacy::vector &passwords, std::v // Remove duplicates from the imported passwords using the erase-remove idiom auto dups = std::ranges::unique(imports, [](const auto &lhs, const auto &rhs) noexcept -> bool { - // the binary predicate should check equivalence, not order + // The binary predicate should check equivalence, not order return std::tie(std::get<0>(lhs), std::get<1>(lhs)) == std::tie(std::get<0>(rhs), std::get<1>(rhs)); }); imports.erase(dups.begin(), dups.end()); @@ -555,7 +541,7 @@ inline void importPasswords(privacy::vector &passwords, std::v if (!duplicates.empty()) { printColor("Warning: The following passwords already exist in the database:", 'y', true); for (const auto &duplicate: duplicates) { - printPasswordDetails(duplicate); + printPasswordDetails(duplicate, isPasswordStrong(std::get<2>(duplicate))); printColor("-------------------------------------------------", 'm', true); } printColor("Do you want to overwrite/update them? (y/n): ", 'b'); @@ -599,17 +585,17 @@ inline void importPasswords(privacy::vector &passwords, std::v } /// \brief Exports passwords to a csv file. -inline void exportPasswords(privacy::vector &passwords, std::vector &) { - auto &&passwordsConstRef = std::as_const(passwords); +void exportPasswords(privacy::vector &passwords, std::vector &) { + auto &&constPasswordsView = std::as_const(passwords); - if (passwordsConstRef.empty()) [[unlikely]] { + if (constPasswordsView.empty()) [[unlikely]] { printColor("No passwords saved yet.", 'r', true, std::cerr); return; } const string fileName = getResponseStr("Enter the path to save the file (leave blank for default): "); // Export the passwords to a csv file - if (const bool exported = fileName.empty() ? exportCsv(passwordsConstRef) : exportCsv(passwordsConstRef, fileName); + if (const bool exported = fileName.empty() ? exportCsv(constPasswordsView) : exportCsv(constPasswordsView, fileName); exported) [[likely]] // Warn the user about the security risk @@ -619,14 +605,14 @@ inline void exportPasswords(privacy::vector &passwords, std::v } /// \brief Analyzes the saved passwords for weak passwords and password reuse. -inline void analyzePasswords(privacy::vector &passwords, std::vector &strengths) { +void analyzePasswords(privacy::vector &passwords, std::vector &strengths) { if (passwords.empty()) { printColor("No passwords to analyze.", 'r', true); return; } const auto total = passwords.size(); - auto &&constPasswordsRef = std::as_const(passwords); + auto &&constPasswordsView = std::ranges::views::as_const(passwords); // Analyze the passwords std::cout << "Analyzing passwords..." << std::endl; @@ -642,9 +628,7 @@ inline void analyzePasswords(privacy::vector &passwords, std:: // Check for reused passwords std::unordered_map > passwordMap; - for (const auto &record: constPasswordsRef) { - const auto &site = std::get<0>(record); - const auto &password = std::get<2>(record); + for (const auto &[site, _, password] : constPasswordsView) { // Add the site to the set of sites that use the password passwordMap[password].insert(site); diff --git a/src/passwordManager/passwords.cpp b/src/passwordManager/passwords.cpp index e5e5e82..b259854 100644 --- a/src/passwordManager/passwords.cpp +++ b/src/passwordManager/passwords.cpp @@ -100,8 +100,7 @@ privacy::string generatePassword(const int length) { for (int i = 0; i < length; ++i) password += characters[distribution(generator)]; - // If the length is >= 8, it is almost impossible that this loop is infinite, - // but let's handle that ultra-rare situation anyway + // Avoid an infinite loop } while (!isPasswordStrong(password) && ++trials <= maxTrials); return password; @@ -372,10 +371,10 @@ bool changeMasterPassword(privacy::string &primaryPassword) { const privacy::string oldPassword{getSensitiveInfo("Enter the current primary password: ")}; // Verify that the old password is correct - auto masterHash = hashPassword(primaryPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, - crypto_pwhash_MEMLIMIT_INTERACTIVE); - if (!verifyPassword(oldPassword, masterHash)) { + if (const auto masterHash = hashPassword(primaryPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE); + !verifyPassword(oldPassword, masterHash)) { std::cerr << "Password verification failed." << std::endl; return false; } @@ -393,11 +392,10 @@ bool changeMasterPassword(privacy::string &primaryPassword) { return false; } - const privacy::string newPassword2{getSensitiveInfo("Enter the new primary password again: ")}; - // Verify that the new password is correct - if (!verifyPassword(newPassword2, hashPassword(newPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, - crypto_pwhash_MEMLIMIT_INTERACTIVE))) { + if (const privacy::string newPassword2{getSensitiveInfo("Enter the new primary password again: ")}; + !verifyPassword(newPassword2, hashPassword(newPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE))) { std::cerr << "Passwords do not match." << std::endl; return false; @@ -601,8 +599,8 @@ privacy::vector importCsv(const std::string &filePath) { privacy::string line, value; if (hasHeader) - std::getline, privacy::Allocator >(file, - line); // Read and discard the first line + // Read and discard the first line + std::getline, privacy::Allocator >(file, line); while (std::getline, privacy::Allocator >(file, line)) { privacy::istringstream iss(line); From 20a5a21f6a2ea736de0608021c314d0079967e2f Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 17 May 2024 15:45:00 +0300 Subject: [PATCH 06/99] Update the GitHub flow configuration --- .github/workflows/cmake-multi-platform.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index da14177..a28cbf3 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -14,11 +14,11 @@ jobs: fail-fast: false matrix: - os: [ ubuntu-latest, macos-13 ] + os: [ ubuntu-latest, macos-latest ] build_type: [ Debug, Release ] c_compiler: [ clang ] include: - - os: macos-13 + - os: macos-latest c_compiler: clang cpp_compiler: clang++ env: @@ -27,7 +27,7 @@ jobs: LD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" DYLD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" -# - os: macos-13 +# - os: macos-latest # c_compiler: gcc # cpp_compiler: g++-13 # @@ -41,13 +41,13 @@ jobs: # Don't include the following configurations in the matrix exclude: - - os: macos-13 + - os: macos-latest build_type: Debug steps: # Install dependencies: cmake, ninja, gcc, libgcrypt, openssl, readline, and libsodium - name: Install Dependencies - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update @@ -82,7 +82,7 @@ jobs: sudo ./scripts/buildscript.sh # OS=${{ matrix.os }} # COMMAND="./scripts/install-blake3.sh ${{ matrix.c_compiler }}" - # if [ "$OS" == "macos-13" ]; then + # if [ "$OS" == "macos-latest" ]; then # $COMMAND # elif [ "$OS" == "ubuntu-latest" ]; then # sudo $COMMAND @@ -90,12 +90,12 @@ jobs: # - name: Install Blake3 - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: | ./scripts/install-blake3.sh ${{ matrix.c_compiler }} - name: Configure CMake - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: > export LDFLAGS="-L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13"; export CPPFLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22"; @@ -110,7 +110,7 @@ jobs: -S ${{ github.workspace }} -G Ninja - name: Build - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} -j 4 # # - name: Test @@ -119,7 +119,7 @@ jobs: # run: ctest --build-config ${{ matrix.build_type }} # - name: Package - if: matrix.os == 'macos-13' && matrix.build_type == 'Release' + if: matrix.os == 'macos-latest' && matrix.build_type == 'Release' working-directory: ${{ steps.strings.outputs.build-output-dir }} run: | cpack -G DragNDrop From 67520e5bad2132a1ad0caf725a6c17c7b57a05b0 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Sun, 19 May 2024 17:57:15 +0300 Subject: [PATCH 07/99] Configure the colored output options --- README.md | 18 ++++++++++++ src/main.cpp | 26 ++++++++++++++--- src/utils/utils.cpp | 4 +-- src/utils/utils.cppm | 67 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 11ba8f9..63a529d 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,24 @@ and are not saved in the shell command history. Any operation with any tool can be canceled at any time by pressing `Ctrl+C`, and confirming the cancellation. +**Note:**\ +The program uses ANSI escape codes for colors and formatting. If you experience issues with the colors, +you can disable them by setting the `NO_COLOR` environment variable to `true` (or `1`), +or by using the `--no-color` or `-nc` option. + +```bash +export NO_COLOR=true && privacyShield +``` + +or + +```bash +privacyShield --no-color +``` + +The program will automatically detect the `NO_COLOR` environment variable, and the terminal capabilities +to determine if colors should be used. + ### Password Manager The password manager requires a primary password to encrypt/decrypt your passwords. diff --git a/src/main.cpp b/src/main.cpp index 11a7953..3309f6e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,7 +31,7 @@ import passwordManager; import fileShredder; import utils; -constexpr const char *const MINIMUM_LIBGCRYPT_VERSION = "1.10.0"; +constexpr auto MINIMUM_LIBGCRYPT_VERSION = "1.10.0"; int main(const int argc, const char **argv) { @@ -48,17 +48,35 @@ int main(const int argc, const char **argv) { return 1; } - // No arguments required + // Configure the color output, if necessary + configureColor(); + + // Only the first argument is considered if (argc > 1) { + // Disable color output if requested + if (std::string_view(argv[1]) == "--no-color" || std::string_view(argv[1]) == "-nc") { + configureColor(true); + } else { + printColor("The option ", 'y'); + printColor(std::format("{} ", argv[1]), 'r'); + printColor("is not recognized.", 'y', true, std::cerr); + + printColor("Usage: ", 'y'); + printColor(std::format("{} [--no-color | -nc]", argv[0]), 'r', true, std::cerr); + } + } + + if (argc > 2) { printColor("Ignoring extra arguments: ", 'y'); - for (int i = 1; i < argc; printColor(std::format("{} ", argv[i++]), 'r')) {} + for (int i = 2; i < argc; printColor(std::format("{} ", argv[i++]), 'r')) { + } std::cout << std::endl; } // Handle the keyboard interrupt (SIGINT) signal (i.e., Ctrl+C) struct sigaction act{}; act.sa_handler = [](int /* unused */) noexcept -> void { - printColor("Keyboard interrupt detected. Unsaved data might be lost if you quit now." + printColor("Keyboard interrupt detected.\nUnsaved data might be lost if you quit now." "\nDo you still want to quit? (y/n):", 'r'); if (validateYesNo()) std::exit(1); }; diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 8edf6a9..4cc3d1f 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -283,9 +283,9 @@ std::optional getEnv(const char *const var) { std::string getHomeDir() noexcept { std::error_code ec; // Try to get the home directory from the environment variables - if (auto envHome = getEnv("HOME"); envHome) + if (const auto envHome = getEnv("HOME"); envHome) return *envHome; - if (auto envUserProfile = getEnv("USERPROFILE"); envUserProfile) + if (const auto envUserProfile = getEnv("USERPROFILE"); envUserProfile) return *envUserProfile; // If the environment variables are not set, use the current working directory diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 42e34b8..a779f37 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -30,6 +30,7 @@ import secureAllocator; namespace fs = std::filesystem; +/// A map of colors for use with the printColor function. static const std::unordered_map COLOR = { {'r', "\033[1;31m"}, // Red {'g', "\033[1;32m"}, // Green @@ -40,6 +41,47 @@ static const std::unordered_map COLOR = { {'w', "\033[1;37m"}, // White }; +/// \class ColorConfig +/// \brief A singleton class used to manage the color configuration of the terminal output. +/// This class encapsulates the \p suppressColor functionality, ensuring that there is only +/// one \p suppressColor instance throughout the application. +/// It provides methods to get and set the \p suppressColor value. +class ColorConfig { +public: + /// \brief Gets the instance of the \p ColorConfig singleton. + /// \return A reference to the singleton instance of the \p ColorConfig class. + static ColorConfig &getInstance() noexcept { + static ColorConfig instance; + return instance; + } + + // Delete the copy constructor and assignment operator + ColorConfig(ColorConfig const &) = delete; + + void operator=(ColorConfig const &) = delete; + + /// \brief Gets the \p suppressColor value. + /// \return The current value of the \p suppressColor variable. + [[nodiscard]] bool getSuppressColor() const noexcept { + return suppressColor; + } + + /// \brief Sets the \p suppressColor value. + /// \param value The new value for the \p suppressColor variable. + void setSuppressColor(const bool value) noexcept { + suppressColor = value; + } + +private: + /// \brief Private constructor for the \p ColorConfig class. + /// This constructor initializes the \p suppressColor variable to false. + ColorConfig() : suppressColor(false) { + } + + // The suppressColor value + bool suppressColor; +}; + template // Describes a type that can be formatted to the output stream concept PrintableToStream = requires(std::ostream &os, const T &t) { @@ -68,12 +110,33 @@ export { void printColor(const PrintableToStream auto &text, const char &color = 'w', const bool &printNewLine = false, std::ostream &os = std::cout) { // Print the text in the desired color - os << (COLOR.contains(color) ? COLOR.at(color) : "") << text << "\033[0m"; + ColorConfig::getInstance().getSuppressColor() + ? os << text + : os << (COLOR.contains(color) ? COLOR.at(color) : "") << text << "\033[0m"; // Print a newline if requested if (printNewLine) os << std::endl; } + std::optional getEnv(const char *var); + + /// \brief Configures the color output of the terminal. + /// \param disable a flag to indicate whether color output should be disabled. + void configureColor(const bool disable = false) noexcept { + // Check if the user has requested no color + if (disable) { + ColorConfig::getInstance().setSuppressColor(true); + return; + } + // Process the environment variable to suppress color output + const auto noColorEnv = getEnv("NO_COLOR"); + const auto termEnv = getEnv("TERM"); + const bool suppressColor = noColorEnv.has_value() || + (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "vt100" || + termEnv.value() == "vt102")); + ColorConfig::getInstance().setSuppressColor(suppressColor); + } + /// \brief Performs Base64 encoding of binary data into a string. /// \param input a vector of the binary data to be encoded. /// \return Base64-encoded string. @@ -132,6 +195,4 @@ export { bool validateYesNo(std::string_view prompt = ""); std::string getHomeDir() noexcept; - - std::optional getEnv(const char *var); } From 7d23a5a493f04a4c365de66ede33f0986ea7c203 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Sun, 19 May 2024 17:58:17 +0300 Subject: [PATCH 08/99] Update CMake and CPack configurations --- CMakeLists.txt | 45 ++++++++++++++++++++++++++------------ CMakeModules/Packing.cmake | 26 ++++++++++++---------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aedd90d..71f7881 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,10 +20,9 @@ cmake_minimum_required(VERSION 3.28) project(privacyShield VERSION 2.5.0 DESCRIPTION "A suite of tools for privacy and security" + HOMEPAGE_URL "https://shield.iandee.tech" LANGUAGES CXX) -set(CMAKE_PROJECT_HOMEPAGE_URL "https://shield.boujee.tech") - # C++23 support is required for this project set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -39,10 +38,19 @@ if (NOT CMAKE_BUILD_TYPE) endif () # Set the path to additional CMake modules -set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") # Additional checks for the Debug build -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wpedantic") +if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang|AppleClang") + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_options( + -Wall + -Wextra + -Werror + -Wpedantic + ) + endif () +endif () # Find the required packages find_package(OpenSSL REQUIRED) @@ -55,19 +63,28 @@ find_package(BLAKE3 REQUIRED) # See https://github.com/BLAKE3-team/BLAKE3 add_executable(privacyShield) # Add sources for the target -file(GLOB_RECURSE PRIVACY_SHIELD_SOURCES - "${CMAKE_SOURCE_DIR}/src/*.cpp") - -target_sources(privacyShield PRIVATE ${PRIVACY_SHIELD_SOURCES}) +target_sources(privacyShield PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryptDecrypt.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryptFiles.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryptStrings.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwordManager.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwords.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/utils/utils.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" +) # C++20 Modules -file(GLOB_RECURSE PRIVACY_SHIELD_MODULES - "${CMAKE_SOURCE_DIR}/src/*.cppm") - -target_sources(privacyShield - PRIVATE +target_sources(privacyShield PRIVATE FILE_SET CXX_MODULES FILES - ${PRIVACY_SHIELD_MODULES} + "${CMAKE_CURRENT_SOURCE_DIR}/src/duplicateFinder/duplicateFinder.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/cryptoCipher.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryption.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/fileShredder/fileShredder.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/FuzzyMatcher.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwordManager.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/privacyTracks/privacyTracks.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/utils/utils.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/src/secureAllocator.cppm" ) # Link libraries diff --git a/CMakeModules/Packing.cmake b/CMakeModules/Packing.cmake index 1b1cfe2..6c07e51 100644 --- a/CMakeModules/Packing.cmake +++ b/CMakeModules/Packing.cmake @@ -22,12 +22,13 @@ # Set the CPack variables set(CPACK_PACKAGE_NAME "PrivacyShield") set(CPACK_PACKAGE_VENDOR "Ian Duncan") -set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A suite of tools for privacy and security") -set(CPACK_PACKAGE_VERSION "2.5.0") -set(CPACK_PACKAGE_CONTACT "dr8co@duck.com") - -SET(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_SOURCE_DIR}/Packages") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_CONTACT "Ian Duncan ") +set(CPACK_PACKAGE_HOMEPAGE_URL "${PROJECT_HOMEPAGE_URL}") +set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_SOURCE_DIR}/Packages") +set(CPACK_STRIP_FILES YES) set(CPACK_SOURCE_IGNORE_FILES /.git /.idea @@ -56,11 +57,14 @@ set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) set(CPACK_RPM_FILE_NAME RPM-DEFAULT) -# Set the type of installer you want to generate -set(CPACK_GENERATOR "DEB;RPM") - -# Strip the executable from debug symbols -set(CPACK_STRIP_FILES YES) +# Set the package generator +if (APPLE) + set(CPACK_GENERATOR "TGZ;DragNDrop") +elseif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") + set(CPACK_GENERATOR "TGZ;DEB;RPM") +else () + set(CPACK_GENERATOR "TGZ") +endif () # Set the package dependencies set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.35), libstdc++6 (>= 13.2.0), openssl (>= 3.0.0), libsodium23 (>= 1.0.18), libreadline8 (>= 8.0), libgcrypt20 (>= 1.10.0), libgcc-s1 (>= 13.2.0)") @@ -76,4 +80,4 @@ set(CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE ON) set(CPACK_PACKAGE_CHECKSUM "SHA256") -include(CPack) \ No newline at end of file +include(CPack) From 828eaebdeba74e6ebf18b6cd942e86cec7760389 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Sun, 19 May 2024 18:01:01 +0300 Subject: [PATCH 09/99] Update the CI --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index a28cbf3..451357b 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -122,7 +122,7 @@ jobs: if: matrix.os == 'macos-latest' && matrix.build_type == 'Release' working-directory: ${{ steps.strings.outputs.build-output-dir }} run: | - cpack -G DragNDrop + cpack - name: Package if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Release' From 1a5363209f24c5d25489476ae8539c405dabdbdf Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Tue, 21 May 2024 23:39:20 +0300 Subject: [PATCH 10/99] Update GitHub workflow configuration --- .github/workflows/cmake-multi-platform.yml | 2 +- scripts/buildscript.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 451357b..f25e8f4 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -37,7 +37,7 @@ jobs: - os: ubuntu-latest c_compiler: clang - cpp_compiler: clang++-17 + cpp_compiler: clang++-18 # Don't include the following configurations in the matrix exclude: diff --git a/scripts/buildscript.sh b/scripts/buildscript.sh index d1e3909..cd7781f 100755 --- a/scripts/buildscript.sh +++ b/scripts/buildscript.sh @@ -29,7 +29,7 @@ function install_dependencies() { add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update - apt install -y unzip gcc-13 g++-13 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev + apt install -y unzip gcc clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev # Install CMake 3.28.3 if dpkg -s "cmake" >/dev/null 2>&1; then From 96885bcf141552368d7f7e5d3a152b1ec2b2237e Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 22 May 2024 00:16:40 +0300 Subject: [PATCH 11/99] Update GitHub workflow configuration --- .github/workflows/cmake-multi-platform.yml | 38 ++-------------------- scripts/buildscript.sh | 2 +- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index f25e8f4..8794eed 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -27,14 +27,6 @@ jobs: LD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" DYLD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" -# - os: macos-latest -# c_compiler: gcc -# cpp_compiler: g++-13 -# -# - os: ubuntu-latest -# c_compiler: gcc -# cpp_compiler: g++-13 - - os: ubuntu-latest c_compiler: clang cpp_compiler: clang++-18 @@ -51,22 +43,11 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install llvm cmake ninja gcc libgcrypt openssl@3 readline libsodium + brew install llvm cmake ninja gcc@13 libgcrypt openssl@3 readline libsodium echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/usr/local/opt/gcc@13/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/usr/local/opt/gcc@13/lib/gcc/13:$PATH"' >> ~/.bash_profile - # - name: Install Dependencies -# if: matrix.os == 'ubuntu-latest' -# run: | -# wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc -# sudo add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main" -# sudo add-apt-repository -y ppa:ubuntu-toolchain-r/ppa -# sudo apt update -# sudo apt install -y cmake ninja-build gcc-13 g++-13 clang-17 lldb-17 lld-17 libc++-17-dev libc++abi-17-dev \ -# libomp-17-dev libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev - - - uses: actions/checkout@v4 - name: Set reusable strings @@ -80,19 +61,11 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo ./scripts/buildscript.sh - # OS=${{ matrix.os }} - # COMMAND="./scripts/install-blake3.sh ${{ matrix.c_compiler }}" - # if [ "$OS" == "macos-latest" ]; then - # $COMMAND - # elif [ "$OS" == "ubuntu-latest" ]; then - # sudo $COMMAND - # fi - # - name: Install Blake3 if: matrix.os == 'macos-latest' run: | - ./scripts/install-blake3.sh ${{ matrix.c_compiler }} + sudo ./scripts/install-blake3.sh ${{ matrix.c_compiler }} - name: Configure CMake if: matrix.os == 'macos-latest' @@ -112,12 +85,7 @@ jobs: - name: Build if: matrix.os == 'macos-latest' run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} -j 4 -# -# - name: Test -# working-directory: ${{ steps.strings.outputs.build-output-dir }} -# # Execute tests defined by the CMake configuration -# run: ctest --build-config ${{ matrix.build_type }} -# + - name: Package if: matrix.os == 'macos-latest' && matrix.build_type == 'Release' working-directory: ${{ steps.strings.outputs.build-output-dir }} diff --git a/scripts/buildscript.sh b/scripts/buildscript.sh index cd7781f..f7f7e13 100755 --- a/scripts/buildscript.sh +++ b/scripts/buildscript.sh @@ -29,7 +29,7 @@ function install_dependencies() { add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update - apt install -y unzip gcc clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev + apt install -y unzip gcc-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev # Install CMake 3.28.3 if dpkg -s "cmake" >/dev/null 2>&1; then From 5556b7caf9bbe83da3020ec8c48c59dc83be0c03 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Sun, 26 May 2024 23:04:33 +0300 Subject: [PATCH 12/99] Refactor color printing The printing functions can now accept a format string and variable arguments. The interface is refactored for simplicity and to be intuitive. --- src/utils/utils.cpp | 130 +++++++++++++++++++++++++++++++++++++++++++ src/utils/utils.cppm | 110 ++++++------------------------------ 2 files changed, 146 insertions(+), 94 deletions(-) diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 4cc3d1f..3436eb1 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -27,6 +27,7 @@ module; #include #include #include +#include module utils; @@ -34,6 +35,46 @@ import secureAllocator; constexpr int MAX_PASSPHRASE_LEN = 1024; // Maximum length of a passphrase +/// \class ColorConfig +/// \brief A singleton class used to manage the color configuration of the terminal output. +/// This class encapsulates the \p suppressColor functionality, ensuring that there is only +/// one \p suppressColor instance throughout the application. +/// It provides methods to get and set the \p suppressColor value. +class ColorConfig { +public: + /// \brief Gets the instance of the \p ColorConfig singleton. + /// \return A reference to the singleton instance of the \p ColorConfig class. + static ColorConfig &getInstance() noexcept { + static ColorConfig instance; + return instance; + } + + // Delete the copy constructor and assignment operator + ColorConfig(ColorConfig const &) = delete; + + void operator=(ColorConfig const &) = delete; + + /// \brief Gets the \p suppressColor value. + /// \return The current value of the \p suppressColor variable. + [[nodiscard]] bool getSuppressColor() const noexcept { + return suppressColor; + } + + /// \brief Sets the \p suppressColor value. + /// \param value The new value for the \p suppressColor variable. + void setSuppressColor(const bool value) noexcept { + suppressColor = value; + } + +private: + /// \brief Private constructor for the \p ColorConfig class. + /// This constructor initializes the \p suppressColor variable to false. + ColorConfig() : suppressColor(false) { + } + + // The suppressColor value + bool suppressColor; +}; /// \brief Performs Base64 decoding of a string into binary data. /// \param encodedData Base64 encoded string. @@ -296,3 +337,92 @@ std::string getHomeDir() noexcept { return currentDir; } + +/// \brief Configures the color output of the terminal. +/// \param disable a flag to indicate whether color output should be disabled. +void configureColor(const bool disable) noexcept { + // Check if the user has requested no color + if (disable) { + ColorConfig::getInstance().setSuppressColor(true); + return; + } + // Process the environment variable to suppress color output + const auto noColorEnv = getEnv("NO_COLOR"); + const auto termEnv = getEnv("TERM"); + const bool suppressColor = noColorEnv.has_value() || + (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "vt100" || + termEnv.value() == "vt102")); + ColorConfig::getInstance().setSuppressColor(suppressColor); +} + +/// \brief Returns the ANSI color code for the given character. +/// \param color The character representing the color. +/// \return The ANSI color code corresponding to the input character. +constexpr auto getColorCode(const char color) noexcept { + switch (color) { + case 'r': // Red + return "\033[1;31m"; + case 'g': // Green + return "\033[1;32m"; + case 'y': // Yellow + return "\033[1;33m"; + case 'b': // Blue + return "\033[1;34m"; + case 'm': // Magenta + return "\033[1;35m"; + case 'c': // Cyan + return "\033[1;36m"; + case 'w': // White + return "\033[1;37m"; + default: // No color + return ""; + } +} + +/// \brief Prints colored output to the console. +/// \tparam Args Variadic template for all types of arguments that can be passed. +/// \param color The color code for the output. +/// \param fmt The format string for the output. +/// \param args The arguments to be printed. +template +void printColoredOutput(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::print(fmt, std::forward(args)...); + else std::print("{}{}\033[0m", getColorCode(color), std::format(fmt, std::forward(args)...)); +} + +/// \brief Prints colored output to the console and adds a newline at the end. +/// \tparam Args Variadic template for all types of arguments that can be passed. +/// \param color The color code for the output. +/// \param fmt The format string for the output. +/// \param args The arguments to be printed. +template +void printColoredOutputln(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::println(fmt, std::forward(args)...); + else std::print("{}{}\033[0m\n", getColorCode(color), std::format(fmt, std::forward(args)...)); +} + +/// \brief Prints colored error messages to the console. +/// \tparam Args Variadic template for all types of arguments that can be passed. +/// \param color The color code for the output. +/// \param fmt The format string for the output. +/// \param args The arguments to be printed. +template +void printColoredError(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::print(std::cerr, fmt, std::forward(args)...); + else std::print(std::cerr, "{}{}\033[0m", getColorCode(color), std::format(fmt, std::forward(args)...)); +} + +/// \brief This function prints colored error messages to the console and adds a newline at the end. +/// \tparam Args Variadic template for all types of arguments that can be passed. +/// \param color The color code for the output. +/// \param fmt The format string for the output. +/// \param args The arguments to be printed. +template +void printColoredErrorln(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::println(std::cerr, fmt, std::forward(args)...); + else std::print(std::cerr, "{}{}\033[0m\n", getColorCode(color), std::format(fmt, std::forward(args)...)); +} diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index a779f37..1acf52d 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -30,64 +30,6 @@ import secureAllocator; namespace fs = std::filesystem; -/// A map of colors for use with the printColor function. -static const std::unordered_map COLOR = { - {'r', "\033[1;31m"}, // Red - {'g', "\033[1;32m"}, // Green - {'y', "\033[1;33m"}, // Yellow - {'b', "\033[1;34m"}, // Blue - {'m', "\033[1;35m"}, // Magenta - {'c', "\033[1;36m"}, // Cyan - {'w', "\033[1;37m"}, // White -}; - -/// \class ColorConfig -/// \brief A singleton class used to manage the color configuration of the terminal output. -/// This class encapsulates the \p suppressColor functionality, ensuring that there is only -/// one \p suppressColor instance throughout the application. -/// It provides methods to get and set the \p suppressColor value. -class ColorConfig { -public: - /// \brief Gets the instance of the \p ColorConfig singleton. - /// \return A reference to the singleton instance of the \p ColorConfig class. - static ColorConfig &getInstance() noexcept { - static ColorConfig instance; - return instance; - } - - // Delete the copy constructor and assignment operator - ColorConfig(ColorConfig const &) = delete; - - void operator=(ColorConfig const &) = delete; - - /// \brief Gets the \p suppressColor value. - /// \return The current value of the \p suppressColor variable. - [[nodiscard]] bool getSuppressColor() const noexcept { - return suppressColor; - } - - /// \brief Sets the \p suppressColor value. - /// \param value The new value for the \p suppressColor variable. - void setSuppressColor(const bool value) noexcept { - suppressColor = value; - } - -private: - /// \brief Private constructor for the \p ColorConfig class. - /// This constructor initializes the \p suppressColor variable to false. - ColorConfig() : suppressColor(false) { - } - - // The suppressColor value - bool suppressColor; -}; - -template -// Describes a type that can be formatted to the output stream -concept PrintableToStream = requires(std::ostream &os, const T &t) { - os << t; -}; - template // Describes a vector of unsigned characters (For use with vectors using different allocators) concept uCharVector = std::copy_constructible && requires(T t, unsigned char c) { @@ -101,42 +43,6 @@ concept uCharVector = std::copy_constructible && requires(T t, unsigned char }; export { - - /// \brief Prints colored text to a stream. - /// \param text the text to print. - /// \param color a character representing the desired color. - /// \param printNewLine a flag to indicate whether a newline should be printed after the text. - /// \param os the stream object to print to. - void printColor(const PrintableToStream auto &text, const char &color = 'w', const bool &printNewLine = false, - std::ostream &os = std::cout) { - // Print the text in the desired color - ColorConfig::getInstance().getSuppressColor() - ? os << text - : os << (COLOR.contains(color) ? COLOR.at(color) : "") << text << "\033[0m"; - - // Print a newline if requested - if (printNewLine) os << std::endl; - } - - std::optional getEnv(const char *var); - - /// \brief Configures the color output of the terminal. - /// \param disable a flag to indicate whether color output should be disabled. - void configureColor(const bool disable = false) noexcept { - // Check if the user has requested no color - if (disable) { - ColorConfig::getInstance().setSuppressColor(true); - return; - } - // Process the environment variable to suppress color output - const auto noColorEnv = getEnv("NO_COLOR"); - const auto termEnv = getEnv("TERM"); - const bool suppressColor = noColorEnv.has_value() || - (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "vt100" || - termEnv.value() == "vt102")); - ColorConfig::getInstance().setSuppressColor(suppressColor); - } - /// \brief Performs Base64 encoding of binary data into a string. /// \param input a vector of the binary data to be encoded. /// \return Base64-encoded string. @@ -195,4 +101,20 @@ export { bool validateYesNo(std::string_view prompt = ""); std::string getHomeDir() noexcept; + + std::optional getEnv(const char *var); + + void configureColor(bool disable = false) noexcept; + + template + void printColoredOutput(char color, std::format_string fmt, Args &&... args); + + template + void printColoredOutputln(char color, std::format_string fmt, Args &&... args); + + template + void printColoredError(char color, std::format_string fmt, Args &&... args); + + template + void printColoredErrorln(char color, std::format_string fmt, Args &&... args); } From cb6c6e815913f55399659fb2bbe81bbc0d339a05 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 27 May 2024 00:39:31 +0300 Subject: [PATCH 13/99] Bugfix: Fix linker errors due to template instantiation issues --- src/utils/utils.cpp | 113 ------------------------------------- src/utils/utils.cppm | 130 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 118 insertions(+), 125 deletions(-) diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 3436eb1..1604f96 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -35,47 +35,6 @@ import secureAllocator; constexpr int MAX_PASSPHRASE_LEN = 1024; // Maximum length of a passphrase -/// \class ColorConfig -/// \brief A singleton class used to manage the color configuration of the terminal output. -/// This class encapsulates the \p suppressColor functionality, ensuring that there is only -/// one \p suppressColor instance throughout the application. -/// It provides methods to get and set the \p suppressColor value. -class ColorConfig { -public: - /// \brief Gets the instance of the \p ColorConfig singleton. - /// \return A reference to the singleton instance of the \p ColorConfig class. - static ColorConfig &getInstance() noexcept { - static ColorConfig instance; - return instance; - } - - // Delete the copy constructor and assignment operator - ColorConfig(ColorConfig const &) = delete; - - void operator=(ColorConfig const &) = delete; - - /// \brief Gets the \p suppressColor value. - /// \return The current value of the \p suppressColor variable. - [[nodiscard]] bool getSuppressColor() const noexcept { - return suppressColor; - } - - /// \brief Sets the \p suppressColor value. - /// \param value The new value for the \p suppressColor variable. - void setSuppressColor(const bool value) noexcept { - suppressColor = value; - } - -private: - /// \brief Private constructor for the \p ColorConfig class. - /// This constructor initializes the \p suppressColor variable to false. - ColorConfig() : suppressColor(false) { - } - - // The suppressColor value - bool suppressColor; -}; - /// \brief Performs Base64 decoding of a string into binary data. /// \param encodedData Base64 encoded string. /// \return a vector of the decoded binary data. @@ -354,75 +313,3 @@ void configureColor(const bool disable) noexcept { termEnv.value() == "vt102")); ColorConfig::getInstance().setSuppressColor(suppressColor); } - -/// \brief Returns the ANSI color code for the given character. -/// \param color The character representing the color. -/// \return The ANSI color code corresponding to the input character. -constexpr auto getColorCode(const char color) noexcept { - switch (color) { - case 'r': // Red - return "\033[1;31m"; - case 'g': // Green - return "\033[1;32m"; - case 'y': // Yellow - return "\033[1;33m"; - case 'b': // Blue - return "\033[1;34m"; - case 'm': // Magenta - return "\033[1;35m"; - case 'c': // Cyan - return "\033[1;36m"; - case 'w': // White - return "\033[1;37m"; - default: // No color - return ""; - } -} - -/// \brief Prints colored output to the console. -/// \tparam Args Variadic template for all types of arguments that can be passed. -/// \param color The color code for the output. -/// \param fmt The format string for the output. -/// \param args The arguments to be printed. -template -void printColoredOutput(const char color, std::format_string fmt, Args &&... args) { - if (ColorConfig::getInstance().getSuppressColor()) - std::print(fmt, std::forward(args)...); - else std::print("{}{}\033[0m", getColorCode(color), std::format(fmt, std::forward(args)...)); -} - -/// \brief Prints colored output to the console and adds a newline at the end. -/// \tparam Args Variadic template for all types of arguments that can be passed. -/// \param color The color code for the output. -/// \param fmt The format string for the output. -/// \param args The arguments to be printed. -template -void printColoredOutputln(const char color, std::format_string fmt, Args &&... args) { - if (ColorConfig::getInstance().getSuppressColor()) - std::println(fmt, std::forward(args)...); - else std::print("{}{}\033[0m\n", getColorCode(color), std::format(fmt, std::forward(args)...)); -} - -/// \brief Prints colored error messages to the console. -/// \tparam Args Variadic template for all types of arguments that can be passed. -/// \param color The color code for the output. -/// \param fmt The format string for the output. -/// \param args The arguments to be printed. -template -void printColoredError(const char color, std::format_string fmt, Args &&... args) { - if (ColorConfig::getInstance().getSuppressColor()) - std::print(std::cerr, fmt, std::forward(args)...); - else std::print(std::cerr, "{}{}\033[0m", getColorCode(color), std::format(fmt, std::forward(args)...)); -} - -/// \brief This function prints colored error messages to the console and adds a newline at the end. -/// \tparam Args Variadic template for all types of arguments that can be passed. -/// \param color The color code for the output. -/// \param fmt The format string for the output. -/// \param args The arguments to be printed. -template -void printColoredErrorln(const char color, std::format_string fmt, Args &&... args) { - if (ColorConfig::getInstance().getSuppressColor()) - std::println(std::cerr, fmt, std::forward(args)...); - else std::print(std::cerr, "{}{}\033[0m\n", getColorCode(color), std::format(fmt, std::forward(args)...)); -} diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 1acf52d..0741182 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -30,6 +30,48 @@ import secureAllocator; namespace fs = std::filesystem; + +/// \class ColorConfig +/// \brief A singleton class used to manage the color configuration of the terminal output. +/// This class encapsulates the \p suppressColor functionality, ensuring that there is only +/// one \p suppressColor instance throughout the application. +/// It provides methods to get and set the \p suppressColor value. +class ColorConfig { +public: + /// \brief Gets the instance of the \p ColorConfig singleton. + /// \return A reference to the singleton instance of the \p ColorConfig class. + static ColorConfig &getInstance() noexcept { + static ColorConfig instance; + return instance; + } + + // Delete the copy constructor and assignment operator + ColorConfig(ColorConfig const &) = delete; + + void operator=(ColorConfig const &) = delete; + + /// \brief Gets the \p suppressColor value. + /// \return The current value of the \p suppressColor variable. + [[nodiscard]] bool getSuppressColor() const noexcept { + return suppressColor; + } + + /// \brief Sets the \p suppressColor value. + /// \param value The new value for the \p suppressColor variable. + void setSuppressColor(const bool value) noexcept { + suppressColor = value; + } + +private: + /// \brief Private constructor for the \p ColorConfig class. + /// This constructor initializes the \p suppressColor variable to false. + ColorConfig() : suppressColor(false) { + } + + // The suppressColor value + bool suppressColor; +}; + template // Describes a vector of unsigned characters (For use with vectors using different allocators) concept uCharVector = std::copy_constructible && requires(T t, unsigned char c) { @@ -42,6 +84,30 @@ concept uCharVector = std::copy_constructible && requires(T t, unsigned char t.shrink_to_fit(); }; +/// \brief Returns the ANSI color code for the given character. +/// \param color The character representing the color. +/// \return The ANSI color code corresponding to the input character. +constexpr const char *getColorCode(const char color) noexcept { + switch (color) { + case 'r': // Red + return "\033[1;31m"; + case 'g': // Green + return "\033[1;32m"; + case 'y': // Yellow + return "\033[1;33m"; + case 'b': // Blue + return "\033[1;34m"; + case 'm': // Magenta + return "\033[1;35m"; + case 'c': // Cyan + return "\033[1;36m"; + case 'w': // White + return "\033[1;37m"; + default: // No color + return ""; + } +} + export { /// \brief Performs Base64 encoding of binary data into a string. /// \param input a vector of the binary data to be encoded. @@ -82,6 +148,58 @@ export { return encodedData; } + /// \brief Prints colored output to the console. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredOutput(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::cout << std::vformat(fmt.get(), std::make_format_args(args...)); + else std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; + // else std::print("{}{}\033[0m", getColorCode(color), std::format(fmt, std::forward(args)...)); + } + + /// \brief Prints colored output to the console and adds a newline at the end. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredOutputln(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::cout << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; + else std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << + std::endl; + // else std::print("{}{}\033[0m\n", getColorCode(color), std::format(fmt, std::forward(args)...)); + } + + /// \brief Prints colored error messages to the console. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredError(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)); + else std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; + } + + /// \brief This function prints colored error messages to the console and adds a newline at the end. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredErrorln(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; + else std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << + std::endl; + } + std::vector base64Decode(std::string_view encodedData); int getResponseInt(std::string_view prompt = ""); @@ -105,16 +223,4 @@ export { std::optional getEnv(const char *var); void configureColor(bool disable = false) noexcept; - - template - void printColoredOutput(char color, std::format_string fmt, Args &&... args); - - template - void printColoredOutputln(char color, std::format_string fmt, Args &&... args); - - template - void printColoredError(char color, std::format_string fmt, Args &&... args); - - template - void printColoredErrorln(char color, std::format_string fmt, Args &&... args); } From 031d09b29a64b8815110d63a5427f4e4eca995e2 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 27 May 2024 00:45:42 +0300 Subject: [PATCH 14/99] Refactor: Update color printing and output formatting --- src/duplicateFinder/duplicateFinder.cppm | 108 +++----- src/encryption/encryptDecrypt.cpp | 139 +++++----- src/fileShredder/fileShredder.cppm | 119 ++++----- src/main.cpp | 70 ++--- src/passwordManager/passwordManager.cpp | 320 +++++++++++------------ src/passwordManager/passwords.cpp | 31 ++- src/privacyTracks/privacyTracks.cppm | 114 ++++---- 7 files changed, 428 insertions(+), 473 deletions(-) diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index 42cbbaa..83a9616 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -16,7 +16,7 @@ module; -#include +#include #include #include #include @@ -24,6 +24,7 @@ module; #include #include #include +#include #include #include @@ -74,36 +75,8 @@ std::string calculateBlake3(const std::string &filePath) { /// \brief handles file i/o errors during low-level file operations. /// \param filename path to the file on which an error occurred. inline void handleAccessError(const std::string_view filename) { - std::string errMsg; - errMsg.reserve(50); - - switch (errno) { - case EACCES: // Permission denied - errMsg = "You do not have permission to access this item"; - break; - case EEXIST: // File exists - errMsg = "already exists"; - break; - case EISDIR: // Is a directory - errMsg = "is a directory"; - break; - case ELOOP: // Too many symbolic links encountered - errMsg = "is a loop"; - break; - case ENAMETOOLONG: // The filename is too long - errMsg = "the path is too long"; - break; - case ENOENT: // No such file or directory - errMsg = "path does not exist"; - break; - case EROFS: // Read-only file system - errMsg = "the file system is read-only"; - break; - default: [[likely]] // Success (most likely) - return; - } - - printColor(std::format("Skipping '{}': {}.", filename, errMsg), 'r', true, std::cerr); + if (errno) printColoredErrorln('r', "Skipping '{}': {}.", filename, std::strerror(errno)); + errno = 0; } /// \brief recursively traverses a directory and collects file information. @@ -118,8 +91,7 @@ void traverseDirectory(const std::string_view directoryPath, std::vector 1) { ++duplicatesSet; // Show the duplicates in their sets - printColor("Duplicate files set ", 'c'); - printColor(duplicatesSet, 'g'); - printColor(":", 'c', true); + printColoredOutput('c', "Duplicate files set "); + printColoredOutput('g', "{}", duplicatesSet); + printColoredOutputln('c', ":"); for (const auto &filePath: duplicates) { ++numDuplicates; - std::cout << " " << filePath << std::endl; + std::println(" {}", filePath); } } } - printColor("\nFiles processed: ", 'c'); - printColor(filesProcessed, 'g', true); + printColoredOutput('c', "\nFiles processed: "); + printColoredOutputln('g', "{}", filesProcessed); return numDuplicates; } @@ -220,18 +191,20 @@ std::size_t findDuplicates(const std::string_view directoryPath) { /// \brief A simple duplicate file detective. export void duplicateFinder() { while (true) { - std::cout << "\n-------------------"; - printColor(" Duplicate Finder ", 'm'); - std::cout << "-------------------\n"; - printColor("1. Scan for duplicate files\n", 'g'); - printColor("2. Exit\n", 'r'); - std::cout << "--------------------------------------------------------" << std::endl; + std::print("\n-------------------"); + // std::cout << "\n-------------------"; + printColoredOutput('m', " Duplicate Finder "); + std::println("-------------------"); + // std::cout << "-------------------\n"; + printColoredOutputln('g', "1. Scan for duplicate files"); + printColoredOutputln('r', "2. Exit"); + std::println("--------------------------------------------------------"); - printColor("Enter your choice:", 'b'); + printColoredOutput('b', "Enter your choice:"); if (const int resp = getResponseInt(); resp == 1) { try { - printColor("Enter the path to the directory to scan:", 'b'); + printColoredOutput('b', "Enter the path to the directory to scan:"); std::string dirPath = getResponseStr(); if (const auto len = dirPath.size(); len > 1 && (dirPath.ends_with('/') || dirPath.ends_with('\\'))) @@ -240,48 +213,47 @@ export void duplicateFinder() { std::error_code ec; const fs::file_status fileStatus = fs::status(dirPath, ec); if (ec) { - printColor("Unable to determine ", 'y', false, std::cerr); - printColor(dirPath, 'b', false, std::cerr); + printColoredError('y', "Unable to determine "); + printColoredError('b', "{}", dirPath); - printColor("'s status: ", 'y', false, std::cerr); - printColor(ec.message(), 'r', true, std::cerr); + printColoredError('y', "'s status: "); + printColoredErrorln('r', "{}", ec.message()); ec.clear(); continue; } if (!exists(fileStatus)) { - printColor(dirPath, 'c', false, std::cerr); - printColor(" does not exist.", 'r', true, std::cerr); + printColoredError('c', "{}",dirPath); + printColoredErrorln('r', " does not exist."); continue; } if (!is_directory(fileStatus)) { - printColor(dirPath, 'c', false, std::cerr); - printColor(" is not a directory.", 'r', true, std::cerr); + printColoredError('c', "{}", dirPath); + printColoredErrorln('r', " is not a directory."); continue; } if (fs::is_empty(dirPath, ec)) { if (ec) ec.clear(); else { - printColor("The directory is empty.", 'r', true, std::cerr); + printColoredErrorln('r', "The directory is empty."); continue; } } - printColor("Scanning ", 'c'); - printColor(fs::canonical(dirPath).string(), 'g'); - printColor(" ...", 'c', true); + printColoredOutput('c', "Scanning "); + printColoredOutput('g', "{}", fs::canonical(dirPath).string()); + printColoredOutputln('c', " ..."); const std::size_t duplicateFiles = findDuplicates(dirPath); - std::cout << "Duplicates " - << (duplicateFiles > 0 ? "found: " + std::to_string(duplicateFiles) : "not found.") - << std::endl; + std::println("Duplicates {}", + duplicateFiles > 0 ? "found: " + std::to_string(duplicateFiles) : "not found."); } catch (const std::exception &ex) { - printColor("An error occurred: ", 'y', false, std::cerr); - printColor(ex.what(), 'r', true, std::cerr); + printColoredError('y', "An error occurred: "); + printColoredErrorln('r', "{}", ex.what()); } } else if (resp == 2) break; else { - printColor("Invalid option!", 'r', true, std::cerr); + printColoredErrorln('r', "Invalid option!"); } } } diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index 1bf9e90..eaffc62 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -23,9 +23,9 @@ module; #include #include #include -#include #include #include +#include import utils; import secureAllocator; @@ -35,38 +35,6 @@ module encryption; namespace fs = std::filesystem; -template -/// \brief A concept describing a type convertible and comparable with uintmax_t. -/// \tparam T - An integral type. -concept Num = std::integral && std::convertible_to && - std::equality_comparable_with; - - -/// \brief A class to make file sizes more readable. -/// \details Adapted from https://en.cppreference.com/w/cpp/filesystem/file_size -class FormatFileSize { -public: - explicit FormatFileSize(const Num auto &size) { - // Default negative values to zero - if (std::cmp_greater(size, size_)) - size_ = static_cast(size); - } - -private: - std::uintmax_t size_{0}; - - friend - std::ostream &operator<<(std::ostream &os, const FormatFileSize ffs) { - int i{}; - auto mantissa = static_cast(ffs.size_); - for (; mantissa >= 1024.; mantissa /= 1024., ++i) { - } - mantissa = std::ceil(mantissa * 10.) / 10.; - os << mantissa << "BKMGTPE"[i]; - return i == 0 ? os : os << "B (" << ffs.size_ << ')'; - } -}; - /// \brief Available encryption/decryption ciphers. enum class Algorithms : std::uint_fast8_t { AES = 1 << 0, @@ -91,6 +59,20 @@ constexpr struct { const gcry_cipher_algos Twofish = GCRY_CIPHER_TWOFISH; } AlgoSelection; + +/// \brief Formats a file size into a human-readable string. +/// \param size The file size as an unsigned integer. +/// \return A string representing the formatted file size. +std::string formatFileSize(const std::uintmax_t &size) { + int i{}; + auto mantissa = static_cast(size); + for (; mantissa >= 1024.; mantissa /= 1024., ++i) { + } + mantissa = std::ceil(mantissa * 10.) / 10.; + std::string result = std::to_string(mantissa) + "BKMGTPE"[i]; + return i == 0 ? result : result + "B (" + std::to_string(size) + ')'; +} + /// \brief Checks for issues with the input file, that may hinder encryption/decryption. /// \param inFile the input file, to be encrypted/decrypted. /// \param mode the mode of operation: encryption or decryption. @@ -111,7 +93,7 @@ void checkInputFile(const fs::path &inFile, const OperationMode &mode) { if (!is_regular_file(inFile)) { if (mode == OperationMode::Encryption) { // Encryption - std::cout << inFile.string() << " is not a regular file. \nDo you want to continue? (y/n): "; + std::print("{} is not a regular file.\nDo you want to continue? (y/n): ", inFile.string()); if (!validateYesNo()) throw std::runtime_error(std::format("{} is not a regular file.", inFile.string())); } else @@ -183,8 +165,8 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope } // If the output file exists, ask for confirmation for overwriting if (exists(outFile, ec)) { - printColor(canonical(outFile).string(), 'b', false, std::cerr); - printColor(" already exists. \nDo you want to overwrite it? (y/n): ", 'r', false, std::cerr); + printColoredError('b', "{}", canonical(outFile).string()); + printColoredError('r', " already exists.\nDo you want to overwrite it? (y/n): "); if (!validateYesNo()) throw std::runtime_error("Operation aborted."); @@ -201,16 +183,16 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope // Check if there is enough space on the disk to save the output file. const auto availableSpace = getAvailableSpace(weakly_canonical(outFile)); if (const auto fileSize = file_size(inFile); std::cmp_less(availableSpace, fileSize)) { - printColor("Not enough space to save ", 'r', false, std::cerr); - printColor(weakly_canonical(outFile).string(), 'c', true, std::cerr); + printColoredError('r', "Not enough space to save "); + printColoredError('c', "{}", weakly_canonical(outFile).string()); - printColor("Required: ", 'y', false, std::cerr); - printColor(FormatFileSize(fileSize), 'g', true, std::cerr); + printColoredError('y', "Required: "); + printColoredError('g', "{}", formatFileSize(fileSize)); - printColor("Available: ", 'y', false, std::cerr); - printColor(FormatFileSize(availableSpace), 'r', true, std::cerr); + printColoredError('y', "Available: "); + printColoredErrorln('r', "{}", formatFileSize(availableSpace)); - printColor("\nDo you still want to continue? (y/n):", 'b'); + printColoredOutput('b', "\nDo you still want to continue? (y/n):"); if (!validateYesNo()) throw std::runtime_error("Insufficient storage space."); } @@ -234,7 +216,7 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin const privacy::string &password, const Algorithms &algo, const OperationMode &mode) { // The mode must be valid: must be either encryption or decryption if (mode != OperationMode::Encryption && mode != OperationMode::Decryption) [[unlikely]] { - printColor("Invalid mode of operation.", 'r', true, std::cerr); + printColoredErrorln('r', "Invalid mode of operation."); return; } @@ -276,18 +258,18 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin // If we reach here, the operation was successful auto pre = mode == OperationMode::Encryption ? "En" : "De"; - printColor(std::format("{}cryption completed successfully. \n{}crypted file saved as ", pre, pre), 'g'); - printColor(outputFileName, 'b', true); + printColoredOutput('g', "{}cryption completed successfully.\n{}crypted file saved as ", pre, pre); + printColoredOutputln('b', "{}", outputFileName); // Preserve file permissions if (!copyFilePermissions(inputFileName, outputFileName)) [[unlikely]] - printColor(std::format("Check the permissions of the {}crypted file.", pre), 'm', true); + printColoredOutputln('m', "Check the permissions of the {}crypted file.", pre); // Try to preserve the time of last modification copyLastWrite(inputFileName, outputFileName); } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); } } @@ -312,13 +294,13 @@ void encryptDecrypt() { }; while (true) { - std::cout << "-------------"; - printColor(" file encryption/decryption utility ", 'c'); - std::cout << "-------------\n"; - printColor("1. Encrypt a file\n", 'g'); - printColor("2. Decrypt a file\n", 'm'); - printColor("3. Exit\n", 'r'); - std::cout << "--------------------------------------------------------------" << std::endl; + std::print("-------------"); + printColoredOutput('c', " file encryption/decryption utility "); + std::println("-------------"); + printColoredOutputln('g', "1. Encrypt a file"); + printColoredOutputln('m', "2. Decrypt a file"); + printColoredOutputln('r', "3. Exit"); + std::println("--------------------------------------------------------------"); if (const int choice = getResponseInt("Enter your choice: "); choice == 1 || choice == 2) { try { @@ -331,7 +313,7 @@ void encryptDecrypt() { return std::tolower(c); }); - printColor(std::format("Enter the path to the file to {}crypt:", pre_l), 'c', true); + printColoredOutputln('c', "Enter the path to the file to {}crypt:", pre_l); std::string inputFile = getResponseStr(); // Remove the trailing directory separator @@ -344,28 +326,27 @@ void encryptDecrypt() { inputPath = fs::current_path() / inputPath; checkInputFile(inputPath, static_cast(choice)); - printColor(std::format("Enter the path to save the {}crypted file " - "\n(or leave it blank to save it in the same directory):", - pre_l), 'c', true); + printColoredOutputln('c', "Enter the path to save the {}crypted file" + "\n(or leave it blank to save it in the same directory):", pre_l); fs::path outputPath{getResponseStr()}; if (!outputPath.is_absolute()) // If the path is not absolute outputPath = fs::current_path() / outputPath; checkOutputFile(inputPath, outputPath, static_cast(choice)); - std::cout << "Choose a cipher (All are 256-bit):\n"; - printColor("1. Advanced Encryption Standard (AES)\n", 'b'); - printColor("2. Camellia\n", 'c'); - printColor("3. Aria\n", 'g'); - printColor("4. Serpent\n", 'y'); - printColor("5. Twofish\n", 'm'); + std::println("Choose a cipher (All are 256-bit):"); + printColoredOutputln('b', "1. Advanced Encryption Standard (AES)"); + printColoredOutputln('c', "2. Camellia"); + printColoredOutputln('g', "3. Aria"); + printColoredOutputln('y', "4. Serpent"); + printColoredOutputln('m', "5. Twofish"); - std::cout << "Leave blank to use the default (AES)" << std::endl; + std::println("Leave blank to use the default (AES)"); int algo = getResponseInt(); if (algo < 0 || algo > 5) { // 0 is default (AES) - printColor("Invalid choice!", 'r', true, std::cerr); + printColoredErrorln('r', "Invalid choice!"); continue; } const auto it = algoChoice.find(algo); @@ -377,7 +358,7 @@ void encryptDecrypt() { if (choice == 1) { int tries{0}; while (password.empty() && ++tries < 3) { - printColor("Please avoid empty or weak passwords. Please try again.", 'r', true, std::cerr); + printColoredErrorln('r', "Please avoid empty or weak passwords. Please try again."); password = getSensitiveInfo("Enter the password: "); } if (tries >= 3) @@ -386,25 +367,25 @@ void encryptDecrypt() { if (const privacy::string password2{getSensitiveInfo("Enter the password again: ")}; !verifyPassword(password2, hashPassword(password, crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE))) { - printColor("Passwords do not match.", 'r', true, std::cerr); + printColoredErrorln('r', "Passwords do not match."); continue; } } - printColor(std::format("{}crypting ", pre), 'g'); - printColor(canonical(inputPath).string(), 'b'); - printColor(" with ", 'g'); - printColor(algoDescription.find(cipher)->second, 'c'); - printColor("...", 'g', true); + printColoredOutput('g', "{}crypting ", pre); + printColoredOutput('g', "{}", canonical(inputPath).string()); + printColoredOutput('g', " with "); + printColoredOutput('c', "{}", algoDescription.find(cipher)->second); + printColoredOutputln('g', "..."); fileEncryptionDecryption(canonical(inputPath).string(), weakly_canonical(outputPath).string(), password, cipher, static_cast(choice)); - std::cout << std::endl; + std::println(""); } catch (const std::exception &ex) { - printColor("Error: ", 'y', false, std::cerr); - printColor(ex.what(), 'r', true, std::cerr); - std::cerr << std::endl; + printColoredError('y', "Error: "); + printColoredErrorln('r', "{}", ex.what()); + std::println(""); } } else if (choice == 3) break; - else printColor("Invalid choice!", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice!"); } } diff --git a/src/fileShredder/fileShredder.cppm b/src/fileShredder/fileShredder.cppm index 23bdf33..9a10ffd 100644 --- a/src/fileShredder/fileShredder.cppm +++ b/src/fileShredder/fileShredder.cppm @@ -17,7 +17,6 @@ module; #include -#include #include #include #include @@ -25,7 +24,9 @@ module; #include #include #include +#include #include +#include using StatType = struct stat; @@ -161,9 +162,9 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { fs::remove(path, ec); if (ec) { - printColor("Failed to delete ", 'r', false, std::cerr); - printColor(filename, 'm', false, std::cerr); - printColor(std::format(": {}", ec.message()), 'r', true, std::cerr); + printColoredError('r', "Failed to delete "); + printColoredError('m', "{}", filename); + printColoredErrorln('r', ": {}", ec.message()); } } @@ -362,17 +363,17 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c std::error_code ec; const fs::file_status fileStatus = fs::status(filePath, ec); if (ec) { - printColor("Unable to determine ", 'y', false, std::cerr); - printColor(filePath, 'b', false, std::cerr); + printColoredError('y', "Unable to determine "); + printColoredError('b', "{}", filePath); - printColor("'s status: ", 'y', false, std::cerr); - printColor(ec.message(), 'r', true, std::cerr); + printColoredError('y', "'s status: "); + printColoredErrorln('r', "{}", ec.message()); return false; } // Check if the file exists and is a regular file. if (!exists(fileStatus)) { - printColor(filePath, 'c', false, std::cerr); - printColor(" does not exist.", 'r', true, std::cerr); + printColoredError('c', "{}", filePath); + printColoredErrorln('r', " does not exist."); return false; } // If the filepath is a directory, shred all the files in the directory and all its subdirectories @@ -380,8 +381,8 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c if (fs::is_empty(filePath, ec)) { if (ec) ec.clear(); else { - printColor(filePath, 'c'); - printColor(" is an empty directory.", 'y', true); + printColoredOutput('c', "{}", filePath); + printColoredOutputln('y', " is an empty directory."); return true; } } @@ -391,48 +392,48 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c for (const auto &entry: fs::recursive_directory_iterator(filePath)) { if (entry.exists(ec)) { if (ec) { - printColor(ec.message(), 'r', true, std::cerr); + printColoredErrorln('r', "{}", ec.message()); ec.clear(); continue; } if (!is_directory(entry.status())) { - printColor("Shredding ", 'c'); - printColor(canonical(entry.path()).string(), 'b'); - printColor(" ...", 'c'); + printColoredOutput('c', "Shredding "); + printColoredOutput('b', "{}", canonical(entry.path()).string()); + printColoredOutput('c', " ..."); try { const bool shredded = shredFiles(entry.path().string(), options); - printColor(shredded ? "\tshredded successfully." : "\tshredding failed.", shredded ? 'g' : 'r', - true); + printColoredOutputln(shredded ? 'g' : 'r', "{}", + shredded ? "\tshredded successfully." : "\tshredding failed."); ++(shredded ? numShredded : numNotShredded); } catch (const std::runtime_error &err) { - printColor("Shredding failed: ", 'y', false, std::cerr); - printColor(err.what(), 'r', true, std::cerr); + printColoredError('y', "Shredding failed: "); + printColoredErrorln('r', "{}", err.what()); } } } } if (numNotShredded == 0) // All files in the directory and all subdirectories were shredded successfully. remove_all(fs::canonical(filePath)); - else printColor("Failed to shred some files.", 'r', true, std::cerr); + else printColoredErrorln('r', "Failed to shred some files."); - std::cout << "\nProcessed " << numShredded + numNotShredded << " files." << std::endl; + std::println("\nProcessed {} files.", numShredded + numNotShredded); if (numShredded) { - printColor("Successfully shredded and deleted: ", 'g'); - printColor(numShredded, 'b', true); + printColoredOutput('g', "Successfully shredded and deleted: "); + printColoredOutputln('b', "{}", numShredded); } if (numNotShredded) { - printColor("Failed to shred ", 'r', false, std::cerr); - printColor(numNotShredded, 'b', false, std::cerr); - printColor(" files.", 'r', true, std::cerr); + printColoredError('r', "Failed to shred "); + printColoredError('b', "{}", numNotShredded); + printColoredErrorln('r', " files."); } return true; } if (!is_regular_file(fileStatus)) { - printColor(filePath, 'c', false, std::cerr); - printColor(" is not a regular file.", 'r', true, std::cerr); - printColor("Do you want to (try to) shred the file anyway? (y/n):", 'y', true); + printColoredError('c', "{}", filePath); + printColoredError('r', " is not a regular file."); + printColoredOutputln('y', "Do you want to (try to) shred the file anyway? (y/n):"); if (!validateYesNo()) return false; } @@ -440,8 +441,8 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c // Check file permissions if (!isWritable(filePath) || !isReadable(filePath)) { if (!addReadWritePermissions(filePath)) { - printColor("\nInsufficient permissions to shred file: ", 'r', false, std::cerr); - printColor(filePath, 'c', true, std::cerr); + printColoredError('r', "\nInsufficient permissions to shred file: "); + printColoredErrorln('c', "{}", filePath); return false; } } @@ -498,7 +499,7 @@ export void fileShredder() { } else if (simpleConfig == 4) { // Abort throw std::runtime_error("Operation aborted."); - } else printColor("Invalid option", 'r', true, std::cerr); + } else printColoredErrorln('r', "Invalid option"); } while (true); } else if (alg == 2 || alg == 3) { // DoD 5220.22-M Standard algorithms @@ -511,15 +512,15 @@ export void fileShredder() { }; while (true) { - printColor("\n------------------", 'g'); - printColor(" file shredder ", 'm'); - printColor("------------------", 'g', true); + printColoredOutput('g', "\n------------------"); + printColoredOutput('m', " file shredder "); + printColoredOutputln('g', "------------------"); - printColor("1. Shred a file", 'y', true); - printColor("2. Shred a directory", 'y', true); - printColor("3. Exit", 'r', true); + printColoredOutputln('y', "1. Shred a file"); + printColoredOutputln('y', "2. Shred a directory"); + printColoredOutputln('r', "3. Exit"); - printColor("---------------------------------------------------", 'g', true); + printColoredOutputln('g', "---------------------------------------------------"); if (const int choice = getResponseInt("Enter your choice: "); choice == 1 || choice == 2) { try { @@ -534,7 +535,7 @@ export void fileShredder() { std::error_code ec; const fs::file_status fileStatus = fs::status(path, ec); if (ec) { - printColor(ec.message(), 'r', true, std::cerr); + printColoredErrorln('r', "{}", ec.message()); ec.clear(); continue; } @@ -543,23 +544,23 @@ export void fileShredder() { // Check if the file or directory exists if (!exists(fileStatus)) { - printColor(canonicalPath, 'c', false, std::cerr); - printColor(" does not exist.", 'r', true, std::cerr); + printColoredError('c', "{}", canonicalPath); + printColoredErrorln('r', " does not exist."); continue; } // If the path is a directory, shred all the files in the directory and all subdirectories (with confirmation) if (choice == 1 && isDir) { - printColor(canonicalPath, 'c'); - printColor(" is a directory.", 'r', true); + printColoredOutput('c', "{}", canonicalPath); + printColoredOutputln('r', " is a directory."); - printColor("Shred all files in '", 'y'); - printColor(canonicalPath, 'c'); - printColor("'\nand all its subdirectories? (y/n):", 'y', true); + printColoredOutput('y', "Shred all files in '"); + printColoredOutput('c', "{}", canonicalPath); + printColoredOutputln('y', "'\nand all its subdirectories? (y/n):"); if (!validateYesNo()) continue; } else if (choice == 2 && !isDir) { // If the path is a file, shred it without confirmation - printColor(canonicalPath, 'c'); - printColor(" is not a directory.", 'r', true); + printColoredOutput('c', "{}", canonicalPath); + printColoredOutputln('r', " is not a directory."); if (!validateYesNo("Shred it anyway? (y/n):")) continue; } std::uint_fast8_t preferences{0}; @@ -568,27 +569,27 @@ export void fileShredder() { try { selectPreferences(preferences, simpleNumPass); } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); continue; } - printColor(std::format("The {} contents will be lost permanently.\nContinue? (y/n)", - isDir ? "directory's (and all its subdirectories')" : "file"), 'r', true); + printColoredOutputln('r', "The {} contents will be lost permanently.\nContinue? (y/n)", + isDir ? "directory's (and all its subdirectories')" : "file"); if (validateYesNo()) { std::cout << "Shredding '"; - printColor(canonicalPath, 'c'); + printColoredOutput('c', "{}", canonicalPath); std::cout << "'..." << std::endl; const bool shredded = shredFiles(path, preferences, simpleNumPass); if (!isDir) { - printColor(shredded ? "Successfully shredded " : "Failed to shred ", shredded ? 'g' : 'r', - false, shredded ? std::cout : std::cerr); - printColor(canonicalPath, 'c', true, shredded ? std::cout : std::cerr); + printColoredOutput(shredded ? 'g' : 'r', "{}", + shredded ? "Successfully shredded " : "Failed to shred "); + printColoredOutputln('c', "{}", canonicalPath); } } } catch (const std::exception &err) { - printColor(std::format("Error: {}", err.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", err.what()); } } else if (choice == 3) break; - else printColor("Invalid choice.", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice."); } } diff --git a/src/main.cpp b/src/main.cpp index 3309f6e..9c8669d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,13 +38,13 @@ int main(const int argc, const char **argv) { // The program should be launched in interactive mode if (!isatty(STDIN_FILENO)) { if (errno == ENOTTY) { - printColor(std::format("{} is meant to be run interactively.", argv[0]), 'r', true, std::cerr); + printColoredErrorln('r', "{} is meant to be run interactively.", argv[0]); return 1; } } // Disable core dumping for security reasons if (constexpr rlimit coreLimit{0, 0}; setrlimit(RLIMIT_CORE, &coreLimit) != 0) { - printColor("Failed to disable core dumps.", 'r', true, std::cerr); + printColoredErrorln('r', "Failed to disable core dumps."); return 1; } @@ -57,18 +57,18 @@ int main(const int argc, const char **argv) { if (std::string_view(argv[1]) == "--no-color" || std::string_view(argv[1]) == "-nc") { configureColor(true); } else { - printColor("The option ", 'y'); - printColor(std::format("{} ", argv[1]), 'r'); - printColor("is not recognized.", 'y', true, std::cerr); + printColoredError('y', "The option "); + printColoredError('r', "{} ", argv[1]); + printColoredErrorln('y', "is not recognized."); - printColor("Usage: ", 'y'); - printColor(std::format("{} [--no-color | -nc]", argv[0]), 'r', true, std::cerr); + printColoredError('y', "Usage: "); + printColoredErrorln('r', "{} [--no-color | -nc]", argv[0]); } } if (argc > 2) { - printColor("Ignoring extra arguments: ", 'y'); - for (int i = 2; i < argc; printColor(std::format("{} ", argv[i++]), 'r')) { + printColoredOutput('y', "Ignoring extra arguments: ", 'y'); + for (int i = 2; i < argc; printColoredOutput('r', "{} ", argv[i++])) { } std::cout << std::endl; } @@ -76,8 +76,8 @@ int main(const int argc, const char **argv) { // Handle the keyboard interrupt (SIGINT) signal (i.e., Ctrl+C) struct sigaction act{}; act.sa_handler = [](int /* unused */) noexcept -> void { - printColor("Keyboard interrupt detected.\nUnsaved data might be lost if you quit now." - "\nDo you still want to quit? (y/n):", 'r'); + printColoredOutput('r', "Keyboard interrupt detected.\nUnsaved data might be lost if you quit now." + "\nDo you still want to quit? (y/n):"); if (validateYesNo()) std::exit(1); }; @@ -115,19 +115,19 @@ int main(const int argc, const char **argv) { throw std::runtime_error("Failed to initialize libsodium."); // Display information about the program - printColor("\nPrivacy Shield 2.5.0\n", 'c'); - printColor("Copyright (C) 2024 Ian Duncan.\n", 'b'); + printColoredOutputln('c', "\nPrivacy Shield 2.5.0"); + printColoredOutputln('b', "Copyright (C) 2024 Ian Duncan."); - printColor("This program comes with ", 'g'); - printColor("ABSOLUTELY NO WARRANTY.", 'r'); + printColoredOutput('g', "This program comes with "); + printColoredOutputln('r', "ABSOLUTELY NO WARRANTY."); - printColor("\nThis is a free software; you are free to change and redistribute it\n" - "under the terms of the ", 'g'); - printColor("GNU General Public License v3 ", 'r'); - printColor("or later.", 'g'); + printColoredOutput('g', "This is a free software; you are free to change and redistribute it\n" + "under the terms of the "); + printColoredOutput('r', "GNU General Public License v3 "); + printColoredOutputln('g', "or later."); - printColor("\nFor more information, see ", 'g'); - printColor("https://www.gnu.org/licenses/gpl.html.\n", 'b', true); + printColoredOutput('g', "For more information, see "); + printColoredOutputln('b', "https://www.gnu.org/licenses/gpl.html."); // All the available tools std::unordered_map > apps = { @@ -140,14 +140,14 @@ int main(const int argc, const char **argv) { // Applications loop while (true) { - printColor("-------------------------------------\n", 'c'); - printColor("1. Manage passwords\n", 'b'); - printColor("2. Encrypt/decrypt files\n", 'g'); - printColor("3. Shred files\n", 'm'); - printColor("4. Clear browser privacy traces\n", 'y'); - printColor("5. Find duplicate files\n", 'b'); - printColor("6. Exit\n", 'r'); - printColor("-------------------------------------", 'c', true); + printColoredOutputln('c', "-------------------------------------"); + printColoredOutputln('b', "1. Manage passwords"); + printColoredOutputln('g', "2. Encrypt/decrypt files"); + printColoredOutputln('m', "3. Shred files"); + printColoredOutputln('y', "4. Clear browser privacy traces"); + printColoredOutputln('b', "5. Find duplicate files"); + printColoredOutputln('r', "6. Exit"); + printColoredOutputln('c', "-------------------------------------"); const int choice = getResponseInt("What would you like to do? (Enter 1 or 2, 3..)"); @@ -156,24 +156,24 @@ int main(const int argc, const char **argv) { iter->second(); else if (choice == 6) break; - else printColor("Invalid choice!", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice!"); } catch (const std::bad_function_call &bc) { // In case the std::function objects are called inappropriately - printColor(std::format("Bad function call: {}", bc.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Bad function call: {}", bc.what()); } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); } catch (...) { // All other exceptions, if any - printColor("An error occurred.", 'r', true, std::cerr); + printColoredErrorln('r', "An error occurred."); } } return 0; } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); return 1; } catch (...) { - printColor("Something went wrong.", 'r', true, std::cerr); + printColoredErrorln('r', "Something went wrong."); return 1; } } diff --git a/src/passwordManager/passwordManager.cpp b/src/passwordManager/passwordManager.cpp index 577727b..4af77ed 100644 --- a/src/passwordManager/passwordManager.cpp +++ b/src/passwordManager/passwordManager.cpp @@ -47,7 +47,7 @@ bool constexpr comparator #if __clang__ || __GNUC__ [[gnu::always_inline]] #endif - (const auto &lhs, const auto &rhs) noexcept { +(const auto &lhs, const auto &rhs) noexcept { // Compare the site and username members of the tuples return std::tie(std::get<0>(lhs), std::get<1>(lhs)) < std::tie(std::get<0>(rhs), std::get<1>(rhs)); @@ -61,16 +61,16 @@ constexpr void printPasswordDetails(const auto &pw, const bool &isStrong = false if (!site.empty()) { // Skip blank entries std::cout << "Site/app: "; - printColor(site, 'c'); + printColoredOutput('c', "{}", site); } if (!username.empty()) { std::cout << "\nUsername: "; - printColor(username, 'b'); + printColoredOutput('b', "{}", username); } // Highlight a weak password std::cout << "\nPassword: "; - printColor(pass, isStrong ? 'g' : 'r', true); + printColoredOutputln(isStrong ? 'g' : 'r', "{}", pass); } /// \brief This function computes the strength of each password in the provided list of passwords. @@ -88,7 +88,7 @@ constexpr void computeStrengths #if __clang__ || __GNUC__ [[gnu::always_inline]] #endif - (const privacy::vector &passwords, std::vector &pwStrengths) { +(const privacy::vector &passwords, std::vector &pwStrengths) { pwStrengths.resize(passwords.size()); for (std::size_t i = 0; i < passwords.size(); ++i) { pwStrengths[i] = isPasswordStrong(std::get<2>(passwords[i])); @@ -96,11 +96,11 @@ constexpr void computeStrengths } /// \brief Adds a new password to the saved records. -void addPassword(privacy::vector &passwords, std::vector &strengths) { +void addPassword(privacy::vector &passwords, std::vector &strengths) { privacy::string site{getResponseStr("Enter the name of the site/app: ")}; // The site name must be non-empty if (site.empty()) { - printColor("\nThe site/app name cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "\nThe site/app name cannot be blank."); return; } privacy::string username{getResponseStr("Username (leave blank if N/A): ")}; @@ -114,8 +114,8 @@ void addPassword(privacy::vector &passwords, std::vector // If the record already exists, ask the user if they want to update it bool update{false}; if (it != passwords.end() && std::get<0>(*it) == site && std::get<1>(*it) == username) { - printColor("\nA record with the same site and username already exists.", 'y', true); - printColor("Do you want to update it? (y/n): ", 'b'); + printColoredOutput('y', "\nA record with the same site and username already exists."); + printColoredOutput('b', "Do you want to update it? (y/n):"); update = validateYesNo(); if (!update) return; @@ -126,21 +126,21 @@ void addPassword(privacy::vector &passwords, std::vector // The password can't be empty. Give the user 2 more tries to enter a non-empty password int attempts{0}; while (password.empty() && ++attempts < 3) { - printColor("Password can't be blank. Please try again: ", 'y'); + printColoredOutput('y', "Password can't be blank. Please try again: "); password = getSensitiveInfo(); } // If the password is still empty, return if (password.empty()) { - printColor("Password can't be blank. Try again later.", 'r', true, std::cerr); + printColoredErrorln('r', "Password can't be blank. Try again later."); return; } // Always warn on weak passwords if (!isPasswordStrong(password)) { - printColor( - "The password you entered is weak! A password should have at least 8 characters \nand include at least an " - "uppercase character, a lowercase, a punctuator, \nand a digit.", 'y', true); - printColor("Please consider using a stronger one.", 'r', true); + printColoredOutputln('y', + "The password you entered is weak! A password should have at least 8 characters \nand include at least an " + "uppercase character, a lowercase, a punctuator, \nand a digit."); + printColoredOutputln('r', "Please consider using a stronger one."); } // Update the record if it already exists, else add a new one @@ -148,7 +148,7 @@ void addPassword(privacy::vector &passwords, std::vector std::get<2>(*it) = password; else passwords.emplace_back(site, username, password); - printColor(std::format("Password {} successfully.", update ? "updated" : "added"), 'g', true); + printColoredOutputln('g', "Password {} successfully.", update ? "updated" : "added"); // Entries should always be sorted std::ranges::sort(passwords, [](const auto &tuple1, const auto &tuple2) { @@ -160,53 +160,52 @@ void addPassword(privacy::vector &passwords, std::vector } /// \brief Generates a random password. -void generatePassword(privacy::vector &, std::vector &) { +void generatePassword(privacy::vector &, std::vector &) { int length = getResponseInt("Enter the length of the password to generate: "); int tries{0}; // The password must be at least 8 characters long while (length < 8 && ++tries < 3) { - printColor("A strong password should be at least 8 characters long.", 'y', true); - printColor(std::format("{}", tries == 2 ? "Last chance:" : "Please try again:"), - tries == 2 ? 'r' : 'y'); + printColoredOutputln('y', "A strong password should be at least 8 characters long."); + printColoredOutputln(tries == 2 ? 'r' : 'y', "{}", tries == 2 ? "Last chance:" : "Please try again:"); length = getResponseInt(); } // The password length must not exceed 256 characters if (length > 256) { - printColor("The password length cannot exceed 256 characters.", 'r', true, std::cerr); + printColoredErrorln('r', "The password length cannot exceed 256 characters."); return; } if (length < 8) return; - printColor("Generated password: ", 'c'); - printColor(generatePassword(length), 'g', true); + printColoredOutput('c', "Generated password: "); + printColoredOutputln('g', "{}", generatePassword(length)); } /// \brief Shows all saved passwords. -void viewAllPasswords(privacy::vector &passwords, std::vector &strengths) { +void viewAllPasswords(privacy::vector &passwords, std::vector &strengths) { // Check if there are any passwords saved - if ( auto &&constPasswordsView = std::ranges::views::as_const(passwords); constPasswordsView.empty()) { - printColor("You haven't saved any password yet.", 'r', true); + if (auto &&constPasswordsView = std::ranges::views::as_const(passwords); constPasswordsView.empty()) { + printColoredOutputln('r', "You haven't saved any password yet."); } else { std::cout << "All passwords: ("; - printColor("red is weak", 'r'); + printColoredOutput('r', "red is weak"); std::cout << ", "; - printColor("green is strong", 'g'); + printColoredOutput('g', "green is strong"); std::cout << ")" << std::endl; - printColor("-----------------------------------------------------", 'w', true); + printColoredOutputln('w', "-----------------------------------------------------"); // Print all the passwords for (std::size_t i = 0; i < constPasswordsView.size(); ++i) { printPasswordDetails(constPasswordsView[i], strengths[i]); - printColor("-----------------------------------------------------", 'w', true); + printColoredOutputln('w', "-----------------------------------------------------"); } } } /// \brief Handles fuzzy matching for update and deletion of passwords. -void checkFuzzyMatches(auto &iter, privacy::vector &records, privacy::string &query) { +void checkFuzzyMatches(auto &iter, privacy::vector &records, privacy::string &query) { // Fuzzy-match the query against the site names const FuzzyMatcher matcher(records | std::ranges::views::elements<0>); @@ -214,9 +213,9 @@ void checkFuzzyMatches(auto &iter, privacy::vector &records, p if (const auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; fuzzyMatched.size() == 1) { const auto &match = fuzzyMatched.at(0); - printColor("Did you mean '", 'c'); - printColor(match, 'g'); - printColor("'? (y/n):", 'c'); + printColoredOutput('c', "Did you mean '"); + printColoredOutput('g', "{}", match); + printColoredOutput('c', "'? (y/n):"); if (validateYesNo()) { // Update the iterator @@ -228,26 +227,26 @@ void checkFuzzyMatches(auto &iter, privacy::vector &records, p } } else if (!fuzzyMatched.empty()) { // multiple matches - printColor("Did you mean one of these?:", 'b', true); + printColoredOutputln('b', "Did you mean one of these?:"); // Print all the matches for (const auto &el: fuzzyMatched) { - printColor(el, 'g', true); - printColor("-----------------------------------------", 'b', true); + printColoredOutputln('g', "{}", el); + printColoredOutputln('b', "-----------------------------------------"); } } } /// \brief Updates a password record. -void updatePassword(privacy::vector &passwords, std::vector &strengths) { +void updatePassword(privacy::vector &passwords, std::vector &strengths) { if (passwords.empty()) [[unlikely]] { // There is nothing to update - printColor("No passwords saved yet.", 'r', true, std::cerr); + printColoredErrorln('r', "No passwords saved yet."); return; } privacy::string site{getResponseStr("Enter the name of the site/app to update: ")}; if (site.empty()) { - printColor("\nThe site/app name cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "\nThe site/app name cannot be blank."); return; } @@ -271,9 +270,9 @@ void updatePassword(privacy::vector &passwords, std::vector &passwords, std::vector(*it) != username) { - printColor("No such username as '", 'r', false, std::cerr); - printColor(username, 'y', false, std::cerr); - printColor("' under ", 'r', false, std::cerr); - printColor(site, 'c', true, std::cerr); + printColoredError('r', "No such username as '"); + printColoredError('y', "{}", username); + printColoredError('r', "' under "); + printColoredErrorln('c', "{}", site); return; } @@ -304,21 +303,20 @@ void updatePassword(privacy::vector &passwords, std::vector(match)) { std::cerr << "Username already exists for this site. Try again later." << std::endl; - return; } } } const privacy::string newPassword{ - getSensitiveInfo("Enter the new password (Leave blank to keep the current one): ") + getSensitiveInfo("Enter the new password (Leave blank to keep the current one): ") }; // Warn if the password is weak if (!newPassword.empty() && !isPasswordStrong(newPassword)) { - printColor( - "The password you entered is weak! A password should have at least 8 characters \nand include at least an " - "uppercase character, a lowercase, a punctuator, \nand a digit.", 'y', true); - printColor("Please consider using a stronger one.", 'r', true); + printColoredOutputln('y', + "The password you entered is weak! A password should have at least 8 characters \nand include at least an " + "uppercase character, a lowercase, a punctuator, \nand a digit."); + printColoredOutputln('r', "Please consider using a stronger one."); } // Update the record @@ -331,28 +329,28 @@ void updatePassword(privacy::vector &passwords, std::vector &passwords, std::vector &strengths) { +void deletePassword(privacy::vector &passwords, std::vector &strengths) { if (passwords.empty()) { - printColor("No passwords saved yet.", 'r', true, std::cerr); + printColoredErrorln('r', "No passwords saved yet."); return; } privacy::string site{getResponseStr("Enter the name of the site/app to delete: ")}; if (site.empty()) { - printColor("The site/app name cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "The site/app name cannot be blank."); return; } @@ -374,35 +372,37 @@ void deletePassword(privacy::vector &passwords, std::vector 1) { std::cout << "Found the following usernames for " << std::quoted(site) << ":\n"; for (const auto &[_, username, pass]: matches) - printColor(username.empty() - ? "'' [no username, reply with a blank to select]" - : username, 'c', true); + printColoredOutputln('c', "{}", username.empty() + ? "'' [no username, reply with a blank to select]" + : username); privacy::string username{ - getResponseStr("\nEnter one of the above usernames to delete (Enter \"All\" to delete all):")}; + getResponseStr("\nEnter one of the above usernames to delete (Enter \"All\" to delete all):") + }; // Update the iterator it = std::ranges::lower_bound(matches, std::tie(site, username, std::ignore), [](const auto &tuple1, const auto &tuple2) { return comparator(tuple1, tuple2); }); - if (it == matches.end() || std::get<1>(*it) != username) { // the entered username is incorrect + if (it == matches.end() || std::get<1>(*it) != username) { + // the entered username is incorrect // If the entered username is 'All', delete all the records under the site if (username == "All") { passwords.erase(matches.begin(), matches.end()); - printColor("All records under ", 'g'); - printColor(site, 'c'); - printColor(" deleted successfully.", 'g', true); + printColoredOutput('g', "All records under "); + printColoredOutput('c', "{}", site); + printColoredOutputln('g', " deleted successfully."); // Recompute strengths computeStrengths(passwords, strengths); return; } - printColor("No such username as '", 'r', false, std::cerr); - printColor(username, 'y', false, std::cerr); - printColor("' under ", 'r', false, std::cerr); - printColor(site, 'c', true, std::cerr); + printColoredError('r', "No such username as '"); + printColoredError('y', "{}", username); + printColoredError('r', "' under "); + printColoredErrorln('c', "{}", site); return; } @@ -419,29 +419,29 @@ void deletePassword(privacy::vector &passwords, std::vector &passwords, std::vector &) { +void searchPasswords(privacy::vector &passwords, std::vector &) { if (passwords.empty()) [[unlikely]] { // There is nothing to search - printColor("No passwords saved yet.", 'r', true, std::cerr); + printColoredErrorln('r', "No passwords saved yet."); return; } privacy::string query{getResponseStr("Enter the name of the site/app: ")}; // The query must be non-empty if (query.empty()) { - printColor("\nThe search query cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "\nThe search query cannot be blank."); return; } @@ -455,13 +455,13 @@ void searchPasswords(privacy::vector &passwords, std::vector(el))); - printColor("------------------------------------------------------", 'm', true); + printColoredOutputln('m', "------------------------------------------------------"); } } else { - printColor(std::format("No matches found for '{}'", query), 'r', true); + printColoredErrorln('r', "No matches found for '{}'", query); // Fuzzy-match the query against the site names const FuzzyMatcher matcher(constPasswordsView | std::ranges::views::elements<0>); @@ -470,9 +470,9 @@ void searchPasswords(privacy::vector &passwords, std::vector &passwords, std::vector bool { return std::get<0>(lhs) < std::get<0>(rhs); }); !matched.empty()) [[likely]] { - printColor("-----------------------------------------------------", 'w', true); + printColoredOutputln('w', "-----------------------------------------------------"); for (const auto &pass: matched) { printPasswordDetails(pass, isPasswordStrong(std::get<2>(pass))); - printColor("-----------------------------------------------------", 'w', true); + printColoredOutputln('w', "-----------------------------------------------------"); } } - } else printColor("Sorry, '" + query + "' not found.", 'r', true); + } else printColoredErrorln('r', "Sorry, '{}' not found.", query); } else if (!fuzzyMatched.empty()) { // multiple matches - printColor("Did you mean one of these?:", 'b', true); + printColoredOutputln('b', "Did you mean one of these?:"); // Print all the matches for (const auto &el: fuzzyMatched) { - printColor(el, 'g', true); + printColoredOutput('g', "{}", el); std::cout << "---------------------------------------" << std::endl; } } @@ -500,13 +500,13 @@ void searchPasswords(privacy::vector &passwords, std::vector &passwords, std::vector &strengths) { +void importPasswords(privacy::vector &passwords, std::vector &strengths) { const string fileName = getResponseStr("Enter the path to the csv file: "); - privacy::vector imports{importCsv(fileName)}; + privacy::vector imports{importCsv(fileName)}; if (imports.empty()) { - printColor("No passwords imported.", 'y', true); + printColoredOutputln('y', "No passwords imported."); return; } @@ -523,7 +523,7 @@ void importPasswords(privacy::vector &passwords, std::vector duplicates; + privacy::vector duplicates; duplicates.reserve(imports.size()); // Find the passwords that already exist in the database @@ -532,19 +532,19 @@ void importPasswords(privacy::vector &passwords, std::vector recordsUnion; + privacy::vector recordsUnion; recordsUnion.reserve(passwords.size() + imports.size()); bool overwrite{true}; // If there are duplicates, ask the user if they want to overwrite them if (!duplicates.empty()) { - printColor("Warning: The following passwords already exist in the database:", 'y', true); + printColoredOutputln('y', "Warning: The following passwords already exist in the database:"); for (const auto &duplicate: duplicates) { printPasswordDetails(duplicate, isPasswordStrong(std::get<2>(duplicate))); - printColor("-------------------------------------------------", 'm', true); + printColoredOutputln('m', "-------------------------------------------------"); } - printColor("Do you want to overwrite/update them? (y/n): ", 'b'); + printColoredOutput('b', "Do you want to overwrite/update them? (y/n): "); overwrite = validateYesNo(); } @@ -564,7 +564,7 @@ void importPasswords(privacy::vector &passwords, std::vector &passwords, std::vector &passwords, std::vector &) { +void exportPasswords(privacy::vector &passwords, std::vector &) { auto &&constPasswordsView = std::as_const(passwords); if (constPasswordsView.empty()) [[unlikely]] { - printColor("No passwords saved yet.", 'r', true, std::cerr); + printColoredOutputln('r', "No passwords saved yet."); return; } const string fileName = getResponseStr("Enter the path to save the file (leave blank for default): "); // Export the passwords to a csv file - if (const bool exported = fileName.empty() ? exportCsv(constPasswordsView) : exportCsv(constPasswordsView, fileName); - exported) - [[likely]] - // Warn the user about the security risk - printColor("WARNING: The exported file contains all your passwords in plain text." - "\nPlease delete it securely after use.", 'r', true); - else printColor("Passwords not exported.", 'r', true, std::cerr); + if (const bool exported = fileName.empty() + ? exportCsv(constPasswordsView) + : exportCsv(constPasswordsView, fileName); exported) [[likely]] + // Warn the user about the security risk + printColoredOutputln('r', "WARNING: The exported file contains all your passwords in plain text." + "\nPlease delete it securely after use."); + else printColoredErrorln('r', "Passwords not exported."); } /// \brief Analyzes the saved passwords for weak passwords and password reuse. -void analyzePasswords(privacy::vector &passwords, std::vector &strengths) { +void analyzePasswords(privacy::vector &passwords, std::vector &strengths) { if (passwords.empty()) { - printColor("No passwords to analyze.", 'r', true); + printColoredOutputln('r', "No passwords to analyze."); return; } @@ -618,7 +618,7 @@ void analyzePasswords(privacy::vector &passwords, std::vector< std::cout << "Analyzing passwords..." << std::endl; // Scan for weak passwords - privacy::vector weakPasswords; + privacy::vector weakPasswords; weakPasswords.reserve(total); for (std::size_t i = 0; i < passwords.size(); ++i) { @@ -628,8 +628,7 @@ void analyzePasswords(privacy::vector &passwords, std::vector< // Check for reused passwords std::unordered_map > passwordMap; - for (const auto &[site, _, password] : constPasswordsView) { - + for (const auto &[site, _, password]: constPasswordsView) { // Add the site to the set of sites that use the password passwordMap[password].insert(site); } @@ -637,16 +636,15 @@ void analyzePasswords(privacy::vector &passwords, std::vector< // Print the weak passwords auto weak{weakPasswords.size()}; if (!weakPasswords.empty()) [[likely]] { - printColor(std::format("Found {} account{} with weak passwords:", weak, weak == 1 ? "" : "s"), 'r', true); - printColor("------------------------------------------------------", 'r', true); + printColoredOutputln('r', "Found {} account{} with weak passwords:", weak, weak == 1 ? "" : "s"); + printColoredErrorln('r', "------------------------------------------------------"); for (const auto &password: weakPasswords) { printPasswordDetails(password); - printColor("------------------------------------------------------", 'r', true); + printColoredErrorln('r', "------------------------------------------------------"); } - printColor(std::format("Please change the weak passwords above. " - "\nYou can use the 'generate password' option to generate strong passwords.\n"), 'r', - true); - } else printColor("No weak passwords found. Keep it up!\n", 'g', true); + printColoredOutputln('r', "Please change the weak passwords above. " + "\nYou can use the 'generate password' option to generate strong passwords."); + } else printColoredOutputln('g', "No weak passwords found. Keep it up!"); // Find reused passwords using PasswordSites = std::pair >; @@ -661,11 +659,11 @@ void analyzePasswords(privacy::vector &passwords, std::vector< // Print reused passwords in descending order of counts std::size_t reused{0}; for (const auto &[count, password_sites]: countMap) { - printColor("Password '", 'y'); - printColor(password_sites.first, 'r'); - printColor(std::format("' is reused on {} sites:", count), 'y', true); + printColoredOutput('y', "Password '"); + printColoredOutput('r', "{}", password_sites.first); + printColoredOutputln('y', "' is reused on {} sites:", count); for (const auto &site: password_sites.second) - printColor(site + "\n", 'm'); + printColoredOutputln('m', "{}", site); std::cout << std::endl; ++reused; @@ -673,22 +671,21 @@ void analyzePasswords(privacy::vector &passwords, std::vector< // Print summary if (reused) { - printColor(std::format("{} password{} been reused.", reused, - reused == 1 ? " has" : "s have"), 'r', true); - } else printColor("Nice!! No password reuse detected.", 'g', true); + printColoredOutputln('r', "{} password{} been reused.", reused, + reused == 1 ? " has" : "s have"); + } else printColoredOutputln('g', "Nice!! No password reuse detected."); - printColor(std::format("{} use unique passwords to minimize the impact of their compromise.", - reused ? "Please" : "Always"), reused ? 'r' : 'c', true); + printColoredOutputln(reused ? 'r' : 'c', "{} use unique passwords to minimize the impact of their compromise.", + reused ? "Please" : "Always"); // Print the statistics std::cout << "\nTotal passwords: " << total << std::endl; if (weak > 0) [[likely]] { const char col{std::cmp_greater(weak, total / 4) ? 'r' : 'y'}; - printColor(std::format("{}% of your passwords are weak.", - std::round(static_cast(weak) / static_cast(total) * 100 * 100) / - 100), col, true); - } else printColor("All your passwords are strong. Keep it up!", 'g', true); + printColoredOutputln(col, "{}% of your passwords are weak.", + std::round(static_cast(weak) / static_cast(total) * 100 * 100) / 100); + } else printColoredOutputln('g', "All your passwords are strong. Keep it up!"); } /// \brief A simple, minimalistic password manager. @@ -718,7 +715,7 @@ void passwordManager() { } } - privacy::vector passwords; + privacy::vector passwords; if (!newSetup) { // preprocess the passwordFile @@ -732,16 +729,15 @@ void passwordManager() { encryptionKey = getSensitiveInfo("Enter the primary password: "); isCorrect = verifyPassword(encryptionKey, pwHash); if (!isCorrect && attempts < 2) - printColor("Wrong password, please try again.", 'r', true, std::cerr); + printColoredErrorln('r', "Wrong password, please try again."); } while (!isCorrect && ++attempts < 3); // If the password is still incorrect, exit - if (!isCorrect) { + if (!isCorrect) throw std::runtime_error("3 incorrect password attempts."); - } // Load the saved passwords - printColor("Decrypting passwords...", 'c', true); + printColoredOutputln('c', "Decrypting passwords..."); passwords = loadPasswords(passwordFile, encryptionKey); } @@ -757,16 +753,16 @@ void passwordManager() { } // A map of choices and their corresponding functions - std::unordered_map &, std::vector &)> choices = { - {1, addPassword}, - {2, updatePassword}, - {3, deletePassword}, - {4, viewAllPasswords}, - {5, searchPasswords}, - {6, generatePassword}, - {7, analyzePasswords}, - {8, importPasswords}, - {9, exportPasswords} + std::unordered_map &, std::vector &)> choices = { + {1, addPassword}, + {2, updatePassword}, + {3, deletePassword}, + {4, viewAllPasswords}, + {5, searchPasswords}, + {6, generatePassword}, + {7, analyzePasswords}, + {8, importPasswords}, + {9, exportPasswords} }; // A fast, lightweight random number generator std::minstd_rand gen(std::random_device{}()); // seed the generator @@ -775,9 +771,9 @@ void passwordManager() { while (true) { // Colors to use for the menu constexpr auto colors = "rgbymcw"; - auto color = colors[dist(gen)]; + const auto color = colors[dist(gen)]; - printColor("-------------------------------------------", color, true); + printColoredOutputln(color, "-------------------------------------------"); std::cout << "1. Add new password\n"; std::cout << "2. Update password\n"; std::cout << "3. Delete password\n"; @@ -789,7 +785,7 @@ void passwordManager() { std::cout << "9. Export passwords\n"; std::cout << "10. Change the primary Password\n"; std::cout << "11. Save and Exit\n"; - printColor("-------------------------------------------", color, true); + printColoredOutputln(color, "-------------------------------------------"); try { int choice = getResponseInt("Enter your choice: "); @@ -798,13 +794,13 @@ void passwordManager() { iter->second(passwords, passwordStrength); else if (choice == 10) { if (changeMasterPassword(encryptionKey)) - printColor("Master password changed successfully.", 'g', true); - else printColor("Master password not changed.", 'r', true, std::cerr); + printColoredOutputln('g', "Primary password changed successfully."); + else printColoredErrorln('r', "Primary password not changed."); } else if (choice == 11) break; - else printColor("Invalid choice!", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice!"); } catch (const std::exception &ex) { - printColor(ex.what(), 'r', true, std::cerr); + printColoredErrorln('r', "{}" ,ex.what()); } catch (...) { throw std::runtime_error("An error occurred."); } } @@ -816,12 +812,12 @@ void passwordManager() { if (const auto home{getHomeDir()}; fs::exists(home)) fs::create_directory(home + "/.privacyShield", home, ec); if (ec) { - printColor(std::format("Failed to create '{}': ", DefaultPasswordFile), 'y', false, std::cerr); - printColor(ec.message(), 'r', true, std::cerr); + printColoredError('y', "Failed to create '{}': ", DefaultPasswordFile); + printColoredErrorln('r', "{}", ec.message()); } } // Save the passwords if (savePasswords(passwords, DefaultPasswordFile, encryptionKey)) - printColor("Passwords saved successfully.", 'g', true); - else printColor("Passwords not saved!", 'r', true, std::cerr); + printColoredOutputln('g', "Passwords saved successfully."); + else printColoredErrorln('r', "Passwords not saved!"); } diff --git a/src/passwordManager/passwords.cpp b/src/passwordManager/passwords.cpp index b259854..4592d96 100644 --- a/src/passwordManager/passwords.cpp +++ b/src/passwordManager/passwords.cpp @@ -159,7 +159,7 @@ encryptDecryptRange(privacy::vector &passwords, const privacy:: key); } } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); std::exit(1); } } @@ -194,7 +194,7 @@ encryptDecryptRangeAllFields(privacy::vector &passwords, const : decryptString(std::string{std::get<2>(passwords[i])}, key); } } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); std::exit(1); } } @@ -283,7 +283,7 @@ bool savePasswords(privacy::vector &passwords, const std::strin file << "PLEASE DO NOT EDIT THIS FILE!" << std::endl; file << hashPassword(encryptionKey) << std::endl; - printColor("Encrypting your passwords...", 'c', true); + printColoredOutputln('c', "Encrypting your passwords..."); // Encrypt the password field with Serpent encryptDecryptConcurrently(passwords, encryptionKey, true, false); @@ -303,7 +303,7 @@ bool savePasswords(privacy::vector &passwords, const std::strin // Rename the temporary file to the original file fs::rename(tempFile, filePath, ec); - if (ec) printColor(ec.message(), 'r', true, std::cerr); + if (ec) printColoredErrorln('r', "{}", ec.message()); return !ec; } @@ -395,7 +395,7 @@ bool changeMasterPassword(privacy::string &primaryPassword) { // Verify that the new password is correct if (const privacy::string newPassword2{getSensitiveInfo("Enter the new primary password again: ")}; !verifyPassword(newPassword2, hashPassword(newPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, - crypto_pwhash_MEMLIMIT_INTERACTIVE))) { + crypto_pwhash_MEMLIMIT_INTERACTIVE))) { std::cerr << "Passwords do not match." << std::endl; return false; @@ -425,11 +425,11 @@ std::pair initialSetup() noexcept { int count{0}; while (!isPasswordStrong(pass) && ++count < 3) { const bool last{count == 2}; - printColor(last - ? "Last chance: " - : "Weak password! The password must be at least 8 characters long and include \nat least an" - " uppercase character, a lowercase, a punctuator, and a digit.", last ? 'r' : 'y', - !last); + printColoredOutput(last ? 'r' : 'y', "{}", last + ? "Last chance: " + : "Weak password! The password must be at least 8 characters long and include \nat least an" + " uppercase character, a lowercase, a punctuator, and a digit."); + if (last) std::cout << std::endl; pass = getSensitiveInfo(last ? "" : "Please enter a stronger password: "); } @@ -511,7 +511,7 @@ bool exportCsv(const privacy::vector &records, const std::strin // Check if the file path is valid if (!fs::path(filepath).has_filename()) { - printColor(std::format("Invalid file path: {}", filePath), 'r', true, std::cerr); + printColoredErrorln('r', "Invalid file path: {}", filePath); return false; } @@ -526,7 +526,7 @@ bool exportCsv(const privacy::vector &records, const std::strin if (exists(filepath, ec)) { // Check if the file is a regular file if (!is_regular_file(filepath)) [[unlikely]] { - printColor(std::format("The destination file ({}) is not a regular file.", filePath), 'r', true, std::cerr); + printColoredErrorln('r', "The destination file ({}) is not a regular file.", filePath); return false; } @@ -548,8 +548,7 @@ bool exportCsv(const privacy::vector &records, const std::strin // Open the file for writing std::ofstream file(filepath); if (!file) { - printColor(std::format("Failed to open the destination file ({}) for writing.", filePath), - 'r', true, std::cerr); + printColoredErrorln('r', "Failed to open the destination file ({}) for writing.", filePath); return false; } @@ -562,8 +561,8 @@ bool exportCsv(const privacy::vector &records, const std::strin file.close(); // Notify the user that the export was successful - printColor("Export successful. The file was saved as ", 'g'); - printColor(filepath, 'c', true); + printColoredOutput('g', "Export successful. The file was saved as "); + printColoredOutputln('c', "{}", filepath.string()); return true; } diff --git a/src/privacyTracks/privacyTracks.cppm b/src/privacyTracks/privacyTracks.cppm index 460b8b0..7bb51b2 100644 --- a/src/privacyTracks/privacyTracks.cppm +++ b/src/privacyTracks/privacyTracks.cppm @@ -46,7 +46,7 @@ enum class Browser : std::uint_fast8_t { inline void handleFileError(std::error_code &ec, const std::string_view context = "", const std::string_view path = "") noexcept { if (ec) { - printColor(std::format("Error {} {}: {}", context, path, ec.message()), 'r', true, std::cerr); + printColoredErrorln('r', "Error {} {}: {}", context, path, ec.message()); ec.clear(); } } @@ -59,7 +59,7 @@ std::uint_fast8_t detectBrowsers(const std::string_view pathEnv) { // Check if the passed string is empty if (pathEnv.empty()) { - printColor("PATH environment variable not found.", 'r', true, std::cerr); + printColoredErrorln('r', "PATH environment variable not found."); return detectedBrowsers; } @@ -117,7 +117,7 @@ std::uint_fast8_t detectBrowsers() { if (const auto pathEnv = getEnv("PATH"); pathEnv) return detectBrowsers(*pathEnv); - printColor("PATH environment variable not found.", 'r', true, std::cerr); + printColoredErrorln('r', "PATH environment variable not found."); return 0; } @@ -126,7 +126,7 @@ std::uint_fast8_t detectBrowsers() { /// \return true if successful, false otherwise. bool clearFirefoxTracks(const std::string_view configDir) { if (!fs::exists(configDir)) { - printColor("Firefox config directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Firefox config directory not found."); return false; } @@ -147,7 +147,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { if (!defaultProfileDirs.empty()) { std::cout << "Deleting cookies and history for the following default profiles:" << std::endl; for (const auto &profile: defaultProfileDirs) { - printColor(profile.filename().string(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); // Clearing cookies fs::remove(profile / "cookies.sqlite", ec); handleFileError(ec, "deleting", (profile / "cookies.sqlite").string()); @@ -156,7 +156,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { fs::remove(profile / "places.sqlite", ec); handleFileError(ec, "deleting", (profile / "places.sqlite").string()); } - } else printColor("No default profiles found.", 'r', true); + } else printColoredErrorln('r', "No default profiles found."); // Treat the other directories as profiles std::vector profileDirs; @@ -191,7 +191,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { handleFileError(ec, "deleting", entry.path().string()); else { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; alreadyCounted = true; } @@ -199,7 +199,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { if (entry.path().filename() == "places.sqlite") { if (!alreadyCounted) { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; } fs::remove(entry.path(), ec); @@ -210,9 +210,11 @@ bool clearFirefoxTracks(const std::string_view configDir) { } } } - printColor(nonDefaultProfiles - ? std::format("Deleted cookies and history for {} non-default profiles.", nonDefaultProfiles) - : "Non-default profiles not found.", nonDefaultProfiles ? 'g' : 'r', true); + printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", nonDefaultProfiles + ? std::format( + "Deleted cookies and history for {} non-default profiles.", + nonDefaultProfiles) + : "Non-default profiles not found."); return true; } @@ -222,7 +224,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { /// \return true if successful, false otherwise. bool clearChromiumTracks(const std::string_view configDir) { if (!fs::exists(configDir)) { - printColor("Config directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Chromium config directory not found."); return false; } @@ -252,7 +254,7 @@ bool clearChromiumTracks(const std::string_view configDir) { // Clearing history fs::remove(defaultProfileDir / "History", ec); handleFileError(ec, "deleting", (defaultProfileDir / "History").string()); - } else printColor("Default profile directory not found.", 'r', true, std::cerr); + } else printColoredErrorln('r', "Default profile directory not found."); // Find other profile directories std::vector profileDirs; @@ -287,7 +289,7 @@ bool clearChromiumTracks(const std::string_view configDir) { handleFileError(ec, "deleting", entry.path().string()); else { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; alreadyCounted = true; } @@ -296,7 +298,7 @@ bool clearChromiumTracks(const std::string_view configDir) { if (entry.path().filename() == "History") { if (!alreadyCounted) { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; } fs::remove(entry.path(), ec); @@ -307,9 +309,11 @@ bool clearChromiumTracks(const std::string_view configDir) { } } } - printColor(nonDefaultProfiles - ? std::format("Deleted cookies and history for {} non-default profiles.", nonDefaultProfiles) - : "Non-default profiles not found.", nonDefaultProfiles ? 'g' : 'r', true); + printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", nonDefaultProfiles + ? std::format( + "Deleted cookies and history for {} non-default profiles.", + nonDefaultProfiles) + : "Non-default profiles not found."); return true; } @@ -322,7 +326,7 @@ bool clearOperaTracks(const std::string_view profilePath) { // Check if the Opera config directory exists if (!fs::exists(profilePath)) { - printColor("Opera config directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Opera config directory not found."); return false; } @@ -363,7 +367,7 @@ bool clearChromiumTracks() { #elif __APPLE__ return clearChromiumTracks(getHomeDir() + "/Library/Application Support/Chromium"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -376,7 +380,7 @@ bool clearChromeTracks() { #elif __APPLE__ return clearChromiumTracks(getHomeDir() + "/Library/Application Support/Google/Chrome"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -389,7 +393,7 @@ bool clearOperaTracks() { #elif __APPLE__ return clearOperaTracks(getHomeDir() + "/Library/Application Support/com.operasoftware.Opera"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -400,7 +404,7 @@ bool clearSafariTracks() { #if __APPLE__ const std::string cookiesPath = getHomeDir() + "/Library/Cookies"; if (!fs::exists(cookiesPath)) { - printColor("Safari cookies directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Safari cookies directory not found."); return false; } @@ -416,7 +420,7 @@ bool clearSafariTracks() { const std::string historyPath = getHomeDir() + "/Library/Safari"; if (!fs::exists(historyPath)) { - printColor("Safari history directory not found.", 'r', true, std::cerr); + printColoredErrorln('c', "Safari history directory not found."); return false; } @@ -434,7 +438,8 @@ bool clearSafariTracks() { return true; #else - printColor("Safari is only available on macOS.", 'r', true, std::cerr); + printColoredErrorln('r', "Safari is only available on macOS."); + return false; #endif } @@ -447,7 +452,7 @@ bool clearFirefoxTracks() { #elif __APPLE__ return clearFirefoxTracks(getHomeDir() + "/Library/Application Support/Firefox"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -461,40 +466,41 @@ bool clearTracks(const std::uint_fast8_t &browsers) { if (browsers & std::to_underlying(Browser::Firefox)) { std::cout << "Clearing Firefox tracks..." << std::endl; - ret = clearFirefoxTracks(); - printColor(ret ? "Firefox tracks cleared successfully." : "Failed to clear Firefox tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearFirefoxTracks()) + printColoredOutputln('g', "Firefox tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Firefox tracks."); } if (browsers & std::to_underlying(Browser::Chrome)) { std::cout << "\nClearing Chrome tracks..." << std::endl; - ret = clearChromeTracks(); - printColor(ret ? "Chrome tracks cleared successfully." : "Failed to clear Chrome tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearChromeTracks()) + printColoredOutputln('g', "Chrome tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Chrome tracks."); } if (browsers & std::to_underlying(Browser::Chromium)) { std::cout << "\nClearing Chromium tracks..." << std::endl; - ret = clearChromiumTracks(); - printColor(ret ? "Chromium tracks cleared successfully." : "Failed to clear Chromium tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearChromiumTracks()) + printColoredOutputln('g', "Chromium tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Chromium tracks."); } if (browsers & std::to_underlying(Browser::Opera)) { std::cout << "\nClearing Opera tracks..." << std::endl; - ret = clearOperaTracks(); - printColor(ret ? "Opera tracks cleared successfully." : "Failed to clear Opera tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearOperaTracks()) + printColoredOutputln('g', "Opera tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Opera tracks."); } if (browsers & std::to_underlying(Browser::Safari)) { #if __APPLE__ std::cout << "Clearing Safari tracks..." << std::endl; - ret = clearSafariTracks(); - printColor(ret ? "Safari tracks cleared successfully." : "Failed to clear Safari tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearSafariTracks()) + printColoredOutputln('g', "Safari tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Safari tracks."); + #else - printColor("\nSafari is only available on macOS.", 'r', true, std::cerr); + printColoredErrorln('r', "\nSafari is only available on macOS."); ret = false; #endif } @@ -509,31 +515,31 @@ export void clearPrivacyTracks() { const std::uint_fast8_t browsers = detectBrowsers(); if (browsers == 0) [[unlikely]] { - printColor("No supported browsers found.", 'r', true, std::cerr); + printColoredErrorln('r', "No supported browsers found."); return; } - printColor("Supported browsers found:", 'y', true); + printColoredOutputln('y', "Supported browsers found:"); if (browsers & std::to_underlying(Browser::Firefox)) - printColor("Firefox", 'c', true); + printColoredOutputln('c', "Firefox"); if (browsers & std::to_underlying(Browser::Chrome)) - printColor("Chrome", 'c', true); + printColoredOutputln('c', "Chrome"); if (browsers & std::to_underlying(Browser::Chromium)) - printColor("Chromium", 'c', true); + printColoredOutputln('c', "Chromium"); if (browsers & std::to_underlying(Browser::Opera)) - printColor("Opera", 'c', true); + printColoredOutputln('c', "Opera"); if (browsers & std::to_underlying(Browser::Safari)) - printColor("Safari", 'c', true); + printColoredOutputln('c', "Safari"); - printColor("\nAll the cookies and browsing history of the above browsers will be deleted.", 'r', true); - printColor("Continue? (y/n): ", 'c'); + printColoredOutputln('r', "\nAll the cookies and browsing history of the above browsers will be deleted."); + printColoredOutput('c', "Continue? (y/n): "); if (validateYesNo()) { const auto cleared{clearTracks(browsers)}; - printColor(cleared ? "\nAll tracks cleared successfully.\n" : "\nFailed to clear all tracks.\n", - cleared ? 'g' : 'r', true, cleared ? std::cout : std::cerr); - } else printColor("Aborted.", 'r', true); + printColoredOutputln(cleared ? 'g' : 'r', "{}", + cleared ? "\nAll tracks cleared successfully." : "\nFailed to clear all tracks."); + } else printColoredOutputln('r', "Aborted."); } From 8466504d307dee4351dffcec11bb44715e69c548 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 27 May 2024 01:07:42 +0300 Subject: [PATCH 15/99] Update IDE configuration --- .idea/misc.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.idea/misc.xml b/.idea/misc.xml index 79b3c94..0b76fe5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ + + \ No newline at end of file From 351c9293be3c0c1dac00ebd9bd93ae7ee3ac3723 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 27 May 2024 23:10:37 +0300 Subject: [PATCH 16/99] Bugfix: failure in encryption/decryption of files --- src/encryption/encryptDecrypt.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index eaffc62..990d8d4 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -177,8 +177,8 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope } // Check if the input and output files are the same - if (equivalent(inFile, outFile)) - throw std::runtime_error("The input and output files are the same."); + if (std::error_code ec; exists(outFile, ec) && equivalent(inFile, outFile)) + throw std::runtime_error("The input and the output file both refer to the same object."); // Check if there is enough space on the disk to save the output file. const auto availableSpace = getAvailableSpace(weakly_canonical(outFile)); From 0da0a1f1d2ddf45b7a434ec4132847724ecd1361 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 27 May 2024 23:11:28 +0300 Subject: [PATCH 17/99] Delay throwing of errors during hashing --- src/duplicateFinder/duplicateFinder.cppm | 31 ++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index 83a9616..5085702 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -49,8 +49,14 @@ struct FileInfo { std::string calculateBlake3(const std::string &filePath) { // Open the file std::ifstream file(filePath, std::ios::binary); - if (!file) - throw std::runtime_error(std::format("Failed to open '{}' for hashing.", filePath)); + if (!file) { + if (std::error_code ec; fs::exists(filePath, ec)) + throw std::runtime_error(std::format("Failed to open '{}' for hashing.", filePath)); + + printColoredError('b', "{} ", filePath); + printColoredErrorln('r', "existed during scan but was not found during hashing."); + return ""; + } // Initialize the BLAKE3 hasher blake3_hasher hasher; @@ -75,8 +81,13 @@ std::string calculateBlake3(const std::string &filePath) { /// \brief handles file i/o errors during low-level file operations. /// \param filename path to the file on which an error occurred. inline void handleAccessError(const std::string_view filename) { - if (errno) printColoredErrorln('r', "Skipping '{}': {}.", filename, std::strerror(errno)); - errno = 0; + if (errno) { + printColoredError('r', "Skipping "); + printColoredError('c', "{}", filename); + printColoredErrorln('r', ": {}", std::strerror(errno)); + + errno = 0; + } } /// \brief recursively traverses a directory and collects file information. @@ -91,7 +102,9 @@ void traverseDirectory(const std::string_view directoryPath, std::vector Date: Tue, 28 May 2024 14:17:35 +0300 Subject: [PATCH 18/99] Refactor code for style --- src/duplicateFinder/duplicateFinder.cppm | 4 ++-- src/encryption/encryptDecrypt.cpp | 6 +++--- src/passwordManager/passwordManager.cpp | 19 +++++++++++-------- src/passwordManager/passwordManager.cppm | 11 ++++++----- src/passwordManager/passwords.cpp | 17 +++++++++-------- src/privacyTracks/privacyTracks.cppm | 20 ++++++++++---------- src/secureAllocator.cppm | 7 ++----- src/utils/utils.cppm | 12 ++++++------ 8 files changed, 49 insertions(+), 47 deletions(-) diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index 5085702..a94a5d7 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -84,7 +84,7 @@ inline void handleAccessError(const std::string_view filename) { if (errno) { printColoredError('r', "Skipping "); printColoredError('c', "{}", filename); - printColoredErrorln('r', ": {}", std::strerror(errno)); + printColoredErrorln('r', ": {}", std::strerror(errno)); errno = 0; } @@ -240,7 +240,7 @@ export void duplicateFinder() { continue; } if (!exists(fileStatus)) { - printColoredError('c', "{}",dirPath); + printColoredError('c', "{}", dirPath); printColoredErrorln('r', " does not exist."); continue; } diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index 990d8d4..3d800b3 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -371,9 +371,9 @@ void encryptDecrypt() { continue; } } - printColoredOutput('g', "{}crypting ", pre); - printColoredOutput('g', "{}", canonical(inputPath).string()); - printColoredOutput('g', " with "); + printColoredOutput('g', "{}crypting '", pre); + printColoredOutput('m', "{}", canonical(inputPath).string()); + printColoredOutput('g', "' with "); printColoredOutput('c', "{}", algoDescription.find(cipher)->second); printColoredOutputln('g', "..."); diff --git a/src/passwordManager/passwordManager.cpp b/src/passwordManager/passwordManager.cpp index 4af77ed..069f1a6 100644 --- a/src/passwordManager/passwordManager.cpp +++ b/src/passwordManager/passwordManager.cpp @@ -271,8 +271,8 @@ void updatePassword(privacy::vector &passwords, std::vector &passwords, std::vectorsecond(passwords, passwordStrength); else if (choice == 10) { - if (changeMasterPassword(encryptionKey)) + if (changePrimaryPassword(encryptionKey)) printColoredOutputln('g', "Primary password changed successfully."); else printColoredErrorln('r', "Primary password not changed."); } else if (choice == 11) break; else printColoredErrorln('r', "Invalid choice!"); } catch (const std::exception &ex) { - printColoredErrorln('r', "{}" ,ex.what()); + printColoredErrorln('r', "{}", ex.what()); } catch (...) { throw std::runtime_error("An error occurred."); } } diff --git a/src/passwordManager/passwordManager.cppm b/src/passwordManager/passwordManager.cppm index 048f440..cabd2b9 100644 --- a/src/passwordManager/passwordManager.cppm +++ b/src/passwordManager/passwordManager.cppm @@ -27,29 +27,30 @@ import secureAllocator; using passwordRecords = std::tuple; -privacy::vector loadPasswords(std::string_view filePath, const privacy::string &decryptionKey); +privacy::vector loadPasswords(std::string_view filePath, const privacy::string &decryptionKey); -bool savePasswords(privacy::vector &passwords, std::string_view filePath, +bool savePasswords(privacy::vector &passwords, std::string_view filePath, const privacy::string &encryptionKey); bool isPasswordStrong(std::string_view password) noexcept; privacy::string generatePassword(int length); -bool changeMasterPassword(privacy::string &primaryPassword); +bool changePrimaryPassword(privacy::string &primaryPassword); std::pair initialSetup() noexcept; privacy::string getHash(std::string_view filePath); -privacy::vector importCsv(const std::string &filePath); +privacy::vector importCsv(const std::string &filePath); -bool exportCsv(const privacy::vector &records, std::string_view filePath = getHomeDir()); +bool exportCsv(const privacy::vector &records, std::string_view filePath = getHomeDir()); export { privacy::string hashPassword(const privacy::string &password, const std::size_t &opsLimit = crypto_pwhash_OPSLIMIT_SENSITIVE, const std::size_t &memLimit = crypto_pwhash_MEMLIMIT_SENSITIVE); + void passwordManager(); bool verifyPassword(const privacy::string &password, const privacy::string &storedHash); diff --git a/src/passwordManager/passwords.cpp b/src/passwordManager/passwords.cpp index 4592d96..66e65eb 100644 --- a/src/passwordManager/passwords.cpp +++ b/src/passwordManager/passwords.cpp @@ -367,14 +367,14 @@ privacy::vector loadPasswords(const std::string_view filePath, /// \brief Helps the user change the primary password. /// \param primaryPassword the current primary password. /// \return True if the password is changed successfully, else false. -bool changeMasterPassword(privacy::string &primaryPassword) { +bool changePrimaryPassword(privacy::string &primaryPassword) { const privacy::string oldPassword{getSensitiveInfo("Enter the current primary password: ")}; // Verify that the old password is correct - if (const auto masterHash = hashPassword(primaryPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, - crypto_pwhash_MEMLIMIT_INTERACTIVE); - !verifyPassword(oldPassword, masterHash)) { + if (const auto primaryHash = hashPassword(primaryPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE); + !verifyPassword(oldPassword, primaryHash)) { std::cerr << "Password verification failed." << std::endl; return false; } @@ -425,10 +425,11 @@ std::pair initialSetup() noexcept { int count{0}; while (!isPasswordStrong(pass) && ++count < 3) { const bool last{count == 2}; - printColoredOutput(last ? 'r' : 'y', "{}", last - ? "Last chance: " - : "Weak password! The password must be at least 8 characters long and include \nat least an" - " uppercase character, a lowercase, a punctuator, and a digit."); + printColoredOutput(last ? 'r' : 'y', "{}", + last + ? "Last chance: " + : "Weak password! The password must be at least 8 characters long and include \nat least an" + " uppercase character, a lowercase, a punctuator, and a digit."); if (last) std::cout << std::endl; pass = getSensitiveInfo(last ? "" : "Please enter a stronger password: "); } diff --git a/src/privacyTracks/privacyTracks.cppm b/src/privacyTracks/privacyTracks.cppm index 7bb51b2..708b2e1 100644 --- a/src/privacyTracks/privacyTracks.cppm +++ b/src/privacyTracks/privacyTracks.cppm @@ -210,11 +210,11 @@ bool clearFirefoxTracks(const std::string_view configDir) { } } } - printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", nonDefaultProfiles - ? std::format( - "Deleted cookies and history for {} non-default profiles.", - nonDefaultProfiles) - : "Non-default profiles not found."); + printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", + nonDefaultProfiles + ? std::format("Deleted cookies and history for {} non-default profiles.", + nonDefaultProfiles) + : "Non-default profiles not found."); return true; } @@ -309,11 +309,11 @@ bool clearChromiumTracks(const std::string_view configDir) { } } } - printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", nonDefaultProfiles - ? std::format( - "Deleted cookies and history for {} non-default profiles.", - nonDefaultProfiles) - : "Non-default profiles not found."); + printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", + nonDefaultProfiles + ? std::format("Deleted cookies and history for {} non-default profiles.", + nonDefaultProfiles) + : "Non-default profiles not found."); return true; } diff --git a/src/secureAllocator.cppm b/src/secureAllocator.cppm index c1f30a3..3201ed9 100644 --- a/src/secureAllocator.cppm +++ b/src/secureAllocator.cppm @@ -25,7 +25,6 @@ module; export module secureAllocator; export namespace privacy { - template /// \class Allocator /// \brief Custom allocator for STL containers, which locks and zeroizes memory. @@ -33,7 +32,6 @@ export namespace privacy { /// \details Adapted from https://en.cppreference.com/w/cpp/named_req/Allocator class Allocator { public: - [[maybe_unused]] typedef T value_type; /// Default constructor @@ -64,7 +62,7 @@ export namespace privacy { /// Deallocate memory [[maybe_unused]] constexpr void deallocate(T *p, std::size_t n) noexcept { - sodium_munlock(p, n * sizeof(T)); // Unlock and zeroize memory + sodium_munlock(p, n * sizeof(T)); // Unlock and zeroize memory ::operator delete(p); } }; @@ -88,5 +86,4 @@ export namespace privacy { using vector = std::vector>; using istringstream = std::basic_istringstream, Allocator >; - -} // namespace privacy +} // namespace privacy diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 0741182..65bee81 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -158,7 +158,6 @@ export { if (ColorConfig::getInstance().getSuppressColor()) std::cout << std::vformat(fmt.get(), std::make_format_args(args...)); else std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; - // else std::print("{}{}\033[0m", getColorCode(color), std::format(fmt, std::forward(args)...)); } /// \brief Prints colored output to the console and adds a newline at the end. @@ -170,9 +169,9 @@ export { void printColoredOutputln(const char color, std::format_string fmt, Args &&... args) { if (ColorConfig::getInstance().getSuppressColor()) std::cout << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; - else std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << - std::endl; - // else std::print("{}{}\033[0m\n", getColorCode(color), std::format(fmt, std::forward(args)...)); + else + std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << + std::endl; } /// \brief Prints colored error messages to the console. @@ -196,8 +195,9 @@ export { void printColoredErrorln(const char color, std::format_string fmt, Args &&... args) { if (ColorConfig::getInstance().getSuppressColor()) std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; - else std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << - std::endl; + else + std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << + std::endl; } std::vector base64Decode(std::string_view encodedData); From 081f7152c5094f91f373e81979e81a8c8f1c9fa3 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 00:59:42 +0300 Subject: [PATCH 19/99] Update the build scripts --- scripts/{buildscript.sh => build.sh} | 17 ++++++++--------- scripts/install-blake3.sh | 6 +++--- 2 files changed, 11 insertions(+), 12 deletions(-) rename scripts/{buildscript.sh => build.sh} (69%) diff --git a/scripts/buildscript.sh b/scripts/build.sh similarity index 69% rename from scripts/buildscript.sh rename to scripts/build.sh index f7f7e13..a695cad 100755 --- a/scripts/buildscript.sh +++ b/scripts/build.sh @@ -25,26 +25,25 @@ function check_dependencies() { } function install_dependencies() { - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" - add-apt-repository -y ppa:ubuntu-toolchain-r/ppa +# wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc +# add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" +# add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update - apt install -y unzip gcc-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev + apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev - # Install CMake 3.28.3 + # Install CMake 3.29.3 if dpkg -s "cmake" >/dev/null 2>&1; then apt remove -y --purge --auto-remove cmake fi - wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local + wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.29.3/cmake-3.29.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local - # Install Ninja 1.11 + # Install Ninja 1.12 if dpkg -s "ninja-build" >/dev/null 2>&1; then apt remove -y --purge --auto-remove ninja-build fi - wget -q "https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-linux.zip" - unzip ninja-linux.zip -d /usr/local/bin + wget -qO- "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" | unzip -d /usr/local/bin } function build_blake3() { diff --git a/scripts/install-blake3.sh b/scripts/install-blake3.sh index 9e81789..91c6f00 100755 --- a/scripts/install-blake3.sh +++ b/scripts/install-blake3.sh @@ -19,7 +19,7 @@ detect_os() { # Function to check root access check_root() { - [[ "$CURRENT_OS" != "macos" && "$EUID" -ne 0 ]] && error_exit "This script must be run as root." + [[ "$CURRENT_OS" == "linux" && "$EUID" -ne 0 ]] && error_exit "This script must be run as root." } get_number_of_processors() { @@ -36,9 +36,9 @@ install_blake3() { cd ~ || error_exit "Failed to change to home directory." # Download BLAKE3 and extract to current directory - wget -qO- https://github.com/BLAKE3-team/BLAKE3/archive/refs/tags/1.5.0.tar.gz | tar -xz -C . + wget -qO- https://github.com/BLAKE3-team/BLAKE3/archive/refs/tags/1.5.1.tar.gz | tar -xz -C . - cd BLAKE3-1.5.0/c || error_exit "Failed to navigate to BLAKE3/c directory." + cd BLAKE3-1.5.1/c || error_exit "Failed to navigate to BLAKE3/c directory." cmake -B build -DCMAKE_C_COMPILER="$C_COMPILER" -G Ninja || error_exit "Failed to run cmake." get_number_of_processors From 2e71db36b0c0b942c4d205dbcc37ca9f6af9715e Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 01:01:06 +0300 Subject: [PATCH 20/99] Fix a typo in the CMake config --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 71f7881..0ae6e91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ endif () list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") # Additional checks for the Debug build -if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang|AppleClang") +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") if (CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_options( -Wall From 098a2d4ddc77a539b8f619f19c65ed8888c305b7 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 01:01:43 +0300 Subject: [PATCH 21/99] Update GitHub workflow config --- .github/workflows/cmake-multi-platform.yml | 44 +++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 8794eed..426c953 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: - os: [ ubuntu-latest, macos-latest ] + os: [ ubuntu-24.04, macos-latest ] build_type: [ Debug, Release ] c_compiler: [ clang ] include: @@ -22,12 +22,12 @@ jobs: c_compiler: clang cpp_compiler: clang++ env: - LDFLAGS=: "-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib" - CPPFLAGS: "-I/usr/local/opt/llvm/include I/usr/local/opt/llvm/include/c++/v1" - LD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" - DYLD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" + LDFLAGS=: "-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++" + CPPFLAGS: "-I/opt/homebrew/opt/llvm/include -I/opt/homebrew/opt/llvm/include/c++/v1" + LD_LIBRARY_PATH: "/opt/homebrew/opt/llvm/lib" + DYLD_LIBRARY_PATH: "/opt/homebrew/opt/llvm/lib" - - os: ubuntu-latest + - os: ubuntu-24.04 c_compiler: clang cpp_compiler: clang++-18 @@ -43,10 +43,10 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install llvm cmake ninja gcc@13 libgcrypt openssl@3 readline libsodium - echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/usr/local/opt/gcc@13/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/usr/local/opt/gcc@13/lib/gcc/13:$PATH"' >> ~/.bash_profile + brew install llvm cmake ninja gcc libgcrypt openssl@3 readline libsodium + echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile + # echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile - uses: actions/checkout@v4 @@ -56,11 +56,11 @@ jobs: run: | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" - # Build project + # Build the project - name: Build PrivacyShield - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: | - sudo ./scripts/buildscript.sh + sudo ./scripts/build.sh - name: Install Blake3 if: matrix.os == 'macos-latest' @@ -70,17 +70,17 @@ jobs: - name: Configure CMake if: matrix.os == 'macos-latest' run: > - export LDFLAGS="-L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13"; - export CPPFLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22"; - export LD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; - export DYLD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; + # export LDFLAGS="-L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13"; + # export CPPFLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22"; + # export LD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; + # export DYLD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_CXX_COMPILER=/usr/local/opt/llvm/bin/clang++ - -DCMAKE_C_COMPILER=/usr/local/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22 -L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13 -stdlib=libstdc++" + -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ + -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang + -DCMAKE_CXX_FLAGS="-I/opt/homebrew/opt/gcc@14/include/c++/14 -I/opt/homebrew/opt/gcc@14/include/c++/14/x86_64-apple-darwin* -L/opt/homebrew/opt/gcc@14/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc@14/lib/gcc/14 -stdlib=libstdc++" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - -S ${{ github.workspace }} -G Ninja + -S ${{ github.workspace }} -G Xcode - name: Build if: matrix.os == 'macos-latest' @@ -93,7 +93,7 @@ jobs: cpack - name: Package - if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Release' + if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'Release' working-directory: ${{ steps.strings.outputs.build-output-dir }} run: | sudo cpack From 80cb5c296c64239df9fe08a7d8a352fe92025aa5 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 01:44:06 +0300 Subject: [PATCH 22/99] Figure out homebrew directories --- .github/workflows/cmake-multi-platform.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 426c953..4845ce1 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -43,9 +43,11 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install llvm cmake ninja gcc libgcrypt openssl@3 readline libsodium + brew install llvm ninja libsodium + brew reinstall gcc readline echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile + echo "Homebrew prefix: $HOMEBREW_PREFIX" # echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile - uses: actions/checkout@v4 From eb936658fb4eadfe7165af848ae0e37d828b6593 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 01:51:34 +0300 Subject: [PATCH 23/99] Disable restarting of services after an update --- scripts/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build.sh b/scripts/build.sh index a695cad..62e97e7 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -29,6 +29,7 @@ function install_dependencies() { # add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" # add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update + export NEEDRESTART_SUSPEND=1 apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev # Install CMake 3.29.3 From 790a30454034b31cc74105cfb2e4c9d38a12bcbd Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 01:59:38 +0300 Subject: [PATCH 24/99] Fix Ninja installation --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 62e97e7..e609545 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -44,7 +44,7 @@ function install_dependencies() { apt remove -y --purge --auto-remove ninja-build fi - wget -qO- "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" | unzip -d /usr/local/bin + wget -qO- "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" | funzip > /usr/local/bin/ninja } function build_blake3() { From b62034d04023173769b217a8f71ffd27156238a0 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:08:58 +0300 Subject: [PATCH 25/99] Update `Readline` search paths --- .github/workflows/cmake-multi-platform.yml | 2 +- CMakeModules/FindReadline.cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 4845ce1..afeca67 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -43,7 +43,7 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install llvm ninja libsodium + brew install llvm ninja brew reinstall gcc readline echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile diff --git a/CMakeModules/FindReadline.cmake b/CMakeModules/FindReadline.cmake index b789688..90bb198 100644 --- a/CMakeModules/FindReadline.cmake +++ b/CMakeModules/FindReadline.cmake @@ -69,7 +69,7 @@ if (NOT READLINE_FOUND AND APPLE) # Find library manually find_library(READLINE_LIBRARY REQUIRED NAMES libreadline.dylib libreadline.a - PATHS /usr/local/opt/readline/lib /usr/local/lib /opt/local/lib /usr/lib + PATHS /usr/local/opt/readline/lib /usr/local/lib /opt/local/lib /usr/lib /opt/homebrew/lib /opt/homebrew/Cellar/readline NO_DEFAULT_PATH ) From 773fb68fbf421850b5f09bf9717061ca6e34b8c4 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:17:30 +0300 Subject: [PATCH 26/99] Fix Ninja extraction --- .github/workflows/cmake-multi-platform.yml | 2 +- CMakeModules/FindReadline.cmake | 9 ++++++++- scripts/build.sh | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index afeca67..9a4f609 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -80,7 +80,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS="-I/opt/homebrew/opt/gcc@14/include/c++/14 -I/opt/homebrew/opt/gcc@14/include/c++/14/x86_64-apple-darwin* -L/opt/homebrew/opt/gcc@14/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc@14/lib/gcc/14 -stdlib=libstdc++" + -DCMAKE_CXX_FLAGS="-I/opt/homebrew/opt/gcc@14/include/c++/14 -I/opt/homebrew/opt/gcc@14/include/c++/14/* -L/opt/homebrew/opt/gcc@14/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc@14/lib/gcc/14 -stdlib=libstdc++" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Xcode diff --git a/CMakeModules/FindReadline.cmake b/CMakeModules/FindReadline.cmake index 90bb198..70df224 100644 --- a/CMakeModules/FindReadline.cmake +++ b/CMakeModules/FindReadline.cmake @@ -69,7 +69,14 @@ if (NOT READLINE_FOUND AND APPLE) # Find library manually find_library(READLINE_LIBRARY REQUIRED NAMES libreadline.dylib libreadline.a - PATHS /usr/local/opt/readline/lib /usr/local/lib /opt/local/lib /usr/lib /opt/homebrew/lib /opt/homebrew/Cellar/readline + PATHS + /usr/local/opt/readline/lib + /usr/local/lib + /opt/local/lib + /usr/lib + /opt/homebrew/lib + /opt/homebrew/opt/readline/lib + /opt/homebrew/Cellar/readline/*/lib NO_DEFAULT_PATH ) diff --git a/scripts/build.sh b/scripts/build.sh index e609545..10f3de3 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -44,7 +44,8 @@ function install_dependencies() { apt remove -y --purge --auto-remove ninja-build fi - wget -qO- "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" | funzip > /usr/local/bin/ninja + wget -qO- "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" + unzip ninja-linux.zip -d /usr/local/bin } function build_blake3() { From 512cd404897a16b75b5b0b5cbbff58efb55f4147 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:23:20 +0300 Subject: [PATCH 27/99] Fix Ninja extraction --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 10f3de3..8d12eac 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -44,7 +44,7 @@ function install_dependencies() { apt remove -y --purge --auto-remove ninja-build fi - wget -qO- "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" + wget -q "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" unzip ninja-linux.zip -d /usr/local/bin } From c9d0cc92528f624a2dcda6f79204d3c1e5277a39 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:26:10 +0300 Subject: [PATCH 28/99] Xcode does not support C++ modules yet --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 9a4f609..0cffc1c 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -82,7 +82,7 @@ jobs: -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang -DCMAKE_CXX_FLAGS="-I/opt/homebrew/opt/gcc@14/include/c++/14 -I/opt/homebrew/opt/gcc@14/include/c++/14/* -L/opt/homebrew/opt/gcc@14/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc@14/lib/gcc/14 -stdlib=libstdc++" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - -S ${{ github.workspace }} -G Xcode + -S ${{ github.workspace }} -G Ninja - name: Build if: matrix.os == 'macos-latest' From 92cc0c59ef21c0fc3fd81cfb2292b05a2311e26b Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:34:22 +0300 Subject: [PATCH 29/99] Update Readline search directories --- CMakeModules/FindReadline.cmake | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CMakeModules/FindReadline.cmake b/CMakeModules/FindReadline.cmake index 70df224..a458036 100644 --- a/CMakeModules/FindReadline.cmake +++ b/CMakeModules/FindReadline.cmake @@ -48,8 +48,15 @@ if (READLINE_FOUND AND NOT APPLE) # Find the actual location of the Readline library file find_library(READLINE_LIBRARY - NAMES libreadline.so libreadline.dylib libreadline.a + NAMES libreadline.so libreadline.a HINTS ${READLINE_LIBRARY_DIRS} + PATHS + /usr/lib + /usr/lib/* + /opt/local/lib + /opt/homebrew/lib + /opt/homebrew/opt/readline/lib + /opt/homebrew/Cellar/readline/*/lib ) # Set the imported location dynamically From 68579fefd45163cf884d4b9d6a75d7ab719ea644 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:43:35 +0300 Subject: [PATCH 30/99] Update Readline search directories It's annoying now --- CMakeModules/FindReadline.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeModules/FindReadline.cmake b/CMakeModules/FindReadline.cmake index a458036..a07eb80 100644 --- a/CMakeModules/FindReadline.cmake +++ b/CMakeModules/FindReadline.cmake @@ -52,7 +52,7 @@ if (READLINE_FOUND AND NOT APPLE) HINTS ${READLINE_LIBRARY_DIRS} PATHS /usr/lib - /usr/lib/* + /usr/lib/x86_64-linux-gnu /opt/local/lib /opt/homebrew/lib /opt/homebrew/opt/readline/lib From 4548b97df82fce56f40c7a23c0bddeb6b6c1360e Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:49:11 +0300 Subject: [PATCH 31/99] Update Readline search directories It's annoying now --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 8d12eac..905d0a2 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -30,7 +30,7 @@ function install_dependencies() { # add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update export NEEDRESTART_SUSPEND=1 - apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev + apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libreadline-dev libsodium23 libsodium-dev # Install CMake 3.29.3 if dpkg -s "cmake" >/dev/null 2>&1; then From f4864806dfc3cbe99c492f3beacf7e423e9388f6 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 02:58:35 +0300 Subject: [PATCH 32/99] Fix issues finding gcrypt --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 905d0a2..a2b0e3d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -30,7 +30,7 @@ function install_dependencies() { # add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update export NEEDRESTART_SUSPEND=1 - apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libreadline-dev libsodium23 libsodium-dev + apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libreadline8 libreadline-dev libsodium23 libsodium-dev # Install CMake 3.29.3 if dpkg -s "cmake" >/dev/null 2>&1; then From 892846976a136093c5eb44aebae367674bf78cd8 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 03:43:31 +0300 Subject: [PATCH 33/99] Check configurations --- .github/workflows/cmake-multi-platform.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 0cffc1c..888e412 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -47,8 +47,19 @@ jobs: brew reinstall gcc readline echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + + - name: Check configuration + run: | echo "Homebrew prefix: $HOMEBREW_PREFIX" - # echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + echo "PATH: $PATH" + echo "LDFLAGS: $LDFLAGS" + echo "CPPFLAGS: $CPPFLAGS" + echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" + echo "DYLD_LIBRARY_PATH: $DYLD_LIBRARY_PATH" + echo "GCC: $(which gcc-14)" + echo "GLIBC: $(ldd $(which gcc-14) | grep -i libc)" + echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" - uses: actions/checkout@v4 From dc68a7e7c22c8690a05ffda42b2edf2c3c806272 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 04:15:05 +0300 Subject: [PATCH 34/99] Check configurations --- .github/workflows/cmake-multi-platform.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 888e412..7a38fb4 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -43,8 +43,8 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install llvm ninja - brew reinstall gcc readline + brew install gcc readline ninja + brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile @@ -58,7 +58,8 @@ jobs: echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" echo "DYLD_LIBRARY_PATH: $DYLD_LIBRARY_PATH" echo "GCC: $(which gcc-14)" - echo "GLIBC: $(ldd $(which gcc-14) | grep -i libc)" + echo "GLIBC: $(otool -L $(which gcc-14) | grep -i libc)" + echo "Other Glibc: $(cat $(gcc -print-file-name=libc.so))" echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" - uses: actions/checkout@v4 From 22ebabe93d197fb4eb2a7ae54dbd2d3efe5df65b Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 04:20:33 +0300 Subject: [PATCH 35/99] Check configurations --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 7a38fb4..ac77f08 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -59,7 +59,7 @@ jobs: echo "DYLD_LIBRARY_PATH: $DYLD_LIBRARY_PATH" echo "GCC: $(which gcc-14)" echo "GLIBC: $(otool -L $(which gcc-14) | grep -i libc)" - echo "Other Glibc: $(cat $(gcc -print-file-name=libc.so))" + echo "Other Glibc: $(cat $(gcc -print-file-name=libc.dylib))" echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" - uses: actions/checkout@v4 From f5c0e33492676922dc257cc5ca4d120fd8f4ce71 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 04:23:46 +0300 Subject: [PATCH 36/99] Check configurations --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index ac77f08..66d4ad3 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -59,7 +59,7 @@ jobs: echo "DYLD_LIBRARY_PATH: $DYLD_LIBRARY_PATH" echo "GCC: $(which gcc-14)" echo "GLIBC: $(otool -L $(which gcc-14) | grep -i libc)" - echo "Other Glibc: $(cat $(gcc -print-file-name=libc.dylib))" + echo "Other Glibc: $(cat $(gcc-14 -print-file-name=libc.dylib))" echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" - uses: actions/checkout@v4 From 0d8ddf4e6c3c741650dcba14b60b0fa08d761412 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 04:37:03 +0300 Subject: [PATCH 37/99] Check configurations I'm so tired --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 66d4ad3..5ea9f87 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -92,7 +92,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS="-I/opt/homebrew/opt/gcc@14/include/c++/14 -I/opt/homebrew/opt/gcc@14/include/c++/14/* -L/opt/homebrew/opt/gcc@14/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc@14/lib/gcc/14 -stdlib=libstdc++" + -DCMAKE_CXX_FLAGS="-I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -Wl,-rpath,/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -stdlib=libstdc++" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From 37a61b063a8b45ffd47517e65c75cfb25c778c71 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 14:30:27 +0300 Subject: [PATCH 38/99] Try using libc++ --- .github/workflows/cmake-multi-platform.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 5ea9f87..efe0d66 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -88,11 +88,15 @@ jobs: # export CPPFLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22"; # export LD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; # export DYLD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; + export LDFLAGS="-L/opt/homebrew/opt/llvm/lib" + export CPPFLAGS="-I/opt/homebrew/opt/llvm/include" cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS="-I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -Wl,-rpath,/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -stdlib=libstdc++" + # -DCMAKE_CXX_FLAGS="-I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -Wl,-rpath,/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -stdlib=libstdc++" + -DCMAKE_CXX_FLAGS="-I/opt/homebrew/opt/llvm/include -L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++" + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From ba1d48d035769e2606d1bbb79b6cbf6668e2116a Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 15:07:14 +0300 Subject: [PATCH 39/99] Try using libc++ --- .github/workflows/cmake-multi-platform.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index efe0d66..13ddfa6 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -58,9 +58,13 @@ jobs: echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" echo "DYLD_LIBRARY_PATH: $DYLD_LIBRARY_PATH" echo "GCC: $(which gcc-14)" - echo "GLIBC: $(otool -L $(which gcc-14) | grep -i libc)" + echo "Otool output: $(otool -L $(which gcc-14))" echo "Other Glibc: $(cat $(gcc-14 -print-file-name=libc.dylib))" echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" + echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" + echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14)" + echo "Homebrew bin dir: $(ls -l /opt/homebrew/bin)" + echo "GCC bin dir: $(ls -l /opt/homebrew/opt/gcc/bin)" - uses: actions/checkout@v4 @@ -94,9 +98,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - # -DCMAKE_CXX_FLAGS="-I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -Wl,-rpath,/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -stdlib=libstdc++" - -DCMAKE_CXX_FLAGS="-I/opt/homebrew/opt/llvm/include -L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++" - + -DCMAKE_CXX_FLAGS="-I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -Wl,-rpath,/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -stdlib=libstdc++" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From 6f6e1b9cfea83323c8f712614252cb868af9deb3 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 15:19:23 +0300 Subject: [PATCH 40/99] Try using libc++ --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 13ddfa6..db60236 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -63,8 +63,8 @@ jobs: echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14)" - echo "Homebrew bin dir: $(ls -l /opt/homebrew/bin)" echo "GCC bin dir: $(ls -l /opt/homebrew/opt/gcc/bin)" + echo "GCC Cellar bin dir: $(ls -l /opt/homebrew/Cellar/gcc/14.1.0/bin)" - uses: actions/checkout@v4 From 9d03aaefed597822ee639fbfb8e86c0e17e1c960 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 15:31:42 +0300 Subject: [PATCH 41/99] Try using libc++ --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index db60236..0569005 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -64,7 +64,7 @@ jobs: echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14)" echo "GCC bin dir: $(ls -l /opt/homebrew/opt/gcc/bin)" - echo "GCC Cellar bin dir: $(ls -l /opt/homebrew/Cellar/gcc/14.1.0/bin)" + echo "GCC info: $(gcc-14 -v)" - uses: actions/checkout@v4 From 7f8ebce9aed31e09f099f8742ca11e3fb05e5f77 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 16:32:58 +0300 Subject: [PATCH 42/99] Try using gcc instead of clang --- .github/workflows/cmake-multi-platform.yml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 0569005..f96c54e 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -51,19 +51,12 @@ jobs: - name: Check configuration run: | - echo "Homebrew prefix: $HOMEBREW_PREFIX" - echo "PATH: $PATH" - echo "LDFLAGS: $LDFLAGS" - echo "CPPFLAGS: $CPPFLAGS" - echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" - echo "DYLD_LIBRARY_PATH: $DYLD_LIBRARY_PATH" echo "GCC: $(which gcc-14)" - echo "Otool output: $(otool -L $(which gcc-14))" - echo "Other Glibc: $(cat $(gcc-14 -print-file-name=libc.dylib))" + echo "Other Glibc: $(cat $(gcc-14 -print-file-name=libstdc++.dylib))" echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" - echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14)" - echo "GCC bin dir: $(ls -l /opt/homebrew/opt/gcc/bin)" + echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include)" + echo "GCC lib dir: $(ls -l /opt/homebrew/opt/gcc/lib/gcc/current)" echo "GCC info: $(gcc-14 -v)" - uses: actions/checkout@v4 @@ -96,8 +89,8 @@ jobs: export CPPFLAGS="-I/opt/homebrew/opt/llvm/include" cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ - -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang + -DCMAKE_CXX_COMPILER=g++-14 + -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_FLAGS="-I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -Wl,-rpath,/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -stdlib=libstdc++" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From 640ca7a39de4fb3dc0734460ab2bfde63981ab59 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 17:02:26 +0300 Subject: [PATCH 43/99] Running out of patience --- .github/workflows/cmake-multi-platform.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index f96c54e..40ff79a 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -83,15 +83,13 @@ jobs: run: > # export LDFLAGS="-L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13"; # export CPPFLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22"; - # export LD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; - # export DYLD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; - export LDFLAGS="-L/opt/homebrew/opt/llvm/lib" - export CPPFLAGS="-I/opt/homebrew/opt/llvm/include" + export LD_LIBRARY_PATH="/opt/homebrew/opt/gcc/lib/gcc/current"; + export DYLD_LIBRARY_PATH="/opt/homebrew/opt/gcc/lib/gcc/current"; cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_CXX_COMPILER=g++-14 - -DCMAKE_C_COMPILER=gcc-14 - -DCMAKE_CXX_FLAGS="-I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -Wl,-rpath,/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -stdlib=libstdc++" + -DCMAKE_CXX_COMPILER=clang++-18 + -DCMAKE_C_COMPILER=clang-18 + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From 13861f538ccb02b59b46f991da736c15d06bdd6f Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 17:11:45 +0300 Subject: [PATCH 44/99] Running out of patience --- .github/workflows/cmake-multi-platform.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 40ff79a..1fbf8e6 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -87,8 +87,8 @@ jobs: export DYLD_LIBRARY_PATH="/opt/homebrew/opt/gcc/lib/gcc/current"; cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_CXX_COMPILER=clang++-18 - -DCMAKE_C_COMPILER=clang-18 + -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ + -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From 90b5f5b108eb1dc43d2dd1d7bd5223461cb67da7 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 17:26:00 +0300 Subject: [PATCH 45/99] Reconfigure libstdc++ --- .github/workflows/cmake-multi-platform.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 1fbf8e6..d8d6e92 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -52,7 +52,7 @@ jobs: - name: Check configuration run: | echo "GCC: $(which gcc-14)" - echo "Other Glibc: $(cat $(gcc-14 -print-file-name=libstdc++.dylib))" + echo "Other Glibc: $(gcc-14 -print-file-name=libstdc++.dylib)" echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include)" @@ -89,7 +89,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From befad4baabf6b36084679be1b45b236854a508cb Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 17:52:35 +0300 Subject: [PATCH 46/99] Reconfigure libstdc++ --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index d8d6e92..631691f 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -89,7 +89,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -I/opt/homebrew/opt/llvm/include/c++/v1 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From c7bd9e78baa8c899a9f0cd6f906b3e750d1b6d0c Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 17:58:39 +0300 Subject: [PATCH 47/99] Reconfigure libstdc++ --- .github/workflows/cmake-multi-platform.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 631691f..9848500 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -58,6 +58,8 @@ jobs: echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include)" echo "GCC lib dir: $(ls -l /opt/homebrew/opt/gcc/lib/gcc/current)" echo "GCC info: $(gcc-14 -v)" + echo "" + echo "GCC extra info: $(gcc-14 -v -x c++ /dev/null -o /dev/null)" - uses: actions/checkout@v4 From f90aa5ee720d8a776436bc3976194bcbadfa9e5c Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 18:45:12 +0300 Subject: [PATCH 48/99] Install gcc from source --- .github/workflows/cmake-multi-platform.yml | 28 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 9848500..0818683 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -43,11 +43,33 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install gcc readline ninja + brew install ninja gmp isl libmpc mpfr zstd brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + + - name: Build GCC From Source + if: matrix.os == 'macos-latest' + run: | + git clone https://github.com/iains/gcc-darwin-arm64.git + cd gcc-darwin-arm64 + mkdir build && cd build + ../configure --prefix=/opt/homebrew/opt/gcc \ + --libdir=/opt/homebrew/opt/gcc/lib/gcc/current \ + --disable-nls --enable-checking=release \ + --with-gcc-major-version-only \ + --enable-languages=c,c++,objc,obj-c++,fortran \ + --program-suffix=-15 --with-gmp=/opt/homebrew/opt/gmp \ + --with-mpfr=/opt/homebrew/opt/mpfr \ + --with-mpc=/opt/homebrew/opt/libmpc \ + --with-isl=/opt/homebrew/opt/isl \ + --with-zstd=/opt/homebrew/opt/zstd \ + --with-pkgversion='Draco's GCC 15.0.0' \ + --with-system-zlib --build=aarch64-apple-darwin23 \ + --with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk + make -j 4 + sudo make install - name: Check configuration run: | @@ -91,7 +113,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -I/opt/homebrew/opt/llvm/include/c++/v1 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/15 -I/opt/homebrew/opt/llvm/include/c++/v1 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/15/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From 17208c4a37cdf20fd22db89f3339851c16969dd1 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 18:49:02 +0300 Subject: [PATCH 49/99] Fix gcc-15 paths --- .github/workflows/cmake-multi-platform.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 0818683..4e440a0 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -73,15 +73,15 @@ jobs: - name: Check configuration run: | - echo "GCC: $(which gcc-14)" - echo "Other Glibc: $(gcc-14 -print-file-name=libstdc++.dylib)" - echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" - echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" - echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include)" + echo "GCC: $(which /opt/homebrew/opt/gcc/bin/gcc-15)" + echo "Other Glibc: $(/opt/homebrew/opt/gcc/bin/gcc-15 -print-file-name=libstdc++.dylib)" + echo "GCC INCLUDES: $(/opt/homebrew/opt/gcc/bin/gcc-15 -print-search-dirs | grep install)" + echo "GCC LIBS: $(/opt/homebrew/opt/gcc/bin/gcc-15 -print-search-dirs | grep libraries)" + echo "Include dir: $(ls -l /opt/homebrew/opt/gcc/include)" echo "GCC lib dir: $(ls -l /opt/homebrew/opt/gcc/lib/gcc/current)" - echo "GCC info: $(gcc-14 -v)" + echo "GCC info: $(/opt/homebrew/opt/gcc/bin/gcc-15 -v)" echo "" - echo "GCC extra info: $(gcc-14 -v -x c++ /dev/null -o /dev/null)" + echo "GCC extra info: $(/opt/homebrew/opt/gcc/bin/gcc-15 -v -x c++ /dev/null -o /dev/null)" - uses: actions/checkout@v4 From 6beacfe479a315b490decd35e6e0e2b5a1a128d2 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 18:58:29 +0300 Subject: [PATCH 50/99] Fix gcc-15 paths --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 4e440a0..a3ceb1e 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -65,7 +65,7 @@ jobs: --with-mpc=/opt/homebrew/opt/libmpc \ --with-isl=/opt/homebrew/opt/isl \ --with-zstd=/opt/homebrew/opt/zstd \ - --with-pkgversion='Draco's GCC 15.0.0' \ + --with-pkgversion='Draco\'s GCC 15.0.0' \ --with-system-zlib --build=aarch64-apple-darwin23 \ --with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk make -j 4 From 066a5d7e44d670c6890bbec76b2d8c6434184bec Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 19:08:23 +0300 Subject: [PATCH 51/99] Fix gcc-15 paths --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index a3ceb1e..62f82ae 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -65,7 +65,7 @@ jobs: --with-mpc=/opt/homebrew/opt/libmpc \ --with-isl=/opt/homebrew/opt/isl \ --with-zstd=/opt/homebrew/opt/zstd \ - --with-pkgversion='Draco\'s GCC 15.0.0' \ + --with-pkgversion='Draco'\''s GCC 15.0.0' \ --with-system-zlib --build=aarch64-apple-darwin23 \ --with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk make -j 4 From 22d8f9bb21a422afac7634a1a668a1fb1749cd96 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 19:30:32 +0300 Subject: [PATCH 52/99] Revert to gcc 14 --- .github/workflows/cmake-multi-platform.yml | 42 ++++++---------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 62f82ae..9848500 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -43,45 +43,23 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install ninja gmp isl libmpc mpfr zstd + brew install gcc readline ninja brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile - - - name: Build GCC From Source - if: matrix.os == 'macos-latest' - run: | - git clone https://github.com/iains/gcc-darwin-arm64.git - cd gcc-darwin-arm64 - mkdir build && cd build - ../configure --prefix=/opt/homebrew/opt/gcc \ - --libdir=/opt/homebrew/opt/gcc/lib/gcc/current \ - --disable-nls --enable-checking=release \ - --with-gcc-major-version-only \ - --enable-languages=c,c++,objc,obj-c++,fortran \ - --program-suffix=-15 --with-gmp=/opt/homebrew/opt/gmp \ - --with-mpfr=/opt/homebrew/opt/mpfr \ - --with-mpc=/opt/homebrew/opt/libmpc \ - --with-isl=/opt/homebrew/opt/isl \ - --with-zstd=/opt/homebrew/opt/zstd \ - --with-pkgversion='Draco'\''s GCC 15.0.0' \ - --with-system-zlib --build=aarch64-apple-darwin23 \ - --with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk - make -j 4 - sudo make install + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile - name: Check configuration run: | - echo "GCC: $(which /opt/homebrew/opt/gcc/bin/gcc-15)" - echo "Other Glibc: $(/opt/homebrew/opt/gcc/bin/gcc-15 -print-file-name=libstdc++.dylib)" - echo "GCC INCLUDES: $(/opt/homebrew/opt/gcc/bin/gcc-15 -print-search-dirs | grep install)" - echo "GCC LIBS: $(/opt/homebrew/opt/gcc/bin/gcc-15 -print-search-dirs | grep libraries)" - echo "Include dir: $(ls -l /opt/homebrew/opt/gcc/include)" + echo "GCC: $(which gcc-14)" + echo "Other Glibc: $(gcc-14 -print-file-name=libstdc++.dylib)" + echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" + echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" + echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include)" echo "GCC lib dir: $(ls -l /opt/homebrew/opt/gcc/lib/gcc/current)" - echo "GCC info: $(/opt/homebrew/opt/gcc/bin/gcc-15 -v)" + echo "GCC info: $(gcc-14 -v)" echo "" - echo "GCC extra info: $(/opt/homebrew/opt/gcc/bin/gcc-15 -v -x c++ /dev/null -o /dev/null)" + echo "GCC extra info: $(gcc-14 -v -x c++ /dev/null -o /dev/null)" - uses: actions/checkout@v4 @@ -113,7 +91,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/15 -I/opt/homebrew/opt/llvm/include/c++/v1 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/15/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -I/opt/homebrew/opt/llvm/include/c++/v1 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From 19aa7462c627295e0b1bc9901620f35ed0b69767 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 19:42:19 +0300 Subject: [PATCH 53/99] Try different settings --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 9848500..be00ad1 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -91,7 +91,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14 -I/opt/homebrew/opt/llvm/include/c++/v1 -I/opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include -L/opt/homebrew/opt/gcc/lib/gcc/current -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From b4ab72a4fc057ae5b57c6a9c3e079f3ed79f795f Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 19:49:58 +0300 Subject: [PATCH 54/99] Find essential locations --- .github/workflows/cmake-multi-platform.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index be00ad1..a1495fa 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -60,6 +60,8 @@ jobs: echo "GCC info: $(gcc-14 -v)" echo "" echo "GCC extra info: $(gcc-14 -v -x c++ /dev/null -o /dev/null)" + echo "Headers location: $(find /opt/homebrew/opt/gcc -name 'algorithm' -type f)" + echo "libstdc++ location: $(find /opt/homebrew/opt/gcc -name 'libstdc++.dylib' -type f)" - uses: actions/checkout@v4 From c00242447183791cca502f167bc94ba1398aa3df Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 31 May 2024 19:57:42 +0300 Subject: [PATCH 55/99] Clean the GitHub workflow config --- .github/workflows/cmake-multi-platform.yml | 34 ++++------------------ 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index a1495fa..518b459 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -20,12 +20,7 @@ jobs: include: - os: macos-latest c_compiler: clang - cpp_compiler: clang++ - env: - LDFLAGS=: "-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++" - CPPFLAGS: "-I/opt/homebrew/opt/llvm/include -I/opt/homebrew/opt/llvm/include/c++/v1" - LD_LIBRARY_PATH: "/opt/homebrew/opt/llvm/lib" - DYLD_LIBRARY_PATH: "/opt/homebrew/opt/llvm/lib" + cpp_compiler: clang++-18 - os: ubuntu-24.04 c_compiler: clang @@ -37,7 +32,6 @@ jobs: build_type: Debug steps: - # Install dependencies: cmake, ninja, gcc, libgcrypt, openssl, readline, and libsodium - name: Install Dependencies if: matrix.os == 'macos-latest' run: | @@ -48,20 +42,7 @@ jobs: echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile - - - name: Check configuration - run: | - echo "GCC: $(which gcc-14)" - echo "Other Glibc: $(gcc-14 -print-file-name=libstdc++.dylib)" - echo "GCC INCLUDES: $(gcc-14 -print-search-dirs | grep install)" - echo "GCC LIBS: $(gcc-14 -print-search-dirs | grep libraries)" - echo "Include dir: $(ls -l /opt/homebrew/lib/gcc/current/gcc/aarch64-apple-darwin23/14/include)" - echo "GCC lib dir: $(ls -l /opt/homebrew/opt/gcc/lib/gcc/current)" - echo "GCC info: $(gcc-14 -v)" - echo "" - echo "GCC extra info: $(gcc-14 -v -x c++ /dev/null -o /dev/null)" - echo "Headers location: $(find /opt/homebrew/opt/gcc -name 'algorithm' -type f)" - echo "libstdc++ location: $(find /opt/homebrew/opt/gcc -name 'libstdc++.dylib' -type f)" + sudo source ~/.bash_profile - uses: actions/checkout@v4 @@ -84,12 +65,7 @@ jobs: - name: Configure CMake if: matrix.os == 'macos-latest' - run: > - # export LDFLAGS="-L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13"; - # export CPPFLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22"; - export LD_LIBRARY_PATH="/opt/homebrew/opt/gcc/lib/gcc/current"; - export DYLD_LIBRARY_PATH="/opt/homebrew/opt/gcc/lib/gcc/current"; - + run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang @@ -130,8 +106,8 @@ jobs: gpg --batch --status-file ~/gpg_log.txt --passphrase ${{ secrets.GPG_PASS }} --default-key dr8co@duck.com \ --pinentry-mode=loopback --detach-sign "$file" || (cat ~/gpg_log.txt && exit 1) done -# -# # Upload the built artifacts + + # Upload the built artifacts - name: Upload Artifacts if: matrix.build_type == 'Release' uses: actions/upload-artifact@v4 From 78604cadb61e47fb51ef1d202e17cf17b51b5f82 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:16:28 +0300 Subject: [PATCH 56/99] Refactor code --- .github/workflows/cmake-multi-platform.yml | 1 - CMakeLists.txt | 13 +++++++------ src/duplicateFinder/duplicateFinder.cppm | 6 ++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 518b459..003bbce 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -42,7 +42,6 @@ jobs: echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile - sudo source ~/.bash_profile - uses: actions/checkout@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ae6e91..ffbfb55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,12 +88,13 @@ target_sources(privacyShield PRIVATE ) # Link libraries -target_link_libraries(privacyShield - PRIVATE OpenSSL::Crypto - PRIVATE Readline::Readline - PRIVATE Sodium::sodium - PRIVATE Gcrypt::Gcrypt - PRIVATE BLAKE3::blake3) +target_link_libraries(privacyShield PRIVATE + OpenSSL::Crypto + Readline::Readline + Sodium::sodium + Gcrypt::Gcrypt + BLAKE3::blake3 +) # Install the binary (optional), with 0755 permissions include(GNUInstallDirs) diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index a94a5d7..b2e5c7c 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -63,12 +63,12 @@ std::string calculateBlake3(const std::string &filePath) { blake3_hasher_init(&hasher); // Update the hasher with the file contents in chunks of 4 kB - std::vector buffer(CHUNK_SIZE); + std::array buffer{}; while (file.read(buffer.data(), CHUNK_SIZE)) blake3_hasher_update(&hasher, buffer.data(), CHUNK_SIZE); // Update the hasher with the last chunk of data - std::size_t remainingBytes = file.gcount(); + const std::size_t remainingBytes = file.gcount(); blake3_hasher_update(&hasher, buffer.data(), remainingBytes); // Finalize the hash calculation @@ -209,10 +209,8 @@ std::size_t findDuplicates(const std::string_view directoryPath) { export void duplicateFinder() { while (true) { std::print("\n-------------------"); - // std::cout << "\n-------------------"; printColoredOutput('m', " Duplicate Finder "); std::println("-------------------"); - // std::cout << "-------------------\n"; printColoredOutputln('g', "1. Scan for duplicate files"); printColoredOutputln('r', "2. Exit"); std::println("--------------------------------------------------------"); From b4ac003b62beec2734f8e5292b86a69833c36ff3 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:24:08 +0300 Subject: [PATCH 57/99] Refactor scripts --- scripts/install-blake3.sh | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/scripts/install-blake3.sh b/scripts/install-blake3.sh index 91c6f00..77c4c8a 100755 --- a/scripts/install-blake3.sh +++ b/scripts/install-blake3.sh @@ -47,24 +47,6 @@ install_blake3() { } -# Function to clone repository -clone_repo() { - git clone https://github.com/BLAKE3-team/BLAKE3.git || error_exit "Failed to clone BLAKE3 repository." -} - -# Function to build and install BLAKE3 -build_install() { - cd BLAKE3/c || error_exit "Failed to navigate to BLAKE3/c directory." - cmake -B build -DCMAKE_C_COMPILER="$C_COMPILER" -G Ninja || error_exit "Failed to run cmake." - get_number_of_processors - - cmake --build build --config Release --target install -j "$NUMBER_OF_PROCESSORS" || error_exit "Failed to build and install." - - # Cleanup - cd ../.. - rm -rf BLAKE3 -} - # Set C compiler C_COMPILER=${1:-gcc} @@ -75,8 +57,4 @@ cd "${0%/*}" || error_exit "Failed to change directory to script location." echo "Compiling BLAKE3 with $C_COMPILER compiler.." -# Call functions -# clone_repo -# build_install - install_blake3 From 825d7c01766949aa417daf1b0a50831a0bbd037d Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:24:31 +0300 Subject: [PATCH 58/99] Refactor CMake config --- CMakeLists.txt | 69 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ffbfb55..23c6eec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ project(privacyShield VERSION 2.5.0 DESCRIPTION "A suite of tools for privacy and security" HOMEPAGE_URL "https://shield.iandee.tech" - LANGUAGES CXX) + LANGUAGES C CXX) # C++23 support is required for this project set(CMAKE_CXX_STANDARD 23) @@ -40,16 +40,21 @@ endif () # Set the path to additional CMake modules list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") +# Options +include(CMakeDependentOption) +# GCC does not support all sanitizers +cmake_dependent_option(ENABLE_SANITIZERS + "Enable sanitizers (Ignored if not using Clang compiler)" OFF + "${CMAKE_CXX_COMPILER_ID} STREQUAL \"Clang\"" OFF) + # Additional checks for the Debug build -if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") - if (CMAKE_BUILD_TYPE STREQUAL "Debug") - add_compile_options( - -Wall - -Wextra - -Werror - -Wpedantic - ) - endif () +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_options( + -Wall + -Wextra + -Werror + -Wpedantic + ) endif () # Find the required packages @@ -57,7 +62,24 @@ find_package(OpenSSL REQUIRED) find_package(Sodium REQUIRED) find_package(Readline REQUIRED) find_package(Gcrypt REQUIRED) -find_package(BLAKE3 REQUIRED) # See https://github.com/BLAKE3-team/BLAKE3 + +find_package(BLAKE3 QUIET) # See https://github.com/BLAKE3-team/BLAKE3 + +if (NOT TARGET BLAKE3::blake3) + message(STATUS "BLAKE3 not found. Fetching from GitHub...") + include(FetchContent) + + FetchContent_Declare( + blake3 + GIT_REPOSITORY https://github.com/BLAKE3-team/BLAKE3.git + GIT_TAG 454ee5a7c73583cb3060d1464a5d3a4e65f06062 + SOURCE_SUBDIR c + EXCLUDE_FROM_ALL + ) + + FetchContent_MakeAvailable(blake3) + +endif () # Add the executable target add_executable(privacyShield) @@ -87,6 +109,31 @@ target_sources(privacyShield PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/secureAllocator.cppm" ) +# Sanitizers for debugging and testing +# Requires llvm-symbolizer and sanitizer libraries (asan, ubsan, msan, tsan) +if (ENABLE_SANITIZERS) + # Common flags for all sanitizers + set(sanitizer_common_flags "-fno-omit-frame-pointer -g -O1") + + # Address, leak, undefined, integer, nullability sanitizers + set(address_sanitizer_flags "-fsanitize=address,leak,undefined,integer,nullability") + + # Thread sanitizer, cannot be used with address sanitizer + set(thread_sanitizer_flags "-fsanitize=thread -fPIE") + + # Memory sanitizer, cannot be used with address sanitizer. + set(memory_sanitizer_flags "-fsanitize=memory -fPIE -fno-optimize-sibling-calls") + + # Add compile options + add_compile_options( + "SHELL:${sanitizer_common_flags}" + "SHELL:${address_sanitizer_flags}" + ) + + # Link the enabled sanitizers. + target_link_libraries(privacyShield PRIVATE asan ubsan) +endif () + # Link libraries target_link_libraries(privacyShield PRIVATE OpenSSL::Crypto From 733953eb0b7c45897ffb6b9a696e3cb1a763b8cb Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:54:16 +0300 Subject: [PATCH 59/99] Refactor CMake config --- CMakeLists.txt | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 23c6eec..d89da0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,30 +57,6 @@ if (CMAKE_BUILD_TYPE STREQUAL "Debug") ) endif () -# Find the required packages -find_package(OpenSSL REQUIRED) -find_package(Sodium REQUIRED) -find_package(Readline REQUIRED) -find_package(Gcrypt REQUIRED) - -find_package(BLAKE3 QUIET) # See https://github.com/BLAKE3-team/BLAKE3 - -if (NOT TARGET BLAKE3::blake3) - message(STATUS "BLAKE3 not found. Fetching from GitHub...") - include(FetchContent) - - FetchContent_Declare( - blake3 - GIT_REPOSITORY https://github.com/BLAKE3-team/BLAKE3.git - GIT_TAG 454ee5a7c73583cb3060d1464a5d3a4e65f06062 - SOURCE_SUBDIR c - EXCLUDE_FROM_ALL - ) - - FetchContent_MakeAvailable(blake3) - -endif () - # Add the executable target add_executable(privacyShield) @@ -109,6 +85,31 @@ target_sources(privacyShield PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/secureAllocator.cppm" ) +# Find the required packages +find_package(OpenSSL REQUIRED) +find_package(Sodium REQUIRED) +find_package(Readline REQUIRED) +find_package(Gcrypt REQUIRED) + +find_package(BLAKE3 QUIET) # See https://github.com/BLAKE3-team/BLAKE3 + +if (NOT TARGET BLAKE3::blake3) + message(STATUS "BLAKE3 not found. Fetching from GitHub...") + include(FetchContent) + + FetchContent_Declare( + blake3 + GIT_REPOSITORY https://github.com/BLAKE3-team/BLAKE3.git + GIT_TAG 454ee5a7c73583cb3060d1464a5d3a4e65f06062 + SOURCE_SUBDIR c + EXCLUDE_FROM_ALL + ) + + FetchContent_MakeAvailable(blake3) + target_include_directories(privacyShield PRIVATE "${blake3_SOURCE_DIR}") + +endif () + # Sanitizers for debugging and testing # Requires llvm-symbolizer and sanitizer libraries (asan, ubsan, msan, tsan) if (ENABLE_SANITIZERS) From 6ef7b80fd31f1c68ffead7ba6ac27d496e4b4188 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:00:59 +0300 Subject: [PATCH 60/99] Refactor build scripts --- scripts/build-functions.sh | 63 ++++++++++++++++++++++++++++ scripts/build.sh | 86 +++++++++----------------------------- 2 files changed, 82 insertions(+), 67 deletions(-) create mode 100755 scripts/build-functions.sh diff --git a/scripts/build-functions.sh b/scripts/build-functions.sh new file mode 100755 index 0000000..1233b8e --- /dev/null +++ b/scripts/build-functions.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -e + +# This script is used to build the project on the Ubuntu Jammy (22.04) distribution. +# It is not intended to be used on other distributions, and must be run from the project root. + +PARALLELISM_LEVEL=4 + +function check_root() { + # Root access is required to install the dependencies. + if [ "$EUID" -ne 0 ]; then + echo "Please run as root." + abort + fi +} + +function check_dependencies() { + for cmd in wget add-apt-repository; do + if ! command -v $cmd &>/dev/null; then + echo "$cmd could not be found" + exit + fi + done +} + +function install_dependencies() { +# wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc +# add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" +# add-apt-repository -y ppa:ubuntu-toolchain-r/ppa + apt update + export NEEDRESTART_SUSPEND=1 + apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libreadline8 libreadline-dev libsodium23 libsodium-dev + + # Install CMake 3.29.3 + if dpkg -s "cmake" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove cmake + fi + + wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.29.3/cmake-3.29.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local + + # Install Ninja 1.12 + if dpkg -s "ninja-build" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove ninja-build + fi + + wget -q "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" + unzip ninja-linux.zip -d /usr/local/bin +} + +function build_blake3() { + ./install-blake3.sh clang-18 +} + +function configure_cmake() { + cmake -B build -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_BUILD_TYPE=Debug -G Ninja +} + +function build_project() { + cmake --build build --config Debug -j "$PARALLELISM_LEVEL" +} + +trap "echo 'An unexpected error occurred. Program aborted.'" ERR diff --git a/scripts/build.sh b/scripts/build.sh index a2b0e3d..df8169a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,76 +1,28 @@ -#!/bin/env bash +#!/usr/bin/env bash -set -e +# Run from this directory +cd "${0%/*}" || abort -# This script is used to build the project on the Ubuntu Jammy (22.04) distribution. -# It is not intended to be used on other distributions, and must be run from the project root. +# Include the build functions +. ./build-functions.sh -PARALLELISM_LEVEL=4 +# Root access is required to install the dependencies. +check_root -function check_root() { - # Root access is required to install the dependencies. - if [ "$EUID" -ne 0 ]; then - echo "Please run as root." - abort - fi -} +# Check for required commands +check_dependencies -function check_dependencies() { - for cmd in wget add-apt-repository; do - if ! command -v $cmd &>/dev/null; then - echo "$cmd could not be found" - exit - fi - done -} +# Install dependencies +install_dependencies -function install_dependencies() { -# wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc -# add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" -# add-apt-repository -y ppa:ubuntu-toolchain-r/ppa - apt update - export NEEDRESTART_SUSPEND=1 - apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libreadline8 libreadline-dev libsodium23 libsodium-dev +echo "Ninja: $(ninja --version), CMake: $(cmake --version)" - # Install CMake 3.29.3 - if dpkg -s "cmake" >/dev/null 2>&1; then - apt remove -y --purge --auto-remove cmake - fi +# Build and install BLAKE3 +build_blake3 - wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.29.3/cmake-3.29.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local +# Configure CMake +cd .. || abort +configure_cmake - # Install Ninja 1.12 - if dpkg -s "ninja-build" >/dev/null 2>&1; then - apt remove -y --purge --auto-remove ninja-build - fi - - wget -q "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" - unzip ninja-linux.zip -d /usr/local/bin -} - -function build_blake3() { - ./install-blake3.sh clang-18 -} - -function configure_cmake() { - cmake -B build -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_BUILD_TYPE=Debug -G Ninja -} - -function build_project() { - cmake --build build --config Debug -j "$PARALLELISM_LEVEL" -} - -main() { - trap "echo 'An unexpected error occurred. Program aborted.'" ERR - check_root - check_dependencies - cd "${0%/*}" || abort - install_dependencies - echo "Ninja: $(ninja --version), CMake: $(cmake --version)" - build_blake3 - cd .. || abort - configure_cmake - build_project -} - -main +# Build the project +build_project \ No newline at end of file From cad180a91c3db663920939f588085108f94b6c12 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:01:46 +0300 Subject: [PATCH 61/99] Configure Qodana code analysis --- .github/workflows/qodana_code_quality.yml | 20 ++++++++++++++++ qodana.yaml | 29 +++++++++++++++++++++++ scripts/prepare-qodana.sh | 26 ++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 .github/workflows/qodana_code_quality.yml create mode 100644 qodana.yaml create mode 100755 scripts/prepare-qodana.sh diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000..2fdd415 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,20 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: + - dev + - main + +jobs: + qodana: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2024.1 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} \ No newline at end of file diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..52b97e8 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.recommended + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +bootstrap: sudo ./scripts/prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-clang:2024.1-eap diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh new file mode 100755 index 0000000..1517a34 --- /dev/null +++ b/scripts/prepare-qodana.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Run from this directory +cd "$(dirname "$0")" || (echo "Running from $(pwd)" && exit 1) + +# Include the build functions +. ./build-functions.sh + +# Root access is required to install the dependencies. +check_root + +# Check for required commands +check_dependencies + +# Install dependencies +install_dependencies + +echo "Ninja: $(ninja --version), CMake: $(cmake --version)" + +# Build and install BLAKE3 +build_blake3 + +# Configure CMake +cd .. || abort +/usr/local/bin/cmake -S . -B build -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja + From d025f7f99254896c8d7c9267e2a7660ea38cbce2 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:09:34 +0300 Subject: [PATCH 62/99] GitHub actions 'checkout' version update --- .github/workflows/qodana_code_quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml index 2fdd415..aea7177 100644 --- a/.github/workflows/qodana_code_quality.yml +++ b/.github/workflows/qodana_code_quality.yml @@ -11,7 +11,7 @@ jobs: qodana: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 'Qodana Scan' From 48559eadfe741210496f723696dbd8974552750b Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:11:57 +0300 Subject: [PATCH 63/99] Install wget --- scripts/build-functions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-functions.sh b/scripts/build-functions.sh index 1233b8e..67d2e93 100755 --- a/scripts/build-functions.sh +++ b/scripts/build-functions.sh @@ -30,7 +30,7 @@ function install_dependencies() { # add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update export NEEDRESTART_SUSPEND=1 - apt install -y unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libreadline8 libreadline-dev libsodium23 libsodium-dev + apt install -y wget unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libreadline8 libreadline-dev libsodium23 libsodium-dev # Install CMake 3.29.3 if dpkg -s "cmake" >/dev/null 2>&1; then From 8dbc4e5598bdeacd02ae158104763438994b5987 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 6 Jun 2024 00:28:12 +0300 Subject: [PATCH 64/99] Grant write permissions to qodana --- .github/workflows/qodana_code_quality.yml | 5 +++++ scripts/build-functions.sh | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml index aea7177..e7c9bfe 100644 --- a/.github/workflows/qodana_code_quality.yml +++ b/.github/workflows/qodana_code_quality.yml @@ -10,9 +10,14 @@ on: jobs: qodana: runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + checks: write steps: - uses: actions/checkout@v4 with: + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2024.1 diff --git a/scripts/build-functions.sh b/scripts/build-functions.sh index 67d2e93..3887099 100755 --- a/scripts/build-functions.sh +++ b/scripts/build-functions.sh @@ -2,7 +2,7 @@ set -e -# This script is used to build the project on the Ubuntu Jammy (22.04) distribution. +# This script is used to build the project on the Ubuntu Noble (24.04) distribution. # It is not intended to be used on other distributions, and must be run from the project root. PARALLELISM_LEVEL=4 From f84c665cbe4061e1404912bb5c9c1a619d062083 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 6 Jun 2024 00:36:33 +0300 Subject: [PATCH 65/99] Merge dependency installation steps --- scripts/build-functions.sh | 9 --------- scripts/build.sh | 3 --- scripts/prepare-qodana.sh | 3 --- 3 files changed, 15 deletions(-) diff --git a/scripts/build-functions.sh b/scripts/build-functions.sh index 3887099..a98c80a 100755 --- a/scripts/build-functions.sh +++ b/scripts/build-functions.sh @@ -15,15 +15,6 @@ function check_root() { fi } -function check_dependencies() { - for cmd in wget add-apt-repository; do - if ! command -v $cmd &>/dev/null; then - echo "$cmd could not be found" - exit - fi - done -} - function install_dependencies() { # wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc # add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" diff --git a/scripts/build.sh b/scripts/build.sh index df8169a..c0fdbe7 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -9,9 +9,6 @@ cd "${0%/*}" || abort # Root access is required to install the dependencies. check_root -# Check for required commands -check_dependencies - # Install dependencies install_dependencies diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh index 1517a34..58618e2 100755 --- a/scripts/prepare-qodana.sh +++ b/scripts/prepare-qodana.sh @@ -9,9 +9,6 @@ cd "$(dirname "$0")" || (echo "Running from $(pwd)" && exit 1) # Root access is required to install the dependencies. check_root -# Check for required commands -check_dependencies - # Install dependencies install_dependencies From 9e85138241943792c314b17319748f5b7988649a Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 6 Jun 2024 00:49:15 +0300 Subject: [PATCH 66/99] Update gcc path on macOS --- .github/workflows/cmake-multi-platform.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 003bbce..bca9a19 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -37,11 +37,12 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install gcc readline ninja + brew install gcc readline ninja cmake git brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + . ~/.bash_profile - uses: actions/checkout@v4 @@ -68,7 +69,7 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja From a4e5d218101ffd9d4c053bccf9d40f1d220d948d Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:19:55 +0300 Subject: [PATCH 67/99] Install Qodana dependencies --- scripts/prepare-qodana.sh | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh index 58618e2..66dbaa6 100755 --- a/scripts/prepare-qodana.sh +++ b/scripts/prepare-qodana.sh @@ -10,7 +10,27 @@ cd "$(dirname "$0")" || (echo "Running from $(pwd)" && exit 1) check_root # Install dependencies -install_dependencies +apt update && apt install -y wget unzip build-essential openssl libreadline8 libreadline-dev libsodium23 libsodium-dev libgcrypt20-dev +wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc +add-apt-repository -y "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" +apt update +export NEEDRESTART_SUSPEND=1 +apt install -y clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 + +# Install CMake 3.29.3 +if dpkg -s "cmake" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove cmake +fi + +wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.29.3/cmake-3.29.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local + +# Install Ninja 1.12 +if dpkg -s "ninja-build" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove ninja-build +fi + +wget -q "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" +unzip ninja-linux.zip -d /usr/local/bin echo "Ninja: $(ninja --version), CMake: $(cmake --version)" From 61596a1d75f1f9510e6e0b61b5c7aa33f5b8f3fb Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:42:19 +0300 Subject: [PATCH 68/99] Install Qodana dependencies --- scripts/prepare-qodana.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh index 66dbaa6..e388950 100755 --- a/scripts/prepare-qodana.sh +++ b/scripts/prepare-qodana.sh @@ -10,7 +10,7 @@ cd "$(dirname "$0")" || (echo "Running from $(pwd)" && exit 1) check_root # Install dependencies -apt update && apt install -y wget unzip build-essential openssl libreadline8 libreadline-dev libsodium23 libsodium-dev libgcrypt20-dev +apt update && apt install -y software-properties-common wget unzip build-essential openssl libreadline8 libreadline-dev libsodium23 libsodium-dev libgcrypt20-dev wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc add-apt-repository -y "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" apt update From 71110cbe2d491cf95e013838bf5d11fc511fc048 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 6 Jun 2024 03:05:57 +0300 Subject: [PATCH 69/99] Install Qodana dependencies --- scripts/prepare-qodana.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh index e388950..29be1b0 100755 --- a/scripts/prepare-qodana.sh +++ b/scripts/prepare-qodana.sh @@ -10,12 +10,17 @@ cd "$(dirname "$0")" || (echo "Running from $(pwd)" && exit 1) check_root # Install dependencies +apt remove -y --purge --auto-remove llvm-toolchain-bookworm-16 clang-16 clang-tidy-16 clang-format-16 lld-16 libc++-16-dev libc++abi-16-dev apt update && apt install -y software-properties-common wget unzip build-essential openssl libreadline8 libreadline-dev libsodium23 libsodium-dev libgcrypt20-dev wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc add-apt-repository -y "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" apt update export NEEDRESTART_SUSPEND=1 -apt install -y clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 +apt install -y llvm-18-dev clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 clang-tidy-18 clang-format-18 + +for f in /usr/lib/llvm-18/bin/*; do + ln -sf "$f" /usr/bin; +done # Install CMake 3.29.3 if dpkg -s "cmake" >/dev/null 2>&1; then From 8ff62db6add506f9fcd35fb8fc252ceb7148edf8 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 6 Jun 2024 03:20:36 +0300 Subject: [PATCH 70/99] Build gcc-14 --- scripts/build-functions.sh | 21 +++++++++++++++++++++ scripts/prepare-qodana.sh | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/scripts/build-functions.sh b/scripts/build-functions.sh index a98c80a..95c5062 100755 --- a/scripts/build-functions.sh +++ b/scripts/build-functions.sh @@ -51,4 +51,25 @@ function build_project() { cmake --build build --config Debug -j "$PARALLELISM_LEVEL" } +function abort() { + echo "An unexpected error occurred. Program aborted." + exit 1 +} + +function build_install_gcc_14() { + apt update + apt install -y software-properties-common build-essential wget libgmp-dev libmpfr-dev libmpc-dev + wget -q https://ftp.gnu.org/gnu/gcc/gcc-14.1.0/gcc-14.1.0.tar.xz + tar -xf gcc-14.1.0.tar.xz + cd gcc-14.1.0 || abort + ./contrib/download_prerequisites + mkdir build + cd build || abort + ../configure --enable-languages=c,c++ --disable-multilib + make -j "$PARALLELISM_LEVEL" + make install + update-alternatives --install /usr/bin/gcc gcc /usr/local/bin/gcc 60 --slave /usr/bin/g++ g++ /usr/local/bin/g++ + apt purge -y gcc cpp g++ +} + trap "echo 'An unexpected error occurred. Program aborted.'" ERR diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh index 29be1b0..4eee182 100755 --- a/scripts/prepare-qodana.sh +++ b/scripts/prepare-qodana.sh @@ -9,8 +9,11 @@ cd "$(dirname "$0")" || (echo "Running from $(pwd)" && exit 1) # Root access is required to install the dependencies. check_root +# Build and install GCC 14 +build_install_gcc_14 + # Install dependencies -apt remove -y --purge --auto-remove llvm-toolchain-bookworm-16 clang-16 clang-tidy-16 clang-format-16 lld-16 libc++-16-dev libc++abi-16-dev +apt remove -y --purge --auto-remove llvm-16-dev clang-16 clang-tidy-16 clang-format-16 lld-16 libc++-16-dev libc++abi-16-dev apt update && apt install -y software-properties-common wget unzip build-essential openssl libreadline8 libreadline-dev libsodium23 libsodium-dev libgcrypt20-dev wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc add-apt-repository -y "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" From 518746c7e3f07b5640fff6009dc3f76d533e9a33 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:09:27 +0300 Subject: [PATCH 71/99] Cancel the Qodana action --- .../{workflows => cancelled_workflows}/qodana_code_quality.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => cancelled_workflows}/qodana_code_quality.yml (100%) diff --git a/.github/workflows/qodana_code_quality.yml b/.github/cancelled_workflows/qodana_code_quality.yml similarity index 100% rename from .github/workflows/qodana_code_quality.yml rename to .github/cancelled_workflows/qodana_code_quality.yml From 4869059ea7622f1d3059d6a3720285f517366753 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:28:30 +0300 Subject: [PATCH 72/99] Separate testing and packaging in GitHub Actions --- .github/workflows/cmake-multi-platform.yml | 55 ++-------- .github/workflows/cpack-multi-platform.yml | 111 +++++++++++++++++++++ 2 files changed, 121 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/cpack-multi-platform.yml diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index bca9a19..74ce635 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -3,8 +3,6 @@ name: CMake Build on: push: branches: [ "main", "dev" ] - pull_request: - branches: [ "main" ] jobs: build: @@ -15,7 +13,7 @@ jobs: matrix: os: [ ubuntu-24.04, macos-latest ] - build_type: [ Debug, Release ] + build_type: [ Debug ] #[ Debug, Release ] c_compiler: [ clang ] include: - os: macos-latest @@ -27,9 +25,12 @@ jobs: cpp_compiler: clang++-18 # Don't include the following configurations in the matrix - exclude: - - os: macos-latest - build_type: Debug +# exclude: +# - os: macos-latest +# build_type: Debug +# +# - os: ubuntu-24.04 +# build_type: Release steps: - name: Install Dependencies @@ -77,43 +78,7 @@ jobs: if: matrix.os == 'macos-latest' run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} -j 4 - - name: Package - if: matrix.os == 'macos-latest' && matrix.build_type == 'Release' + # Run Tests + - name: Test working-directory: ${{ steps.strings.outputs.build-output-dir }} - run: | - cpack - - - name: Package - if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'Release' - working-directory: ${{ steps.strings.outputs.build-output-dir }} - run: | - sudo cpack - sudo chown -R $USER:$USER "${{ github.workspace }}/Packages" - - - name: Import GPG Key - if: matrix.build_type == 'Release' - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} - passphrase: ${{ secrets.GPG_PASS }} - trust_level: 5 - - - name: Sign Package - if: matrix.build_type == 'Release' - working-directory: ${{ github.workspace }} - run: | - for file in Packages/*; do - gpg --batch --status-file ~/gpg_log.txt --passphrase ${{ secrets.GPG_PASS }} --default-key dr8co@duck.com \ - --pinentry-mode=loopback --detach-sign "$file" || (cat ~/gpg_log.txt && exit 1) - done - - # Upload the built artifacts - - name: Upload Artifacts - if: matrix.build_type == 'Release' - uses: actions/upload-artifact@v4 - with: - name: "${{ matrix.os }}-${{ matrix.build_type }}" - path: "${{ github.workspace }}/Packages" - overwrite: true - if-no-files-found: 'warn' - + run: ctest -j 4 diff --git a/.github/workflows/cpack-multi-platform.yml b/.github/workflows/cpack-multi-platform.yml new file mode 100644 index 0000000..720e96c --- /dev/null +++ b/.github/workflows/cpack-multi-platform.yml @@ -0,0 +1,111 @@ +name: CPack Multi-Platform + +on: + pull_request: + branches: [ "main" ] + +jobs: + build_then_package: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + + matrix: + os: [ ubuntu-24.04, macos-latest ] + build_type: [ Release ] + c_compiler: [ clang ] + include: + - os: macos-latest + c_compiler: clang + cpp_compiler: clang++-18 + + - os: ubuntu-24.04 + c_compiler: clang + cpp_compiler: clang++-18 + + steps: + - name: Install Dependencies + if: matrix.os == 'macos-latest' + run: | + export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE + brew update + brew install gcc readline ninja cmake git + brew reinstall llvm + echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + . ~/.bash_profile + + - uses: actions/checkout@v4 + + - name: Set reusable strings + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + + # Build the project + - name: Build PrivacyShield + if: matrix.os == 'ubuntu-24.04' + run: | + sudo ./scripts/build.sh + + - name: Install Blake3 + if: matrix.os == 'macos-latest' + run: | + sudo ./scripts/install-blake3.sh ${{ matrix.c_compiler }} + + - name: Configure CMake + if: matrix.os == 'macos-latest' + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ + -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -S ${{ github.workspace }} -G Ninja + + - name: Build + if: matrix.os == 'macos-latest' + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} -j 4 + + - name: Package + if: matrix.os == 'macos-latest' && matrix.build_type == 'Release' + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + cpack + + - name: Package + if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'Release' + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + sudo cpack + sudo chown -R $USER:$USER "${{ github.workspace }}/Packages" + + - name: Import GPG Key + if: matrix.build_type == 'Release' + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} + passphrase: ${{ secrets.GPG_PASS }} + trust_level: 5 + + - name: Sign Package + if: matrix.build_type == 'Release' + working-directory: ${{ github.workspace }} + run: | + for file in Packages/*; do + gpg --batch --status-file ~/gpg_log.txt --passphrase ${{ secrets.GPG_PASS }} --default-key dr8co@duck.com \ + --pinentry-mode=loopback --detach-sign "$file" || (cat ~/gpg_log.txt && exit 1) + done + + # Upload the built artifacts + - name: Upload Artifacts + if: matrix.build_type == 'Release' + uses: actions/upload-artifact@v4 + with: + name: "${{ matrix.os }}-${{ matrix.build_type }}" + path: "${{ github.workspace }}/Packages" + overwrite: true + if-no-files-found: 'warn' From 0b5ee89beb77021e7bebdff58ef704aafe280731 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:34:09 +0300 Subject: [PATCH 73/99] Remove unused linker command-line argument --- .github/workflows/cmake-multi-platform.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 74ce635..4fb0e2f 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -70,9 +70,10 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja + # -DCMAKE_CXX_FLAGS="-stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" - name: Build if: matrix.os == 'macos-latest' From 36482525a1ffe7b99e4b5e26e7453b39b48442f4 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 13 Jun 2024 00:10:38 +0300 Subject: [PATCH 74/99] Refactor compile and link flags --- .github/workflows/cmake-multi-platform.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 4fb0e2f..daaffca 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -70,7 +70,8 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14" + -DCMAKE_CXX_FLAGS="-stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23" + -DCMAKE_EXE_LINKER_FLAGS="-L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja # -DCMAKE_CXX_FLAGS="-stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" From 7d79b838a54d10530c5ef491eb64ebcee3b64ebe Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 13 Jun 2024 00:14:51 +0300 Subject: [PATCH 75/99] Refactor compile and link flags --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index daaffca..505854c 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -71,7 +71,7 @@ jobs: -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang -DCMAKE_CXX_FLAGS="-stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23" - -DCMAKE_EXE_LINKER_FLAGS="-L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja # -DCMAKE_CXX_FLAGS="-stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" From 97d7babafb10805aac298f68980adbfebcc485f5 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 13 Jun 2024 00:25:01 +0300 Subject: [PATCH 76/99] Refactor GitHub Actions --- .github/workflows/cmake-multi-platform.yml | 12 ++---------- .github/workflows/cpack-multi-platform.yml | 5 +++-- scripts/install-blake3.sh | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 505854c..468629b 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -13,7 +13,7 @@ jobs: matrix: os: [ ubuntu-24.04, macos-latest ] - build_type: [ Debug ] #[ Debug, Release ] + build_type: [ Debug ] c_compiler: [ clang ] include: - os: macos-latest @@ -24,14 +24,6 @@ jobs: c_compiler: clang cpp_compiler: clang++-18 - # Don't include the following configurations in the matrix -# exclude: -# - os: macos-latest -# build_type: Debug -# -# - os: ubuntu-24.04 -# build_type: Release - steps: - name: Install Dependencies if: matrix.os == 'macos-latest' @@ -66,7 +58,7 @@ jobs: - name: Configure CMake if: matrix.os == 'macos-latest' - run: > + run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang diff --git a/.github/workflows/cpack-multi-platform.yml b/.github/workflows/cpack-multi-platform.yml index 720e96c..757adf6 100644 --- a/.github/workflows/cpack-multi-platform.yml +++ b/.github/workflows/cpack-multi-platform.yml @@ -58,11 +58,12 @@ jobs: - name: Configure CMake if: matrix.os == 'macos-latest' - run: > + run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS=" -stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS="-stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23" + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja diff --git a/scripts/install-blake3.sh b/scripts/install-blake3.sh index 77c4c8a..cd740d9 100755 --- a/scripts/install-blake3.sh +++ b/scripts/install-blake3.sh @@ -40,7 +40,7 @@ install_blake3() { cd BLAKE3-1.5.1/c || error_exit "Failed to navigate to BLAKE3/c directory." - cmake -B build -DCMAKE_C_COMPILER="$C_COMPILER" -G Ninja || error_exit "Failed to run cmake." + cmake -B build -DCMAKE_C_COMPILER="$C_COMPILER" -DCMAKE_BUILD_TYPE=Release -G Ninja || error_exit "Failed to configure CMake." get_number_of_processors cmake --build build --config Release --target install -j "$NUMBER_OF_PROCESSORS" || error_exit "Failed to build and install." From 50208c59ac101257b2de547dcd776d40f45e2207 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 13 Jun 2024 23:07:37 +0300 Subject: [PATCH 77/99] Use daanx/isocline in place of readline --- CMakeLists.txt | 19 +++++- src/duplicateFinder/duplicateFinder.cppm | 15 ++--- src/encryption/encryptDecrypt.cpp | 28 +++----- src/fileShredder/fileShredder.cppm | 8 +-- src/passwordManager/passwordManager.cpp | 8 +-- src/passwordManager/passwordManager.cppm | 5 +- src/passwordManager/passwords.cpp | 22 +++---- src/utils/utils.cpp | 84 +++++++++++++++++------- src/utils/utils.cppm | 10 +-- 9 files changed, 119 insertions(+), 80 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d89da0a..b377127 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,14 +88,15 @@ target_sources(privacyShield PRIVATE # Find the required packages find_package(OpenSSL REQUIRED) find_package(Sodium REQUIRED) -find_package(Readline REQUIRED) find_package(Gcrypt REQUIRED) find_package(BLAKE3 QUIET) # See https://github.com/BLAKE3-team/BLAKE3 +include(FetchContent) + +# Fetch BLAKE3 from GitHub if it is not found if (NOT TARGET BLAKE3::blake3) message(STATUS "BLAKE3 not found. Fetching from GitHub...") - include(FetchContent) FetchContent_Declare( blake3 @@ -110,6 +111,17 @@ if (NOT TARGET BLAKE3::blake3) endif () +# Fetch Isocline from GitHub +FetchContent_Declare( + isocline + GIT_REPOSITORY https://github.com/daanx/isocline.git + GIT_TAG c9310ae58941559d761fe5d2dd2713d245f18da6 + EXCLUDE_FROM_ALL +) +FetchContent_MakeAvailable(isocline) +target_include_directories(privacyShield PRIVATE "${isocline_SOURCE_DIR}/include") +add_library(ISOCline::isocline ALIAS isocline) + # Sanitizers for debugging and testing # Requires llvm-symbolizer and sanitizer libraries (asan, ubsan, msan, tsan) if (ENABLE_SANITIZERS) @@ -137,11 +149,12 @@ endif () # Link libraries target_link_libraries(privacyShield PRIVATE + OpenSSL::Crypto - Readline::Readline Sodium::sodium Gcrypt::Gcrypt BLAKE3::blake3 + ISOCline::isocline ) # Install the binary (optional), with 0755 permissions diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index b2e5c7c..25630ac 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -93,7 +93,7 @@ inline void handleAccessError(const std::string_view filename) { /// \brief recursively traverses a directory and collects file information. /// \param directoryPath the directory to process. /// \param files a vector to store the information from the files found in the directory. -void traverseDirectory(const std::string_view directoryPath, std::vector &files) { +void traverseDirectory(const fs::path &directoryPath, std::vector &files) { std::error_code ec; for (const auto &entry: fs::recursive_directory_iterator(directoryPath, @@ -146,7 +146,7 @@ void calculateHashes(std::vector &files, const std::size_t start, cons /// \brief finds duplicate files (by content) in a directory. /// \param directoryPath the directory to process. /// \return True if duplicates are found, else False. -std::size_t findDuplicates(const std::string_view directoryPath) { +std::size_t findDuplicates(const fs::path &directoryPath) { // Collect file information std::vector files; traverseDirectory(directoryPath, files); @@ -220,16 +220,13 @@ export void duplicateFinder() { if (const int resp = getResponseInt(); resp == 1) { try { printColoredOutput('b', "Enter the path to the directory to scan:"); - std::string dirPath = getResponseStr(); - - if (const auto len = dirPath.size(); len > 1 && (dirPath.ends_with('/') || dirPath.ends_with('\\'))) - dirPath.erase(len - 1); + fs::path dirPath = getFilesystemPath(); std::error_code ec; const fs::file_status fileStatus = fs::status(dirPath, ec); if (ec) { printColoredError('y', "Unable to determine "); - printColoredError('b', "{}", dirPath); + printColoredError('b', "{}", dirPath.string()); printColoredError('y', "'s status: "); printColoredErrorln('r', "{}", ec.message()); @@ -238,12 +235,12 @@ export void duplicateFinder() { continue; } if (!exists(fileStatus)) { - printColoredError('c', "{}", dirPath); + printColoredError('c', "{}", dirPath.string()); printColoredErrorln('r', " does not exist."); continue; } if (!is_directory(fileStatus)) { - printColoredError('c', "{}", dirPath); + printColoredError('c', "{}", dirPath.string()); printColoredErrorln('r', " is not a directory."); continue; } diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index 3d800b3..6cdc3c9 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -147,15 +147,14 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope // If the output file is not specified, name it appropriately if (equivalent(fs::current_path(), outFile)) { outFile = inFile; - if (inFile.extension() == ".enc") { - outFile.replace_extension(""); - } else if (mode == OperationMode::Encryption) { - outFile += ".enc"; - } else { - outFile.replace_extension(""); - outFile += "_decrypted"; - outFile += inFile.extension(); - } + if (mode == OperationMode::Decryption) { + if (inFile.extension() == ".enc") + outFile.replace_extension(""); + else { + outFile += "_decrypted"; + outFile += inFile.extension(); + } + } else outFile += ".enc"; } else if (is_directory(outFile)) { // If the output file is a directory, rename it appropriately. if (mode == OperationMode::Encryption) { @@ -314,14 +313,8 @@ void encryptDecrypt() { }); printColoredOutputln('c', "Enter the path to the file to {}crypt:", pre_l); - std::string inputFile = getResponseStr(); + fs::path inputPath = getFilesystemPath(); - // Remove the trailing directory separator - // ('\\' is considered as well in case the program is to be extended to Windows) - if ((inputFile.ends_with('/') || inputFile.ends_with('\\')) && inputFile.size() > 1) - inputFile.erase(inputFile.size() - 1); - - fs::path inputPath(inputFile); if (!inputPath.is_absolute()) // The path should be absolute inputPath = fs::current_path() / inputPath; checkInputFile(inputPath, static_cast(choice)); @@ -329,7 +322,7 @@ void encryptDecrypt() { printColoredOutputln('c', "Enter the path to save the {}crypted file" "\n(or leave it blank to save it in the same directory):", pre_l); - fs::path outputPath{getResponseStr()}; + fs::path outputPath = getFilesystemPath(); if (!outputPath.is_absolute()) // If the path is not absolute outputPath = fs::current_path() / outputPath; checkOutputFile(inputPath, outputPath, static_cast(choice)); @@ -383,7 +376,6 @@ void encryptDecrypt() { } catch (const std::exception &ex) { printColoredError('y', "Error: "); printColoredErrorln('r', "{}", ex.what()); - std::println(""); } } else if (choice == 3) break; else printColoredErrorln('r', "Invalid choice!"); diff --git a/src/fileShredder/fileShredder.cppm b/src/fileShredder/fileShredder.cppm index 9a10ffd..2385511 100644 --- a/src/fileShredder/fileShredder.cppm +++ b/src/fileShredder/fileShredder.cppm @@ -525,12 +525,8 @@ export void fileShredder() { if (const int choice = getResponseInt("Enter your choice: "); choice == 1 || choice == 2) { try { // Get the path to the file or directory to shred - std::string path = getResponseStr(std::format("Enter the path to the {} you would like to shred:", - choice == 1 ? "file" : "directory")); - - // Remove trailing slashes - if (const auto len = path.size(); len > 1 && (path.ends_with('/') || path.ends_with('\\'))) - path.erase(len - 1); + fs::path path = getFilesystemPath(std::format("Enter the path to the {} you would like to shred:", + choice == 1 ? "file" : "directory").c_str()); std::error_code ec; const fs::file_status fileStatus = fs::status(path, ec); diff --git a/src/passwordManager/passwordManager.cpp b/src/passwordManager/passwordManager.cpp index 069f1a6..53e0a9a 100644 --- a/src/passwordManager/passwordManager.cpp +++ b/src/passwordManager/passwordManager.cpp @@ -501,7 +501,7 @@ void searchPasswords(privacy::vector &passwords, std::vector &passwords, std::vector &strengths) { - const string fileName = getResponseStr("Enter the path to the csv file: "); + const fs::path fileName = getFilesystemPath("Enter the path to the csv file: "); privacy::vector imports{importCsv(fileName)}; @@ -592,10 +592,10 @@ void exportPasswords(privacy::vector &passwords, std::vector #include #include +#include export module passwordManager; @@ -42,9 +43,9 @@ std::pair initialSetup() noexcept; privacy::string getHash(std::string_view filePath); -privacy::vector importCsv(const std::string &filePath); +privacy::vector importCsv(const std::filesystem::path &filePath); -bool exportCsv(const privacy::vector &records, std::string_view filePath = getHomeDir()); +bool exportCsv(const privacy::vector &records, const std::filesystem::path &filePath = getHomeDir()); export { privacy::string hashPassword(const privacy::string &password, diff --git a/src/passwordManager/passwords.cpp b/src/passwordManager/passwords.cpp index 66e65eb..4115526 100644 --- a/src/passwordManager/passwords.cpp +++ b/src/passwordManager/passwords.cpp @@ -453,8 +453,8 @@ std::pair initialSetup() noexcept { } if (resp == 2) { // Enter the path to an existing password file - std::string path = getResponseStr("Enter the path to the file: "); - if (!(fs::exists(path) && fs::is_regular_file(path))) { + fs::path path = getFilesystemPath("Enter the path to the file: "); + if (std::error_code ec; !(exists(path, ec) && is_regular_file(path, ec))) { std::cerr << "That file doesn't exist or is not a regular file." << std::endl; continue; } @@ -506,13 +506,13 @@ privacy::string getHash(const std::string_view filePath) { /// \brief Export the password records to a CSV file. /// \param records the password records to export. /// \param filePath the file to export to. -bool exportCsv(const privacy::vector &records, const std::string_view filePath) { - fs::path filepath(filePath); +bool exportCsv(const privacy::vector &records, const std::filesystem::path &filePath) { + fs::path filepath = filePath; std::error_code ec; // Check if the file path is valid - if (!fs::path(filepath).has_filename()) { - printColoredErrorln('r', "Invalid file path: {}", filePath); + if (!filepath.has_filename()) { + printColoredErrorln('r', "Invalid file path: {}", filePath.string()); return false; } @@ -527,7 +527,7 @@ bool exportCsv(const privacy::vector &records, const std::strin if (exists(filepath, ec)) { // Check if the file is a regular file if (!is_regular_file(filepath)) [[unlikely]] { - printColoredErrorln('r', "The destination file ({}) is not a regular file.", filePath); + printColoredErrorln('r', "The destination file ({}) is not a regular file.", filepath.string()); return false; } @@ -549,7 +549,7 @@ bool exportCsv(const privacy::vector &records, const std::strin // Open the file for writing std::ofstream file(filepath); if (!file) { - printColoredErrorln('r', "Failed to open the destination file ({}) for writing.", filePath); + printColoredErrorln('r', "Failed to open the destination file ({}) for writing.", filepath.string()); return false; } @@ -587,15 +587,15 @@ inline void trim(std::string &str) { /// Non-compliant rows will be ignored entirely. /// /// \throws std::runtime_error if the file couldn't be opened for reading. -privacy::vector importCsv(const std::string &filePath) { +privacy::vector importCsv(const fs::path &filePath) { privacy::vector passwords; - checkCommonErrors(filePath); + checkCommonErrors(filePath.string()); bool hasHeader = validateYesNo("Does the file have a header? (Skip the first line?) (y/n): "); std::ifstream file(filePath); if (!file) - throw std::runtime_error(std::format("Failed to open the file ({}) for reading.", filePath)); + throw std::runtime_error(std::format("Failed to open the file ({}) for reading.", filePath.string())); privacy::string line, value; if (hasHeader) diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 1604f96..c6c3e4e 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -16,7 +16,6 @@ module; #include -#include #include #include #include @@ -28,6 +27,8 @@ module; #include #include #include +#include +#include module utils; @@ -88,34 +89,72 @@ void stripString(StringLike auto &str) noexcept { str.erase(str.find_last_not_of(space) + 1); } -/// \brief Gets a response string from user input. -/// -/// This function prompts the user with the given prompt and reads a response string -/// from the standard input. -/// -/// \param prompt The prompt to display to the user. -/// \return The response string entered by the user if successful, else nullptr. -std::string getResponseStr(const std::string_view prompt) { - std::cout << prompt << std::endl; - char *tmp = readline("> "); - if (tmp == nullptr) return std::string{}; +/// \brief Completes a filename based on the user's input. +/// This function is used as a callback for the isocline library's readline function. +/// It provides filename completion when the user presses the Tab key. +/// \param cenv A pointer to the completion environment provided by readline. +/// \param input The user's input. +static void normal_completer(ic_completion_env_t *cenv, const char *input) { + ic_complete_filename(cenv, input, 0, nullptr, nullptr); +} - auto str = std::string{tmp}; +/// \brief A null completer function for the isocline library's readline function. +/// This function is used as a callback for the isocline library's readline function. +/// It does not provide any completion, and is used when no completion is desired. +static void null_completer(ic_completion_env_t *, const char *) {} - // Trim leading and trailing spaces - stripString(str); +std::mutex m; ///< A mutex to prevent concurrent access to the terminal. - // tmp must be freed - std::free(tmp); +/// \brief Prompts the user for a filesystem path. +/// \param prompt The prompt to display to the user. +/// \return The filesystem path entered by the user if successful, else an empty path. +fs::path getFilesystemPath(const char *prompt) { + // Lock the mutex to prevent concurrent access + std::scoped_lock lock(m); + + // Enable filename completion and automatic tab completion + ic_set_default_completer(normal_completer, nullptr); + ic_enable_auto_tab(true); + + // Display the prompt + std::puts(prompt); + // Read the input from the user + if (char *input = ic_readline("")) { + fs::path result(input); + // Free the input buffer + std::free(input); + return result; + } + return fs::path{}; +} - return str; + +/// \brief Gets a response string from user input. +/// This function prompts the user with the given prompt and reads a response string +/// from the standard input. +/// \param prompt The prompt to display to the user. +/// \return The response string entered by the user if successful, else an empty string. +std::string getResponseStr(const char *prompt) { + std::scoped_lock lock(m); + // Disable completions + ic_set_default_completer(null_completer, nullptr); + + // Read the response from the user + std::puts(prompt); + if (char *input = ic_readline("")) { + std::string result{input}; + std::free(input); + stripString(result); + return result; + } + return ""; } /// \brief Captures the user's response while offering editing capabilities. /// while the user is entering the data. /// \param prompt the prompt displayed to the user for the input. /// \return the user's input (an integer) on if it's convertible to integer, else 0. -int getResponseInt(const std::string_view prompt) { +int getResponseInt(const char *prompt) { // A lambda to convert a string to an integer constexpr auto toInt = [](const std::string_view s) noexcept -> int { int value; @@ -130,7 +169,7 @@ int getResponseInt(const std::string_view prompt) { /// \return the user's input. /// \throws std::bad_alloc if memory allocation fails. /// \throws std::runtime_error if memory locking/unlocking fails. -privacy::string getSensitiveInfo(const std::string_view prompt) { +privacy::string getSensitiveInfo(const char *prompt) { // Allocate a buffer for the password auto *buffer = static_cast(sodium_malloc(MAX_PASSPHRASE_LEN)); if (buffer == nullptr) @@ -193,7 +232,7 @@ privacy::string getSensitiveInfo(const std::string_view prompt) { /// \brief Confirms a user's response to a yes/no (y/n) situation. /// \param prompt The confirmation prompt. /// \return True if the user confirms the action, else false. -bool validateYesNo(const std::string_view prompt) { +bool validateYesNo(const char *prompt) { const std::string resp = getResponseStr(prompt); if (resp.empty()) return false; return std::tolower(resp.at(0)) == 'y'; @@ -309,7 +348,6 @@ void configureColor(const bool disable) noexcept { const auto noColorEnv = getEnv("NO_COLOR"); const auto termEnv = getEnv("TERM"); const bool suppressColor = noColorEnv.has_value() || - (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "vt100" || - termEnv.value() == "vt102")); + (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "emacs")); ColorConfig::getInstance().setSuppressColor(suppressColor); } diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 65bee81..ebdc8e6 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -200,11 +200,13 @@ export { std::endl; } + fs::path getFilesystemPath(const char* prompt = ""); + std::vector base64Decode(std::string_view encodedData); - int getResponseInt(std::string_view prompt = ""); + int getResponseInt(const char* prompt = ""); - std::string getResponseStr(std::string_view prompt = ""); + std::string getResponseStr(const char* prompt = ""); bool isWritable(const std::string &filename); @@ -214,9 +216,9 @@ export { bool copyFilePermissions(std::string_view srcFile, std::string_view destFile) noexcept; - privacy::string getSensitiveInfo(std::string_view prompt = ""); + privacy::string getSensitiveInfo(const char* prompt = ""); - bool validateYesNo(std::string_view prompt = ""); + bool validateYesNo(const char* prompt = ""); std::string getHomeDir() noexcept; From 8c7cd08b21ea62ed3faa59f8cdb8e0f525cf6556 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 13 Jun 2024 23:40:19 +0300 Subject: [PATCH 78/99] Refactor utility functions --- CMakeLists.txt | 1 - src/utils/utils.cpp | 353 ------------------------------------- src/utils/utils.cppm | 407 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 358 insertions(+), 403 deletions(-) delete mode 100644 src/utils/utils.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b377127..f01d376 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,6 @@ target_sources(privacyShield PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryptStrings.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwordManager.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwords.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/utils/utils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" ) diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp deleted file mode 100644 index c6c3e4e..0000000 --- a/src/utils/utils.cpp +++ /dev/null @@ -1,353 +0,0 @@ -// Privacy Shield: A Suite of Tools Designed to Facilitate Privacy Management. -// Copyright (C) 2024 Ian Duncan -// -// 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 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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, see https://www.gnu.org/licenses. -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -module utils; - -import secureAllocator; - -constexpr int MAX_PASSPHRASE_LEN = 1024; // Maximum length of a passphrase - -/// \brief Performs Base64 decoding of a string into binary data. -/// \param encodedData Base64 encoded string. -/// \return a vector of the decoded binary data. -/// \throws std::bad_alloc if memory allocation fails. -/// \throws std::runtime_error if the decoding operation fails. -std::vector base64Decode(const std::string_view encodedData) { - // Create a BIO object to decode the data - std::unique_ptr bio( - BIO_new_mem_buf(encodedData.data(), static_cast(encodedData.size())), &BIO_free_all); - if (bio == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Create a base64 BIO - BIO *b64 = BIO_new(BIO_f_base64()); - if (b64 == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Don't use newlines to flush buffer - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - - // Push the base64 BIO to the memory BIO - bio.reset(BIO_push(b64, bio.release())); // Transfer ownership to bio - - std::vector decodedData(encodedData.size()); - - // Decode the data - const int len = BIO_read(bio.get(), decodedData.data(), static_cast(decodedData.size())); - if (len < 0) - throw std::runtime_error("BIO_read() failed."); - - // Resize to the actual length of the decoded data - decodedData.resize(len); - - return decodedData; -} - -// This concept checks if the type provides the functionality of a string -template -concept StringLike = std::same_as >; - -/// \brief Trims space (whitespace) off the beginning and end of a string. -/// \param str the string to trim. -void stripString(StringLike auto &str) noexcept { - constexpr std::string_view space = " \t\n\r\f\v"; - - // Trim the leading space - str.erase(0, str.find_first_not_of(space)); - - // Trim the trailing space - str.erase(str.find_last_not_of(space) + 1); -} - -/// \brief Completes a filename based on the user's input. -/// This function is used as a callback for the isocline library's readline function. -/// It provides filename completion when the user presses the Tab key. -/// \param cenv A pointer to the completion environment provided by readline. -/// \param input The user's input. -static void normal_completer(ic_completion_env_t *cenv, const char *input) { - ic_complete_filename(cenv, input, 0, nullptr, nullptr); -} - -/// \brief A null completer function for the isocline library's readline function. -/// This function is used as a callback for the isocline library's readline function. -/// It does not provide any completion, and is used when no completion is desired. -static void null_completer(ic_completion_env_t *, const char *) {} - -std::mutex m; ///< A mutex to prevent concurrent access to the terminal. - -/// \brief Prompts the user for a filesystem path. -/// \param prompt The prompt to display to the user. -/// \return The filesystem path entered by the user if successful, else an empty path. -fs::path getFilesystemPath(const char *prompt) { - // Lock the mutex to prevent concurrent access - std::scoped_lock lock(m); - - // Enable filename completion and automatic tab completion - ic_set_default_completer(normal_completer, nullptr); - ic_enable_auto_tab(true); - - // Display the prompt - std::puts(prompt); - // Read the input from the user - if (char *input = ic_readline("")) { - fs::path result(input); - // Free the input buffer - std::free(input); - return result; - } - return fs::path{}; -} - - -/// \brief Gets a response string from user input. -/// This function prompts the user with the given prompt and reads a response string -/// from the standard input. -/// \param prompt The prompt to display to the user. -/// \return The response string entered by the user if successful, else an empty string. -std::string getResponseStr(const char *prompt) { - std::scoped_lock lock(m); - // Disable completions - ic_set_default_completer(null_completer, nullptr); - - // Read the response from the user - std::puts(prompt); - if (char *input = ic_readline("")) { - std::string result{input}; - std::free(input); - stripString(result); - return result; - } - return ""; -} - -/// \brief Captures the user's response while offering editing capabilities. -/// while the user is entering the data. -/// \param prompt the prompt displayed to the user for the input. -/// \return the user's input (an integer) on if it's convertible to integer, else 0. -int getResponseInt(const char *prompt) { - // A lambda to convert a string to an integer - constexpr auto toInt = [](const std::string_view s) noexcept -> int { - int value; - return std::from_chars(s.begin(), s.end(), value).ec == std::errc{} ? value : 0; - }; - - return toInt(getResponseStr(prompt)); -} - -/// \brief Reads sensitive input from a terminal without echoing them. -/// \param prompt the prompt to display. -/// \return the user's input. -/// \throws std::bad_alloc if memory allocation fails. -/// \throws std::runtime_error if memory locking/unlocking fails. -privacy::string getSensitiveInfo(const char *prompt) { - // Allocate a buffer for the password - auto *buffer = static_cast(sodium_malloc(MAX_PASSPHRASE_LEN)); - if (buffer == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Lock the memory to prevent swapping - if (sodium_mlock(buffer, MAX_PASSPHRASE_LEN) == -1) { - sodium_free(buffer); - throw std::runtime_error("Failed to lock memory."); - } - - // Turn off terminal echoing - termios oldSettings{}, newSettings{}; - - tcgetattr(STDIN_FILENO, &oldSettings); - newSettings = oldSettings; - newSettings.c_lflag &= ~ECHO; - tcsetattr(STDIN_FILENO, TCSANOW, &newSettings); - - // Prompt the user for the password - std::cout << prompt; - - int index = 0; // current position in the buffer - char ch; - while (std::cin.get(ch) && ch != '\n') { - // check for backspace - if (ch == '\b') { - if (index > 0) { - --index; // move back one position in the buffer - } - } else { - // Check if buffer is not full - if (index < MAX_PASSPHRASE_LEN - 1) { - buffer[index++] = ch; - } - } - } - buffer[index] = '\0'; // Null-terminate the string - - // Restore terminal settings - tcsetattr(STDIN_FILENO, TCSANOW, &oldSettings); - - privacy::string passphrase{buffer}; - - // Unlock the memory - if (sodium_munlock(buffer, MAX_PASSPHRASE_LEN) == -1) - throw std::runtime_error("Failed to unlock memory."); - - // Free the buffer - sodium_free(buffer); - - // Trim leading and trailing spaces - stripString(passphrase); - - std::cout << std::endl; - - return passphrase; -} - -/// \brief Confirms a user's response to a yes/no (y/n) situation. -/// \param prompt The confirmation prompt. -/// \return True if the user confirms the action, else false. -bool validateYesNo(const char *prompt) { - const std::string resp = getResponseStr(prompt); - if (resp.empty()) return false; - return std::tolower(resp.at(0)) == 'y'; -} - -/// \brief Checks if an existing file grants write permissions. -/// to the current user. -/// \param filename the path to the file. -/// \return true if the current user has write permissions, else false. -bool isWritable(const std::string &filename) { - return access(filename.c_str(), F_OK | W_OK) == 0; -} - -/// \brief Checks if an existing file grants read permissions. -/// to the current user. -/// \param filename the path to the file. -/// \return true if the current user has read permissions, else false. -bool isReadable(const std::string &filename) { - return access(filename.c_str(), F_OK | R_OK) == 0; -} - -/// \brief Checks the available space on disk. -/// \param path The path to check. -/// \return The available space in bytes. -/// -/// \warning This function does not throw, and returns 0 in case of an error. -/// \note This function is meant to be used to detect possible errors -/// early enough before file operations, and to warn the user to -/// check their filesystem storage space when it seems insufficient. -std::uintmax_t getAvailableSpace(const fs::path &path) noexcept { - fs::path filePath{path}; - - std::error_code ec; // For ignoring errors to avoid throwing - - // Find an existing component of the path - while ((!exists(filePath, ec)) && filePath.has_parent_path()) - filePath = filePath.parent_path(); - if (ec) ec.clear(); - - auto [capacity, free, available] = space(canonical(filePath, ec), ec); - - // Return 0 in case of an error - return std::cmp_less(available, 0) || std::cmp_equal(available, UINTMAX_MAX) ? 0 : available; -} - - -/// \brief Copies a file's permissions to another, replacing if necessary. -/// \param srcFile The source file. -/// \param destFile The destination file. -/// \return True if the operation is successful, else false. -/// -/// \note This function is only needed for the preservation of file permissions -/// during encryption and decryption. -bool copyFilePermissions(const std::string_view srcFile, const std::string_view destFile) noexcept { - std::error_code ec; - // Get the permissions of the input file - const auto permissions = fs::status(srcFile, ec).permissions(); - if (ec) return false; - - // Set the permissions to the output file - fs::permissions(destFile, permissions, fs::perm_options::replace, ec); - if (ec) return false; - - return true; -} - -/// \brief Gets the value of an environment variable. -/// \param var an environment variable to query. -/// \return the value of the environment variable if it exists, else nullopt (nothing). -/// \note The returned value MUST be checked before access. -std::optional getEnv(const char *const var) { - // Use secure_getenv() if available -#if _GNU_SOURCE - if (const char *value = secure_getenv(var)) - return value; -#else - if (const char *value = std::getenv(var)) - return value; -#endif - return std::nullopt; -} - -/// \brief Retrieves the user's home directory -/// \return The home directory read from {'HOME', 'USERPROFILE'} -/// environment variables, else the current working directory (or an empty -/// string if the current directory couldn't be determined). -std::string getHomeDir() noexcept { - std::error_code ec; - // Try to get the home directory from the environment variables - if (const auto envHome = getEnv("HOME"); envHome) - return *envHome; - if (const auto envUserProfile = getEnv("USERPROFILE"); envUserProfile) - return *envUserProfile; - - // If the environment variables are not set, use the current working directory - std::cerr << "\nCouldn't find your home directory, using the current working directory instead.." << std::endl; - - std::string currentDir = std::filesystem::current_path(ec); - if (ec) std::cerr << ec.message() << std::endl; - - return currentDir; -} - -/// \brief Configures the color output of the terminal. -/// \param disable a flag to indicate whether color output should be disabled. -void configureColor(const bool disable) noexcept { - // Check if the user has requested no color - if (disable) { - ColorConfig::getInstance().setSuppressColor(true); - return; - } - // Process the environment variable to suppress color output - const auto noColorEnv = getEnv("NO_COLOR"); - const auto termEnv = getEnv("TERM"); - const bool suppressColor = noColorEnv.has_value() || - (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "emacs")); - ColorConfig::getInstance().setSuppressColor(suppressColor); -} diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index ebdc8e6..9fd2282 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -16,13 +16,20 @@ module; +#include +#include +#include +#include #include #include #include -#include -#include #include #include +#include +#include +#include +#include +#include export module utils; @@ -30,6 +37,10 @@ import secureAllocator; namespace fs = std::filesystem; +constexpr int MAX_PASSPHRASE_LEN = 1024; ///< Maximum length of a passphrase +std::mutex termutex; ///< A mutex to prevent concurrent access to the terminal. + + /// \class ColorConfig /// \brief A singleton class used to manage the color configuration of the terminal output. @@ -108,46 +119,40 @@ constexpr const char *getColorCode(const char color) noexcept { } } -export { - /// \brief Performs Base64 encoding of binary data into a string. - /// \param input a vector of the binary data to be encoded. - /// \return Base64-encoded string. - /// \throws std::bad_alloc if memory allocation fails. - /// \throws std::runtime_error if encoding fails. - std::string base64Encode(const uCharVector auto &input) { - // Create a BIO object to encode the data - const std::unique_ptr b64(BIO_new(BIO_f_base64()), &BIO_free_all); - if (b64 == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Create a memory BIO to store the encoded data - BIO *bio = BIO_new(BIO_s_mem()); - if (bio == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Don't use newlines to flush buffer - BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL); - - // Push the memory BIO to the base64 BIO - bio = BIO_push(b64.get(), bio); // Transfer ownership to b64 +/// \brief Completes a filename based on the user's input. +/// This function is used as a callback for the isocline library's readline function. +/// It provides filename completion when the user presses the Tab key. +/// \param cenv A pointer to the completion environment provided by readline. +/// \param input The user's input. +static void normal_completer(ic_completion_env_t *cenv, const char *input) { + ic_complete_filename(cenv, input, 0, nullptr, nullptr); +} - // Write the data to the BIO - if (BIO_write(bio, input.data(), static_cast(input.size())) < 0) - throw std::runtime_error("BIO_write() failed."); +/// \brief A null completer function for the isocline library's readline function. +/// This function is used as a callback for the isocline library's readline function. +/// It does not provide any completion, and is used when no completion is desired. +static void null_completer(ic_completion_env_t *, const char *) { +} - // Flush the BIO - BIO_flush(bio); +/// \brief This concept checks if a type provides the functionality of a string +/// \tparam T The type to check. +template +concept StringLike = std::same_as >; - // Get the pointer to the BIO's data - BUF_MEM *bufferPtr; - BIO_get_mem_ptr(b64.get(), &bufferPtr); +/// \brief Trims space (whitespace) off the beginning and end of a string. +/// \param str the string to trim. +void stripString(StringLike auto &str) noexcept { + constexpr std::string_view space = " \t\n\r\f\v"; - // Create a string from the data - std::string encodedData(bufferPtr->data, bufferPtr->length); + // Trim the leading space + str.erase(0, str.find_first_not_of(space)); - return encodedData; - } + // Trim the trailing space + str.erase(str.find_last_not_of(space) + 1); +} +export { /// \brief Prints colored output to the console. /// \tparam Args Variadic template for all types of arguments that can be passed. /// \param color The color code for the output. @@ -155,6 +160,10 @@ export { /// \param args The arguments to be printed. template void printColoredOutput(const char color, std::format_string fmt, Args &&... args) { + // Lock the mutex to prevent concurrent access + std::scoped_lock lock(termutex); + + // Print the output depending on the color configuration if (ColorConfig::getInstance().getSuppressColor()) std::cout << std::vformat(fmt.get(), std::make_format_args(args...)); else std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; @@ -167,6 +176,8 @@ export { /// \param args The arguments to be printed. template void printColoredOutputln(const char color, std::format_string fmt, Args &&... args) { + std::scoped_lock lock(termutex); + if (ColorConfig::getInstance().getSuppressColor()) std::cout << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; else @@ -181,6 +192,8 @@ export { /// \param args The arguments to be printed. template void printColoredError(const char color, std::format_string fmt, Args &&... args) { + std::scoped_lock lock(termutex); + if (ColorConfig::getInstance().getSuppressColor()) std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)); else std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; @@ -193,6 +206,8 @@ export { /// \param args The arguments to be printed. template void printColoredErrorln(const char color, std::format_string fmt, Args &&... args) { + std::scoped_lock lock(termutex); + if (ColorConfig::getInstance().getSuppressColor()) std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; else @@ -200,29 +215,323 @@ export { std::endl; } - fs::path getFilesystemPath(const char* prompt = ""); + /// \brief Performs Base64 encoding of binary data into a string. + /// \param input a vector of the binary data to be encoded. + /// \return Base64-encoded string. + /// \throws std::bad_alloc if memory allocation fails. + /// \throws std::runtime_error if encoding fails. + std::string base64Encode(const uCharVector auto &input) { + // Create a BIO object to encode the data + const std::unique_ptr b64(BIO_new(BIO_f_base64()), &BIO_free_all); + if (b64 == nullptr) + throw std::bad_alloc(); // Memory allocation failed - std::vector base64Decode(std::string_view encodedData); + // Create a memory BIO to store the encoded data + BIO *bio = BIO_new(BIO_s_mem()); + if (bio == nullptr) + throw std::bad_alloc(); // Memory allocation failed + + // Don't use newlines to flush buffer + BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL); + + // Push the memory BIO to the base64 BIO + bio = BIO_push(b64.get(), bio); // Transfer ownership to b64 + + // Write the data to the BIO + if (BIO_write(bio, input.data(), static_cast(input.size())) < 0) + throw std::runtime_error("BIO_write() failed."); + + // Flush the BIO + BIO_flush(bio); + + // Get the pointer to the BIO's data + BUF_MEM *bufferPtr; + BIO_get_mem_ptr(b64.get(), &bufferPtr); - int getResponseInt(const char* prompt = ""); + // Create a string from the data + std::string encodedData(bufferPtr->data, bufferPtr->length); - std::string getResponseStr(const char* prompt = ""); + return encodedData; + } - bool isWritable(const std::string &filename); + /// \brief Prompts the user for a filesystem path. + /// \param prompt The prompt to display to the user. + /// \return The filesystem path entered by the user if successful, else an empty path. + fs::path getFilesystemPath(const char *prompt = "") { + // Lock the mutex to prevent concurrent access + std::scoped_lock lock(termutex); + + // Enable filename completion and automatic tab completion + ic_set_default_completer(normal_completer, nullptr); + ic_enable_auto_tab(true); + + // Display the prompt + std::puts(prompt); + // Read the input from the user + if (char *input = ic_readline("")) { + fs::path result(input); + // Free the input buffer + std::free(input); + return result; + } + return fs::path{}; + } - bool isReadable(const std::string &filename); + /// \brief Performs Base64 decoding of a string into binary data. + /// \param encodedData Base64 encoded string. + /// \return a vector of the decoded binary data. + /// \throws std::bad_alloc if memory allocation fails. + /// \throws std::runtime_error if the decoding operation fails. + std::vector base64Decode(const std::string_view encodedData) { + // Create a BIO object to decode the data + std::unique_ptr bio( + BIO_new_mem_buf(encodedData.data(), static_cast(encodedData.size())), &BIO_free_all); + if (bio == nullptr) + throw std::bad_alloc(); // Memory allocation failed - std::uintmax_t getAvailableSpace(const fs::path &path) noexcept; + // Create a base64 BIO + BIO *b64 = BIO_new(BIO_f_base64()); + if (b64 == nullptr) + throw std::bad_alloc(); // Memory allocation failed - bool copyFilePermissions(std::string_view srcFile, std::string_view destFile) noexcept; + // Don't use newlines to flush buffer + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - privacy::string getSensitiveInfo(const char* prompt = ""); + // Push the base64 BIO to the memory BIO + bio.reset(BIO_push(b64, bio.release())); // Transfer ownership to bio - bool validateYesNo(const char* prompt = ""); + std::vector decodedData(encodedData.size()); - std::string getHomeDir() noexcept; + // Decode the data + const int len = BIO_read(bio.get(), decodedData.data(), static_cast(decodedData.size())); + if (len < 0) + throw std::runtime_error("BIO_read() failed."); - std::optional getEnv(const char *var); + // Resize to the actual length of the decoded data + decodedData.resize(len); - void configureColor(bool disable = false) noexcept; + return decodedData; + } + + /// \brief Gets a response string from user input. + /// This function prompts the user with the given prompt and reads a response string + /// from the standard input. + /// \param prompt The prompt to display to the user. + /// \return The response string entered by the user if successful, else an empty string. + std::string getResponseStr(const char *prompt = "") { + std::scoped_lock lock(termutex); + // Disable completions + ic_set_default_completer(null_completer, nullptr); + + // Read the response from the user + std::puts(prompt); + if (char *input = ic_readline("")) { + std::string result{input}; + std::free(input); + stripString(result); + return result; + } + return ""; + } + + /// \brief Captures the user's response while offering editing capabilities. + /// while the user is entering the data. + /// \param prompt the prompt displayed to the user for the input. + /// \return the user's input (an integer) on if it's convertible to integer, else 0. + int getResponseInt(const char *prompt = "") { + // A lambda to convert a string to an integer + constexpr auto toInt = [](const std::string_view s) noexcept -> int { + int value; + return std::from_chars(s.begin(), s.end(), value).ec == std::errc{} ? value : 0; + }; + + return toInt(getResponseStr(prompt)); + } + + /// \brief Reads sensitive input from a terminal without echoing them. + /// \param prompt the prompt to display. + /// \return the user's input. + /// \throws std::bad_alloc if memory allocation fails. + /// \throws std::runtime_error if memory locking/unlocking fails. + privacy::string getSensitiveInfo(const char *prompt = "") { + // Allocate a buffer for the password + auto *buffer = static_cast(sodium_malloc(MAX_PASSPHRASE_LEN)); + if (buffer == nullptr) + throw std::bad_alloc(); // Memory allocation failed + + // Lock the memory to prevent swapping + if (sodium_mlock(buffer, MAX_PASSPHRASE_LEN) == -1) { + sodium_free(buffer); + throw std::runtime_error("Failed to lock memory."); + } + + // Turn off terminal echoing + termios oldSettings{}, newSettings{}; + + tcgetattr(STDIN_FILENO, &oldSettings); + newSettings = oldSettings; + newSettings.c_lflag &= ~ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &newSettings); + + // Prompt the user for the password + std::cout << prompt; + + int index = 0; // current position in the buffer + char ch; + while (std::cin.get(ch) && ch != '\n') { + // check for backspace + if (ch == '\b') { + if (index > 0) { + --index; // move back one position in the buffer + } + } else { + // Check if buffer is not full + if (index < MAX_PASSPHRASE_LEN - 1) { + buffer[index++] = ch; + } + } + } + buffer[index] = '\0'; // Null-terminate the string + + // Restore terminal settings + tcsetattr(STDIN_FILENO, TCSANOW, &oldSettings); + + privacy::string passphrase{buffer}; + + // Unlock the memory + if (sodium_munlock(buffer, MAX_PASSPHRASE_LEN) == -1) + throw std::runtime_error("Failed to unlock memory."); + + // Free the buffer + sodium_free(buffer); + + // Trim leading and trailing spaces + stripString(passphrase); + + std::cout << std::endl; + + return passphrase; + } + + /// \brief Checks if an existing file grants write permissions. + /// to the current user. + /// \param filename the path to the file. + /// \return true if the current user has write permissions, else false. + bool isWritable(const std::string &filename) { + return access(filename.c_str(), F_OK | W_OK) == 0; + } + + /// \brief Checks if an existing file grants read permissions. + /// to the current user. + /// \param filename the path to the file. + /// \return true if the current user has read permissions, else false. + bool isReadable(const std::string &filename) { + return access(filename.c_str(), F_OK | R_OK) == 0; + } + + /// \brief Checks the available space on disk. + /// \param path The path to check. + /// \return The available space in bytes. + /// + /// \warning This function does not throw, and returns 0 in case of an error. + /// \note This function is meant to be used to detect possible errors + /// early enough before file operations, and to warn the user to + /// check their filesystem storage space when it seems insufficient. + std::uintmax_t getAvailableSpace(const fs::path &path) noexcept { + fs::path filePath{path}; + + std::error_code ec; // For ignoring errors to avoid throwing + + // Find an existing component of the path + while ((!exists(filePath, ec)) && filePath.has_parent_path()) + filePath = filePath.parent_path(); + if (ec) ec.clear(); + + auto [capacity, free, available] = space(canonical(filePath, ec), ec); + + // Return 0 in case of an error + return std::cmp_less(available, 0) || std::cmp_equal(available, UINTMAX_MAX) ? 0 : available; + } + + /// \brief Copies a file's permissions to another, replacing if necessary. + /// \param srcFile The source file. + /// \param destFile The destination file. + /// \return True if the operation is successful, else false. + /// + /// \note This function is only needed for the preservation of file permissions + /// during encryption and decryption. + bool copyFilePermissions(const std::string_view srcFile, const std::string_view destFile) noexcept { + std::error_code ec; + // Get the permissions of the input file + const auto permissions = fs::status(srcFile, ec).permissions(); + if (ec) return false; + + // Set the permissions to the output file + fs::permissions(destFile, permissions, fs::perm_options::replace, ec); + if (ec) return false; + + return true; + } + + /// \brief Confirms a user's response to a yes/no (y/n) situation. + /// \param prompt The confirmation prompt. + /// \return True if the user confirms the action, else false. + bool validateYesNo(const char *prompt = "") { + const std::string resp = getResponseStr(prompt); + if (resp.empty()) return false; + return std::tolower(resp.at(0)) == 'y'; + } + + /// \brief Gets the value of an environment variable. + /// \param var an environment variable to query. + /// \return the value of the environment variable if it exists, else nullopt (nothing). + /// \note The returned value MUST be checked before access. + std::optional getEnv(const char *const var) { + // Use secure_getenv() if available +#if _GNU_SOURCE + if (const char *value = secure_getenv(var)) + return value; +#else + if (const char *value = std::getenv(var)) + return value; +#endif + return std::nullopt; + } + + /// \brief Retrieves the user's home directory + /// \return The home directory read from {'HOME', 'USERPROFILE'} + /// environment variables, else the current working directory (or an empty + /// string if the current directory couldn't be determined). + std::string getHomeDir() noexcept { + std::error_code ec; + // Try to get the home directory from the environment variables + if (const auto envHome = getEnv("HOME"); envHome) + return *envHome; + if (const auto envUserProfile = getEnv("USERPROFILE"); envUserProfile) + return *envUserProfile; + + // If the environment variables are not set, use the current working directory + std::cerr << "\nCouldn't find your home directory, using the current working directory instead.." << std::endl; + + std::string currentDir = std::filesystem::current_path(ec); + if (ec) std::cerr << ec.message() << std::endl; + + return currentDir; + } + + /// \brief Configures the color output of the terminal. + /// \param disable a flag to indicate whether color output should be disabled. + void configureColor(const bool disable = false) noexcept { + // Check if the user has requested no color + if (disable) { + ColorConfig::getInstance().setSuppressColor(true); + return; + } + // Process the environment variable to suppress color output + const auto noColorEnv = getEnv("NO_COLOR"); + const auto termEnv = getEnv("TERM"); + const bool suppressColor = noColorEnv.has_value() || + (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "emacs")); + ColorConfig::getInstance().setSuppressColor(suppressColor); + } } From dfa83d911465d7e4b01dc5ca6fd1009f643822f0 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 14 Jun 2024 00:49:50 +0300 Subject: [PATCH 79/99] Update documentation --- README.md | 15 ++++++++++----- media/isocline.svg | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 media/isocline.svg diff --git a/README.md b/README.md index 63a529d..5c7f060 100644 --- a/README.md +++ b/README.md @@ -350,14 +350,14 @@ operating system, such as [Linux](https://en.wikipedia.org/wiki/Linux), * A C++ compiler with [C++23](https://en.cppreference.com/w/cpp/23) support, and [C++20 Modules](https://en.cppreference.com/w/cpp/language/modules) support. For this project, [GCC 14](https://gcc.gnu.org/gcc-14/) (or newer), -or [LLVM Clang 17](https://clang.llvm.org/) (or newer) is required. +or [LLVM Clang 18](https://clang.llvm.org/) (or newer) is required. * [CMake](https://cmake.org/) 3.28+ * [Ninja](https://ninja-build.org/) 1.11+, or any other build system compatible with CMake and **C++20 Modules**. * [OpenSSL](https://www.openssl.org/) 3+ * [Sodium](https://libsodium.org/) 1.0.18+ * [GCrypt](https://gnupg.org/software/libgcrypt/index.html) 1.10+ -* [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) 1.4+ (see the note below) -* [GNU Readline](https://tiswww.case.edu/php/chet/readline/rltop.html) 8+ +* [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) 1.4+ (Fetched automatically by CMake, if not already installed) +* [Isocline](https://github.com/daanx/isocline) (Fetched automatically by CMake) **Note:**\ This project utilizes the [C++20 Modules](https://en.cppreference.com/w/cpp/language/modules) feature, @@ -433,6 +433,9 @@ The package will contain the built executable, and you can install it using the For the macOS package, you can simply drag the .dmg file to your Applications folder. +The current macOS package was built on macOS 14.5 arm64 (M1 chip), +and might not work on older versions of macOS. + For the Linux package, you can install the .deb or .rpm file using the package manager of your distribution.\ Internet connection might be required to install the dependencies. @@ -491,6 +494,8 @@ There is no need to remember commands or arguments, as the CLI will guide you th To use the CLI, simply run the program by typing `privacyShield` in your terminal. +**Tab completion is supported** for most input fields. + ## Contributing Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, @@ -532,11 +537,11 @@ However, the feeling of empowered privacy protection is a strong possibility! ![""](./media/blank.svg) -[![Readline](./media/Heckert_GNU_white.svg)](https://tiswww.case.edu/php/chet/readline/rltop.html) +[![CMake](./media/Cmake.svg)](https://cmake.org/) ![""](./media/blank.svg) -[![CMake](./media/Cmake.svg)](https://cmake.org/) +[![Isocline](./media/isocline.svg)](https://github.com/daanx/isocline) ## License diff --git a/media/isocline.svg b/media/isocline.svg new file mode 100644 index 0000000..f6c19db --- /dev/null +++ b/media/isocline.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + From badf82cfa32c3d48ab3e3771d57cac98a4932fbf Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 14 Jun 2024 00:54:37 +0300 Subject: [PATCH 80/99] Avoid Readline installation on GitHub Actions --- .github/workflows/cmake-multi-platform.yml | 2 +- .github/workflows/cpack-multi-platform.yml | 2 +- scripts/build-functions.sh | 2 +- scripts/prepare-qodana.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 468629b..3dd4c2a 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -30,7 +30,7 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install gcc readline ninja cmake git + brew install gcc ninja cmake git brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile diff --git a/.github/workflows/cpack-multi-platform.yml b/.github/workflows/cpack-multi-platform.yml index 757adf6..bc275f9 100644 --- a/.github/workflows/cpack-multi-platform.yml +++ b/.github/workflows/cpack-multi-platform.yml @@ -30,7 +30,7 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install gcc readline ninja cmake git + brew install gcc ninja cmake git brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile diff --git a/scripts/build-functions.sh b/scripts/build-functions.sh index 95c5062..172956a 100755 --- a/scripts/build-functions.sh +++ b/scripts/build-functions.sh @@ -21,7 +21,7 @@ function install_dependencies() { # add-apt-repository -y ppa:ubuntu-toolchain-r/ppa apt update export NEEDRESTART_SUSPEND=1 - apt install -y wget unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libreadline8 libreadline-dev libsodium23 libsodium-dev + apt install -y wget unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libsodium23 libsodium-dev # Install CMake 3.29.3 if dpkg -s "cmake" >/dev/null 2>&1; then diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh index 4eee182..96906cf 100755 --- a/scripts/prepare-qodana.sh +++ b/scripts/prepare-qodana.sh @@ -14,7 +14,7 @@ build_install_gcc_14 # Install dependencies apt remove -y --purge --auto-remove llvm-16-dev clang-16 clang-tidy-16 clang-format-16 lld-16 libc++-16-dev libc++abi-16-dev -apt update && apt install -y software-properties-common wget unzip build-essential openssl libreadline8 libreadline-dev libsodium23 libsodium-dev libgcrypt20-dev +apt update && apt install -y software-properties-common wget unzip build-essential openssl libsodium23 libsodium-dev libgcrypt20-dev wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc add-apt-repository -y "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" apt update From 0c11911c6d8f2e0cc508cb288bde1b7b5e40ae4b Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 14 Jun 2024 03:35:52 +0300 Subject: [PATCH 81/99] Dynamically set up gcc and libstdc++ on macOS --- .github/workflows/cmake-multi-platform.yml | 17 ++++++++--- .github/workflows/cpack-multi-platform.yml | 17 ++++++++--- scripts/search.py | 33 ++++++++++++++++++++++ scripts/search.sh | 24 ++++++++++++++++ 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100755 scripts/search.py create mode 100755 scripts/search.sh diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 3dd4c2a..97d49cd 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -19,6 +19,8 @@ jobs: - os: macos-latest c_compiler: clang cpp_compiler: clang++-18 + env: + GCC_MAJOR: 14 - os: ubuntu-24.04 c_compiler: clang @@ -30,11 +32,11 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install gcc ninja cmake git + brew install ninja cmake git "gcc@${{ env.GCC_MAJOR }}" brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/${{ env.GCC_MAJOR }}:$PATH"' >> ~/.bash_profile . ~/.bash_profile - uses: actions/checkout@v4 @@ -42,8 +44,15 @@ jobs: - name: Set reusable strings id: strings shell: bash + working-directory: ${{ github.workspace }} run: | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + # Set the paths to the GCC include and lib directories on macOS + if [ "${{ matrix.os }}" == "macos-latest" ]; then + echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" + echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/lib/gcc/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + fi # Build the project - name: Build PrivacyShield @@ -62,8 +71,8 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS="-stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23" - -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS="-stdlib++-isystem ${{ steps.strings.outputs.gcc-include-dir }} -cxx-isystem ${{ steps.strings.outputs.gcc-sys-include-dir }}" + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L ${{ steps.strings.outputs.gcc-lib-dir }} -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja # -DCMAKE_CXX_FLAGS="-stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" diff --git a/.github/workflows/cpack-multi-platform.yml b/.github/workflows/cpack-multi-platform.yml index bc275f9..a5f3448 100644 --- a/.github/workflows/cpack-multi-platform.yml +++ b/.github/workflows/cpack-multi-platform.yml @@ -19,6 +19,8 @@ jobs: - os: macos-latest c_compiler: clang cpp_compiler: clang++-18 + env: + GCC_MAJOR: 14 - os: ubuntu-24.04 c_compiler: clang @@ -30,11 +32,11 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install gcc ninja cmake git + brew install ninja cmake git "gcc@${{ env.GCC_MAJOR }}" brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/14:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/${{ env.GCC_MAJOR }}:$PATH"' >> ~/.bash_profile . ~/.bash_profile - uses: actions/checkout@v4 @@ -42,8 +44,15 @@ jobs: - name: Set reusable strings id: strings shell: bash + working-directory: ${{ github.workspace }} run: | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + # Set the paths to the GCC include and lib directories on macOS + if [ "${{ matrix.os }}" == "macos-latest" ]; then + echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" + echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/lib/gcc/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + fi # Build the project - name: Build PrivacyShield @@ -62,8 +71,8 @@ jobs: cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS="-stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23" - -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_CXX_FLAGS="-stdlib++-isystem ${{ steps.strings.outputs.gcc-include-dir }} -cxx-isystem ${{ steps.strings.outputs.gcc-sys-include-dir }}" + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L ${{ steps.strings.outputs.gcc-lib-dir }} -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja diff --git a/scripts/search.py b/scripts/search.py new file mode 100755 index 0000000..433c486 --- /dev/null +++ b/scripts/search.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import sys +import glob + + +def search_filesystem(pattern: str) -> None: + """ + Search the filesystem for files or directories that match the pattern + :param pattern: The pattern to search for + :return: None + """ + # Find files or directories that match the pattern + matches = glob.glob(pattern, recursive=True) + + # Check if there are any matches + if not matches: + print(f"No matches found for pattern: {pattern}") + else: + # Sort matches in reverse order + matches.sort(reverse=True) + + # Select the first match (after sorting in reverse order) + print(matches[0]) + + +if __name__ == "__main__": + # Check if exactly one argument is provided + if len(sys.argv) != 2: + print("Usage: python3 search.py ") + sys.exit(1) + + search_filesystem(sys.argv[1]) diff --git a/scripts/search.sh b/scripts/search.sh new file mode 100755 index 0000000..b2848cc --- /dev/null +++ b/scripts/search.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# This script is used to search for files or directories that match a pattern. +# It searches recursively and returns the first match found. + +# Check if exactly one argument is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +pattern=$1 + +# Find files or directories that match the pattern and store them in an array +IFS=$'\n' read -d '' -r -a matches < <(find $(dirname "$pattern") -name "$(basename "$pattern")" | sort -r 2>/dev/null) + +# Check if any matches were found +if [ ${#matches[@]} -eq 0 ]; then + echo "No matches found." + exit 1 +else + # Print the first match + echo "${matches[0]}" +fi From f8704b514dca22bfa6cfacbddbb5de9319cd65a4 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 14 Jun 2024 03:46:29 +0300 Subject: [PATCH 82/99] CI env bug fix --- .github/workflows/cmake-multi-platform.yml | 5 +++-- .github/workflows/cpack-multi-platform.yml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 97d49cd..7e10413 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -7,6 +7,8 @@ on: jobs: build: runs-on: ${{ matrix.os }} + env: + GCC_MAJOR: 14 strategy: fail-fast: false @@ -19,8 +21,6 @@ jobs: - os: macos-latest c_compiler: clang cpp_compiler: clang++-18 - env: - GCC_MAJOR: 14 - os: ubuntu-24.04 c_compiler: clang @@ -30,6 +30,7 @@ jobs: - name: Install Dependencies if: matrix.os == 'macos-latest' run: | + echo "GCC_MAJOR: ${{ env.GCC_MAJOR }}" export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update brew install ninja cmake git "gcc@${{ env.GCC_MAJOR }}" diff --git a/.github/workflows/cpack-multi-platform.yml b/.github/workflows/cpack-multi-platform.yml index a5f3448..f09123d 100644 --- a/.github/workflows/cpack-multi-platform.yml +++ b/.github/workflows/cpack-multi-platform.yml @@ -7,6 +7,8 @@ on: jobs: build_then_package: runs-on: ${{ matrix.os }} + env: + GCC_MAJOR: 14 strategy: fail-fast: false @@ -19,8 +21,6 @@ jobs: - os: macos-latest c_compiler: clang cpp_compiler: clang++-18 - env: - GCC_MAJOR: 14 - os: ubuntu-24.04 c_compiler: clang From 5db7950b58387ccf7febb3956ef044ff741eebfa Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:02:29 +0300 Subject: [PATCH 83/99] CI env bug fix --- .github/workflows/cmake-multi-platform.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 7e10413..0cc8293 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -30,10 +30,9 @@ jobs: - name: Install Dependencies if: matrix.os == 'macos-latest' run: | - echo "GCC_MAJOR: ${{ env.GCC_MAJOR }}" export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install ninja cmake git "gcc@${{ env.GCC_MAJOR }}" + brew install ninja cmake git gcc@${{ env.GCC_MAJOR }} brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile @@ -50,9 +49,9 @@ jobs: echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" # Set the paths to the GCC include and lib directories on macOS if [ "${{ matrix.os }}" == "macos-latest" ]; then - echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" - echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" - echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/lib/gcc/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" + echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/lib/gcc/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" fi # Build the project From 69b8f78898383aa113bc443a8d217149035808bf Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:06:55 +0300 Subject: [PATCH 84/99] CI env bug fix --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 0cc8293..10a338f 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -51,7 +51,7 @@ jobs: if [ "${{ matrix.os }}" == "macos-latest" ]; then echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" - echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/lib/gcc/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/lib/gcc/current")" >> "$GITHUB_OUTPUT" fi # Build the project From 6c66717fa1f705cfe995dcc1a45a3ef97db0e103 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:11:31 +0300 Subject: [PATCH 85/99] Packaging ci env bug fix --- .github/workflows/cpack-multi-platform.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cpack-multi-platform.yml b/.github/workflows/cpack-multi-platform.yml index f09123d..c5d4608 100644 --- a/.github/workflows/cpack-multi-platform.yml +++ b/.github/workflows/cpack-multi-platform.yml @@ -32,7 +32,7 @@ jobs: run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install ninja cmake git "gcc@${{ env.GCC_MAJOR }}" + brew install ninja cmake git gcc@${{ env.GCC_MAJOR }} brew reinstall llvm echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile @@ -51,7 +51,7 @@ jobs: if [ "${{ matrix.os }}" == "macos-latest" ]; then echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" - echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/lib/gcc/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/lib/gcc/current")" >> "$GITHUB_OUTPUT" fi # Build the project From 97892af3c91748b93eb6e5e9a586b63bfba78877 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Sun, 16 Jun 2024 23:47:35 +0300 Subject: [PATCH 86/99] Use mimalloc as the main memory allocator --- CMakeLists.txt | 53 ++-- src/duplicateFinder/duplicateFinder.cppm | 25 +- src/encryption/encryptDecrypt.cpp | 25 +- src/encryption/encryptFiles.cpp | 31 +-- src/encryption/encryptStrings.cpp | 9 +- src/encryption/encryption.cppm | 17 +- src/fileShredder/fileShredder.cppm | 37 +-- src/main.cpp | 1 + src/mimallocSTL.cppm | 294 +++++++++++++++++++++++ src/passwordManager/FuzzyMatcher.cppm | 9 +- src/passwordManager/passwordManager.cpp | 37 +-- src/passwordManager/passwordManager.cppm | 3 +- src/passwordManager/passwords.cpp | 27 ++- src/privacyTracks/privacyTracks.cppm | 21 +- src/secureAllocator.cppm | 16 +- src/utils/utils.cppm | 25 +- 16 files changed, 479 insertions(+), 151 deletions(-) create mode 100644 src/mimallocSTL.cppm diff --git a/CMakeLists.txt b/CMakeLists.txt index f01d376..4148bb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,26 +62,27 @@ add_executable(privacyShield) # Add sources for the target target_sources(privacyShield PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryptDecrypt.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryptFiles.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryptStrings.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwordManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwords.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" + src/encryption/encryptDecrypt.cpp + src/encryption/encryptFiles.cpp + src/encryption/encryptStrings.cpp + src/passwordManager/passwordManager.cpp + src/passwordManager/passwords.cpp + src/main.cpp ) # C++20 Modules target_sources(privacyShield PRIVATE FILE_SET CXX_MODULES FILES - "${CMAKE_CURRENT_SOURCE_DIR}/src/duplicateFinder/duplicateFinder.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/cryptoCipher.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/encryption/encryption.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/fileShredder/fileShredder.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/FuzzyMatcher.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/passwordManager/passwordManager.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/privacyTracks/privacyTracks.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/utils/utils.cppm" - "${CMAKE_CURRENT_SOURCE_DIR}/src/secureAllocator.cppm" + src/duplicateFinder/duplicateFinder.cppm + src/encryption/cryptoCipher.cppm + src/encryption/encryption.cppm + src/fileShredder/fileShredder.cppm + src/passwordManager/FuzzyMatcher.cppm + src/passwordManager/passwordManager.cppm + src/privacyTracks/privacyTracks.cppm + src/utils/utils.cppm + src/secureAllocator.cppm + src/mimallocSTL.cppm ) # Find the required packages @@ -110,6 +111,26 @@ if (NOT TARGET BLAKE3::blake3) endif () +# Mimalloc allocator +find_package(mimalloc 2.1 QUIET) +if (NOT TARGET mimalloc) + message(STATUS "mimalloc not found. Fetching from GitHub...") + + FetchContent_Declare( + mimalloc + GIT_REPOSITORY https://github.com/microsoft/mimalloc.git + GIT_TAG v2.1.7 + EXCLUDE_FROM_ALL + ) +endif () + +set(MI_BUILD_TESTS OFF) # Do not build tests + +FetchContent_MakeAvailable(mimalloc) +target_include_directories(privacyShield PRIVATE "${mimalloc_SOURCE_DIR}/include") +add_library(Mimalloc::mimalloc-static ALIAS mimalloc-static) +add_library(Mimalloc::mimalloc ALIAS mimalloc) + # Fetch Isocline from GitHub FetchContent_Declare( isocline @@ -148,12 +169,12 @@ endif () # Link libraries target_link_libraries(privacyShield PRIVATE - OpenSSL::Crypto Sodium::sodium Gcrypt::Gcrypt BLAKE3::blake3 ISOCline::isocline + Mimalloc::mimalloc-static ) # Install the binary (optional), with 0755 permissions diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index 25630ac..f15343d 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -30,6 +30,7 @@ module; export module duplicateFinder; import utils; +import mimallocSTL; namespace fs = std::filesystem; @@ -38,17 +39,17 @@ constexpr std::size_t CHUNK_SIZE = 4096; ///< Read and process files in chunks o /// \brief Represents a file by its path (canonical) and hash. struct FileInfo { - std::string path; ///< the path to the file. - std::string hash; ///< the file's BLAKE3 hash + miSTL::string path; ///< the path to the file. + miSTL::string hash; ///< the file's BLAKE3 hash }; /// \brief Calculates the 256-bit BLAKE3 hash of a file. /// \param filePath path to the file. /// \return Base64-encoded hash of the file. /// \throws std::runtime_error if the file cannot be opened. -std::string calculateBlake3(const std::string &filePath) { +miSTL::string calculateBlake3(const miSTL::string &filePath) { // Open the file - std::ifstream file(filePath, std::ios::binary); + std::ifstream file(filePath.c_str(), std::ios::binary); if (!file) { if (std::error_code ec; fs::exists(filePath, ec)) throw std::runtime_error(std::format("Failed to open '{}' for hashing.", filePath)); @@ -72,7 +73,7 @@ std::string calculateBlake3(const std::string &filePath) { blake3_hasher_update(&hasher, buffer.data(), remainingBytes); // Finalize the hash calculation - std::vector digest(BLAKE3_OUT_LEN); + miSTL::vector digest(BLAKE3_OUT_LEN); blake3_hasher_finalize(&hasher, digest.data(), BLAKE3_OUT_LEN); return base64Encode(digest); @@ -93,7 +94,7 @@ inline void handleAccessError(const std::string_view filename) { /// \brief recursively traverses a directory and collects file information. /// \param directoryPath the directory to process. /// \param files a vector to store the information from the files found in the directory. -void traverseDirectory(const fs::path &directoryPath, std::vector &files) { +void traverseDirectory(const fs::path &directoryPath, miSTL::vector &files) { std::error_code ec; for (const auto &entry: fs::recursive_directory_iterator(directoryPath, @@ -109,7 +110,7 @@ void traverseDirectory(const fs::path &directoryPath, std::vector &fil continue; } // Make sure we can read the entry - if (isReadable(entry.path())) [[likely]] { + if (isReadable(entry.path().string().c_str())) [[likely]] { // process only regular files if (entry.is_regular_file()) [[likely]] { FileInfo fileInfo; @@ -133,14 +134,14 @@ void traverseDirectory(const fs::path &directoryPath, std::vector &fil /// \param files the files to process. /// \param start the index where processing starts. /// \param end the index where processing ends. -void calculateHashes(std::vector &files, const std::size_t start, const std::size_t end) { +void calculateHashes(miSTL::vector &files, const std::size_t start, const std::size_t end) { // Check if the range is valid if (start > end || end > files.size()) throw std::range_error("Invalid range."); // Calculate hashes for the files in the range for (std::size_t i = start; i < end; ++i) - files[i].hash = calculateBlake3(files[i].path); + files[i].hash = calculateBlake3(files[i].path).c_str(); } /// \brief finds duplicate files (by content) in a directory. @@ -148,7 +149,7 @@ void calculateHashes(std::vector &files, const std::size_t start, cons /// \return True if duplicates are found, else False. std::size_t findDuplicates(const fs::path &directoryPath) { // Collect file information - std::vector files; + miSTL::vector files; traverseDirectory(directoryPath, files); const std::size_t filesProcessed = files.size(); if (filesProcessed < 2) return 0; @@ -158,7 +159,7 @@ std::size_t findDuplicates(const fs::path &directoryPath) { const unsigned int numThreads{n ? n : 8}; // Use 8 threads if hardware_concurrency() fails // Divide the files among the threads - std::vector threads; + miSTL::vector threads; const std::size_t filesPerThread = filesProcessed / numThreads; std::size_t start = 0; @@ -174,7 +175,7 @@ std::size_t findDuplicates(const fs::path &directoryPath) { for (auto &thread: threads) thread.join(); // A hash map to map the files to their corresponding hashes - std::unordered_map > hashMap; + miSTL::unordered_map > hashMap; // Iterate over files and identify duplicates for (const auto &[filePath, hash]: files) diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index 6cdc3c9..f968b63 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -29,6 +29,7 @@ module; import utils; import secureAllocator; +import mimallocSTL; import passwordManager; module encryption; @@ -63,14 +64,14 @@ constexpr struct { /// \brief Formats a file size into a human-readable string. /// \param size The file size as an unsigned integer. /// \return A string representing the formatted file size. -std::string formatFileSize(const std::uintmax_t &size) { +miSTL::string formatFileSize(const std::uintmax_t &size) { int i{}; auto mantissa = static_cast(size); for (; mantissa >= 1024.; mantissa /= 1024., ++i) { } mantissa = std::ceil(mantissa * 10.) / 10.; - std::string result = std::to_string(mantissa) + "BKMGTPE"[i]; - return i == 0 ? result : result + "B (" + std::to_string(size) + ')'; + miSTL::string result { std::to_string(mantissa) + "BKMGTPE"[i]}; + return i == 0 ? result : result + "B (" + std::to_string(size).c_str() + ')'; } /// \brief Checks for issues with the input file, that may hinder encryption/decryption. @@ -101,7 +102,7 @@ void checkInputFile(const fs::path &inFile, const OperationMode &mode) { std::format("{} is not a regular file.", inFile.string())); // Encrypted files are regular } // Check if the input file is readable - if (auto file = inFile.string(); !isReadable(file)) + if (auto file = inFile.string(); !isReadable(file.c_str())) throw std::runtime_error(std::format("{} is not readable.", file)); } @@ -170,7 +171,7 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope throw std::runtime_error("Operation aborted."); // Determine if the output file can be written if it exists - if (auto file = weakly_canonical(outFile).string(); !(isWritable(file) && isReadable(file))) + if (auto file = weakly_canonical(outFile).string(); !(isWritable(file.c_str()) && isReadable(file.c_str()))) throw std::runtime_error(std::format("{} is not writable/readable.", file)); } } @@ -211,7 +212,7 @@ inline void copyLastWrite(const std::string_view srcFile, const std::string_view /// \param password the password to use for encryption/decryption. /// \param algo the algorithm to use for encryption/decryption. /// \param mode the mode of operation: encryption or decryption. -void fileEncryptionDecryption(const std::string &inputFileName, const std::string &outputFileName, +void fileEncryptionDecryption(const miSTL::string &inputFileName, const miSTL::string &outputFileName, const privacy::string &password, const Algorithms &algo, const OperationMode &mode) { // The mode must be valid: must be either encryption or decryption if (mode != OperationMode::Encryption && mode != OperationMode::Decryption) [[unlikely]] { @@ -221,7 +222,7 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin try { /// Encrypts/decrypts a file based on the passed mode and algorithm. - auto encryptDecrypt = [&](const std::string &algorithm) -> void { + auto encryptDecrypt = [&](const miSTL::string &algorithm) -> void { if (mode == OperationMode::Encryption) // Encryption encryptFile(inputFileName, outputFileName, password, algorithm); else // Decryption @@ -275,7 +276,7 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin /// \brief Encrypts and decrypts files. void encryptDecrypt() { // I'm using hashmaps as an alternative to multiple if-else statements - const std::unordered_map algoChoice = { + const miSTL::unordered_map algoChoice = { {0, Algorithms::AES}, // Default {1, Algorithms::AES}, {2, Algorithms::Camellia}, @@ -284,7 +285,7 @@ void encryptDecrypt() { {5, Algorithms::Twofish} }; - const std::unordered_map algoDescription = { + const miSTL::unordered_map algoDescription = { {Algorithms::AES, "256-bit AES in CBC mode"}, {Algorithms::Camellia, "256-bit Camellia in CBC mode"}, {Algorithms::Aria, "256-bit Aria in CBC mode"}, @@ -303,8 +304,8 @@ void encryptDecrypt() { if (const int choice = getResponseInt("Enter your choice: "); choice == 1 || choice == 2) { try { - std::string pre = choice == 1 ? "En" : "De"; // the prefix string - std::string pre_l{pre}; // the prefix in lowercase + miSTL::string pre = choice == 1 ? "En" : "De"; // the prefix string + miSTL::string pre_l{pre}; // the prefix in lowercase // Transform the prefix to lowercase std::ranges::transform(pre_l.begin(), pre_l.end(), pre_l.begin(), @@ -370,7 +371,7 @@ void encryptDecrypt() { printColoredOutput('c', "{}", algoDescription.find(cipher)->second); printColoredOutputln('g', "..."); - fileEncryptionDecryption(canonical(inputPath).string(), weakly_canonical(outputPath).string(), + fileEncryptionDecryption(canonical(inputPath).string().c_str(), weakly_canonical(outputPath).string().c_str(), password, cipher, static_cast(choice)); std::println(""); } catch (const std::exception &ex) { diff --git a/src/encryption/encryptFiles.cpp b/src/encryption/encryptFiles.cpp index 972c707..b45b0c4 100644 --- a/src/encryption/encryptFiles.cpp +++ b/src/encryption/encryptFiles.cpp @@ -30,6 +30,7 @@ module; import cryptoCipher; import secureAllocator; +import mimallocSTL; module encryption; @@ -45,7 +46,7 @@ privacy::vector generateSalt(const int saltSize) { std::mutex m; privacy::vector salt(saltSize); - if (std::scoped_lock lock(m); RAND_bytes(salt.data(), saltSize) != 1) { + if (std::scoped_lock lock(m); RAND_bytes(salt.data(), saltSize) != 1) { std::cerr << "Failed to seed OpenSSL's CSPRNG properly." "\nPlease check your system's randomness utilities." << std::endl; @@ -134,15 +135,15 @@ deriveKey(const privacy::string &password, const privacy::vector /// \details Encryption mode: CBC. /// \details Key derivation function: PBKDF2 with BLAKE2b512 as the digest function (salted). /// \details The IV is generated randomly with a CSPRNG and prepended to the encrypted file. -void encryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo) { +void encryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo) { // Open the input file for reading - std::ifstream inFile(inputFile, std::ios::binary); + std::ifstream inFile(inputFile.c_str(), std::ios::binary); if (!inFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFile)); // Open the output file for writing - std::ofstream outFile(outputFile, std::ios::binary | std::ios::trunc); + std::ofstream outFile(outputFile.c_str(), std::ios::binary | std::ios::trunc); if (!outFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFile)); @@ -227,15 +228,15 @@ void encryptFile(const std::string &inputFile, const std::string &outputFile, co /// \param algo The cipher algorithm used to encrypt the file. /// /// \throws std::runtime_error if the decryption fails, and for other (documented) errors. -void decryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo) { +void decryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo) { // Open the input file for reading - std::ifstream inFile(inputFile, std::ios::binary); + std::ifstream inFile(inputFile.c_str(), std::ios::binary); if (!inFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFile)); // Open the output file for writing - std::ofstream outFile(outputFile, std::ios::binary | std::ios::trunc); + std::ofstream outFile(outputFile.c_str(), std::ios::binary | std::ios::trunc); if (!outFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFile)); @@ -343,15 +344,15 @@ inline void throwSafeError(const gcry_error_t &err, const std::string_view messa /// using PBKDF2 with BLAKE2b-512 as the hash function. /// \details The IV(nonce) is randomly generated and stored in the output file. void -encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, +encryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm) { // Open the input file for reading - std::ifstream inputFile(inputFilePath, std::ios::binary); + std::ifstream inputFile(inputFilePath.c_str(), std::ios::binary); if (!inputFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFilePath)); // Open the output file for writing - std::ofstream outputFile(outputFilePath, std::ios::binary | std::ios::trunc); + std::ofstream outputFile(outputFilePath.c_str(), std::ios::binary | std::ios::trunc); if (!outputFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFilePath)); @@ -426,15 +427,15 @@ encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o /// /// \throws std::runtime_error if the decryption fails, and for other (documented) errors. void -decryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, +decryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm) { // Open the input file for reading - std::ifstream inputFile(inputFilePath, std::ios::binary); + std::ifstream inputFile(inputFilePath.c_str(), std::ios::binary); if (!inputFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFilePath)); // Open the output file for writing - std::ofstream outputFile(outputFilePath, std::ios::binary | std::ios::trunc); + std::ofstream outputFile(outputFilePath.c_str(), std::ios::binary | std::ios::trunc); if (!outputFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFilePath)); diff --git a/src/encryption/encryptStrings.cpp b/src/encryption/encryptStrings.cpp index d781706..b3b26f9 100644 --- a/src/encryption/encryptStrings.cpp +++ b/src/encryption/encryptStrings.cpp @@ -25,6 +25,7 @@ module; import utils; import secureAllocator; +import mimallocSTL; import cryptoCipher; module encryption; @@ -42,7 +43,7 @@ module encryption; /// \details The key is derived from the password using PBKDF2 with 100,000 rounds (salted). /// \details The IV is generated randomly using a CSPRNG and prepended to the ciphertext. privacy::string -encryptString(const privacy::string &plaintext, const privacy::string &password, const std::string &algo) { +encryptString(const privacy::string &plaintext, const privacy::string &password, const miSTL::string &algo) { CryptoCipher cipher; // Create the cipher context @@ -112,7 +113,7 @@ encryptString(const privacy::string &plaintext, const privacy::string &password, /// /// \throws std::runtime_error if the decryption operation fails. privacy::string -decryptString(const std::string_view encodedCiphertext, const privacy::string &password, const std::string &algo) { +decryptString(const std::string_view encodedCiphertext, const privacy::string &password, const miSTL::string &algo) { CryptoCipher cipher; // Create the cipher context @@ -134,7 +135,7 @@ decryptString(const std::string_view encodedCiphertext, const privacy::string &p privacy::vector encryptedText; // Base64 decode the encoded ciphertext - if (std::vector ciphertext = base64Decode(encodedCiphertext); + if (miSTL::vector ciphertext = base64Decode(encodedCiphertext); ciphertext.size() > static_cast(SALT_SIZE) + ivSize) [[likely]] { // Read the salt and IV from the ciphertext salt.assign(ciphertext.begin(), ciphertext.begin() + SALT_SIZE); @@ -277,7 +278,7 @@ decryptStringWithMoreRounds(const std::string_view encodedCiphertext, const priv privacy::vector encryptedText; // Base64-decode the encoded ciphertext - if (std::vector ciphertext = base64Decode(encodedCiphertext); + if (miSTL::vector ciphertext = base64Decode(encodedCiphertext); ciphertext.size() >= SALT_SIZE + ctrSize) [[likely]] { // Read the salt and the counter from the ciphertext salt.assign(ciphertext.begin(), ciphertext.begin() + SALT_SIZE); diff --git a/src/encryption/encryption.cppm b/src/encryption/encryption.cppm index fff6742..6d1284f 100644 --- a/src/encryption/encryption.cppm +++ b/src/encryption/encryption.cppm @@ -22,6 +22,7 @@ module; export module encryption; import secureAllocator; +import mimallocSTL; constexpr int SALT_SIZE = 32; // Default salt length (256 bits) constexpr int KEY_SIZE_256 = 32; // Default key size (256 bits) @@ -37,32 +38,32 @@ export { deriveKey(const privacy::string &password, const privacy::vector &salt, const int &keySize = KEY_SIZE_256); - void encryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + void encryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo = "AES-256-CBC"); void - encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, + encryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); - void decryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + void decryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo = "AES-256-CBC"); void - decryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, + decryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); privacy::string encryptString(const privacy::string &plaintext, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + const miSTL::string &algo = "AES-256-CBC"); privacy::string encryptStringWithMoreRounds(const privacy::string &plaintext, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); privacy::string decryptString(std::string_view encodedCiphertext, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + const miSTL::string &algo = "AES-256-CBC"); privacy::string decryptStringWithMoreRounds(std::string_view encodedCiphertext, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); diff --git a/src/fileShredder/fileShredder.cppm b/src/fileShredder/fileShredder.cppm index 2385511..a866ace 100644 --- a/src/fileShredder/fileShredder.cppm +++ b/src/fileShredder/fileShredder.cppm @@ -32,6 +32,7 @@ using StatType = struct stat; export module fileShredder; import utils; +import mimallocSTL; namespace fs = std::filesystem; constexpr std::streamoff BUFFER_SIZE = 4096; @@ -56,7 +57,7 @@ void overwriteRandom(std::ofstream &file, const std::size_t fileSize, const int // (Re)seed the Mersenne Twister engine in every pass std::mt19937_64 gen(rd()); - std::vector buffer(BUFFER_SIZE); + miSTL::vector buffer(BUFFER_SIZE); // Overwrite the file with random data for (std::size_t pos = 0; pos < fileSize; pos += BUFFER_SIZE) { @@ -91,7 +92,7 @@ void overwriteConstantByte(std::ofstream &file, T &byte, const auto &fileSize) { // seek to the beginning of the file file.seekp(0, std::ios::beg); - std::vector buffer(BUFFER_SIZE, byte); + miSTL::vector buffer(BUFFER_SIZE, byte); for (std::streamoff pos = 0; pos < fileSize; pos += BUFFER_SIZE) { if (pos + BUFFER_SIZE > fileSize) { @@ -125,10 +126,10 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { std::uniform_int_distribution numDist(minNameLength, maxNameLength); // Get the file extension using std::filesystem - const std::string fileExtension = fs::path(filename).extension().string(); + const miSTL::string fileExtension = fs::path(filename).extension().string().c_str(); // Generate a random name using the safe characters (Not exhaustive) - const std::string safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + constexpr std::string_view safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; std::uniform_int_distribution dist(0, safeChars.size() - 1); fs::path path(filename); @@ -142,7 +143,7 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { // Generate a random number of characters for the new name const int numChars = numDist(gen); - std::string newName; + miSTL::string newName; // Generate a random name for (int j = 0; j < numChars; ++j) newName += safeChars[dist(gen)]; @@ -177,9 +178,9 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { struct FileDescriptor { int fd{-1}; - explicit FileDescriptor(const std::string &filename) : fd(open(filename.c_str(), O_RDWR)) { + explicit FileDescriptor(const miSTL::string &filename) : fd(open(filename.c_str(), O_RDWR)) { if (fd == -1) - throw std::runtime_error("Failed to open file: " + filename + " (" + std::strerror(errno) + ")"); + throw std::runtime_error(std::format("Failed to open file: {} ({})", filename, std::strerror(errno))); } ~FileDescriptor() { if (fd != -1) close(fd); } @@ -203,7 +204,7 @@ struct FileStatInfo { /// \brief wipes the cluster tips of a file. /// \param fileName the path to the file to be wiped. /// \throws std::runtime_error if zeroing the cluster tips fails. -inline void wipeClusterTips(const std::string &fileName) { +inline void wipeClusterTips(const miSTL::string &fileName) { const FileDescriptor fileDescriptor(fileName); const FileStatInfo fileInformation(fileDescriptor.fd); @@ -220,7 +221,7 @@ inline void wipeClusterTips(const std::string &fileName) { } // Write zeros to the cluster tip - const std::vector zeroBuffer(clusterTipSize, 0); + const miSTL::vector zeroBuffer(clusterTipSize, 0); if (write(fileDescriptor.fd, zeroBuffer.data(), zeroBuffer.size()) == static_cast(-1)) { throw std::runtime_error(std::format("Failed to write zeros: ({})", std::strerror(errno))); @@ -233,10 +234,10 @@ inline void wipeClusterTips(const std::string &fileName) { /// \param wipeClusterTip whether to wipe the cluster tips of the file. /// /// \throws std::runtime_error if the file cannot be opened. -void simpleShred(const std::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { - std::ofstream file(filename, std::ios::binary | std::ios::in); +void simpleShred(const miSTL::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { + std::ofstream file(filename.c_str(), std::ios::binary | std::ios::in); if (!file) - throw std::runtime_error("\nFailed to open file: " + filename); + throw std::runtime_error(std::format("\nFailed to open file: {}", filename)); std::error_code ec; // Read last write time @@ -268,10 +269,10 @@ void simpleShred(const std::string &filename, const int &nPasses = 3, const bool /// \param wipeClusterTip whether to wipe the cluster tips of the file. /// /// \throws std::runtime_error if the file cannot be opened, or if the number of passes is invalid. -void dod5220Shred(const std::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { - std::ofstream file(filename, std::ios::binary | std::ios::in); +void dod5220Shred(const miSTL::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { + std::ofstream file(filename.c_str(), std::ios::binary | std::ios::in); if (!file) - throw std::runtime_error("\nFailed to open file: " + filename); + throw std::runtime_error(std::format("\nFailed to open file: {}", filename)); std::error_code ec; // Read last write time @@ -359,7 +360,7 @@ static inline bool addReadWritePermissions(const std::string_view fileName) noex /// /// \warning If the filePath is a directory, then all its files and subdirectories /// are shredded without warning. -bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, const int &simplePasses = 3) { +bool shredFiles(const miSTL::string &filePath, const std::uint_fast8_t &options, const int &simplePasses = 3) { std::error_code ec; const fs::file_status fileStatus = fs::status(filePath, ec); if (ec) { @@ -401,7 +402,7 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c printColoredOutput('b', "{}", canonical(entry.path()).string()); printColoredOutput('c', " ..."); try { - const bool shredded = shredFiles(entry.path().string(), options); + const bool shredded = shredFiles(entry.path().string().c_str(), options); printColoredOutputln(shredded ? 'g' : 'r', "{}", shredded ? "\tshredded successfully." : "\tshredding failed."); @@ -575,7 +576,7 @@ export void fileShredder() { std::cout << "Shredding '"; printColoredOutput('c', "{}", canonicalPath); std::cout << "'..." << std::endl; - const bool shredded = shredFiles(path, preferences, simpleNumPass); + const bool shredded = shredFiles(path.string().c_str(), preferences, simpleNumPass); if (!isDir) { printColoredOutput(shredded ? 'g' : 'r', "{}", shredded ? "Successfully shredded " : "Failed to shred "); diff --git a/src/main.cpp b/src/main.cpp index 9c8669d..b499c55 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. +#include #include #include #include diff --git a/src/mimallocSTL.cppm b/src/mimallocSTL.cppm new file mode 100644 index 0000000..03d6cd0 --- /dev/null +++ b/src/mimallocSTL.cppm @@ -0,0 +1,294 @@ +module; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __has_include +#if __has_include() +#include // for feature test macros +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#else // __has_include + +#if __cpp_lib_flat_set +#include +#endif + +#if __cpp_lib_flat_map +#include +#endif + +#if __cpp_lib_span +#include +#endif + +#if __cpp_lib_mdspan +#include +#endif + +#if __cpp_lib_syncbuf +#include +#endif + +#endif // __has_include + +export module mimallocSTL; + +export namespace miSTL { + /* ********** Sequence Containers ********** */ + // std::array doesn't need a custom allocator, we include it here for completeness + template + using array = std::array; + + // std::vector + template + using vector = std::vector >; + + // std::deque + template + using deque = std::deque >; + + // std::list + template + using list = std::list >; + + // std::forward_list + template + using forward_list = std::forward_list >; + + /* ********** Associative Containers ********** */ + // std::set + template > + using set = std::set >; + + // std::map + template< + class Key, + class T, + class Compare = std::less > + using map = std::map > >; + + // std::multiset + template< + class Key, + class Compare = std::less > + using multiset = std::multiset >; + + // std::multimap + template< + class Key, + class T, + class Compare = std::less > + using multimap = std::multimap > >; + + /* ********** Unordered Associative Containers ********** */ + // std::unordered_set + template< + class Key, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_set = std::unordered_set >; + + // std::unordered_map + template< + class Key, + class T, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_map = std::unordered_map > >; + + // std::unordered_multiset + template< + class Key, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_multiset = std::unordered_multiset >; + + // std::unordered_multimap + template< + class Key, + class T, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_multimap = std::unordered_multimap > >; + + /* ********** Container Adaptors ********** */ + // std::stack + template > + using stack = std::stack; + + // std::queue + template > + using queue = std::queue; + + // std::priority_queue + template< + class T, + class Container = vector, + class Compare = std::less > + using priority_queue = std::priority_queue; + + // std::flat_set (C++23) +#if __cpp_lib_flat_set + template< + class Key, + class Compare = std::less, + class KeyContainer = vector > + using flat_set = std::flat_set; +#endif + + // std::flat_map (C++23) +#if __cpp_lib_flat_map + template< + class Key, + class T, + class Compare = std::less, + class KeyContainer = vector, + class MappedContainer = vector > + using flat_map = std::flat_map; +#endif + + // std::flat_multiset (C++23) +#if __cpp_lib_flat_multiset + template< + class Key, + class Compare = std::less, + class KeyContainer = vector > + using flat_multiset = std::flat_multiset; +#endif + + // std::flat_multimap (C++23) +#if __cpp_lib_flat_multimap + template< + class Key, + class T, + class Compare = std::less, + class KeyContainer = vector, + class MappedContainer = vector > + using flat_multimap = std::flat_multimap; +#endif + + /* ********** Views ********** */ + // views do not need custom allocators, they are included here for completeness + // std::span (C++20) +#if __cpp_lib_span + template + using span = std::span; +#endif + + // std::mdspan (C++23) +#if __cpp_lib_mdspan + template< + class T, + class Extents, + class LayoutPolicy = std::layout_right, + class AccessorPolicy = std::default_accessor > + using mdspan = std::mdspan; +#endif + + /* ********** Strings ********** */ + // std::basic_string + template > + using basic_string = std::basic_string >; + + // std::string + using string = basic_string; + + // std::string_view included for completeness, as it doesn't need a custom allocator + template > + using basic_string_view = std::basic_string_view; + using string_view = basic_string_view; + + /* ********** I/O Streams ********** */ + // std::basic_stringbuf + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_stringbuf = std::basic_stringbuf; + + // std::basic_istringstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_istringstream = std::basic_istringstream; + + // std::basic_ostringstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_ostringstream = std::basic_ostringstream; + + // std::basic_stringstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_stringstream = std::basic_stringstream; + + // std::basic_syncbuf + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_syncbuf = std::basic_syncbuf; + + // std::basic_osyncstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_osyncstream = std::basic_osyncstream; + + // std::stringbuf + using stringbuf = basic_stringbuf; + + // std::istringstream + using istringstream = basic_istringstream; + + // std::stringstream + using stringstream = basic_stringstream; + + // std::ostringstream + using ostringstream = basic_ostringstream; + + // std::syncbuf + using syncbuf = basic_syncbuf; + + // std::osyncstream + using osyncstream = basic_osyncstream; +} // namespace miSTL diff --git a/src/passwordManager/FuzzyMatcher.cppm b/src/passwordManager/FuzzyMatcher.cppm index 56dacd9..6b748aa 100644 --- a/src/passwordManager/FuzzyMatcher.cppm +++ b/src/passwordManager/FuzzyMatcher.cppm @@ -22,6 +22,7 @@ module; export module FuzzyMatcher; import secureAllocator; +import mimallocSTL; template /// \brief A concept describing a range of strings. @@ -88,8 +89,8 @@ public: /// \param pattern the pattern to match. /// \param maxDistance the maximum Levenshtein Distance to consider a match. /// \return a vector of strings matching the pattern. - [[nodiscard]] std::vector fuzzyMatch(const std::string_view pattern, const int &maxDistance) const { - std::vector matches{}; + [[nodiscard]] miSTL::vector fuzzyMatch(const std::string_view pattern, const int &maxDistance) const { + miSTL::vector matches{}; matches.reserve(stringList.size()); // Worst case: every string in stringList is a match. // The maximum and minimum size of a string to be considered a match const auto maxSize{pattern.size() + maxDistance + 1}; @@ -110,7 +111,7 @@ public: private: - std::vector stringList{}; + miSTL::vector stringList{}; /// \brief Calculates the Levenshtein Distance between two strings. /// \param str1 the first string. @@ -122,7 +123,7 @@ private: const int m = static_cast(str1.length()); const int n = static_cast(str2.length()); - std::vector> dp(m + 1, std::vector(n + 1)); + miSTL::vector> dp(m + 1, miSTL::vector(n + 1)); // Initialize the first row and column for (int i = 0; i <= m; ++i) diff --git a/src/passwordManager/passwordManager.cpp b/src/passwordManager/passwordManager.cpp index 53e0a9a..3ad0a7f 100644 --- a/src/passwordManager/passwordManager.cpp +++ b/src/passwordManager/passwordManager.cpp @@ -31,11 +31,12 @@ module; import utils; import FuzzyMatcher; import secureAllocator; +import mimallocSTL; module passwordManager; namespace fs = std::filesystem; -using string = std::string; +using string = miSTL::string; const string DefaultPasswordFile = getHomeDir() + "/.privacyShield/passwords"; /// \brief A binary predicate for searching, sorting, and deduplication of the password records, @@ -88,7 +89,7 @@ constexpr void computeStrengths #if __clang__ || __GNUC__ [[gnu::always_inline]] #endif -(const privacy::vector &passwords, std::vector &pwStrengths) { +(const privacy::vector &passwords, miSTL::vector &pwStrengths) { pwStrengths.resize(passwords.size()); for (std::size_t i = 0; i < passwords.size(); ++i) { pwStrengths[i] = isPasswordStrong(std::get<2>(passwords[i])); @@ -96,7 +97,7 @@ constexpr void computeStrengths } /// \brief Adds a new password to the saved records. -void addPassword(privacy::vector &passwords, std::vector &strengths) { +void addPassword(privacy::vector &passwords, miSTL::vector &strengths) { privacy::string site{getResponseStr("Enter the name of the site/app: ")}; // The site name must be non-empty if (site.empty()) { @@ -160,7 +161,7 @@ void addPassword(privacy::vector &passwords, std::vector } /// \brief Generates a random password. -void generatePassword(privacy::vector &, std::vector &) { +void generatePassword(privacy::vector &, miSTL::vector &) { int length = getResponseInt("Enter the length of the password to generate: "); int tries{0}; @@ -183,7 +184,7 @@ void generatePassword(privacy::vector &, std::vector &) { } /// \brief Shows all saved passwords. -void viewAllPasswords(privacy::vector &passwords, std::vector &strengths) { +void viewAllPasswords(privacy::vector &passwords, miSTL::vector &strengths) { // Check if there are any passwords saved if (auto &&constPasswordsView = std::ranges::views::as_const(passwords); constPasswordsView.empty()) { printColoredOutputln('r', "You haven't saved any password yet."); @@ -223,7 +224,7 @@ void checkFuzzyMatches(auto &iter, privacy::vector &records, pr [](const auto &lhs, const auto &rhs) noexcept -> bool { return comparator(lhs, rhs); }); - query = std::string{match}; + query = miSTL::string{match}; } } else if (!fuzzyMatched.empty()) { // multiple matches @@ -237,7 +238,7 @@ void checkFuzzyMatches(auto &iter, privacy::vector &records, pr } /// \brief Updates a password record. -void updatePassword(privacy::vector &passwords, std::vector &strengths) { +void updatePassword(privacy::vector &passwords, miSTL::vector &strengths) { if (passwords.empty()) [[unlikely]] { // There is nothing to update printColoredErrorln('r', "No passwords saved yet."); @@ -342,7 +343,7 @@ void updatePassword(privacy::vector &passwords, std::vector &passwords, std::vector &strengths) { +void deletePassword(privacy::vector &passwords, miSTL::vector &strengths) { if (passwords.empty()) { printColoredErrorln('r', "No passwords saved yet."); return; @@ -431,7 +432,7 @@ void deletePassword(privacy::vector &passwords, std::vector &passwords, std::vector &) { +void searchPasswords(privacy::vector &passwords, miSTL::vector &) { if (passwords.empty()) [[unlikely]] { // There is nothing to search printColoredErrorln('r', "No passwords saved yet."); @@ -500,7 +501,7 @@ void searchPasswords(privacy::vector &passwords, std::vector &passwords, std::vector &strengths) { +void importPasswords(privacy::vector &passwords, miSTL::vector &strengths) { const fs::path fileName = getFilesystemPath("Enter the path to the csv file: "); privacy::vector imports{importCsv(fileName)}; @@ -585,7 +586,7 @@ void importPasswords(privacy::vector &passwords, std::vector &passwords, std::vector &) { +void exportPasswords(privacy::vector &passwords, miSTL::vector &) { auto &&constPasswordsView = std::as_const(passwords); if (constPasswordsView.empty()) [[unlikely]] { @@ -605,7 +606,7 @@ void exportPasswords(privacy::vector &passwords, std::vector &passwords, std::vector &strengths) { +void analyzePasswords(privacy::vector &passwords, miSTL::vector &strengths) { if (passwords.empty()) { printColoredOutputln('r', "No passwords to analyze."); return; @@ -627,7 +628,7 @@ void analyzePasswords(privacy::vector &passwords, std::vector > passwordMap; + miSTL::unordered_map > passwordMap; for (const auto &[site, _, password]: constPasswordsView) { // Add the site to the set of sites that use the password passwordMap[password].insert(site); @@ -647,7 +648,7 @@ void analyzePasswords(privacy::vector &passwords, std::vector >; + using PasswordSites = std::pair >; std::multimap > countMap; for (const auto &[password, sites]: passwordMap) { @@ -692,7 +693,7 @@ void analyzePasswords(privacy::vector &passwords, std::vector passwordStrength(passwords.size(), false); + miSTL::vector passwordStrength(passwords.size(), false); for (std::size_t i = 0; i < passwords.size(); ++i) { passwordStrength[i] = isPasswordStrong(std::get<2>(passwords[i])); } // A map of choices and their corresponding functions - std::unordered_map &, std::vector &)> choices = { + miSTL::unordered_map &, miSTL::vector &)> choices = { {1, addPassword}, {2, updatePassword}, {3, deletePassword}, diff --git a/src/passwordManager/passwordManager.cppm b/src/passwordManager/passwordManager.cppm index cce4132..b7ed75b 100644 --- a/src/passwordManager/passwordManager.cppm +++ b/src/passwordManager/passwordManager.cppm @@ -25,6 +25,7 @@ export module passwordManager; import utils; import secureAllocator; +import mimallocSTL; using passwordRecords = std::tuple; @@ -39,7 +40,7 @@ privacy::string generatePassword(int length); bool changePrimaryPassword(privacy::string &primaryPassword); -std::pair initialSetup() noexcept; +std::pair initialSetup() noexcept; privacy::string getHash(std::string_view filePath); diff --git a/src/passwordManager/passwords.cpp b/src/passwordManager/passwords.cpp index 4115526..9b4aa15 100644 --- a/src/passwordManager/passwords.cpp +++ b/src/passwordManager/passwords.cpp @@ -29,6 +29,7 @@ module; import utils; import encryption; import secureAllocator; +import mimallocSTL; module passwordManager; @@ -78,7 +79,7 @@ privacy::string generatePassword(const int length) { throw std::length_error("Password too long."); // generate from a set of printable ascii characters - const std::string characters = + constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()-=_~+[]{}<>"; // Seed the Mersenne Twister engine with a random source (ideally non-deterministic) @@ -155,7 +156,7 @@ encryptDecryptRange(privacy::vector &passwords, const privacy:: for (std::size_t i = start; i < end; ++i) { std::get<2>(passwords[i]) = encrypt ? encryptStringWithMoreRounds(std::get<2>(passwords[i]), key) - : decryptStringWithMoreRounds(std::string{std::get<2>(passwords[i])}, + : decryptStringWithMoreRounds(miSTL::string{std::get<2>(passwords[i])}, key); } } catch (const std::exception &ex) { @@ -183,15 +184,15 @@ encryptDecryptRangeAllFields(privacy::vector &passwords, const for (std::size_t i = start; i < end; ++i) { std::get<0>(passwords[i]) = encrypt ? encryptString(std::get<0>(passwords[i]), key) - : decryptString(std::string{std::get<0>(passwords[i])}, key); + : decryptString(miSTL::string{std::get<0>(passwords[i])}, key); std::get<1>(passwords[i]) = encrypt ? encryptString(std::get<1>(passwords[i]), key) - : decryptString(std::string{std::get<1>(passwords[i])}, key); + : decryptString(miSTL::string{std::get<1>(passwords[i])}, key); std::get<2>(passwords[i]) = encrypt ? encryptString(std::get<2>(passwords[i]), key) - : decryptString(std::string{std::get<2>(passwords[i])}, key); + : decryptString(miSTL::string{std::get<2>(passwords[i])}, key); } } catch (const std::exception &ex) { printColoredErrorln('r', "Error: {}", ex.what()); @@ -212,7 +213,7 @@ encryptDecryptConcurrently(privacy::vector &passwordEntries, co const unsigned int numThreads{std::jthread::hardware_concurrency() ? std::jthread::hardware_concurrency() : 8}; // Divide the password entries among threads - std::vector threads; + miSTL::vector threads; const std::size_t passPerThread = numPasswords / numThreads; std::size_t start = 0; @@ -259,7 +260,7 @@ inline void checkCommonErrors(const std::string_view path) { /// \return True, if successful. bool savePasswords(privacy::vector &passwords, const std::string_view filePath, const privacy::string &encryptionKey) { - auto tempFile = std::string{filePath} + "XXXXXX"; + auto tempFile = miSTL::string{filePath} + "XXXXXX"; // Create a temporary file // If the temporary file couldn't be created, use the original file path @@ -267,7 +268,7 @@ bool savePasswords(privacy::vector &passwords, const std::strin tempFile = filePath; else close(tmpFileFd); // Close the file descriptor - std::ofstream file(tempFile, std::ios::trunc); + std::ofstream file(tempFile.c_str(), std::ios::trunc); if (!file) { try { checkCommonErrors(tempFile); @@ -407,8 +408,8 @@ bool changePrimaryPassword(privacy::string &primaryPassword) { /// \brief Helps with the initial setup of the password manager. /// \return New primary password and/or path to the password file, whichever is applicable. -std::pair initialSetup() noexcept { - std::pair ret{"", ""}; // ret.first = path to file, ret.second = new primary password +std::pair initialSetup() noexcept { + std::pair ret{"", ""}; // ret.first = path to file, ret.second = new primary password std::cout << "Looks like you don't have any passwords saved yet." << std::endl; @@ -459,7 +460,7 @@ std::pair initialSetup() noexcept { continue; } - ret.first = path; + ret.first = path.string().c_str(); break; } if (resp == 3) return ret; @@ -569,7 +570,7 @@ bool exportCsv(const privacy::vector &records, const std::files /// \brief Trims space (whitespace) off the beginning and end of a string. /// \param str the string to trim. -inline void trim(std::string &str) { +inline void trim(miSTL::string &str) { constexpr std::string_view space = " \t\n\r\f\v"; // Trim the leading space @@ -604,7 +605,7 @@ privacy::vector importCsv(const fs::path &filePath) { while (std::getline, privacy::Allocator >(file, line)) { privacy::istringstream iss(line); - privacy::vector tokens; + privacy::vector tokens; while (std::getline, privacy::Allocator >(iss, value, ',')) tokens.emplace_back(value); diff --git a/src/privacyTracks/privacyTracks.cppm b/src/privacyTracks/privacyTracks.cppm index 708b2e1..e061c6f 100644 --- a/src/privacyTracks/privacyTracks.cppm +++ b/src/privacyTracks/privacyTracks.cppm @@ -26,6 +26,7 @@ module; export module privacyTracks; import utils; +import mimallocSTL; namespace fs = std::filesystem; @@ -64,12 +65,12 @@ std::uint_fast8_t detectBrowsers(const std::string_view pathEnv) { } // Split the PATH variable into individual paths - std::string pathEnvStr{pathEnv}; - std::vector paths; + miSTL::string pathEnvStr{pathEnv}; + miSTL::vector paths; paths.reserve(256); std::size_t pos; - while ((pos = pathEnvStr.find(':')) != std::string::npos) { + while ((pos = pathEnvStr.find(':')) != miSTL::string::npos) { paths.emplace_back(pathEnvStr.substr(0, pos)); pathEnvStr.erase(0, pos + 1); } @@ -133,7 +134,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { std::error_code ec; // Find all default profiles - std::vector defaultProfileDirs; + miSTL::vector defaultProfileDirs; for (const auto &entry: fs::directory_iterator(configDir, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", configDir); @@ -159,7 +160,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { } else printColoredErrorln('r', "No default profiles found."); // Treat the other directories as profiles - std::vector profileDirs; + miSTL::vector profileDirs; for (const auto &entry: fs::directory_iterator(configDir, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", configDir); @@ -257,7 +258,7 @@ bool clearChromiumTracks(const std::string_view configDir) { } else printColoredErrorln('r', "Default profile directory not found."); // Find other profile directories - std::vector profileDirs; + miSTL::vector profileDirs; for (const auto &entry: fs::directory_iterator(configDir, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", configDir); @@ -338,7 +339,7 @@ bool clearOperaTracks(const std::string_view profilePath) { ec.clear(); fs::remove(fs::path{profilePath} / "cookies", ec); if (ec) { - handleFileError(ec, "deleting", std::string{profilePath} + "/cookies"); + handleFileError(ec, "deleting", miSTL::string{profilePath} + "/cookies"); ec.clear(); ret = false; // We don't to return yet, we want to try to clear history too } @@ -350,7 +351,7 @@ bool clearOperaTracks(const std::string_view profilePath) { ec.clear(); fs::remove(fs::path{profilePath} / "history", ec); if (ec) { - handleFileError(ec, "deleting", std::string{profilePath} + "/history"); + handleFileError(ec, "deleting", miSTL::string{profilePath} + "/history"); ec.clear(); return false; // No point in continuing } @@ -402,7 +403,7 @@ bool clearOperaTracks() { /// \return true if successful, false otherwise. bool clearSafariTracks() { #if __APPLE__ - const std::string cookiesPath = getHomeDir() + "/Library/Cookies"; + const miSTL::string cookiesPath = getHomeDir() + "/Library/Cookies"; if (!fs::exists(cookiesPath)) { printColoredErrorln('r', "Safari cookies directory not found."); return false; @@ -418,7 +419,7 @@ bool clearSafariTracks() { } } - const std::string historyPath = getHomeDir() + "/Library/Safari"; + const miSTL::string historyPath = getHomeDir() + "/Library/Safari"; if (!fs::exists(historyPath)) { printColoredErrorln('c', "Safari history directory not found."); return false; diff --git a/src/secureAllocator.cppm b/src/secureAllocator.cppm index 3201ed9..aecfd75 100644 --- a/src/secureAllocator.cppm +++ b/src/secureAllocator.cppm @@ -21,6 +21,7 @@ module; #include #include #include +#include export module secureAllocator; @@ -45,15 +46,15 @@ export namespace privacy { /// Copy constructor template - constexpr explicit Allocator(const Allocator &) noexcept {} + constexpr explicit Allocator(const Allocator &) noexcept { + } /// Allocate memory - [[maybe_unused]] [[nodiscard]] constexpr T *allocate(std::size_t n) { + [[maybe_unused]] [[nodiscard]] constexpr T *allocate(const std::size_t n) { if (n > std::numeric_limits::max() / sizeof(T)) throw std::bad_array_new_length(); - if (auto p = static_cast(::operator new(n * sizeof(T)))) { - sodium_mlock(p, n * sizeof(T)); // Lock the allocated memory + if (auto p = static_cast(sodium_malloc(n * sizeof(T)))) { return p; } @@ -61,9 +62,8 @@ export namespace privacy { } /// Deallocate memory - [[maybe_unused]] constexpr void deallocate(T *p, std::size_t n) noexcept { - sodium_munlock(p, n * sizeof(T)); // Unlock and zeroize memory - ::operator delete(p); + [[maybe_unused]] static constexpr void deallocate(T *p, const std::size_t n [[maybe_unused]]) noexcept { + sodium_free(p); } }; @@ -83,7 +83,7 @@ export namespace privacy { using string = std::basic_string, Allocator >; template - using vector = std::vector>; + using vector = std::vector >; using istringstream = std::basic_istringstream, Allocator >; } // namespace privacy diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 9fd2282..ca54eb0 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -34,6 +34,7 @@ module; export module utils; import secureAllocator; +import mimallocSTL; namespace fs = std::filesystem; @@ -220,7 +221,7 @@ export { /// \return Base64-encoded string. /// \throws std::bad_alloc if memory allocation fails. /// \throws std::runtime_error if encoding fails. - std::string base64Encode(const uCharVector auto &input) { + miSTL::string base64Encode(const uCharVector auto &input) { // Create a BIO object to encode the data const std::unique_ptr b64(BIO_new(BIO_f_base64()), &BIO_free_all); if (b64 == nullptr) @@ -249,7 +250,7 @@ export { BIO_get_mem_ptr(b64.get(), &bufferPtr); // Create a string from the data - std::string encodedData(bufferPtr->data, bufferPtr->length); + miSTL::string encodedData(bufferPtr->data, bufferPtr->length); return encodedData; } @@ -282,7 +283,7 @@ export { /// \return a vector of the decoded binary data. /// \throws std::bad_alloc if memory allocation fails. /// \throws std::runtime_error if the decoding operation fails. - std::vector base64Decode(const std::string_view encodedData) { + miSTL::vector base64Decode(const std::string_view encodedData) { // Create a BIO object to decode the data std::unique_ptr bio( BIO_new_mem_buf(encodedData.data(), static_cast(encodedData.size())), &BIO_free_all); @@ -300,7 +301,7 @@ export { // Push the base64 BIO to the memory BIO bio.reset(BIO_push(b64, bio.release())); // Transfer ownership to bio - std::vector decodedData(encodedData.size()); + miSTL::vector decodedData(encodedData.size()); // Decode the data const int len = BIO_read(bio.get(), decodedData.data(), static_cast(decodedData.size())); @@ -318,7 +319,7 @@ export { /// from the standard input. /// \param prompt The prompt to display to the user. /// \return The response string entered by the user if successful, else an empty string. - std::string getResponseStr(const char *prompt = "") { + miSTL::string getResponseStr(const char *prompt = "") { std::scoped_lock lock(termutex); // Disable completions ic_set_default_completer(null_completer, nullptr); @@ -326,7 +327,7 @@ export { // Read the response from the user std::puts(prompt); if (char *input = ic_readline("")) { - std::string result{input}; + miSTL::string result{input}; std::free(input); stripString(result); return result; @@ -417,7 +418,7 @@ export { /// to the current user. /// \param filename the path to the file. /// \return true if the current user has write permissions, else false. - bool isWritable(const std::string &filename) { + bool isWritable(const miSTL::string &filename) { return access(filename.c_str(), F_OK | W_OK) == 0; } @@ -425,7 +426,7 @@ export { /// to the current user. /// \param filename the path to the file. /// \return true if the current user has read permissions, else false. - bool isReadable(const std::string &filename) { + bool isReadable(const miSTL::string &filename) { return access(filename.c_str(), F_OK | R_OK) == 0; } @@ -477,7 +478,7 @@ export { /// \param prompt The confirmation prompt. /// \return True if the user confirms the action, else false. bool validateYesNo(const char *prompt = "") { - const std::string resp = getResponseStr(prompt); + const miSTL::string resp = getResponseStr(prompt); if (resp.empty()) return false; return std::tolower(resp.at(0)) == 'y'; } @@ -486,7 +487,7 @@ export { /// \param var an environment variable to query. /// \return the value of the environment variable if it exists, else nullopt (nothing). /// \note The returned value MUST be checked before access. - std::optional getEnv(const char *const var) { + std::optional getEnv(const char *const var) { // Use secure_getenv() if available #if _GNU_SOURCE if (const char *value = secure_getenv(var)) @@ -502,7 +503,7 @@ export { /// \return The home directory read from {'HOME', 'USERPROFILE'} /// environment variables, else the current working directory (or an empty /// string if the current directory couldn't be determined). - std::string getHomeDir() noexcept { + miSTL::string getHomeDir() noexcept { std::error_code ec; // Try to get the home directory from the environment variables if (const auto envHome = getEnv("HOME"); envHome) @@ -513,7 +514,7 @@ export { // If the environment variables are not set, use the current working directory std::cerr << "\nCouldn't find your home directory, using the current working directory instead.." << std::endl; - std::string currentDir = std::filesystem::current_path(ec); + miSTL::string currentDir = std::filesystem::current_path(ec).string().c_str(); if (ec) std::cerr << ec.message() << std::endl; return currentDir; From 863a6ed5e8e19fe88fb8843d19903304e3dbb77b Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 17 Jun 2024 01:23:40 +0300 Subject: [PATCH 87/99] Bugfix: overriding malloc --- src/main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index b499c55..9c8669d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include #include #include #include From 69ef49542e25e0c8dd5fdfe8611fded52015b418 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:21:27 +0300 Subject: [PATCH 88/99] Handle Ctrl+C/D during input --- src/utils/utils.cppm | 70 +++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index ca54eb0..83e015b 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -29,7 +29,6 @@ module; #include #include #include -#include export module utils; @@ -39,8 +38,6 @@ import mimallocSTL; namespace fs = std::filesystem; constexpr int MAX_PASSPHRASE_LEN = 1024; ///< Maximum length of a passphrase -std::mutex termutex; ///< A mutex to prevent concurrent access to the terminal. - /// \class ColorConfig @@ -161,9 +158,6 @@ export { /// \param args The arguments to be printed. template void printColoredOutput(const char color, std::format_string fmt, Args &&... args) { - // Lock the mutex to prevent concurrent access - std::scoped_lock lock(termutex); - // Print the output depending on the color configuration if (ColorConfig::getInstance().getSuppressColor()) std::cout << std::vformat(fmt.get(), std::make_format_args(args...)); @@ -177,8 +171,6 @@ export { /// \param args The arguments to be printed. template void printColoredOutputln(const char color, std::format_string fmt, Args &&... args) { - std::scoped_lock lock(termutex); - if (ColorConfig::getInstance().getSuppressColor()) std::cout << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; else @@ -193,8 +185,6 @@ export { /// \param args The arguments to be printed. template void printColoredError(const char color, std::format_string fmt, Args &&... args) { - std::scoped_lock lock(termutex); - if (ColorConfig::getInstance().getSuppressColor()) std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)); else std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; @@ -207,8 +197,6 @@ export { /// \param args The arguments to be printed. template void printColoredErrorln(const char color, std::format_string fmt, Args &&... args) { - std::scoped_lock lock(termutex); - if (ColorConfig::getInstance().getSuppressColor()) std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; else @@ -255,29 +243,6 @@ export { return encodedData; } - /// \brief Prompts the user for a filesystem path. - /// \param prompt The prompt to display to the user. - /// \return The filesystem path entered by the user if successful, else an empty path. - fs::path getFilesystemPath(const char *prompt = "") { - // Lock the mutex to prevent concurrent access - std::scoped_lock lock(termutex); - - // Enable filename completion and automatic tab completion - ic_set_default_completer(normal_completer, nullptr); - ic_enable_auto_tab(true); - - // Display the prompt - std::puts(prompt); - // Read the input from the user - if (char *input = ic_readline("")) { - fs::path result(input); - // Free the input buffer - std::free(input); - return result; - } - return fs::path{}; - } - /// \brief Performs Base64 decoding of a string into binary data. /// \param encodedData Base64 encoded string. /// \return a vector of the decoded binary data. @@ -314,13 +279,39 @@ export { return decodedData; } + bool validateYesNo(const char *prompt = ""); + + /// \brief Prompts the user for a filesystem path. + /// \param prompt The prompt to display to the user. + /// \return The filesystem path entered by the user if successful, else an empty path. + fs::path getFilesystemPath(const char *prompt = "") { + // Enable filename completion and automatic tab completion + ic_set_default_completer(normal_completer, nullptr); + ic_enable_auto_tab(true); + + // Display the prompt + std::puts(prompt); + // Read the input from the user + if (char *input = ic_readline("")) { + fs::path result(input); + // Free the input buffer + std::free(input); + return result; + } + // Handle Ctrl+C/D + printColoredError('r', "Input canceled. Unsaved data might be lost if you quit now." + "\nDo you still want to quit? (y/n):"); + if (validateYesNo()) std::exit(1); + + return fs::path{}; + } + /// \brief Gets a response string from user input. /// This function prompts the user with the given prompt and reads a response string /// from the standard input. /// \param prompt The prompt to display to the user. /// \return The response string entered by the user if successful, else an empty string. miSTL::string getResponseStr(const char *prompt = "") { - std::scoped_lock lock(termutex); // Disable completions ic_set_default_completer(null_completer, nullptr); @@ -332,6 +323,11 @@ export { stripString(result); return result; } + // Handle Ctrl+C/D + printColoredError('r', "Input canceled. Unsaved data might be lost if you quit now." + "\nDo you still want to quit? (y/n):"); + if (validateYesNo()) std::exit(1); + return ""; } @@ -477,7 +473,7 @@ export { /// \brief Confirms a user's response to a yes/no (y/n) situation. /// \param prompt The confirmation prompt. /// \return True if the user confirms the action, else false. - bool validateYesNo(const char *prompt = "") { + bool validateYesNo(const char *prompt) { const miSTL::string resp = getResponseStr(prompt); if (resp.empty()) return false; return std::tolower(resp.at(0)) == 'y'; From 387f11e6db0d4202e08d5b4335455b87aad8d55b Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Mon, 24 Jun 2024 23:59:52 +0300 Subject: [PATCH 89/99] Refactor the file deduplicator algorithm Files are now hashed only if they are of similar sizes --- src/duplicateFinder/duplicateFinder.cppm | 43 ++++++++++++++++-------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index f15343d..8b882b6 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -39,8 +39,8 @@ constexpr std::size_t CHUNK_SIZE = 4096; ///< Read and process files in chunks o /// \brief Represents a file by its path (canonical) and hash. struct FileInfo { - miSTL::string path; ///< the path to the file. - miSTL::string hash; ///< the file's BLAKE3 hash + miSTL::string path{}; ///< the path to the file. + miSTL::string hash{}; ///< the file's BLAKE3 hash }; /// \brief Calculates the 256-bit BLAKE3 hash of a file. @@ -93,13 +93,20 @@ inline void handleAccessError(const std::string_view filename) { /// \brief recursively traverses a directory and collects file information. /// \param directoryPath the directory to process. -/// \param files a vector to store the information from the files found in the directory. -void traverseDirectory(const fs::path &directoryPath, miSTL::vector &files) { +/// \param candidateDuplicates a vector to store the information from the files found in the directory. +std::size_t traverseDirectory(const fs::path &directoryPath, miSTL::vector &candidateDuplicates) { std::error_code ec; + // Number of files processed + std::size_t filesProcessed{0}; + + // Map to store file sizes and their corresponding paths + miSTL::unordered_map > sizeToFileMap; + for (const auto &entry: fs::recursive_directory_iterator(directoryPath, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink)) { + ++filesProcessed; if (entry.exists(ec)) { // In case of broken symlinks if (ec) { @@ -113,12 +120,7 @@ void traverseDirectory(const fs::path &directoryPath, miSTL::vector &f if (isReadable(entry.path().string().c_str())) [[likely]] { // process only regular files if (entry.is_regular_file()) [[likely]] { - FileInfo fileInfo; - - // Update the file details - fileInfo.path = entry.path().string(); - fileInfo.hash = ""; // the hash will be calculated later - files.emplace_back(fileInfo); + sizeToFileMap[fs::file_size(entry.path())].push_back(entry.path()); } else if (!entry.is_directory()) { // Neither regular nor a directory printColoredError('r', "Skipping "); @@ -128,6 +130,17 @@ void traverseDirectory(const fs::path &directoryPath, miSTL::vector &f } else handleAccessError(entry.path().string()); } } + candidateDuplicates.reserve(filesProcessed); + // Report files with the same sizes + for (auto &files: sizeToFileMap | std::views::values) { + if (files.size() > 1) { + for (const auto &file: files) { + candidateDuplicates.emplace_back(FileInfo{file.string().c_str(), ""}); + } + } + } + + return filesProcessed; } /// \brief calculates hashes for a range of files. @@ -150,9 +163,10 @@ void calculateHashes(miSTL::vector &files, const std::size_t start, co std::size_t findDuplicates(const fs::path &directoryPath) { // Collect file information miSTL::vector files; - traverseDirectory(directoryPath, files); - const std::size_t filesProcessed = files.size(); - if (filesProcessed < 2) return 0; + const std::size_t filesProcessed = traverseDirectory(directoryPath, files); + const std::size_t numFiles = files.size(); + + if (filesProcessed < 2 || numFiles < 2) return 0; // Number of threads to use const unsigned int n{std::jthread::hardware_concurrency()}; @@ -160,7 +174,7 @@ std::size_t findDuplicates(const fs::path &directoryPath) { // Divide the files among the threads miSTL::vector threads; - const std::size_t filesPerThread = filesProcessed / numThreads; + const std::size_t filesPerThread = numFiles / numThreads; std::size_t start = 0; // Calculate the files' hashes in parallel @@ -176,6 +190,7 @@ std::size_t findDuplicates(const fs::path &directoryPath) { // A hash map to map the files to their corresponding hashes miSTL::unordered_map > hashMap; + hashMap.reserve(files.size()); // Iterate over files and identify duplicates for (const auto &[filePath, hash]: files) From 60063c229d53e47808cfa6e85398ca7d57441066 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:38:53 +0300 Subject: [PATCH 90/99] Refactor CMakeLists.txt --- CMakeLists.txt | 66 +++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4148bb7..6e6e507 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,9 @@ cmake_dependent_option(ENABLE_SANITIZERS "Enable sanitizers (Ignored if not using Clang compiler)" OFF "${CMAKE_CXX_COMPILER_ID} STREQUAL \"Clang\"" OFF) +# Valgrind support +option(VALGRIND_BUILD "Build with Valgrind support" OFF) + # Additional checks for the Debug build if (CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_options( @@ -85,6 +88,41 @@ target_sources(privacyShield PRIVATE src/mimallocSTL.cppm ) +# Sanitizers for debugging and testing +# Requires llvm-symbolizer and sanitizer libraries (asan, ubsan, msan, tsan) +if (ENABLE_SANITIZERS) + # Common flags for all sanitizers + set(sanitizer_common_flags "-fno-omit-frame-pointer -g -O1") + + # Address, leak, undefined, integer, nullability sanitizers + set(address_sanitizer_flags "-fsanitize=address,leak,undefined,integer,nullability") + + # Thread sanitizer, cannot be used with address sanitizer + set(thread_sanitizer_flags "-fsanitize=thread -fPIE") + + # Memory sanitizer, cannot be used with address sanitizer. + set(memory_sanitizer_flags "-fsanitize=memory -fPIE -fno-optimize-sibling-calls") + + # Add compile options + add_compile_options( + "SHELL:${sanitizer_common_flags}" + "SHELL:${address_sanitizer_flags}" + ) + + # Track mimalloc allocations for AddressSanitizer + set(MI_TRACK_ASAN ON) + + # Link the enabled sanitizers. + target_link_libraries(privacyShield PRIVATE asan ubsan) +endif () + +# Valgrind support +if (VALGRIND_BUILD) + add_compile_options(-g) # Valgrind requires debug symbols + # target_link_libraries(privacyShield PRIVATE valgrind) + set(MI_TRACK_VALGRIND ON) +endif () + # Find the required packages find_package(OpenSSL REQUIRED) find_package(Sodium REQUIRED) @@ -112,8 +150,7 @@ if (NOT TARGET BLAKE3::blake3) endif () # Mimalloc allocator -find_package(mimalloc 2.1 QUIET) -if (NOT TARGET mimalloc) +if (NOT TARGET mimalloc-static OR NOT TARGET mimalloc) message(STATUS "mimalloc not found. Fetching from GitHub...") FetchContent_Declare( @@ -142,31 +179,6 @@ FetchContent_MakeAvailable(isocline) target_include_directories(privacyShield PRIVATE "${isocline_SOURCE_DIR}/include") add_library(ISOCline::isocline ALIAS isocline) -# Sanitizers for debugging and testing -# Requires llvm-symbolizer and sanitizer libraries (asan, ubsan, msan, tsan) -if (ENABLE_SANITIZERS) - # Common flags for all sanitizers - set(sanitizer_common_flags "-fno-omit-frame-pointer -g -O1") - - # Address, leak, undefined, integer, nullability sanitizers - set(address_sanitizer_flags "-fsanitize=address,leak,undefined,integer,nullability") - - # Thread sanitizer, cannot be used with address sanitizer - set(thread_sanitizer_flags "-fsanitize=thread -fPIE") - - # Memory sanitizer, cannot be used with address sanitizer. - set(memory_sanitizer_flags "-fsanitize=memory -fPIE -fno-optimize-sibling-calls") - - # Add compile options - add_compile_options( - "SHELL:${sanitizer_common_flags}" - "SHELL:${address_sanitizer_flags}" - ) - - # Link the enabled sanitizers. - target_link_libraries(privacyShield PRIVATE asan ubsan) -endif () - # Link libraries target_link_libraries(privacyShield PRIVATE OpenSSL::Crypto From b5d9e4ee53b414d044d86d9483d8e091e3489222 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:26:57 +0300 Subject: [PATCH 91/99] More mimalloc integration --- src/main.cpp | 5 +++-- src/utils/utils.cppm | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9c8669d..6a51c7a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,10 +14,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. +#include #include #include #include -#include #include #include #include @@ -29,6 +29,7 @@ import privacyTracks; import encryption; import passwordManager; import fileShredder; +import mimallocSTL; import utils; constexpr auto MINIMUM_LIBGCRYPT_VERSION = "1.10.0"; @@ -130,7 +131,7 @@ int main(const int argc, const char **argv) { printColoredOutputln('b', "https://www.gnu.org/licenses/gpl.html."); // All the available tools - std::unordered_map > apps = { + miSTL::unordered_map > apps = { {1, passwordManager}, {2, encryptDecrypt}, {3, fileShredder}, diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 83e015b..19e44b1 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -319,7 +319,7 @@ export { std::puts(prompt); if (char *input = ic_readline("")) { miSTL::string result{input}; - std::free(input); + ic_free(input); stripString(result); return result; } From 9c369162c448d398ca26d1995fa9dfc5df97bfe2 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 27 Jun 2024 20:19:16 +0300 Subject: [PATCH 92/99] Fix compilation errors on macOS --- src/main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 6a51c7a..ccf43b7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include #include #include #include From 6e57ad94e9b47eaf8b2660d6ce0336808aa2d93d Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 27 Jun 2024 20:43:32 +0300 Subject: [PATCH 93/99] Update documentation --- README.md | 10 +++++++++- media/mimalloc-logo.png | Bin 0 -> 73097 bytes 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 media/mimalloc-logo.png diff --git a/README.md b/README.md index 5c7f060..e8eb457 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ The process might be slow, and multithreading has been leveraged to speed up the The [Serpent cipher](https://en.wikipedia.org/wiki/Serpent_(cipher)) is used for the first step because it is a conservative and secure cipher with more rounds than [AES cipher](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) -(32 rounds vs 14 rounds, hence a larger security margin) that is resistant to cryptanalysis. +(32 rounds vs. 14 rounds, hence a larger security margin) that is resistant to cryptanalysis. The [counter mode (CTR)](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)) is used for it because it is a fast and secure mode that is resistant to padding oracle attacks. A non-deterministic random [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) @@ -358,6 +358,7 @@ or [LLVM Clang 18](https://clang.llvm.org/) (or newer) is required. * [GCrypt](https://gnupg.org/software/libgcrypt/index.html) 1.10+ * [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) 1.4+ (Fetched automatically by CMake, if not already installed) * [Isocline](https://github.com/daanx/isocline) (Fetched automatically by CMake) +* [Mimalloc](https://github.com/microsoft/mimalloc) 2.17+ (Fetched automatically by CMake) **Note:**\ This project utilizes the [C++20 Modules](https://en.cppreference.com/w/cpp/language/modules) feature, @@ -429,6 +430,9 @@ You can then run the program from the build directory: You can download a package for your platform from the [releases page](https://github.com/dr8co/PrivacyShield/releases). +The packages expect the dependencies to be installed on the system, +except the ones that are fetched automatically by CMake (they are statically linked to the executable). + The package will contain the built executable, and you can install it using the package manager of your platform. For the macOS package, you can simply drag the .dmg file to your Applications folder. @@ -541,6 +545,10 @@ However, the feeling of empowered privacy protection is a strong possibility! ![""](./media/blank.svg) +[![Mimalloc](./media/mimalloc-logo.png)](https://github.com/microsoft/mimalloc) + +![""](./media/blank.svg) + [![Isocline](./media/isocline.svg)](https://github.com/daanx/isocline) ## License diff --git a/media/mimalloc-logo.png b/media/mimalloc-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e0a5a8ce258c29f6dfb5d0658880a2d339b5a47b GIT binary patch literal 73097 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ø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 bMI!${0bO36huu=@00000NkvXXu0mjfmW0V> literal 0 HcmV?d00001 From f9c927a18adb441ec641295d80e988efc07ec4b7 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:55:39 +0300 Subject: [PATCH 94/99] Update documentation assets --- README.md | 2 +- media/isocline.png | Bin 0 -> 25194 bytes media/isocline.svg | 21 --------------------- media/mimalloc-logo.png | Bin 73097 -> 7379 bytes 4 files changed, 1 insertion(+), 22 deletions(-) create mode 100644 media/isocline.png delete mode 100644 media/isocline.svg diff --git a/README.md b/README.md index e8eb457..e17ba95 100644 --- a/README.md +++ b/README.md @@ -549,7 +549,7 @@ However, the feeling of empowered privacy protection is a strong possibility! ![""](./media/blank.svg) -[![Isocline](./media/isocline.svg)](https://github.com/daanx/isocline) +[![Isocline](./media/isocline.png)](https://github.com/daanx/isocline) ## License diff --git a/media/isocline.png b/media/isocline.png new file mode 100644 index 0000000000000000000000000000000000000000..f6024447e9b71fca67eec62f74b4592851ef1d9d GIT binary patch literal 25194 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 M+T(|xRp7|~170w>g8%>k literal 0 HcmV?d00001 diff --git a/media/isocline.svg b/media/isocline.svg deleted file mode 100644 index f6c19db..0000000 --- a/media/isocline.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/media/mimalloc-logo.png b/media/mimalloc-logo.png index e0a5a8ce258c29f6dfb5d0658880a2d339b5a47b..32b095311acba5dc55e26a8cad51e888d0f18891 100644 GIT binary patch literal 7379 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ø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 bMI!${0bO36huu=@00000NkvXXu0mjfmW0V> From d93c4f719fc81491e3f759594a79ca8519422a5d Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:58:51 +0300 Subject: [PATCH 95/99] Make CMake the official build system --- vcpkg.json | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 vcpkg.json diff --git a/vcpkg.json b/vcpkg.json deleted file mode 100644 index dbefc28..0000000 --- a/vcpkg.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "privacyshield", - "version-string": "2.5.0", - "builtin-baseline": "7b5ca09708ae42dba9517d4e0a0c975d087f1061", - "dependencies": [ - { - "name": "libsodium", - "version>=": "1.0.18#9" - }, - { - "name": "openssl", - "version>=": "3.1.2#3" - }, - { - "name": "libgcrypt", - "version>=": "1.10.1#1" - }, - { - "name": "readline-unix", - "version>=": "8.2" - }, - { - "name": "readline-osx", - "version>=": "2020-01-04" - }, - { - "name": "blake3", - "version>=": "1.4.0" - } - ] -} From 781488343d0191a583c6f3ee6a0771ae3883e221 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 28 Jun 2024 00:57:17 +0300 Subject: [PATCH 96/99] Refactor code cleanup and use of smart pointers to manage memory. --- src/mimallocSTL.cppm | 16 ++++++++++++++++ src/secureAllocator.cppm | 2 -- src/utils/utils.cppm | 28 +++++++++++++++------------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/mimallocSTL.cppm b/src/mimallocSTL.cppm index 03d6cd0..249f1b7 100644 --- a/src/mimallocSTL.cppm +++ b/src/mimallocSTL.cppm @@ -1,3 +1,19 @@ +// Privacy Shield: A Suite of Tools Designed to Facilitate Privacy Management. +// Copyright (C) 2024 Ian Duncan +// +// 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 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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, see https://www.gnu.org/licenses. + module; #include #include diff --git a/src/secureAllocator.cppm b/src/secureAllocator.cppm index aecfd75..f0a27e2 100644 --- a/src/secureAllocator.cppm +++ b/src/secureAllocator.cppm @@ -16,12 +16,10 @@ module; -#include #include #include #include #include -#include export module secureAllocator; diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 19e44b1..bf425c6 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -351,16 +351,21 @@ export { /// \throws std::bad_alloc if memory allocation fails. /// \throws std::runtime_error if memory locking/unlocking fails. privacy::string getSensitiveInfo(const char *prompt = "") { - // Allocate a buffer for the password - auto *buffer = static_cast(sodium_malloc(MAX_PASSPHRASE_LEN)); - if (buffer == nullptr) + // A lambda to free memory allocated by sodium_malloc + auto deleter = [](char *ptr) noexcept -> void { + sodium_free(ptr); + }; + + // Allocate memory for the passphrase + const std::unique_ptr buffer(static_cast(sodium_malloc(MAX_PASSPHRASE_LEN)), + deleter); + + if (!buffer) throw std::bad_alloc(); // Memory allocation failed // Lock the memory to prevent swapping - if (sodium_mlock(buffer, MAX_PASSPHRASE_LEN) == -1) { - sodium_free(buffer); + if (sodium_mlock(buffer.get(), MAX_PASSPHRASE_LEN) == -1) throw std::runtime_error("Failed to lock memory."); - } // Turn off terminal echoing termios oldSettings{}, newSettings{}; @@ -384,24 +389,21 @@ export { } else { // Check if buffer is not full if (index < MAX_PASSPHRASE_LEN - 1) { - buffer[index++] = ch; + buffer.get()[index++] = ch; } } } - buffer[index] = '\0'; // Null-terminate the string + buffer.get()[index] = '\0'; // Null-terminate the string // Restore terminal settings tcsetattr(STDIN_FILENO, TCSANOW, &oldSettings); - privacy::string passphrase{buffer}; + privacy::string passphrase{buffer.get()}; // Unlock the memory - if (sodium_munlock(buffer, MAX_PASSPHRASE_LEN) == -1) + if (sodium_munlock(buffer.get(), MAX_PASSPHRASE_LEN) == -1) throw std::runtime_error("Failed to unlock memory."); - // Free the buffer - sodium_free(buffer); - // Trim leading and trailing spaces stripString(passphrase); From 52d7a4af0cf635a2ef37e92630362a3061675f18 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:12:11 +0300 Subject: [PATCH 97/99] Add verification info --- README.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e17ba95..9cfc08a 100644 --- a/README.md +++ b/README.md @@ -446,18 +446,40 @@ Internet connection might be required to install the dependencies. For instance, on Ubuntu, you can install the .deb file using the following command: ```bash -sudo dpkg -i privacyshield_2.5.0_amd64.deb # Replace with the actual file path +sudo dpkg -i privacyshield_3.0.0_amd64.deb # Replace with the actual file path # You can also use apt to install it: -sudo apt install ./privacyshield_2.5.0_amd64.deb # Replace with the actual file path +sudo apt install ./privacyshield_3.0.0_amd64.deb # Replace with the actual file path ``` On RPM-based distributions like Fedora, you can install the .rpm file using the following command: ```bash -sudo rpm -i privacyshield-2.5.0-1.x86_64.rpm # Replace with the actual file path +sudo rpm -i privacyshield-3.0.0-1.x86_64.rpm # Replace with the actual file path ``` The packages can be verified using the [GnuPG](https://gnupg.org/) signature files provided. +To verify the packages, first import the public key from the releases page: + +```bash +gpg --import public_gpg_key.asc +``` + +Then verify the package using the signature file (which can be found on the releases page as well): + +```bash +gpg --verify signatures/privacyshield_3.0.0_amd64.deb.sig privacyshield_3.0.0_amd64.deb +``` + +The verification succeeds if the output says +`gpg: Good signature from "Ian Duncan (Signing key for personal projects) ..."`. + +SHA256 checksums are also provided for the packages, and you can verify the integrity of the packages using them. + +```bash +shasum -a 256 -c privacyshield_3.0.0_amd64.deb.sha256 +# Or, if you have the sha256sum command available: +sha256sum -c privacyshield_3.0.0_amd64.deb.sha256 +``` #### Manual Installation From cc1e59bdf86ec67c1438623303a439fbf233ad3d Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:50:55 +0300 Subject: [PATCH 98/99] Update security info --- CONTRIBUTING.md | 4 ++- README.md | 6 ++-- SECURITY.md | 29 +++++++++++++++++ security/privacyShield_pub_key.asc | 52 ++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 SECURITY.md create mode 100644 security/privacyShield_pub_key.asc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f49c21a..c238610 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,8 @@ to help and details about how this project handles them. Please make sure to rea your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 +For security issues, please follow the instructions in the [Security](./SECURITY.md) section. + > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support > the project and show your appreciation, which we would also be very happy about: > @@ -90,7 +92,7 @@ following steps in advance to help us fix any potential bug as fast as possible. #### How Do I Submit a Good Bug Report? > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue -> tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . +> tracker, or elsewhere in public. Check the [Security](./SECURITY.md) section for more information. We use GitHub issues to track bugs and errors. If you run into an issue with the project: diff --git a/README.md b/README.md index 9cfc08a..1e15e06 100644 --- a/README.md +++ b/README.md @@ -458,13 +458,15 @@ sudo rpm -i privacyshield-3.0.0-1.x86_64.rpm # Replace with the actual file path ``` The packages can be verified using the [GnuPG](https://gnupg.org/) signature files provided. -To verify the packages, first import the public key from the releases page: +To verify the packages, first import the [public GPG key](./security/privacyShield_pub_key.asc) provided: ```bash gpg --import public_gpg_key.asc ``` -Then verify the package using the signature file (which can be found on the releases page as well): +The public key is provided in the [releases page](https://github.com/dr8co/PrivacyShield/releases) as well. +Then verify the package using the signature file (which can also be found on the +[releases page](https://github.com/dr8co/PrivacyShield/releases)): ```bash gpg --verify signatures/privacyshield_3.0.0_amd64.deb.sig privacyshield_3.0.0_amd64.deb diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c4acb41 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security + +We take the security of Privacy Shield seriously. +If you believe you have found a security vulnerability in the source code, +please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please send an encrypted email to [dr8co@duck.com](mailto:dr8co@duck.com). +Encrypt your message with our PGP key; it can be found [here](./security/privacyShield_pub_key.asc). + +Please include the requested information listed below (as much as you can provide) +to help us better understand the nature and scope of the possible issue: + +* Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Preferred Languages + +We prefer all communications to be in English. diff --git a/security/privacyShield_pub_key.asc b/security/privacyShield_pub_key.asc new file mode 100644 index 0000000..c9eab7c --- /dev/null +++ b/security/privacyShield_pub_key.asc @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +dhCOLSVqS8XZKOgkOO54MKbmb3YZv5m46DfY +=M91j +-----END PGP PUBLIC KEY BLOCK----- From f41889a34e834c5e4db9d4eb6ae26a26fcb43542 Mon Sep 17 00:00:00 2001 From: Ian Duncan <76043277+dr8co@users.noreply.github.com> Date: Fri, 28 Jun 2024 22:24:27 +0300 Subject: [PATCH 99/99] Version 3.0.0 release --- CMakeLists.txt | 2 +- src/main.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e6e507..a473ce9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ cmake_minimum_required(VERSION 3.28) project(privacyShield - VERSION 2.5.0 + VERSION 3.0.0 DESCRIPTION "A suite of tools for privacy and security" HOMEPAGE_URL "https://shield.iandee.tech" LANGUAGES C CXX) diff --git a/src/main.cpp b/src/main.cpp index ccf43b7..217db68 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -115,7 +115,7 @@ int main(const int argc, const char **argv) { throw std::runtime_error("Failed to initialize libsodium."); // Display information about the program - printColoredOutputln('c', "\nPrivacy Shield 2.5.0"); + printColoredOutputln('c', "\nPrivacy Shield 3.0.0"); printColoredOutputln('b', "Copyright (C) 2024 Ian Duncan."); printColoredOutput('g', "This program comes with "); @@ -167,7 +167,6 @@ int main(const int argc, const char **argv) { printColoredErrorln('r', "An error occurred."); } } - return 0; } catch (const std::exception &ex) { printColoredErrorln('r', "Error: {}", ex.what());