diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 7c9403382..6d588075d 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -3,6 +3,7 @@ #include "AppConfig.hpp" #include "Exception.hpp" #include "Thread.hpp" +#include "format.hpp" #include #include @@ -16,9 +17,13 @@ #include #include #include +#include -//#include -//#include "I18N.hpp" +#ifdef WIN32 +//FIXME replace the two following includes with after it becomes mainstream. +#include +#include +#endif namespace Slic3r { @@ -164,25 +169,114 @@ void AppConfig::set_defaults() erase("", "object_settings_size"); } +#ifdef WIN32 +static std::string appconfig_md5_hash_line(const std::string_view data) +{ + //FIXME replace the two following includes with after it becomes mainstream. + // return boost::md5(data).hex_str_value(); + // boost::uuids::detail::md5 is an internal namespace thus it may change in the future. + // Also this implementation is not the fastest, it was designed for short blocks of text. + using boost::uuids::detail::md5; + md5 md5_hash; + // unsigned int[4], 128 bits + md5::digest_type md5_digest{}; + std::string md5_digest_str; + md5_hash.process_bytes(data.data(), data.size()); + md5_hash.get_digest(md5_digest); + boost::algorithm::hex(md5_digest, md5_digest + std::size(md5_digest), std::back_inserter(md5_digest_str)); + // MD5 hash is 32 HEX digits long. + assert(md5_digest_str.size() == 32); + // This line will be emited at the end of the file. + return "# MD5 checksum " + md5_digest_str + "\n"; +}; + +// Assume that the last line with the comment inside the config file contains a checksum and that the user didn't modify the config file. +static bool verify_config_file_checksum(boost::nowide::ifstream &ifs) +{ + auto read_whole_config_file = [&ifs]() -> std::string { + std::stringstream ss; + ss << ifs.rdbuf(); + return ss.str(); + }; + + ifs.seekg(0, boost::nowide::ifstream::beg); + std::string whole_config = read_whole_config_file(); + + // The checksum should be on the last line in the config file. + if (size_t last_comment_pos = whole_config.find_last_of('#'); last_comment_pos != std::string::npos) { + // Split read config into two parts, one with checksum, and the second part is part with configuration from the checksum was computed. + // Verify existence and validity of the MD5 checksum line at the end of the file. + // When the checksum isn't found, the checksum was not saved correctly, it was removed or it is an older config file without the checksum. + // If the checksum is incorrect, then the file was either not saved correctly or modified. + if (std::string_view(whole_config.c_str() + last_comment_pos, whole_config.size() - last_comment_pos) == appconfig_md5_hash_line({ whole_config.data(), last_comment_pos })) + return true; + } + return false; +} +#endif + std::string AppConfig::load() { // 1) Read the complete config file into a boost::property_tree. namespace pt = boost::property_tree; pt::ptree tree; - boost::nowide::ifstream ifs(AppConfig::config_path()); + boost::nowide::ifstream ifs; + bool recovered = false; + try { + ifs.open(AppConfig::config_path()); +#ifdef WIN32 + // Verify the checksum of the config file without taking just for debugging purpose. + if (!verify_config_file_checksum(ifs)) + BOOST_LOG_TRIVIAL(info) << "The configuration file " << AppConfig::config_path() << + " has a wrong MD5 checksum or the checksum is missing. This may indicate a file corruption or a harmless user edit."; + + ifs.seekg(0, boost::nowide::ifstream::beg); +#endif pt::read_ini(ifs, tree); } catch (pt::ptree_error& ex) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - // ! But to avoid the use of _utf8 (related to use of wxWidgets) - // we will rethrow this exception from the place of load() call, if returned value wouldn't be empty - /* - throw Slic3r::RuntimeError( - _utf8(L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.")) + - "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); - */ - return ex.what(); +#ifdef WIN32 + // The configuration file is corrupted, try replacing it with the backup configuration. + ifs.close(); + std::string backup_path = (boost::format("%1%.bak") % AppConfig::config_path()).str(); + if (boost::filesystem::exists(backup_path)) { + // Compute checksum of the configuration backup file and try to load configuration from it when the checksum is correct. + boost::nowide::ifstream backup_ifs(backup_path); + if (!verify_config_file_checksum(backup_ifs)) { + BOOST_LOG_TRIVIAL(error) << format("Both \"%1%\" and \"%2%\" are corrupted. It isn't possible to restore configuration from the backup.", AppConfig::config_path(), backup_path); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } else if (std::string error_message; copy_file(backup_path, AppConfig::config_path(), error_message, false) != SUCCESS) { + BOOST_LOG_TRIVIAL(error) << format("Configuration file \"%1%\" is corrupted. Failed to restore from backup \"%2%\": %3%", AppConfig::config_path(), backup_path, error_message); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } else { + BOOST_LOG_TRIVIAL(info) << format("Configuration file \"%1%\" was corrupted. It has been succesfully restored from the backup \"%2%\".", AppConfig::config_path(), backup_path); + // Try parse configuration file after restore from backup. + try { + ifs.open(AppConfig::config_path()); + pt::read_ini(ifs, tree); + recovered = true; + } catch (pt::ptree_error& ex) { + BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\" after it has been restored from backup: %2%", AppConfig::config_path(), ex.what()); + } + } + } else +#endif // WIN32 + BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\": %2%", AppConfig::config_path(), ex.what()); + if (! recovered) { + // Report the initial error of parsing PrusaSlicer.ini. + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + // ! But to avoid the use of _utf8 (related to use of wxWidgets) + // we will rethrow this exception from the place of load() call, if returned value wouldn't be empty + /* + throw Slic3r::RuntimeError( + _utf8(L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.")) + + "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); + */ + return ex.what(); + } } // 2) Parse the property_tree, extract the sections and key / value pairs. @@ -259,22 +353,21 @@ void AppConfig::save() const auto path = config_path(); std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str(); - boost::nowide::ofstream c; - c.open(path_pid, std::ios::out | std::ios::trunc); + std::stringstream config_ss; if (m_mode == EAppMode::Editor) - c << "# " << Slic3r::header_slic3r_generated() << std::endl; + config_ss << "# " << Slic3r::header_slic3r_generated() << std::endl; else - c << "# " << Slic3r::header_gcodeviewer_generated() << std::endl; + config_ss << "# " << Slic3r::header_gcodeviewer_generated() << std::endl; // Make sure the "no" category is written first. - for (const std::pair &kvp : m_storage[""]) - c << kvp.first << " = " << kvp.second << std::endl; + for (const auto& kvp : m_storage[""]) + config_ss << kvp.first << " = " << kvp.second << std::endl; // Write the other categories. for (const auto category : m_storage) { if (category.first.empty()) continue; - c << std::endl << "[" << category.first << "]" << std::endl; - for (const std::pair &kvp : category.second) - c << kvp.first << " = " << kvp.second << std::endl; + config_ss << std::endl << "[" << category.first << "]" << std::endl; + for (const auto& kvp : category.second) + config_ss << kvp.first << " = " << kvp.second << std::endl; } // Write vendor sections for (const auto &vendor : m_vendors) { @@ -282,17 +375,42 @@ void AppConfig::save() for (const auto &model : vendor.second) { size_sum += model.second.size(); } if (size_sum == 0) { continue; } - c << std::endl << "[" << VENDOR_PREFIX << vendor.first << "]" << std::endl; + config_ss << std::endl << "[" << VENDOR_PREFIX << vendor.first << "]" << std::endl; for (const auto &model : vendor.second) { - if (model.second.size() == 0) { continue; } + if (model.second.empty()) { continue; } const std::vector variants(model.second.begin(), model.second.end()); const auto escaped = escape_strings_cstyle(variants); - c << MODEL_PREFIX << model.first << " = " << escaped << std::endl; + config_ss << MODEL_PREFIX << model.first << " = " << escaped << std::endl; } } - c.close(); + // One empty line before the MD5 sum. + config_ss << std::endl; + std::string config_str = config_ss.str(); + boost::nowide::ofstream c; + c.open(path_pid, std::ios::out | std::ios::trunc); + c << config_str; +#ifdef WIN32 + // WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API + // provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition, + // we save the config file into a backup first before moving it to the final destination. + c << appconfig_md5_hash_line(config_str); +#endif + c.close(); + +#ifdef WIN32 + // Make a backup of the configuration file before copying it to the final destination. + std::string error_message; + std::string backup_path = (boost::format("%1%.bak") % path).str(); + // Copy configuration file with PID suffix into the configuration file with "bak" suffix. + if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS) + BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration."; +#endif + + // Rename the config atomically. + // On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile. + // To cope with that, we already made a backup of the config on Windows. rename_file(path_pid, path); m_dirty = false; } diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index c8ccd18cd..0a53a5330 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -37,7 +37,7 @@ public: // Load the slic3r.ini from a user profile directory (or a datadir, if configured). // return error string or empty strinf - std::string load(); + std::string load(); // Store the slic3r.ini into a user profile directory (or a datadir, if configured). void save();