From 670061ac333cf96007b3c599fc8981bd7f2150a6 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Fri, 6 Apr 2018 16:49:33 +0200 Subject: [PATCH] Initial implementation of configuration snapshotting. --- xs/CMakeLists.txt | 5 + xs/src/libslic3r/FileParserError.hpp | 48 +++++ xs/src/libslic3r/Utils.hpp | 3 - xs/src/libslic3r/utils.cpp | 28 --- xs/src/slic3r/Config/Snapshot.cpp | 308 +++++++++++++++++++++++++++ xs/src/slic3r/Config/Snapshot.hpp | 106 +++++++++ xs/src/slic3r/Config/Version.cpp | 136 ++++++++++++ xs/src/slic3r/Config/Version.hpp | 75 +++++++ xs/src/slic3r/Utils/Semver.hpp | 24 +++ xs/src/slic3r/Utils/Time.cpp | 63 ++++++ xs/src/slic3r/Utils/Time.hpp | 22 ++ 11 files changed, 787 insertions(+), 31 deletions(-) create mode 100644 xs/src/libslic3r/FileParserError.hpp create mode 100644 xs/src/slic3r/Config/Snapshot.cpp create mode 100644 xs/src/slic3r/Config/Snapshot.hpp create mode 100644 xs/src/slic3r/Config/Version.cpp create mode 100644 xs/src/slic3r/Config/Version.hpp create mode 100644 xs/src/slic3r/Utils/Time.cpp create mode 100644 xs/src/slic3r/Utils/Time.hpp diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index ca398da5a..501c90db2 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -54,6 +54,7 @@ add_library(libslic3r STATIC ${LIBDIR}/libslic3r/ExtrusionEntityCollection.hpp ${LIBDIR}/libslic3r/ExtrusionSimulator.cpp ${LIBDIR}/libslic3r/ExtrusionSimulator.hpp + ${LIBDIR}/libslic3r/FileParserError.hpp ${LIBDIR}/libslic3r/Fill/Fill.cpp ${LIBDIR}/libslic3r/Fill/Fill.hpp ${LIBDIR}/libslic3r/Fill/Fill3DHoneycomb.cpp @@ -201,6 +202,10 @@ add_library(libslic3r_gui STATIC ${LIBDIR}/slic3r/GUI/wxExtensions.hpp ${LIBDIR}/slic3r/GUI/BonjourDialog.cpp ${LIBDIR}/slic3r/GUI/BonjourDialog.hpp + ${LIBDIR}/slic3r/Config/Snapshot.cpp + ${LIBDIR}/slic3r/Config/Snapshot.hpp + ${LIBDIR}/slic3r/Config/Version.cpp + ${LIBDIR}/slic3r/Config/Version.hpp ${LIBDIR}/slic3r/Utils/ASCIIFolding.cpp ${LIBDIR}/slic3r/Utils/ASCIIFolding.hpp ${LIBDIR}/slic3r/Utils/Http.cpp diff --git a/xs/src/libslic3r/FileParserError.hpp b/xs/src/libslic3r/FileParserError.hpp new file mode 100644 index 000000000..82a6b328e --- /dev/null +++ b/xs/src/libslic3r/FileParserError.hpp @@ -0,0 +1,48 @@ +#ifndef slic3r_FileParserError_hpp_ +#define slic3r_FileParserError_hpp_ + +#include "libslic3r.h" + +#include +#include + +namespace Slic3r { + +// Generic file parser error, mostly copied from boost::property_tree::file_parser_error +class file_parser_error: public std::runtime_error +{ +public: + file_parser_error(const std::string &msg, const std::string &file, unsigned long line = 0) : + std::runtime_error(format_what(msg, file, line)), + m_message(msg), m_filename(file), m_line(line) {} + // gcc 3.4.2 complains about lack of throw specifier on compiler + // generated dtor + ~file_parser_error() throw() {} + + // Get error message (without line and file - use what() to get full message) + std::string message() const { return m_message; } + // Get error filename + std::string filename() const { return m_filename; } + // Get error line number + unsigned long line() const { return m_line; } + +private: + std::string m_message; + std::string m_filename; + unsigned long m_line; + + // Format error message to be returned by std::runtime_error::what() + static std::string format_what(const std::string &msg, const std::string &file, unsigned long l) + { + std::stringstream stream; + stream << (file.empty() ? "" : file.c_str()); + if (l > 0) + stream << '(' << l << ')'; + stream << ": " << msg; + return stream.str(); + } +}; + +}; // Slic3r + +#endif // slic3r_FileParserError_hpp_ diff --git a/xs/src/libslic3r/Utils.hpp b/xs/src/libslic3r/Utils.hpp index 27e7fad6b..5727d6c89 100644 --- a/xs/src/libslic3r/Utils.hpp +++ b/xs/src/libslic3r/Utils.hpp @@ -60,9 +60,6 @@ extern std::string timestamp_str(); // to be placed at the top of Slic3r generated files. inline std::string header_slic3r_generated() { return std::string("generated by " SLIC3R_FORK_NAME " " SLIC3R_VERSION " " ) + timestamp_str(); } -// Encode a file into a multi-part HTTP response with a given boundary. -std::string octoprint_encode_file_send_request_content(const char *path, bool select, bool print, const char *boundary); - // Compute the next highest power of 2 of 32-bit v // http://graphics.stanford.edu/~seander/bithacks.html template diff --git a/xs/src/libslic3r/utils.cpp b/xs/src/libslic3r/utils.cpp index 34b9eaa9f..733757e25 100644 --- a/xs/src/libslic3r/utils.cpp +++ b/xs/src/libslic3r/utils.cpp @@ -263,7 +263,6 @@ namespace PerlUtils { std::string timestamp_str() { const auto now = boost::posix_time::second_clock::local_time(); - const auto date = now.date(); char buf[2048]; sprintf(buf, "on %04d-%02d-%02d at %02d:%02d:%02d", // Local date in an ANSII format. @@ -272,31 +271,4 @@ std::string timestamp_str() return buf; } -std::string octoprint_encode_file_send_request_content(const char *cpath, bool select, bool print, const char *boundary) -{ - // Read the complete G-code string into a string buffer. - // It will throw if the file cannot be open or read. - std::stringstream str_stream; - { - boost::nowide::ifstream ifs(cpath); - str_stream << ifs.rdbuf(); - } - - boost::filesystem::path path(cpath); - std::string request = boundary + '\n'; - request += "Content-Disposition: form-data; name=\""; - request += path.stem().string() + "\"; filename=\"" + path.filename().string() + "\"\n"; - request += "Content-Type: application/octet-stream\n\n"; - request += str_stream.str(); - request += boundary + '\n'; - request += "Content-Disposition: form-data; name=\"select\"\n\n"; - request += select ? "true\n" : "false\n"; - request += boundary + '\n'; - request += "Content-Disposition: form-data; name=\"print\"\n\n"; - request += print ? "true\n" : "false\n"; - request += boundary + '\n'; - - return request; -} - }; // namespace Slic3r diff --git a/xs/src/slic3r/Config/Snapshot.cpp b/xs/src/slic3r/Config/Snapshot.cpp new file mode 100644 index 000000000..559e4c63c --- /dev/null +++ b/xs/src/slic3r/Config/Snapshot.cpp @@ -0,0 +1,308 @@ +#include "Snapshot.hpp" +#include "../GUI/AppConfig.hpp" +#include "../Utils/Time.hpp" + +#include + +#include +#include +#include +#include +#include + +#include "../../libslic3r/libslic3r.h" +#include "../../libslic3r/Config.hpp" +#include "../../libslic3r/FileParserError.hpp" +#include "../../libslic3r/Utils.hpp" + +#define SLIC3R_SNAPSHOTS_DIR "snapshots" +#define SLIC3R_SNAPSHOT_FILE "snapshot.ini" + +namespace Slic3r { +namespace GUI { +namespace Config { + +void Snapshot::clear() +{ + this->id.clear(); + this->time_captured = 0; + this->slic3r_version_captured = Semver::invalid(); + this->comment.clear(); + this->reason = SNAPSHOT_UNKNOWN; + this->print.clear(); + this->filaments.clear(); + this->printer.clear(); +} + +void Snapshot::load_ini(const std::string &path) +{ + this->clear(); + + auto throw_on_parse_error = [&path](const std::string &msg) { + throw file_parser_error(std::string("Failed loading the snapshot file. Reason: ") + msg, path); + }; + + // Load the snapshot.ini file. + boost::property_tree::ptree tree; + try { + boost::nowide::ifstream ifs(path); + boost::property_tree::read_ini(ifs, tree); + } catch (const std::ifstream::failure &err) { + throw file_parser_error(std::string("The snapshot file cannot be loaded. Reason: ") + err.what(), path); + } catch (const std::runtime_error &err) { + throw_on_parse_error(err.what()); + } + + // Parse snapshot.ini + std::string group_name_vendor = "Vendor:"; + std::string key_filament = "filament"; + for (auto §ion : tree) { + if (section.first == "snapshot") { + // Parse the common section. + for (auto &kvp : section.second) { + if (kvp.first == "id") + this->id = kvp.second.data(); + else if (kvp.first == "time_captured") { + this->time_captured = Slic3r::Utils::parse_time_ISO8601Z(kvp.second.data()); + if (this->time_captured == (time_t)-1) + throw_on_parse_error("invalid timestamp"); + } else if (kvp.first == "slic3r_version_captured") { + auto semver = Semver::parse(kvp.second.data()); + if (! semver) + throw_on_parse_error("invalid slic3r_version_captured semver"); + this->slic3r_version_captured = *semver; + } else if (kvp.first == "comment") { + this->comment = kvp.second.data(); + } else if (kvp.first == "reason") { + std::string rsn = kvp.second.data(); + if (rsn == "upgrade") + this->reason = SNAPSHOT_UPGRADE; + else if (rsn == "downgrade") + this->reason = SNAPSHOT_DOWNGRADE; + else if (rsn == "user") + this->reason = SNAPSHOT_USER; + else + this->reason = SNAPSHOT_UNKNOWN; + } + } + } else if (section.first == "presets") { + // Load the names of the active presets. + for (auto &kvp : section.second) { + if (kvp.first == "print") { + this->print = kvp.second.data(); + } else if (boost::starts_with(kvp.first, "filament")) { + int idx = 0; + if (kvp.first == "filament" || sscanf(kvp.first.c_str(), "filament_%d", &idx) == 1) { + if (int(this->filaments.size()) <= idx) + this->filaments.resize(idx + 1, std::string()); + this->filaments[idx] = kvp.second.data(); + } + } else if (kvp.first == "printer") { + this->printer = kvp.second.data(); + } + } + } else if (boost::starts_with(section.first, group_name_vendor) && section.first.size() > group_name_vendor.size()) { + // Vendor specific section. + VendorConfig vc; + vc.name = section.first.substr(group_name_vendor.size()); + for (auto &kvp : section.second) { + if (boost::starts_with(kvp.first, "model_")) { + //model:MK2S = 0.4;xxx + //model:MK3 = 0.4;xxx + } else if (kvp.first == "version" || kvp.first == "min_slic3r_version" || kvp.first == "max_slic3r_version") { + // Version of the vendor specific config bundle bundled with this snapshot. + auto semver = Semver::parse(kvp.second.data()); + if (! semver) + throw_on_parse_error("invalid " + kvp.first + " format for " + section.first); + if (kvp.first == "version") + vc.version = *semver; + else if (kvp.first == "min_slic3r_version") + vc.min_slic3r_version = *semver; + else + vc.max_slic3r_version = *semver; + } + } + } + } +} + +void Snapshot::save_ini(const std::string &path) +{ + boost::nowide::ofstream c; + c.open(path, std::ios::out | std::ios::trunc); + c << "# " << Slic3r::header_slic3r_generated() << std::endl; + + // Export the common "snapshot". + c << std::endl << "[snapshot]" << std::endl; + c << "id = " << this->id << std::endl; + c << "time_captured = " << Slic3r::Utils::format_time_ISO8601Z(this->time_captured) << std::endl; + c << "slic3r_version_captured = " << this->slic3r_version_captured.to_string() << std::endl; + c << "comment = " << this->comment << std::endl; + c << "reason = " << this->reason << std::endl; + + // Export the active presets at the time of the snapshot. + c << std::endl << "[presets]" << std::endl; + c << "print = " << this->print << std::endl; + c << "filament = " << this->filaments.front() << std::endl; + for (size_t i = 1; i < this->filaments.size(); ++ i) + c << "filament_" << std::to_string(i) << " = " << this->filaments[i] << std::endl; + c << "printer = " << this->printer << std::endl; + + // Export the vendor configs. + for (const VendorConfig &vc : this->vendor_configs) { + c << std::endl << "[Vendor:" << vc.name << "]" << std::endl; + c << "version = " << vc.version.to_string() << std::endl; + c << "min_slic3r_version = " << vc.min_slic3r_version.to_string() << std::endl; + c << "max_slic3r_version = " << vc.max_slic3r_version.to_string() << std::endl; + } + c.close(); +} + +void Snapshot::export_selections(AppConfig &config) const +{ + assert(filaments.size() >= 1); + config.clear_section("presets"); + config.set("presets", "print", print); + config.set("presets", "filament", filaments.front()); + for (int i = 1; i < filaments.size(); ++i) { + char name[64]; + sprintf(name, "filament_%d", i); + config.set("presets", name, filaments[i]); + } + config.set("presets", "printer", printer); +} + +size_t SnapshotDB::load_db() +{ + boost::filesystem::path snapshots_dir = SnapshotDB::create_db_dir(); + + m_snapshots.clear(); + + // Walk over the snapshot directories and load their index. + std::string errors_cummulative; + for (auto &dir_entry : boost::filesystem::directory_iterator(snapshots_dir)) + if (boost::filesystem::is_directory(dir_entry.status())) { + // Try to read "snapshot.ini". + boost::filesystem::path path_ini = dir_entry.path() / SLIC3R_SNAPSHOT_FILE; + Snapshot snapshot; + try { + snapshot.load_ini(path_ini.string()); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + continue; + } + // Check that the name of the snapshot directory matches the snapshot id stored in the snapshot.ini file. + if (dir_entry.path().filename().string() != snapshot.id) { + errors_cummulative += std::string("Snapshot ID ") + snapshot.id + " does not match the snapshot directory " + dir_entry.path().filename().string() + "\n"; + continue; + } + m_snapshots.emplace_back(std::move(snapshot)); + } + + if (! errors_cummulative.empty()) + throw std::runtime_error(errors_cummulative); + return m_snapshots.size(); +} + +static void copy_config_dir_single_level(const boost::filesystem::path &path_src, const boost::filesystem::path &path_dst) +{ + if (! boost::filesystem::is_directory(path_dst) && + ! boost::filesystem::create_directory(path_dst)) + throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + path_dst.string()); + + for (auto &dir_entry : boost::filesystem::directory_iterator(path_src)) + if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) + boost::filesystem::copy_file(dir_entry.path(), path_dst / dir_entry.path().filename(), boost::filesystem::copy_option::overwrite_if_exists); +} + +static void delete_existing_ini_files(const boost::filesystem::path &path) +{ + if (! boost::filesystem::is_directory(path)) + return; + for (auto &dir_entry : boost::filesystem::directory_iterator(path)) + if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) + boost::filesystem::remove(dir_entry.path()); +} + +const Snapshot& SnapshotDB::make_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment) +{ + boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); + boost::filesystem::path snapshot_db_dir = SnapshotDB::create_db_dir(); + + // 1) Prepare the snapshot structure. + Snapshot snapshot; + // Snapshot header. + snapshot.time_captured = Slic3r::Utils::get_current_time_utc(); + snapshot.id = Slic3r::Utils::format_time_ISO8601Z(snapshot.time_captured); + snapshot.slic3r_version_captured = *Semver::parse(SLIC3R_VERSION); + snapshot.comment = comment; + snapshot.reason = reason; + // Active presets at the time of the snapshot. + snapshot.print = app_config.get("presets", "print"); + snapshot.filaments.emplace_back(app_config.get("presets", "filament")); + snapshot.printer = app_config.get("presets", "printer"); + for (unsigned int i = 1; i < 1000; ++ i) { + char name[64]; + sprintf(name, "filament_%d", i); + if (! app_config.has("presets", name)) + break; + snapshot.filaments.emplace_back(app_config.get("presets", name)); + } + // Vendor specific config bundles and installed printers. + + // Backup the presets. + boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id; + for (const char *subdir : { "print", "filament", "printer", "vendor" }) + copy_config_dir_single_level(data_dir / subdir, snapshot_dir / subdir); + snapshot.save_ini((snapshot_dir / "snapshot.ini").string()); + m_snapshots.emplace_back(std::move(snapshot)); + return m_snapshots.back(); +} + +void SnapshotDB::restore_snapshot(const std::string &id, AppConfig &app_config) +{ + for (const Snapshot &snapshot : m_snapshots) + if (snapshot.id == id) { + this->restore_snapshot(snapshot, app_config); + return; + } + throw std::runtime_error(std::string("Snapshot with id " + id + " was not found.")); +} + +void SnapshotDB::restore_snapshot(const Snapshot &snapshot, AppConfig &app_config) +{ + boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); + boost::filesystem::path snapshot_db_dir = SnapshotDB::create_db_dir(); + boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id; + + // Remove existing ini files and restore the ini files from the snapshot. + for (const char *subdir : { "print", "filament", "printer", "vendor" }) { + delete_existing_ini_files(data_dir / subdir); + copy_config_dir_single_level(snapshot_dir / subdir, data_dir / subdir); + } + + // Update app_config from the snapshot. + snapshot.export_selections(app_config); + + // Store information about the snapshot. + +} + +boost::filesystem::path SnapshotDB::create_db_dir() +{ + boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); + boost::filesystem::path snapshots_dir = data_dir / SLIC3R_SNAPSHOTS_DIR; + for (const boost::filesystem::path &path : { data_dir, snapshots_dir }) { + boost::filesystem::path subdir = path; + subdir.make_preferred(); + if (! boost::filesystem::is_directory(subdir) && + ! boost::filesystem::create_directory(subdir)) + throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + subdir.string()); + } + return snapshots_dir; +} + +} // namespace Config +} // namespace GUI +} // namespace Slic3r diff --git a/xs/src/slic3r/Config/Snapshot.hpp b/xs/src/slic3r/Config/Snapshot.hpp new file mode 100644 index 000000000..358797bf7 --- /dev/null +++ b/xs/src/slic3r/Config/Snapshot.hpp @@ -0,0 +1,106 @@ +#ifndef slic3r_GUI_Snapshot_ +#define slic3r_GUI_Snapshot_ + +#include +#include + +#include + +#include "../Utils/Semver.hpp" + +namespace Slic3r { + +class AppConfig; + +namespace GUI { +namespace Config { + +// A snapshot contains: +// Slic3r.ini +// vendor/ +// print/ +// filament/ +// printer/ +class Snapshot +{ +public: + enum Reason { + SNAPSHOT_UNKNOWN, + SNAPSHOT_UPGRADE, + SNAPSHOT_DOWNGRADE, + SNAPSHOT_USER, + }; + + Snapshot() { clear(); } + + void clear(); + void load_ini(const std::string &path); + void save_ini(const std::string &path); + + // Export the print / filament / printer selections to be activated into the AppConfig. + void export_selections(AppConfig &config) const; + + // ID of a snapshot should equal to the name of the snapshot directory. + // The ID contains the date/time, reason and comment to be human readable. + std::string id; + std::time_t time_captured; + // Which Slic3r version captured this snapshot? + Semver slic3r_version_captured = Semver::invalid(); + // Comment entered by the user at the start of the snapshot capture. + std::string comment; + Reason reason; + + // Active presets at the time of the snapshot. + std::string print; + std::vector filaments; + std::string printer; + + // Annotation of the vendor configuration stored in the snapshot. + // This information is displayed to the user and used to decide compatibility + // of the configuration stored in the snapshot with the running Slic3r version. + struct VendorConfig { + // Name of the vendor contained in this snapshot. + std::string name; + // Version of the vendor config contained in this snapshot. + Semver version = Semver::invalid(); + // Minimum Slic3r version compatible with this vendor configuration. + Semver min_slic3r_version = Semver::zero(); + // Maximum Slic3r version compatible with this vendor configuration, or empty. + Semver max_slic3r_version = Semver::inf(); + }; + // List of vendor configs contained in this snapshot. + std::vector vendor_configs; +}; + +class SnapshotDB +{ +public: + typedef std::vector::const_iterator const_iterator; + + // Load the snapshot database from the snapshots directory. + // If the snapshot directory or its parent does not exist yet, it will be created. + // Returns a number of snapshots loaded. + size_t load_db(); + + // Create a snapshot directory, copy the vendor config bundles, user print/filament/printer profiles, + // create an index. + const Snapshot& make_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment); + void restore_snapshot(const std::string &id, AppConfig &app_config); + void restore_snapshot(const Snapshot &snapshot, AppConfig &app_config); + + const_iterator begin() const { return m_snapshots.begin(); } + const_iterator end() const { return m_snapshots.end(); } + const std::vector& snapshots() const { return m_snapshots; } + +private: + // Create the snapshots directory if it does not exist yet. + static boost::filesystem::path create_db_dir(); + + std::vector m_snapshots; +}; + +} // namespace Config +} // namespace GUI +} // namespace Slic3r + +#endif /* slic3r_GUI_Snapshot_ */ diff --git a/xs/src/slic3r/Config/Version.cpp b/xs/src/slic3r/Config/Version.cpp new file mode 100644 index 000000000..1102f3149 --- /dev/null +++ b/xs/src/slic3r/Config/Version.cpp @@ -0,0 +1,136 @@ +#include "Version.hpp" + +#include +#include +#include + +#include "../../libslic3r/libslic3r.h" +#include "../../libslic3r/Config.hpp" + +namespace Slic3r { +namespace GUI { +namespace Config { + +static boost::optional s_current_slic3r_semver = Semver::parse(SLIC3R_VERSION); + +bool Version::is_current_slic3r_supported() const +{ + return this->is_slic3r_supported(*s_current_slic3r_semver); +} + +inline char* left_trim(char *c) +{ + for (; *c == ' ' || *c == '\t'; ++ c); + return c; +} + +inline char* right_trim(char *start) +{ + char *end = start + strlen(start) - 1; + for (; end >= start && (*end == ' ' || *end == '\t'); -- end); + *(++ end) = 0; + return end; +} + +inline std::string unquote_value(char *value, char *end, const std::string &path, int idx_line) +{ + std::string svalue; + if (value == end) { + // Empty string is a valid string. + } else if (*value == '"') { + if (++ value < -- end || *end != '"') + throw file_parser_error("String not enquoted correctly", path, idx_line); + *end = 0; + if (! unescape_string_cstyle(value, svalue)) + throw file_parser_error("Invalid escape sequence inside a quoted string", path, idx_line); + } + return svalue; +} + +inline std::string unquote_version_comment(char *value, char *end, const std::string &path, int idx_line) +{ + std::string svalue; + if (value == end) { + // Empty string is a valid string. + } else if (*value == '"') { + if (++ value < -- end || *end != '"') + throw file_parser_error("Version comment not enquoted correctly", path, idx_line); + *end = 0; + if (! unescape_string_cstyle(value, svalue)) + throw file_parser_error("Invalid escape sequence inside a quoted version comment", path, idx_line); + } + return svalue; +} + +size_t Index::load(const std::string &path) +{ + m_configs.clear(); + + boost::nowide::ifstream ifs(path); + std::string line; + size_t idx_line = 0; + Version ver; + while (std::getline(ifs, line)) { + ++ idx_line; + // Skip the initial white spaces. + char *key = left_trim(const_cast(line.data())); + // Right trim the line. + char *end = right_trim(key); + // Keyword may only contain alphanumeric characters. Semantic version may in addition contain "+.-". + char *key_end = key; + bool maybe_semver = false; + for (;; ++ key) { + if (strchr("+.-", *key) != nullptr) + maybe_semver = true; + else if (! std::isalnum(*key)) + break; + } + if (*key != 0 && *key != ' ' && *key != '\t' && *key != '=') + throw file_parser_error("Invalid keyword or semantic version", path, idx_line); + *key_end = 0; + boost::optional semver; + if (maybe_semver) + semver = Semver::parse(key); + char *value = left_trim(key_end); + if (*value == '=') { + if (semver) + throw file_parser_error("Key cannot be a semantic version", path, idx_line); + // Verify validity of the key / value pair. + std::string svalue = unquote_value(left_trim(++ value), end, path, idx_line); + if (key == "min_sic3r_version" || key == "max_slic3r_version") { + if (! svalue.empty()) + semver = Semver::parse(key); + if (! semver) + throw file_parser_error(std::string(key) + " must referece a valid semantic version", path, idx_line); + if (key == "min_sic3r_version") + ver.min_slic3r_version = *semver; + else + ver.max_slic3r_version = *semver; + } else { + // Ignore unknown keys, as there may come new keys in the future. + } + } + if (! semver) + throw file_parser_error("Invalid semantic version", path, idx_line); + ver.config_version = *semver; + ver.comment = (end <= key_end) ? "" : unquote_version_comment(value, end, path, idx_line); + m_configs.emplace_back(ver); + } + + return m_configs.size(); +} + +Index::const_iterator Index::recommended() const +{ + int idx = -1; + const_iterator highest = m_configs.end(); + for (const_iterator it = this->begin(); it != this->end(); ++ it) + if (it->is_current_slic3r_supported() && + (highest == this->end() || highest->max_slic3r_version < it->max_slic3r_version)) + highest = it; + return highest; +} + +} // namespace Config +} // namespace GUI +} // namespace Slic3r diff --git a/xs/src/slic3r/Config/Version.hpp b/xs/src/slic3r/Config/Version.hpp new file mode 100644 index 000000000..7af1d4b5b --- /dev/null +++ b/xs/src/slic3r/Config/Version.hpp @@ -0,0 +1,75 @@ +#ifndef slic3r_GUI_ConfigIndex_ +#define slic3r_GUI_ConfigIndex_ + +#include +#include + +#include "../../libslic3r/FileParserError.hpp" +#include "../Utils/Semver.hpp" + +namespace Slic3r { +namespace GUI { +namespace Config { + +// Configuration bundle version. +struct Version +{ + // Version of this config. + Semver config_version = Semver::invalid(); + // Minimum Slic3r version, for which this config is applicable. + Semver min_slic3r_version = Semver::zero(); + // Maximum Slic3r version, for which this config is recommended. + // Slic3r should read older configuration and upgrade to a newer format, + // but likely there has been a better configuration published, using the new features. + Semver max_slic3r_version = Semver::inf(); + // Single comment line. + std::string comment; + + bool is_slic3r_supported(const Semver &slicer_version) const { return slicer_version.in_range(min_slic3r_version, max_slic3r_version); } + bool is_current_slic3r_supported() const; +}; + +// Index of vendor specific config bundle versions and Slic3r compatibilities. +// The index is being downloaded from the internet, also an initial version of the index +// is contained in the Slic3r installation. +// +// The index has a simple format: +// +// min_sic3r_version = +// max_slic3r_version = +// config_version "comment" +// config_version "comment" +// ... +// min_slic3r_version = +// max_slic3r_version = +// config_version comment +// config_version comment +// ... +// +// The min_slic3r_version, max_slic3r_version keys are applied to the config versions below, +// empty slic3r version means an open interval. +class Index +{ +public: + typedef std::vector::const_iterator const_iterator; + // Read a config index file in the simple format described in the Index class comment. + // Throws Slic3r::file_parser_error and the standard std file access exceptions. + size_t load(const std::string &path); + + const_iterator begin() const { return m_configs.begin(); } + const_iterator end() const { return m_configs.end(); } + const std::vector& configs() const { return m_configs; } + // Finds a recommended config to be installed for the current Slic3r version. + // Returns configs().end() if such version does not exist in the index. This shall never happen + // if the index is valid. + const_iterator recommended() const; + +private: + std::vector m_configs; +}; + +} // namespace Config +} // namespace GUI +} // namespace Slic3r + +#endif /* slic3r_GUI_ConfigIndex_ */ diff --git a/xs/src/slic3r/Utils/Semver.hpp b/xs/src/slic3r/Utils/Semver.hpp index 3606e90e4..7fc3b8033 100644 --- a/xs/src/slic3r/Utils/Semver.hpp +++ b/xs/src/slic3r/Utils/Semver.hpp @@ -28,6 +28,24 @@ public: } } + static const Semver zero() + { + static semver_t ver = { 0, 0, 0, nullptr, nullptr }; + return Semver(ver); + } + + static const Semver inf() + { + static semver_t ver = { std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max(), nullptr, nullptr }; + return Semver(ver); + } + + static const Semver invalid() + { + static semver_t ver = { -1, 0, 0, nullptr, nullptr }; + return Semver(ver); + } + Semver(Semver &&other) { *this = std::move(other); } Semver(const Semver &other) { *this = other; } @@ -36,13 +54,16 @@ public: ver = other.ver; other.ver.major = other.ver.minor = other.ver.patch = 0; other.ver.metadata = other.ver.prerelease = nullptr; + return *this; } Semver &operator=(const Semver &other) { + ::semver_free(&ver); ver = other.ver; if (other.ver.metadata != nullptr) { std::strcpy(ver.metadata, other.ver.metadata); } if (other.ver.prerelease != nullptr) { std::strcpy(ver.prerelease, other.ver.prerelease); } + return *this; } ~Semver() { ::semver_free(&ver); } @@ -55,8 +76,10 @@ public: bool operator>=(const Semver &b) const { return ::semver_compare(ver, b.ver) >= 0; } bool operator>(const Semver &b) const { return ::semver_compare(ver, b.ver) == 1; } // We're using '&' instead of the '~' operator here as '~' is unary-only: + // Satisfies patch if Major and minor are equal. bool operator&(const Semver &b) const { return ::semver_satisfies_patch(ver, b.ver); } bool operator^(const Semver &b) const { return ::semver_satisfies_caret(ver, b.ver); } + bool in_range(const Semver &low, const Semver &high) const { return low <= *this && *this <= high; } // Conversion std::string to_string() const { @@ -79,6 +102,7 @@ public: Semver operator-(const Major &b) const { Semver res(*this); return res -= b; } Semver operator-(const Minor &b) const { Semver res(*this); return res -= b; } Semver operator-(const Patch &b) const { Semver res(*this); return res -= b; } + private: semver_t ver; diff --git a/xs/src/slic3r/Utils/Time.cpp b/xs/src/slic3r/Utils/Time.cpp new file mode 100644 index 000000000..c4123c7bb --- /dev/null +++ b/xs/src/slic3r/Utils/Time.cpp @@ -0,0 +1,63 @@ +#include "Time.hpp" + +namespace Slic3r { +namespace Utils { + +time_t parse_time_ISO8601Z(const std::string &sdate) +{ + int y, M, d, h, m; + float s; + if (sscanf(sdate.c_str(), "%d-%d-%dT%d:%d:%fZ", &y, &M, &d, &h, &m, &s) != 6) + return (time_t)-1; + struct tm tms; + tms.tm_year = y - 1900; // Year since 1900 + tms.tm_mon = M - 1; // 0-11 + tms.tm_mday = d; // 1-31 + tms.tm_hour = h; // 0-23 + tms.tm_min = m; // 0-59 + tms.tm_sec = (int)s; // 0-61 (0-60 in C++11) + return mktime(&tms); +} + +std::string format_time_ISO8601Z(time_t time) +{ + struct tm tms; +#ifdef WIN32 + gmtime_s(time, &tms); +#else + gmtime_r(&tms, time); +#endif + char buf[128]; + sprintf(buf, "%d-%d-%dT%d:%d:%fZ", + tms.tm_year + 1900 + tms.tm_mon + 1 + tms.tm_mday + tms.tm_hour + tms.tm_min + tms.tm_sec); + return buf; +} + +time_t get_current_time_utc() +{ +#ifdef WIN32 + SYSTEMTIME st; + ::GetSystemTime(&st); + std::tm tm; + tm.tm_sec = st.wSecond; + tm.tm_min = st.wMinute; + tm.tm_hour = st.wHour; + tm.tm_mday = st.wDay; + tm.tm_mon = st.wMonth - 1; + tm.tm_year = st.wYear - 1900; + tm.tm_isdst = -1; + return mktime(&tm); +#else + return gmtime(); +#endif +} + +}; // namespace Utils +}; // namespace Slic3r + +#endif /* slic3r_Utils_Time_hpp_ */ diff --git a/xs/src/slic3r/Utils/Time.hpp b/xs/src/slic3r/Utils/Time.hpp new file mode 100644 index 000000000..6b2fbf893 --- /dev/null +++ b/xs/src/slic3r/Utils/Time.hpp @@ -0,0 +1,22 @@ +#ifndef slic3r_Utils_Time_hpp_ +#define slic3r_Utils_Time_hpp_ + +#include +#include + +namespace Slic3r { +namespace Utils { + +// Utilities to convert an UTC time_t to/from an ISO8601 time format, +// useful for putting timestamps into file and directory names. +// Returns (time_t)-1 on error. +extern time_t parse_time_ISO8601Z(const std::string &s); +extern std::string format_time_ISO8601Z(time_t time); + +// There is no gmtime() on windows. +time_t get_current_time_utc(); + +}; // namespace Utils +}; // namespace Slic3r + +#endif /* slic3r_Utils_Time_hpp_ */