mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-29 11:51:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22bd674c1d | ||
|
|
536ef75894 | ||
|
|
cda1588578 | ||
|
|
ec0d8c225f | ||
|
|
3040ebaac1 | ||
|
|
5298e49dd2 | ||
|
|
d347a80ef8 | ||
|
|
e340a13c18 | ||
|
|
9e16cdb23b | ||
|
|
58842bab05 | ||
|
|
04204ec0d3 | ||
|
|
9ed459e132 |
+47
-6
@@ -2116,9 +2116,14 @@ int CLI::run(int argc, char **argv)
|
|||||||
|
|
||||||
// One resolver for the whole run, so presets from the same vendor tree share its load.
|
// One resolver for the whole run, so presets from the same vendor tree share its load.
|
||||||
std::unique_ptr<PresetBundle> system_preset_resolver;
|
std::unique_ptr<PresetBundle> system_preset_resolver;
|
||||||
auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
|
auto ensure_system_preset_resolver = [&system_preset_resolver]() -> PresetBundle & {
|
||||||
std::string &config_type, const std::string &config_from,
|
if (!system_preset_resolver)
|
||||||
bool probe_type, std::string &error) {
|
system_preset_resolver = std::make_unique<PresetBundle>();
|
||||||
|
return *system_preset_resolver;
|
||||||
|
};
|
||||||
|
auto resolve_preset = [&ensure_cli_preset_bundle, &ensure_system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
|
||||||
|
std::string &config_type, const std::string &config_from,
|
||||||
|
bool probe_type, std::string &error) {
|
||||||
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
|
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
|
||||||
if (!probe_type && (inherits == nullptr || inherits->value.empty()))
|
if (!probe_type && (inherits == nullptr || inherits->value.empty()))
|
||||||
return true;
|
return true;
|
||||||
@@ -2126,9 +2131,7 @@ int CLI::run(int argc, char **argv)
|
|||||||
PresetBundle *bundle = nullptr;
|
PresetBundle *bundle = nullptr;
|
||||||
bool allow_source_manifest = false;
|
bool allow_source_manifest = false;
|
||||||
if (config_from == "system") {
|
if (config_from == "system") {
|
||||||
if (!system_preset_resolver)
|
bundle = &ensure_system_preset_resolver();
|
||||||
system_preset_resolver = std::make_unique<PresetBundle>();
|
|
||||||
bundle = system_preset_resolver.get();
|
|
||||||
allow_source_manifest = true;
|
allow_source_manifest = true;
|
||||||
} else {
|
} else {
|
||||||
bundle = ensure_cli_preset_bundle(error);
|
bundle = ensure_cli_preset_bundle(error);
|
||||||
@@ -3120,6 +3123,44 @@ int CLI::run(int argc, char **argv)
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load the project's printer and process settings as the GUI loads its presets: over the default preset,
|
||||||
|
// with every key the project does not list as changed, including keys saved before an option existed,
|
||||||
|
// taken from its current system preset.
|
||||||
|
auto load_project_preset = [this, &ensure_system_preset_resolver, ¤t_different_settings, filament_count](const std::string &system_name, Preset::Type type) {
|
||||||
|
if (system_name.empty())
|
||||||
|
return;
|
||||||
|
// Preset bookkeeping the CLI keeps in its own groups, e.g. inherits_group and print_compatible_printers.
|
||||||
|
static const std::set<std::string> bookkeeping_keys = {"inherits", "compatible_printers", "compatible_prints", "compatible_printers_condition",
|
||||||
|
"compatible_prints_condition", "print_settings_id", "printer_settings_id"};
|
||||||
|
const size_t index = type == Preset::TYPE_PRINTER ? filament_count + 1 : 0;
|
||||||
|
PresetBundle &resolver = ensure_system_preset_resolver();
|
||||||
|
DynamicPrintConfig system_config;
|
||||||
|
t_config_option_keys keys;
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(type,
|
||||||
|
type == Preset::TYPE_PRINTER ? resolver.printers.default_preset_for(m_print_config).config : resolver.prints.default_preset().config,
|
||||||
|
m_print_config, PresetBundle::project_different_keys(index < current_different_settings.size() ? current_different_settings[index] : std::string()),
|
||||||
|
[&](const std::string &) -> DynamicPrintConfig * {
|
||||||
|
std::string error;
|
||||||
|
if (resolver.resolve_system_preset(system_config, type, system_name, config_substitution_rule, error))
|
||||||
|
return &system_config;
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system preset '%1%' not resolved (%2%); the project keeps its values") % system_name % error;
|
||||||
|
return nullptr;
|
||||||
|
}, &keys);
|
||||||
|
for (const std::string &key : keys) {
|
||||||
|
const ConfigOption *opt = config.option(key);
|
||||||
|
const ConfigOption *old = m_print_config.option(key);
|
||||||
|
if (bookkeeping_keys.count(key) != 0 || opt == nullptr || (old != nullptr && *old == *opt))
|
||||||
|
continue;
|
||||||
|
BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% from '%2%': %3% -> %4%") % key % system_name % (old ? old->serialize() : std::string("(missing)")) % opt->serialize();
|
||||||
|
m_print_config.set_key_value(key, opt->clone());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// The --uptodate path refreshes the project from its own system configs.
|
||||||
|
if (new_printer_name.empty() && load_machine_config.empty())
|
||||||
|
load_project_preset(current_printer_system_name, Preset::TYPE_PRINTER);
|
||||||
|
if (new_process_name.empty() && load_process_config.empty())
|
||||||
|
load_project_preset(current_process_system_name, Preset::TYPE_PRINT);
|
||||||
|
|
||||||
std::vector<std::string>& different_settings = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values;
|
std::vector<std::string>& different_settings = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values;
|
||||||
std::vector<std::string>& inherits_group = m_print_config.option<ConfigOptionStrings>("inherits_group", true)->values;
|
std::vector<std::string>& inherits_group = m_print_config.option<ConfigOptionStrings>("inherits_group", true)->values;
|
||||||
inherits_group.resize(filament_count + 2, std::string());
|
inherits_group.resize(filament_count + 2, std::string());
|
||||||
|
|||||||
+10
-5
@@ -2504,7 +2504,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2556,7 +2557,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + err_msg;
|
ctx.msg = std::string(path) + "\n" + err_msg;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2580,7 +2582,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
boost::nowide::remove(path_tmp.c_str());
|
boost::nowide::remove(path_tmp.c_str());
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + ex.what();
|
ctx.msg = std::string(path) + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2693,7 +2696,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
if (ret) {
|
if (ret) {
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2712,7 +2716,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
|
|||||||
+50
-32
@@ -974,6 +974,37 @@ void Preset::get_extruder_names_and_keysets(Type type, std::string& extruder_id_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DynamicPrintConfig Preset::load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
|
||||||
|
const std::set<std::string> &different_settings_list,
|
||||||
|
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
|
||||||
|
t_config_option_keys *keys)
|
||||||
|
{
|
||||||
|
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
|
||||||
|
DynamicPrintConfig cfg(default_config);
|
||||||
|
// SoftFever: ignore print connection info from project
|
||||||
|
auto cfg_keys = cfg.keys();
|
||||||
|
cfg_keys.erase(std::remove_if(cfg_keys.begin(), cfg_keys.end(),
|
||||||
|
[](std::string &val) {
|
||||||
|
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
|
||||||
|
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
|
||||||
|
}),
|
||||||
|
cfg_keys.end());
|
||||||
|
cfg.apply_only(project_config, cfg_keys, true);
|
||||||
|
|
||||||
|
//add different settings check logic, replace the old system preset's default value with new system preset's default values
|
||||||
|
if (!different_settings_list.empty()) {
|
||||||
|
if (DynamicPrintConfig *base_config = find_base(Preset::inherits(cfg))) {
|
||||||
|
std::string extruder_id_name, extruder_variant_name;
|
||||||
|
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||||
|
Preset::get_extruder_names_and_keysets(type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||||
|
cfg.update_non_diff_values_to_base_config(*base_config, cfg_keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keys)
|
||||||
|
*keys = std::move(cfg_keys);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
bool Preset::has_lidar(PresetBundle *preset_bundle)
|
bool Preset::has_lidar(PresetBundle *preset_bundle)
|
||||||
{
|
{
|
||||||
bool has_lidar = false;
|
bool has_lidar = false;
|
||||||
@@ -2596,20 +2627,6 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
|||||||
const Semver file_version,
|
const Semver file_version,
|
||||||
const std::string filament_id)
|
const std::string filament_id)
|
||||||
{
|
{
|
||||||
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
|
|
||||||
DynamicPrintConfig cfg(this->default_preset_for(combined_config).config);
|
|
||||||
// SoftFever: ignore print connection info from project
|
|
||||||
auto keys = cfg.keys();
|
|
||||||
keys.erase(std::remove_if(keys.begin(), keys.end(),
|
|
||||||
[](std::string &val) {
|
|
||||||
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
|
|
||||||
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
|
|
||||||
}),
|
|
||||||
keys.end());
|
|
||||||
cfg.apply_only(combined_config, keys, true);
|
|
||||||
std::string &inherits = Preset::inherits(cfg);
|
|
||||||
|
|
||||||
//add different settings check logic, replace the old system preset's default value with new system preset's default values
|
|
||||||
std::deque<Preset>::iterator it = this->find_preset_internal(original_name);
|
std::deque<Preset>::iterator it = this->find_preset_internal(original_name);
|
||||||
bool found = it != m_presets.end() && it->name == original_name;
|
bool found = it != m_presets.end() && it->name == original_name;
|
||||||
if (! found) {
|
if (! found) {
|
||||||
@@ -2618,24 +2635,25 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
|||||||
found = it != m_presets.end();
|
found = it != m_presets.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string extruder_id_name, extruder_variant_name;
|
t_config_option_keys keys;
|
||||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
DynamicPrintConfig cfg = Preset::load_external_config(m_type, this->default_preset_for(combined_config).config, combined_config, different_settings_list,
|
||||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
[this, &original_name, &path, found, it](const std::string &inherits) -> DynamicPrintConfig * {
|
||||||
|
if (!inherits.empty()) {
|
||||||
if (!inherits.empty() && (different_settings_list.size() > 0)) {
|
auto iter = this->find_preset_internal(inherits);
|
||||||
auto iter = this->find_preset_internal(inherits);
|
if (iter == m_presets.end() || iter->name != inherits)
|
||||||
if (iter == m_presets.end() || iter->name != inherits)
|
iter = this->find_preset_renamed(inherits);
|
||||||
iter = this->find_preset_renamed(inherits);
|
if (iter == m_presets.end())
|
||||||
if (iter != m_presets.end()) {
|
return nullptr;
|
||||||
//std::vector<std::string> dirty_options = cfg.diff(iter->config);
|
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
|
return &iter->config;
|
||||||
cfg.update_non_diff_values_to_base_config(iter->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
}
|
||||||
}
|
if (found && it->is_system) {
|
||||||
}
|
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path;
|
||||||
else if (found && it->is_system && (different_settings_list.size() > 0)) {
|
return &it->config;
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path;
|
}
|
||||||
cfg.update_non_diff_values_to_base_config(it->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
return nullptr;
|
||||||
}
|
}, &keys);
|
||||||
|
std::string &inherits = Preset::inherits(cfg);
|
||||||
|
|
||||||
//BBS: add config related logs
|
//BBS: add config related logs
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits;
|
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits;
|
||||||
|
|||||||
@@ -403,6 +403,15 @@ public:
|
|||||||
std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type
|
std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type
|
||||||
|
|
||||||
static void get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set<std::string>** p_key_set1, std::set<std::string>** p_key_set2);
|
static void get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set<std::string>** p_key_set1, std::set<std::string>** p_key_set2);
|
||||||
|
// Config of a preset loaded from a project or config file: the project's values over the type's
|
||||||
|
// default preset config, without the print-host keys. When different_settings_list is not empty,
|
||||||
|
// every key not listed in it is then refreshed from the base system preset, which find_base returns
|
||||||
|
// for the project's "inherits" (nullptr when there is none), with the listed per-variant values
|
||||||
|
// mapped onto the base's extruder variants. keys, if given, receives the keys taken from the project.
|
||||||
|
static DynamicPrintConfig load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
|
||||||
|
const std::set<std::string> &different_settings_list,
|
||||||
|
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
|
||||||
|
t_config_option_keys *keys = nullptr);
|
||||||
std::string get_printer_id() const { return vendor ? vendor->id : ""; }
|
std::string get_printer_id() const { return vendor ? vendor->id : ""; }
|
||||||
|
|
||||||
bool has_lidar(PresetBundle *preset_bundle);
|
bool has_lidar(PresetBundle *preset_bundle);
|
||||||
|
|||||||
@@ -575,18 +575,20 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
|
|||||||
const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir,
|
const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir,
|
||||||
const std::string &vendor_id,
|
const std::string &vendor_id,
|
||||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||||
std::string &error)
|
std::string &error, bool allow_cache)
|
||||||
{
|
{
|
||||||
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule);
|
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule, allow_cache);
|
||||||
if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end())
|
if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end())
|
||||||
return it->second.get();
|
return it->second.get();
|
||||||
|
|
||||||
// The library loads with no base of its own, so the tree a vendor inherits from
|
// The library loads with no base of its own, so the tree a vendor inherits from
|
||||||
// is the same one that resolves the library's own presets.
|
// is the same one that resolves the library's own presets.
|
||||||
const PresetBundle *library = nullptr;
|
const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY);
|
||||||
|
const PresetBundle *library = nullptr;
|
||||||
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
|
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
|
||||||
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
|
(boost::filesystem::is_regular_file(root_dir / (library_file + ".json")) ||
|
||||||
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error);
|
(allow_cache && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"))))) {
|
||||||
|
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache);
|
||||||
if (library == nullptr) {
|
if (library == nullptr) {
|
||||||
error = "OrcaFilamentLibrary contains invalid presets";
|
error = "OrcaFilamentLibrary contains invalid presets";
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -595,7 +597,7 @@ const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::pa
|
|||||||
|
|
||||||
auto bundle = std::make_unique<PresetBundle>();
|
auto bundle = std::make_unique<PresetBundle>();
|
||||||
bundle->m_preserve_vendor_source_paths = true;
|
bundle->m_preserve_vendor_source_paths = true;
|
||||||
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false);
|
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, allow_cache);
|
||||||
if (bundle->error_count() != 0) {
|
if (bundle->error_count() != 0) {
|
||||||
error = "Vendor bundle contains invalid presets";
|
error = "Vendor bundle contains invalid presets";
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -638,6 +640,43 @@ bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PresetBundle::resolve_system_preset(DynamicPrintConfig &config, Preset::Type type, const std::string &name,
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule, std::string &error)
|
||||||
|
{
|
||||||
|
const std::string vendor_id = find_preset_vendor(name, type);
|
||||||
|
if (vendor_id.empty()) {
|
||||||
|
error = "No vendor lists the preset";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Release builds ship a vendor as its preset cache alone, without the profile JSONs.
|
||||||
|
auto installed = [&vendor_id](const fs::path &root) {
|
||||||
|
return fs::is_regular_file(root / (vendor_id + ".json")) || fs::is_regular_file(root / (vendor_id + ".opc"));
|
||||||
|
};
|
||||||
|
fs::path root_dir = fs::path(data_dir()) / PRESET_SYSTEM_DIR;
|
||||||
|
if (!installed(root_dir))
|
||||||
|
root_dir = fs::path(resources_dir()) / PRESET_PROFILES_DIR;
|
||||||
|
const bool cache_only = !fs::is_regular_file(root_dir / (vendor_id + ".json"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const PresetBundle *vendor = load_source_vendor(root_dir, vendor_id, compatibility_rule, error, cache_only);
|
||||||
|
if (vendor == nullptr)
|
||||||
|
return false;
|
||||||
|
const PresetCollection &collection = type == Preset::TYPE_PRINTER ? vendor->printers :
|
||||||
|
type == Preset::TYPE_PRINT ? vendor->prints : vendor->filaments;
|
||||||
|
const Preset *preset = collection.find_preset(name, false);
|
||||||
|
if (preset == nullptr) {
|
||||||
|
error = "Preset was not found in its vendor bundle";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
config = preset->config;
|
||||||
|
} catch (const std::exception &ex) {
|
||||||
|
error = ex.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
PresetBundle::PresetBundle(const PresetBundle &rhs)
|
PresetBundle::PresetBundle(const PresetBundle &rhs)
|
||||||
{
|
{
|
||||||
*this = rhs;
|
*this = rhs;
|
||||||
@@ -4574,6 +4613,15 @@ const std::set<std::string> ignore_settings_list ={
|
|||||||
"print_settings_id", "filament_settings_id", "printer_settings_id"
|
"print_settings_id", "filament_settings_id", "printer_settings_id"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::set<std::string> PresetBundle::project_different_keys(const std::string &different_settings)
|
||||||
|
{
|
||||||
|
std::vector<std::string> keys;
|
||||||
|
Slic3r::unescape_strings_cstyle(different_settings, keys);
|
||||||
|
std::set<std::string> keys_set(keys.begin(), keys.end());
|
||||||
|
keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
|
||||||
|
return keys_set;
|
||||||
|
}
|
||||||
|
|
||||||
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new, std::optional<std::vector<int>> filament_volume_maps_new) const
|
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new, std::optional<std::vector<int>> filament_volume_maps_new) const
|
||||||
{
|
{
|
||||||
DynamicPrintConfig out;
|
DynamicPrintConfig out;
|
||||||
@@ -5279,15 +5327,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
if (!is_published) {
|
if (!is_published) {
|
||||||
//BBS: add different settings logic
|
//BBS: add different settings logic
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id");
|
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id");
|
||||||
std::vector<std::string> print_different_keys_vector;
|
std::set<std::string> print_different_keys_set = project_different_keys(different_values[0]);
|
||||||
std::string print_different_settings = different_values[0];
|
|
||||||
Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector);
|
|
||||||
std::set<std::string> print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end());
|
|
||||||
//if (!has_different_settings_to_system) {
|
|
||||||
// print_different_keys_set.clear();
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
|
|
||||||
if (!print_compatible_printers.empty()) {
|
if (!print_compatible_printers.empty()) {
|
||||||
ConfigOptionStrings* compatible_printers = config.option<ConfigOptionStrings>("compatible_printers", true);
|
ConfigOptionStrings* compatible_printers = config.option<ConfigOptionStrings>("compatible_printers", true);
|
||||||
compatible_printers->values = print_compatible_printers;
|
compatible_printers->values = print_compatible_printers;
|
||||||
@@ -5298,15 +5338,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
//clear compatible printers
|
//clear compatible printers
|
||||||
clear_compatible_printers(config);
|
clear_compatible_printers(config);
|
||||||
|
|
||||||
std::vector<std::string> printer_different_keys_vector;
|
std::set<std::string> printer_different_keys_set = project_different_keys(different_values[num_filaments + 1]);
|
||||||
std::string printer_different_settings = different_values[num_filaments + 1];
|
|
||||||
Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector);
|
|
||||||
std::set<std::string> printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end());
|
|
||||||
//if (!has_different_settings_to_system) {
|
|
||||||
// printer_different_keys_set.clear();
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
|
|
||||||
//BBS: add config related logs
|
//BBS: add config related logs
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id");
|
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id");
|
||||||
load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string());
|
load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string());
|
||||||
@@ -5325,15 +5357,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
Preset *loaded = nullptr;
|
Preset *loaded = nullptr;
|
||||||
|
|
||||||
//BBS: add different settings logic
|
//BBS: add different settings logic
|
||||||
std::vector<std::string> filament_different_keys_vector;
|
std::set<std::string> filament_different_keys_set = project_different_keys(different_values[1]);
|
||||||
std::string filament_different_settings = different_values[1];
|
|
||||||
Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector);
|
|
||||||
std::set<std::string> filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end());
|
|
||||||
//if (!has_different_settings_to_system) {
|
|
||||||
// filament_different_keys_set.clear();
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
|
|
||||||
|
|
||||||
std::string filament_id = filament_ids[0];
|
std::string filament_id = filament_ids[0];
|
||||||
//BBS: add config related logs
|
//BBS: add config related logs
|
||||||
@@ -5396,15 +5420,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
cfg.opt_string("inherits", true) = inherits_values[i + 1];
|
cfg.opt_string("inherits", true) = inherits_values[i + 1];
|
||||||
|
|
||||||
//BBS: add different settings logic
|
//BBS: add different settings logic
|
||||||
std::vector<std::string> filament_different_keys_vector;
|
std::set<std::string> filament_different_keys_set = project_different_keys(different_values[i+1]);
|
||||||
std::string filament_different_settings = different_values[i+1];
|
|
||||||
Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector);
|
|
||||||
std::set<std::string> filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end());
|
|
||||||
//if (!has_different_settings_to_system) {
|
|
||||||
// filament_different_keys_set.clear();
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
|
|
||||||
|
|
||||||
std::string filament_id = filament_ids[i];
|
std::string filament_id = filament_ids[i];
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,9 @@ public:
|
|||||||
|
|
||||||
// ORCA: utility function to find the vendor for a given preset name
|
// ORCA: utility function to find the vendor for a given preset name
|
||||||
static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type);
|
static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type);
|
||||||
|
// Keys a project keeps when its presets are loaded: those listed in its escaped
|
||||||
|
// "different_settings_to_system" entry for the preset, plus the preset bookkeeping keys.
|
||||||
|
static std::set<std::string> project_different_keys(const std::string &different_settings);
|
||||||
|
|
||||||
PresetBundle();
|
PresetBundle();
|
||||||
PresetBundle(const PresetBundle &rhs);
|
PresetBundle(const PresetBundle &rhs);
|
||||||
@@ -273,6 +276,10 @@ public:
|
|||||||
const std::string &source_file,
|
const std::string &source_file,
|
||||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||||
std::string &error, bool allow_source_manifest = true);
|
std::string &error, bool allow_source_manifest = true);
|
||||||
|
// Resolve a system preset by name. The vendor tree is read from data_dir()/system when installed
|
||||||
|
// there, as the GUI reads it, and from the bundled profiles otherwise.
|
||||||
|
bool resolve_system_preset(DynamicPrintConfig &config, Preset::Type type, const std::string &name,
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule, std::string &error);
|
||||||
|
|
||||||
// Load selections (current print, current filaments, current printer) from config.ini
|
// Load selections (current print, current filaments, current printer) from config.ini
|
||||||
// This is done just once on application start up.
|
// This is done just once on application start up.
|
||||||
@@ -663,13 +670,14 @@ private:
|
|||||||
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
|
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
|
||||||
// resolved through this bundle shares one load per source root and vendor. The
|
// resolved through this bundle shares one load per source root and vendor. The
|
||||||
// filament library is one such tree, shared by every vendor under its root.
|
// filament library is one such tree, shared by every vendor under its root.
|
||||||
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
|
// A tree read from its preset cache is kept apart: its presets carry no source file.
|
||||||
|
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule, bool>, std::unique_ptr<PresetBundle>>
|
||||||
m_source_vendor_bundles;
|
m_source_vendor_bundles;
|
||||||
|
|
||||||
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
|
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
|
||||||
const std::string &vendor_id,
|
const std::string &vendor_id,
|
||||||
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
ForwardCompatibilitySubstitutionRule compatibility_rule,
|
||||||
std::string &error);
|
std::string &error, bool allow_cache = false);
|
||||||
|
|
||||||
// Orca: validation only - flag any printer with two or more compatible
|
// Orca: validation only - flag any printer with two or more compatible
|
||||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||||
|
|||||||
+10
-5
@@ -2717,7 +2717,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
||||||
@@ -3344,7 +3345,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
||||||
@@ -4950,7 +4952,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4980,7 +4983,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = file + "\n" + ex.what();
|
ctx.msg = file + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4994,7 +4998,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
|
|||||||
@@ -256,13 +256,10 @@ extern bool is_gallery_file(const std::string& path, char const* type);
|
|||||||
extern bool is_shapes_dir(const std::string& dir);
|
extern bool is_shapes_dir(const std::string& dir);
|
||||||
//BBS: add json support
|
//BBS: add json support
|
||||||
extern bool is_json_file(const std::string& path);
|
extern bool is_json_file(const std::string& path);
|
||||||
// True if rel_path is relative, has no ".." component or embedded NUL and, joined to root, still resolves inside it.
|
// True if rel_path is relative, has no ".." component and, joined to root, still resolves inside it.
|
||||||
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
||||||
// is rejected on all of them.
|
// is rejected on all of them.
|
||||||
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
||||||
// True if a symlink stored at link_rel_path (relative to root) with this target stays inside root: the target
|
|
||||||
// must be relative, and joined to the link's directory it must pass is_path_within_root.
|
|
||||||
extern bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root);
|
|
||||||
|
|
||||||
// Orca: custom protocal support utils
|
// Orca: custom protocal support utils
|
||||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
#include "miniz_extension.hpp"
|
#include "miniz_extension.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
|
||||||
#include <boost/log/trivial.hpp>
|
|
||||||
|
|
||||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||||
#include "boost/nowide/cstdio.hpp"
|
#include "boost/nowide/cstdio.hpp"
|
||||||
#endif
|
#endif
|
||||||
@@ -118,66 +115,6 @@ std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_
|
|||||||
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
|
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir)
|
|
||||||
{
|
|
||||||
mz_zip_archive archive;
|
|
||||||
mz_zip_zero_struct(&archive);
|
|
||||||
|
|
||||||
if (!open_zip_reader(&archive, zip_path_utf8)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for " << zip_path_utf8;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
|
||||||
mz_zip_archive_file_stat stat;
|
|
||||||
|
|
||||||
// Validate every entry first so an archive with a single escaping entry leaves no partial output behind.
|
|
||||||
const boost::filesystem::path root(dest_dir);
|
|
||||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
|
||||||
if (mz_zip_reader_file_stat(&archive, i, &stat) && !is_path_within_root(stat.m_filename, root)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unzip: rejecting " << zip_path_utf8 << ", entry " << stat.m_filename << " resolves outside " << dest_dir;
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
|
||||||
if (!mz_zip_reader_file_stat(&archive, i, &stat)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Unzip: read file stat failed";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
|
||||||
try {
|
|
||||||
if (stat.m_is_directory) {
|
|
||||||
const boost::filesystem::path dest_path(dest_file);
|
|
||||||
if (!boost::filesystem::exists(dest_path))
|
|
||||||
boost::filesystem::create_directories(dest_path);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (stat.m_uncomp_size == 0) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Unzip: invalid size for file " << stat.m_filename;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Replace a symlink at the destination rather than writing through it.
|
|
||||||
const boost::filesystem::path dest_path(dest_file);
|
|
||||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
|
||||||
boost::filesystem::remove(dest_path);
|
|
||||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Unzip: successfully extract file " << stat.m_file_index << " to " << dest_file;
|
|
||||||
} catch (const std::exception &e) {
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unzip: archive read exception: " << e.what();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
MZ_Archive::MZ_Archive()
|
MZ_Archive::MZ_Archive()
|
||||||
{
|
{
|
||||||
mz_zip_zero_struct(&arch);
|
mz_zip_zero_struct(&arch);
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname_utf8);
|
|||||||
bool close_zip_reader(mz_zip_archive *zip);
|
bool close_zip_reader(mz_zip_archive *zip);
|
||||||
bool close_zip_writer(mz_zip_archive *zip);
|
bool close_zip_writer(mz_zip_archive *zip);
|
||||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||||
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
|
||||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
|
||||||
|
|
||||||
class MZ_Archive {
|
class MZ_Archive {
|
||||||
public:
|
public:
|
||||||
|
|||||||
+1
-16
@@ -1093,9 +1093,6 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
||||||
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
||||||
return false;
|
return false;
|
||||||
// The filesystem calls stop at a NUL, so they would act on a shorter path than the one checked here.
|
|
||||||
if (rel_path.find('\0') != std::string::npos)
|
|
||||||
return false;
|
|
||||||
for (size_t start = 0; start <= rel_path.size();) {
|
for (size_t start = 0; start <= rel_path.size();) {
|
||||||
size_t end = start;
|
size_t end = start;
|
||||||
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
||||||
@@ -1106,10 +1103,7 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
}
|
}
|
||||||
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
||||||
try {
|
try {
|
||||||
std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
const std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
||||||
// A trailing separator on root would otherwise fail the prefix match below for every path.
|
|
||||||
while (!root_str.empty() && (root_str.back() == '/' || root_str.back() == boost::filesystem::path::preferred_separator))
|
|
||||||
root_str.pop_back();
|
|
||||||
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
||||||
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
||||||
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
||||||
@@ -1118,15 +1112,6 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root)
|
|
||||||
{
|
|
||||||
if (target.empty() || target.front() == '/' || target.front() == '\\' || (target.size() > 1 && target[1] == ':'))
|
|
||||||
return false;
|
|
||||||
// A relative target without ".." only descends from the link's directory, so no chain of such links can leave root.
|
|
||||||
const size_t sep = link_rel_path.find_last_of("/\\");
|
|
||||||
return is_path_within_root((sep == std::string::npos ? std::string() : link_rel_path.substr(0, sep + 1)) + target, root);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_img_file(const std::string &path)
|
bool is_img_file(const std::string &path)
|
||||||
{
|
{
|
||||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ static const char* Segments_Vertex_Shader =
|
|||||||
// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not
|
// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not
|
||||||
// world_position, so the caster would write a depth the receiver never looks up.
|
// world_position, so the caster would write a depth the receiver never looks up.
|
||||||
"uniform float bias_scale;\n"
|
"uniform float bias_scale;\n"
|
||||||
|
// draw the instances last to first, top layers before the ones they hide, so that early depth
|
||||||
|
// rejection discards most of the hidden fragments; set when the camera looks down on the print
|
||||||
|
"uniform int reverse_order;\n"
|
||||||
|
"uniform int instance_count;\n"
|
||||||
"in int vertex_id;\n"
|
"in int vertex_id;\n"
|
||||||
"out vec3 color;\n"
|
"out vec3 color;\n"
|
||||||
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
|
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
|
||||||
@@ -59,7 +63,8 @@ static const char* Segments_Vertex_Shader =
|
|||||||
" return top_diffuse + front_diffuse + top_specular;\n"
|
" return top_diffuse + front_diffuse + top_specular;\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
"void main() {\n"
|
"void main() {\n"
|
||||||
" int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n"
|
" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n"
|
||||||
|
" int id_a = int(texelFetch(segment_index_tex, instance).r);\n"
|
||||||
" int id_b = id_a + 1;\n"
|
" int id_b = id_a + 1;\n"
|
||||||
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
|
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
|
||||||
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
|
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
|
||||||
|
|||||||
@@ -763,6 +763,8 @@ void ViewerImpl::init(const std::string& opengl_context_version)
|
|||||||
m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex");
|
m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex");
|
||||||
m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex");
|
m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex");
|
||||||
m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex");
|
m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex");
|
||||||
|
m_uni_segments_reverse_order_id = glGetUniformLocation(m_segments_shader_id, "reverse_order");
|
||||||
|
m_uni_segments_instance_count_id = glGetUniformLocation(m_segments_shader_id, "instance_count");
|
||||||
// ORCA: realistic view
|
// ORCA: realistic view
|
||||||
m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map");
|
m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map");
|
||||||
m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp");
|
m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp");
|
||||||
@@ -2090,6 +2092,15 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
|
|||||||
glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data()));
|
glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data()));
|
||||||
glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data()));
|
glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data()));
|
||||||
glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data()));
|
glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data()));
|
||||||
|
// The segments come in print order, bottom layer first. Seen from above, that is back to front,
|
||||||
|
// and every hidden fragment is shaded before the one that covers it. Drawing them last to first
|
||||||
|
// lets the depth test reject the hidden ones instead. The camera looks down when the world's
|
||||||
|
// up axis points towards it, which is the view matrix's (2, 2) entry being positive.
|
||||||
|
const bool top_down = !m_rendering_shadow_casters && view_matrix[10] > 0.0f;
|
||||||
|
glsafe(glUniform1i(m_uni_segments_reverse_order_id, top_down ? 1 : 0));
|
||||||
|
#ifndef ENABLE_OPENGL_ES
|
||||||
|
glsafe(glUniform1i(m_uni_segments_instance_count_id, static_cast<int>(m_enabled_segments_count)));
|
||||||
|
#endif // ENABLE_OPENGL_ES
|
||||||
// ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades
|
// ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades
|
||||||
// with the lookup off.
|
// with the lookup off.
|
||||||
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
|
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
|
||||||
|
|||||||
@@ -362,6 +362,8 @@ private:
|
|||||||
int m_uni_segments_height_width_angle_tex_id{ -1 };
|
int m_uni_segments_height_width_angle_tex_id{ -1 };
|
||||||
int m_uni_segments_colors_tex_id{ -1 };
|
int m_uni_segments_colors_tex_id{ -1 };
|
||||||
int m_uni_segments_segment_index_tex_id{ -1 };
|
int m_uni_segments_segment_index_tex_id{ -1 };
|
||||||
|
int m_uni_segments_reverse_order_id{ -1 };
|
||||||
|
int m_uni_segments_instance_count_id{ -1 };
|
||||||
int m_uni_segments_shadow_map_id{ -1 };
|
int m_uni_segments_shadow_map_id{ -1 };
|
||||||
int m_uni_segments_shadow_light_vp_id{ -1 };
|
int m_uni_segments_shadow_light_vp_id{ -1 };
|
||||||
int m_uni_segments_shadow_intensity_id{ -1 };
|
int m_uni_segments_shadow_intensity_id{ -1 };
|
||||||
|
|||||||
@@ -1512,33 +1512,11 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
|||||||
size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
||||||
dest_file = decode(extra.substr(0, n), stat.m_filename);
|
dest_file = decode(extra.substr(0, n), stat.m_filename);
|
||||||
}
|
}
|
||||||
if (!is_path_within_root(dest_file, plugin_folder)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "[install_plugin] entry " << dest_file << " resolves outside " << plugin_folder.string();
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
|
||||||
return InstallStatusUnzipFailed;
|
|
||||||
}
|
|
||||||
auto dest_path = plugin_folder / dest_file;
|
auto dest_path = plugin_folder / dest_file;
|
||||||
|
boost::filesystem::create_directories(dest_path.parent_path());
|
||||||
std::string dest_zip_file = encode_path(dest_path.string().c_str());
|
std::string dest_zip_file = encode_path(dest_path.string().c_str());
|
||||||
#ifndef WIN32
|
|
||||||
// Validate a symlink's target before anything at the destination is replaced.
|
|
||||||
const bool is_link = S_ISLNK(stat.m_external_attr >> 16);
|
|
||||||
std::string link;
|
|
||||||
if (is_link) {
|
|
||||||
link.assign(stat.m_uncomp_size, 0);
|
|
||||||
if (!mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0) ||
|
|
||||||
!is_symlink_target_within_root(dest_file, link, plugin_folder)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "[install_plugin] link " << dest_file << " -> " << link << " is unreadable or resolves outside " << plugin_folder.string();
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
|
||||||
return InstallStatusUnzipFailed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
try {
|
try {
|
||||||
boost::filesystem::create_directories(dest_path.parent_path());
|
if (fs::exists(dest_path)) {
|
||||||
// symlink_status so that an existing symlink, dangling or not, is replaced rather than written through.
|
|
||||||
if (fs::exists(fs::symlink_status(dest_path))) {
|
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
fs::remove(dest_path, ec);
|
fs::remove(dest_path, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
@@ -1566,8 +1544,9 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
|||||||
}
|
}
|
||||||
mz_bool res = 0;
|
mz_bool res = 0;
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
if (is_link) {
|
if (S_ISLNK(stat.m_external_attr >> 16)) {
|
||||||
res = 1;
|
std::string link(stat.m_uncomp_size + 1, 0);
|
||||||
|
res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0);
|
||||||
try {
|
try {
|
||||||
boost::filesystem::create_symlink(link, dest_path);
|
boost::filesystem::create_symlink(link, dest_path);
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
|
|||||||
@@ -12800,8 +12800,6 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
notification_manager->set_slicing_progress_export_possible();
|
notification_manager->set_slicing_progress_export_possible();
|
||||||
|
|
||||||
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
||||||
const std::string lifecycle_job_name = this->background_process.fff_print() ?
|
|
||||||
this->background_process.fff_print()->output_filename() : std::string();
|
|
||||||
this->background_process.reset_export();
|
this->background_process.reset_export();
|
||||||
// This bool stops showing export finished notification even when process_completed_with_error is false
|
// This bool stops showing export finished notification even when process_completed_with_error is false
|
||||||
bool has_error = false;
|
bool has_error = false;
|
||||||
@@ -12848,8 +12846,18 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
|
|
||||||
{
|
{
|
||||||
Slic3r::LifecycleEventContext ctx;
|
Slic3r::LifecycleEventContext ctx;
|
||||||
ctx.name = lifecycle_job_name;
|
if (const PrintBase* print = this->background_process.current_print()) {
|
||||||
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
const Model& model = print->model();
|
||||||
|
ctx.id = std::to_string(model.id().id);
|
||||||
|
if (model.model_info)
|
||||||
|
ctx.name = model.model_info->model_name;
|
||||||
|
} else {
|
||||||
|
// Realistically Printbase* print will never be null because select_technology already asserts an active print
|
||||||
|
// and the worker thread asserts it before processing.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": slicing completed without an active print; lifecycle event has no model ID";
|
||||||
|
}
|
||||||
|
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn :
|
||||||
|
(has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
||||||
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
||||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,8 +339,62 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe
|
|||||||
//BBS: refine preset update logic
|
//BBS: refine preset update logic
|
||||||
bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path)
|
bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path)
|
||||||
{
|
{
|
||||||
const std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
|
bool res = true;
|
||||||
return extract_archive_confined(source_path.string(), parent_path);
|
std::string file_path = source_path.string();
|
||||||
|
std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
|
||||||
|
mz_zip_archive archive;
|
||||||
|
mz_zip_zero_struct(&archive);
|
||||||
|
|
||||||
|
if (!open_zip_reader(&archive, file_path))
|
||||||
|
{
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for "<<file_path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
||||||
|
|
||||||
|
mz_zip_archive_file_stat stat;
|
||||||
|
// we first loop the entries to read from the archive the .amf file only, in order to extract the version from it
|
||||||
|
for (mz_uint i = 0; i < num_entries; ++i)
|
||||||
|
{
|
||||||
|
if (mz_zip_reader_file_stat(&archive, i, &stat))
|
||||||
|
{
|
||||||
|
std::string dest_file = parent_path+"/"+stat.m_filename;
|
||||||
|
if (stat.m_is_directory) {
|
||||||
|
fs::path dest_path(dest_file);
|
||||||
|
if (!fs::exists(dest_path))
|
||||||
|
fs::create_directories(dest_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (stat.m_uncomp_size == 0) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: invalid size for file "<<stat.m_filename;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
res = mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0);
|
||||||
|
if (!res) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]extract file "<<stat.m_filename<<" to dest "<<dest_file<<" failed";
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]successfully extract file " << stat.m_file_index << " to "<<dest_file;
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
// ensure the zip archive is closed and rethrow the exception
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]Archive read exception:"<<e.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: read file stat failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a leftover partial archive for the vendor about to be synchronized.
|
// Remove a leftover partial archive for the vendor about to be synchronized.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/app.h>
|
#include <wx/app.h>
|
||||||
@@ -115,10 +116,67 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
|
|||||||
return webui_url;
|
return webui_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Moonraker (Klipper's API server) reports a raised exception as { "error": { "code", "message", "traceback" } }
|
||||||
|
// under every host type that connects to it, often with the cause only in the traceback. Returns the reason to show,
|
||||||
|
// or empty for any other body.
|
||||||
|
std::string moonraker_error_reason(const std::string &body)
|
||||||
|
{
|
||||||
|
const auto root = nlohmann::json::parse(body, nullptr, false);
|
||||||
|
const auto err = root.find("error");
|
||||||
|
if (err == root.end())
|
||||||
|
return {};
|
||||||
|
const auto message = err->find("message");
|
||||||
|
const auto traceback = err->find("traceback");
|
||||||
|
if (message == err->end() || traceback == err->end() || !message->is_string() || !traceback->is_string())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto &msg = message->get_ref<const std::string &>();
|
||||||
|
const auto &tb = traceback->get_ref<const std::string &>();
|
||||||
|
if (msg.empty())
|
||||||
|
return {};
|
||||||
|
const auto end = tb.find_last_not_of(" \t\r\n");
|
||||||
|
if (end == std::string::npos)
|
||||||
|
return msg;
|
||||||
|
|
||||||
|
// Chained exceptions each start a new traceback; the one that failed the request is the last.
|
||||||
|
const auto header = tb.rfind("Traceback (most recent call last):", end);
|
||||||
|
|
||||||
|
// Tornado renders a raised HTTPError as "HTTP <code>: <reason>[ (<detail>)]", and the detail may span lines.
|
||||||
|
const auto code = err->find("code");
|
||||||
|
if (code != err->end() && code->is_number_integer()) {
|
||||||
|
const std::string marker = "HTTP " + std::to_string(code->get<int>()) + ": ";
|
||||||
|
const auto pos = tb.rfind(marker, end);
|
||||||
|
if (pos != std::string::npos && (header == std::string::npos || pos > header) && pos + marker.size() <= end) {
|
||||||
|
const std::string reason = tb.substr(pos + marker.size(), end + 1 - pos - marker.size());
|
||||||
|
// An HTTPError whose detail equals its reason, like HTTPError(401, "Unauthorized"), renders the phrase twice.
|
||||||
|
return reason == msg + " (" + msg + ")" ? msg : reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other exception's type and message are everything from the first unindented line after its frames.
|
||||||
|
auto begin = (header == std::string::npos) ? std::string::npos : tb.find('\n', header);
|
||||||
|
while (begin != std::string::npos && begin < end) {
|
||||||
|
++begin;
|
||||||
|
if (tb[begin] != ' ' && tb[begin] != '\r' && tb[begin] != '\n')
|
||||||
|
break;
|
||||||
|
begin = tb.find('\n', begin);
|
||||||
|
}
|
||||||
|
if (begin == std::string::npos || begin > end) {
|
||||||
|
const auto nl = tb.rfind('\n', end);
|
||||||
|
begin = (nl == std::string::npos) ? 0 : nl + 1;
|
||||||
|
}
|
||||||
|
return msg + " (" + tb.substr(begin, end + 1 - begin) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
||||||
{
|
{
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
auto wxbody = wxString::FromUTF8(body.data());
|
const std::string reason = moonraker_error_reason(body);
|
||||||
|
auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason);
|
||||||
return wxString::Format("HTTP %u: %s", status, wxbody);
|
return wxString::Format("HTTP %u: %s", status, wxbody);
|
||||||
} else {
|
} else {
|
||||||
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
||||||
|
|||||||
@@ -15,3 +15,11 @@ set_tests_properties(cli_strict_mode PROPERTIES
|
|||||||
LABELS "CLI;RequiresApp"
|
LABELS "CLI;RequiresApp"
|
||||||
SKIP_RETURN_CODE 77
|
SKIP_RETURN_CODE 77
|
||||||
TIMEOUT 900)
|
TIMEOUT 900)
|
||||||
|
|
||||||
|
add_test(NAME cli_project_missing_keys
|
||||||
|
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_project_missing_keys.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}
|
||||||
|
${CMAKE_SOURCE_DIR}/resources/profiles/BBL)
|
||||||
|
set_tests_properties(cli_project_missing_keys PROPERTIES
|
||||||
|
LABELS "CLI;RequiresApp"
|
||||||
|
SKIP_RETURN_CODE 77
|
||||||
|
TIMEOUT 900)
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# End-to-end check that the CLI loads a project's printer and process settings as the GUI does.
|
||||||
|
#
|
||||||
|
# The GUI takes every key a project does not list as changed from the project's current system preset:
|
||||||
|
# keys saved before an option existed, and keys holding an older system value. Keys the project lists
|
||||||
|
# in different_settings_to_system keep the project's value. A project is exported from the shipped
|
||||||
|
# Bambu Lab P1S presets; one printer key and one process key are removed, one printer key and one
|
||||||
|
# process key are changed without being listed, one key is changed and listed, and it is sliced again.
|
||||||
|
#
|
||||||
|
# usage: test_cli_project_missing_keys.sh <orca-slicer binary> <python3> <resources/profiles/BBL>
|
||||||
|
set -u
|
||||||
|
|
||||||
|
BIN="${1:-}"
|
||||||
|
PY="${2:-python3}"
|
||||||
|
PROFILES="${3:-}"
|
||||||
|
# 77 is the test's SKIP_RETURN_CODE.
|
||||||
|
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
|
||||||
|
[ -d "$PROFILES" ] || { echo "FAIL: profiles directory not found: $PROFILES"; exit 1; }
|
||||||
|
|
||||||
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-missing-keys.XXXXXX")"
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
"$PY" - "$WORK/cube.stl" <<'EOF'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
v = [(x, y, z) for z in (0, 10) for y in (0, 10) for x in (0, 10)]
|
||||||
|
with open(sys.argv[1], "w") as f:
|
||||||
|
f.write("solid cube\n")
|
||||||
|
for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
|
||||||
|
for tri in ((v[a], v[b], v[c]), (v[a], v[c], v[d])):
|
||||||
|
f.write("facet normal 0 0 0\nouter loop\n")
|
||||||
|
for p in tri:
|
||||||
|
f.write("vertex %g %g %g\n" % p)
|
||||||
|
f.write("endloop\nendfacet\n")
|
||||||
|
f.write("endsolid cube\n")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# slice <tag> <input> [option...]: slice into $WORK/<tag>/out.3mf with a fresh data directory.
|
||||||
|
slice() {
|
||||||
|
local out="$WORK/$1" input="$2"; shift 2
|
||||||
|
mkdir -p "$out"
|
||||||
|
timeout 300 "$BIN" --datadir "$out/datadir" "$@" --slice 0 --outputdir "$out" --export-3mf out.3mf "$input" \
|
||||||
|
> "$out/log" 2>&1 || { echo "FAIL: $1: orca-slicer exited $?"; tail -n 40 "$out/log"; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
slice base "$WORK/cube.stl" \
|
||||||
|
--load-settings "$PROFILES/machine/Bambu Lab P1S 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \
|
||||||
|
--load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
|
||||||
|
|
||||||
|
# The removed keys, with their option defaults from PrintConfig.cpp; stale keys changed without being
|
||||||
|
# listed as different, which must come back with the system value; and a listed key the project keeps.
|
||||||
|
"$PY" - "$WORK/base/out.3mf" "$WORK/old.3mf" <<'EOF' || exit $?
|
||||||
|
import json, sys, zipfile
|
||||||
|
|
||||||
|
src, dst = sys.argv[1], sys.argv[2]
|
||||||
|
missing = {"extruder_clearance_dist_to_rod": "40", "sparse_infill_density": "20%"}
|
||||||
|
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
|
||||||
|
for item in zin.infolist():
|
||||||
|
data = zin.read(item.filename)
|
||||||
|
if item.filename == "Metadata/project_settings.config":
|
||||||
|
config = json.loads(data)
|
||||||
|
for key, default in missing.items():
|
||||||
|
if config[key] == default:
|
||||||
|
print("SKIP: %s is %s in the system preset, the option default, so the test cannot tell them apart" % (key, default))
|
||||||
|
sys.exit(77)
|
||||||
|
expected = {key: config.pop(key) for key in missing}
|
||||||
|
for key in ("top_shell_layers", "extruder_clearance_height_to_rod"):
|
||||||
|
expected[key] = config[key]
|
||||||
|
config[key] = str(int(float(config[key])) + 1)
|
||||||
|
expected["wall_loops"] = str(int(config["wall_loops"]) + 1)
|
||||||
|
config["wall_loops"] = expected["wall_loops"]
|
||||||
|
different = config["different_settings_to_system"]
|
||||||
|
different[0] = ";".join([k for k in different[0].split(";") if k] + ["wall_loops"])
|
||||||
|
data = json.dumps(config, indent=4)
|
||||||
|
zout.writestr(item, data)
|
||||||
|
with open(dst + ".expected.json", "w") as f:
|
||||||
|
json.dump(expected, f)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
slice project "$WORK/old.3mf"
|
||||||
|
|
||||||
|
"$PY" - "$WORK/project/out.3mf" "$WORK/old.3mf.expected.json" <<'EOF'
|
||||||
|
import json, sys, zipfile
|
||||||
|
|
||||||
|
with zipfile.ZipFile(sys.argv[1]) as z:
|
||||||
|
config = json.loads(z.read("Metadata/project_settings.config"))
|
||||||
|
with open(sys.argv[2]) as f:
|
||||||
|
expected = json.load(f)
|
||||||
|
errors = ["%s is %r, want %r" % (key, config.get(key), want) for key, want in expected.items() if config.get(key) != want]
|
||||||
|
for e in errors:
|
||||||
|
print("FAIL: " + e)
|
||||||
|
sys.exit(1 if errors else 0)
|
||||||
|
EOF
|
||||||
|
status=$?
|
||||||
|
[ "$status" -eq 0 ] || { tail -n 40 "$WORK/project/log"; exit 1; }
|
||||||
|
echo "PASS"
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
#include "libslic3r/Layer.hpp"
|
#include "libslic3r/Layer.hpp"
|
||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/GCodeReader.hpp"
|
#include "libslic3r/GCodeReader.hpp"
|
||||||
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||||
|
#include "libslic3r/Exception.hpp"
|
||||||
|
#include "libslic3r/LifecycleEvents.hpp"
|
||||||
|
|
||||||
#include "test_helpers.hpp"
|
#include "test_helpers.hpp"
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
@@ -22,7 +25,10 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
using namespace Slic3r::Test;
|
using namespace Slic3r::Test;
|
||||||
@@ -224,8 +230,88 @@ std::string resolved_output_name(Model& model, const std::string& format, const
|
|||||||
return print.output_filename(filename_base);
|
return print.output_filename(filename_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScopedLifecycleHook
|
||||||
|
{
|
||||||
|
explicit ScopedLifecycleHook(LifecycleHookFn hook) { set_lifecycle_hook_fn(std::move(hook)); }
|
||||||
|
~ScopedLifecycleHook() { set_lifecycle_hook_fn(nullptr); }
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("Slicing lifecycle events identify the model", "[Print][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
struct ObservedEvent {
|
||||||
|
LifecycleEvent event;
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
std::vector<ObservedEvent> events;
|
||||||
|
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||||
|
events.push_back({ event, ctx.id, ctx.name });
|
||||||
|
});
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
ModelInfo info;
|
||||||
|
info.model_name = "Lifecycle test model";
|
||||||
|
model.model_info = std::make_shared<ModelInfo>(std::move(info));
|
||||||
|
init_print({cube(20)}, print, model);
|
||||||
|
|
||||||
|
print.process();
|
||||||
|
ScopedTemporaryFile temp(".gcode");
|
||||||
|
print.export_gcode(temp.string(), nullptr, nullptr);
|
||||||
|
GCodeProcessorResult result;
|
||||||
|
print.export_gcode_from_previous_file(temp.string(), &result);
|
||||||
|
|
||||||
|
const std::string expected_id = std::to_string(print.model().id().id);
|
||||||
|
const std::vector<LifecycleEvent> expected_events = {
|
||||||
|
LifecycleEvent::SliceStarted,
|
||||||
|
LifecycleEvent::SliceGeometryFinished,
|
||||||
|
LifecycleEvent::GCodeExportStarted,
|
||||||
|
LifecycleEvent::GCodeExportFinished,
|
||||||
|
LifecycleEvent::GCodeExportStarted,
|
||||||
|
LifecycleEvent::GCodeExportFinished,
|
||||||
|
};
|
||||||
|
REQUIRE(events.size() == expected_events.size());
|
||||||
|
for (size_t i = 0; i < expected_events.size(); ++i) {
|
||||||
|
CHECK(events[i].event == expected_events[i]);
|
||||||
|
CHECK(events[i].id == expected_id);
|
||||||
|
CHECK(events[i].name == "Lifecycle test model");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Slicing lifecycle event name is empty without model metadata", "[Print][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
std::string event_id;
|
||||||
|
std::string event_name = "unset";
|
||||||
|
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||||
|
if (event == LifecycleEvent::SliceStarted) {
|
||||||
|
event_id = ctx.id;
|
||||||
|
event_name = ctx.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({cube(20)}, print, model);
|
||||||
|
print.process();
|
||||||
|
|
||||||
|
CHECK(event_id == std::to_string(print.model().id().id));
|
||||||
|
CHECK(event_name.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Output filenames with numeric statistics fail before slicing finishes", "[Print][Regression]")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.set_key_value("filename_format", new ConfigOptionString("{int(total_weight*10) / 10.0}"));
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({cube(20)}, print, model, config);
|
||||||
|
|
||||||
|
CHECK_THROWS_AS(print.output_filename(), PlaceholderParserError);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
||||||
{
|
{
|
||||||
Model model;
|
Model model;
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_lay_on_face.cpp
|
test_lay_on_face.cpp
|
||||||
test_model.cpp
|
test_model.cpp
|
||||||
test_utils.cpp
|
test_utils.cpp
|
||||||
test_miniz_extension.cpp
|
|
||||||
test_timeutils.cpp
|
test_timeutils.cpp
|
||||||
test_voronoi.cpp
|
test_voronoi.cpp
|
||||||
test_wipe_tower_estimate.cpp
|
test_wipe_tower_estimate.cpp
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
#include <catch2/catch_all.hpp>
|
|
||||||
|
|
||||||
#include "libslic3r/miniz_extension.hpp"
|
|
||||||
|
|
||||||
#include "test_utils.hpp"
|
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iterator>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace Slic3r;
|
|
||||||
namespace fs = boost::filesystem;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void write_zip(const fs::path &zip_file, const std::vector<std::pair<std::string, std::string>> &entries)
|
|
||||||
{
|
|
||||||
mz_zip_archive zip;
|
|
||||||
mz_zip_zero_struct(&zip);
|
|
||||||
REQUIRE(open_zip_writer(&zip, zip_file.string()));
|
|
||||||
for (const auto &[name, content] : entries)
|
|
||||||
REQUIRE(mz_zip_writer_add_mem(&zip, name.c_str(), content.data(), content.size(), MZ_DEFAULT_COMPRESSION));
|
|
||||||
REQUIRE(mz_zip_writer_finalize_archive(&zip));
|
|
||||||
REQUIRE(close_zip_writer(&zip));
|
|
||||||
}
|
|
||||||
|
|
||||||
// miniz refuses to write a name starting with '/', so write a placeholder of the same length and patch it in place.
|
|
||||||
void rename_entry(const fs::path &zip_file, const std::string &from, const std::string &to)
|
|
||||||
{
|
|
||||||
REQUIRE(from.size() == to.size());
|
|
||||||
std::string bytes;
|
|
||||||
{
|
|
||||||
std::ifstream in(zip_file.string(), std::ios::binary);
|
|
||||||
bytes.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
size_t count = 0;
|
|
||||||
for (size_t pos = bytes.find(from); pos != std::string::npos; pos = bytes.find(from, pos + to.size()), ++count)
|
|
||||||
bytes.replace(pos, from.size(), to);
|
|
||||||
// Once in the local header and once in the central directory.
|
|
||||||
REQUIRE(count == 2);
|
|
||||||
std::ofstream out(zip_file.string(), std::ios::binary | std::ios::trunc);
|
|
||||||
out << bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> list_dir(const fs::path &dir)
|
|
||||||
{
|
|
||||||
std::vector<std::string> names;
|
|
||||||
for (const fs::directory_entry &entry : fs::directory_iterator(dir))
|
|
||||||
names.push_back(entry.path().filename().string());
|
|
||||||
std::sort(names.begin(), names.end());
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string read_file(const fs::path &file)
|
|
||||||
{
|
|
||||||
std::ifstream in(file.string(), std::ios::binary);
|
|
||||||
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction writes a well-formed archive under the target directory", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"}, {"vendor/machine/printer.json", "{\"b\":2}"}});
|
|
||||||
|
|
||||||
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK(fs::is_directory(target / "vendor"));
|
|
||||||
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
|
||||||
CHECK(read_file(target / "vendor" / "machine" / "printer.json") == "{\"b\":2}");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction rejects an archive with an entry outside the target directory", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
const std::string escaping_entry = GENERATE(std::string("../escape.txt"), std::string("..\\escape.txt"),
|
|
||||||
std::string("sub/../../escape.txt"), std::string("C:/escape.txt"),
|
|
||||||
std::string("C:escape.txt"), std::string("\\escape.txt"));
|
|
||||||
// The normal entry comes first so a per-entry check would already have written it.
|
|
||||||
write_zip(zip_file, {{"normal.json", "{}"}, {escaping_entry, "escaped"}});
|
|
||||||
|
|
||||||
CAPTURE(escaping_entry);
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction rejects an archive with an absolute entry name", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
const std::string absolute = (tmp.path() / "escape.txt").generic_string();
|
|
||||||
const std::string placeholder = "#" + absolute.substr(1);
|
|
||||||
write_zip(zip_file, {{"normal.json", "{}"}, {placeholder, "escaped"}});
|
|
||||||
rename_entry(zip_file, placeholder, absolute);
|
|
||||||
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction rejects a directory entry outside the target directory", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
write_zip(zip_file, {{"vendor/", ""}, {"../outside/", ""}});
|
|
||||||
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "outside"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction validates zero-size entries like any other", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
SECTION("an empty file inside the target does not fail the archive") {
|
|
||||||
write_zip(zip_file, {{"empty.json", ""}, {"vendor.json", "{}"}});
|
|
||||||
CHECK(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK(read_file(target / "vendor.json") == "{}");
|
|
||||||
}
|
|
||||||
SECTION("an empty file outside the target rejects the archive") {
|
|
||||||
write_zip(zip_file, {{"vendor.json", "{}"}, {"../escape.txt", ""}});
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction writes nothing outside the target for Windows-specific name forms", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
// Windows strips trailing dots and spaces and maps device names; whether these extract depends on the
|
|
||||||
// platform, but none of them may land beside the target.
|
|
||||||
const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("..."), std::string(".. "),
|
|
||||||
std::string(".. /escape.txt"), std::string(".../escape.txt"), std::string("CON"),
|
|
||||||
std::string("sub/NUL.txt"), std::string("C:escape.txt"));
|
|
||||||
write_zip(zip_file, {{name, "payload"}});
|
|
||||||
|
|
||||||
CAPTURE(name);
|
|
||||||
extract_archive_confined(zip_file.string(), target.string());
|
|
||||||
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
const fs::path outside = tmp.path() / "outside";
|
|
||||||
fs::create_directories(target);
|
|
||||||
fs::create_directories(outside);
|
|
||||||
write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}});
|
|
||||||
|
|
||||||
SECTION("a dangling symlink") {
|
|
||||||
fs::create_symlink(outside / "vendor.json", target / "vendor.json");
|
|
||||||
CHECK(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(outside / "vendor.json"));
|
|
||||||
CHECK_FALSE(fs::is_symlink(fs::symlink_status(target / "vendor.json")));
|
|
||||||
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
|
||||||
}
|
|
||||||
SECTION("a symlink to an existing file") {
|
|
||||||
{ std::ofstream((outside / "vendor.json").string()) << "original"; }
|
|
||||||
fs::create_symlink(outside / "vendor.json", target / "vendor.json");
|
|
||||||
extract_archive_confined(zip_file.string(), target.string());
|
|
||||||
CHECK(read_file(outside / "vendor.json") == "original");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -5405,6 +5405,35 @@ struct ScopedDataDir
|
|||||||
~ScopedDataDir() { set_data_dir(previous); }
|
~ScopedDataDir() { set_data_dir(previous); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// resources_dir() is process-wide too; system preset lookups scan its profiles directory.
|
||||||
|
struct ScopedResourcesDir
|
||||||
|
{
|
||||||
|
std::string previous = resources_dir();
|
||||||
|
explicit ScopedResourcesDir(const fs::path &dir) { set_resources_dir(dir.string()); }
|
||||||
|
~ScopedResourcesDir() { set_resources_dir(previous); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// An "Acme" vendor under root whose "Acme Printer" inherits extruder_clearance_dist_to_rod from an
|
||||||
|
// abstract base, with the printer in a nested sub_path so the name cannot be derived from the file.
|
||||||
|
void write_acme_printer_vendor(const fs::path &root, double dist_to_rod)
|
||||||
|
{
|
||||||
|
const fs::path machine_dir = root / "Acme" / "machine";
|
||||||
|
fs::create_directories(machine_dir / "nested");
|
||||||
|
std::ofstream((root / "Acme.json").string())
|
||||||
|
<< R"({"version":"1.0.0","name":"Acme",)"
|
||||||
|
<< R"("machine_model_list":[{"name":"Acme One","sub_path":"machine/model.json"}],"machine_list":[)"
|
||||||
|
<< R"({"name":"fdm_acme_common","sub_path":"machine/base.json"},)"
|
||||||
|
<< R"({"name":"Acme Printer","sub_path":"machine/nested/printer.json"}]})";
|
||||||
|
std::ofstream((machine_dir / "model.json").string())
|
||||||
|
<< R"({"type":"machine_model","name":"Acme One","nozzle_diameter":"0.4"})";
|
||||||
|
std::ofstream((machine_dir / "base.json").string())
|
||||||
|
<< R"({"type":"machine","name":"fdm_acme_common","from":"system","instantiation":"false",)"
|
||||||
|
<< R"("extruder_clearance_dist_to_rod":")" << dist_to_rod << R"("})";
|
||||||
|
std::ofstream((machine_dir / "nested" / "printer.json").string())
|
||||||
|
<< R"({"type":"machine","name":"Acme Printer","from":"system","instantiation":"true","inherits":"fdm_acme_common",)"
|
||||||
|
<< R"("printer_model":"Acme One","printer_variant":"0.4"})";
|
||||||
|
}
|
||||||
|
|
||||||
std::string read_file(const fs::path &file)
|
std::string read_file(const fs::path &file)
|
||||||
{
|
{
|
||||||
std::ifstream in(file.string(), std::ios::binary);
|
std::ifstream in(file.string(), std::ios::binary);
|
||||||
@@ -5482,3 +5511,230 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
|
|||||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A system preset resolves by name from the bundled profiles", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data(temp_dir.path() / "data");
|
||||||
|
ScopedResourcesDir resources(temp_dir.path() / "resources");
|
||||||
|
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
|
||||||
|
|
||||||
|
PresetBundle bundle;
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
std::string error;
|
||||||
|
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||||
|
CHECK(error.empty());
|
||||||
|
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A system preset resolves from the data directory copy of its vendor", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data(temp_dir.path() / "data");
|
||||||
|
ScopedResourcesDir resources(temp_dir.path() / "resources");
|
||||||
|
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
|
||||||
|
write_acme_printer_vendor(temp_dir.path() / "data" / PRESET_SYSTEM_DIR, 35.);
|
||||||
|
|
||||||
|
PresetBundle bundle;
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
std::string error;
|
||||||
|
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||||
|
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(35., 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A system preset resolves from a vendor shipped as its preset cache alone", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data(temp_dir.path() / "data");
|
||||||
|
ScopedResourcesDir resources(temp_dir.path() / "resources");
|
||||||
|
const fs::path profiles = temp_dir.path() / "resources" / "profiles";
|
||||||
|
write_acme_printer_vendor(profiles, 33.);
|
||||||
|
|
||||||
|
PresetBundle writer;
|
||||||
|
writer.set_generate_vendor_caches(true);
|
||||||
|
writer.load_vendor_configs_from_json(profiles.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
REQUIRE(fs::exists(profiles / "Acme.opc"));
|
||||||
|
// Release builds ship the cache and drop the profile JSONs, manifest included.
|
||||||
|
fs::remove(profiles / "Acme.json");
|
||||||
|
fs::remove_all(profiles / "Acme");
|
||||||
|
|
||||||
|
PresetBundle bundle;
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
std::string error;
|
||||||
|
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||||
|
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data(temp_dir.path() / "data");
|
||||||
|
ScopedResourcesDir resources(temp_dir.path() / "resources");
|
||||||
|
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
|
||||||
|
|
||||||
|
PresetBundle bundle;
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
std::string error;
|
||||||
|
CHECK_FALSE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Unknown Printer",
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||||
|
CHECK_FALSE(error.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// A default preset config for type, built the way PresetBundle builds its default presets.
|
||||||
|
DynamicPrintConfig external_default_config(Preset::Type type)
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
config.apply_only(static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()),
|
||||||
|
type == Preset::TYPE_PRINTER ? Preset::printer_options() : Preset::print_options());
|
||||||
|
Preset::inherits(config);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A find_base callback that counts its calls and returns base.
|
||||||
|
auto base_finder(DynamicPrintConfig *base, int &calls)
|
||||||
|
{
|
||||||
|
return [base, &calls](const std::string &) {
|
||||||
|
++calls;
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("A key missing from a project takes the default preset value when there is no base", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
|
||||||
|
defaults.set("wall_loops", 7, true);
|
||||||
|
const DynamicPrintConfig project;
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
|
||||||
|
base_finder(nullptr, calls));
|
||||||
|
CHECK(calls == 1);
|
||||||
|
CHECK(config.opt_int("wall_loops") == 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A key missing from a project takes its base preset value", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
|
||||||
|
defaults.set("wall_loops", 7, true);
|
||||||
|
DynamicPrintConfig base = defaults;
|
||||||
|
base.set("wall_loops", 5, true);
|
||||||
|
const DynamicPrintConfig project;
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
|
||||||
|
base_finder(&base, calls));
|
||||||
|
CHECK(config.opt_int("wall_loops") == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A project key listed as different keeps the project value", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
|
||||||
|
DynamicPrintConfig base = defaults;
|
||||||
|
base.set("wall_loops", 5, true);
|
||||||
|
DynamicPrintConfig project;
|
||||||
|
project.set("wall_loops", 4, true);
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"wall_loops"},
|
||||||
|
base_finder(&base, calls));
|
||||||
|
CHECK(config.opt_int("wall_loops") == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A project key not listed as different is refreshed to the base value", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
|
||||||
|
DynamicPrintConfig base = defaults;
|
||||||
|
base.set("wall_loops", 5, true);
|
||||||
|
DynamicPrintConfig project;
|
||||||
|
project.set("wall_loops", 4, true);
|
||||||
|
project.set("inherits", "Base Process", true);
|
||||||
|
std::string inherits;
|
||||||
|
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
|
||||||
|
[&base, &inherits](const std::string &name) {
|
||||||
|
inherits = name;
|
||||||
|
return &base;
|
||||||
|
});
|
||||||
|
CHECK(inherits == "Base Process");
|
||||||
|
CHECK(config.opt_int("wall_loops") == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An empty different settings list keeps the project values without a base", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
|
||||||
|
defaults.set("top_shell_layers", 7, true);
|
||||||
|
DynamicPrintConfig base = defaults;
|
||||||
|
base.set("wall_loops", 5, true);
|
||||||
|
base.set("top_shell_layers", 9, true);
|
||||||
|
DynamicPrintConfig project;
|
||||||
|
project.set("wall_loops", 4, true);
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {}, base_finder(&base, calls));
|
||||||
|
CHECK(calls == 0);
|
||||||
|
CHECK(config.opt_int("wall_loops") == 4);
|
||||||
|
CHECK(config.opt_int("top_shell_layers") == 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Print-host keys are never taken from a project", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINTER);
|
||||||
|
defaults.set("print_host", "", true);
|
||||||
|
defaults.set("printhost_apikey", "", true);
|
||||||
|
DynamicPrintConfig project;
|
||||||
|
project.set("print_host", "http://project-host", true);
|
||||||
|
project.set("printhost_apikey", "project-key", true);
|
||||||
|
project.set("printer_notes", "project notes", true);
|
||||||
|
t_config_option_keys keys;
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINTER, defaults, project, {"printer_notes"},
|
||||||
|
base_finder(nullptr, calls), &keys);
|
||||||
|
CHECK(config.opt_string("print_host").empty());
|
||||||
|
CHECK(config.opt_string("printhost_apikey").empty());
|
||||||
|
CHECK(config.opt_string("printer_notes") == "project notes");
|
||||||
|
CHECK_FALSE(contains_key(keys, "print_host"));
|
||||||
|
CHECK_FALSE(contains_key(keys, "printhost_apikey"));
|
||||||
|
CHECK(contains_key(keys, "printer_notes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A per-variant project value maps onto its base preset's variant layout", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
|
||||||
|
DynamicPrintConfig base = defaults;
|
||||||
|
base.set_key_value("print_extruder_id", new ConfigOptionInts({1, 1}));
|
||||||
|
base.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
|
||||||
|
base.set_key_value("outer_wall_speed", new ConfigOptionFloats({200., 300.}));
|
||||||
|
base.set_key_value("inner_wall_speed", new ConfigOptionFloats({250., 350.}));
|
||||||
|
// A project saved before its printer had a High Flow variant.
|
||||||
|
DynamicPrintConfig project;
|
||||||
|
project.set_key_value("print_extruder_id", new ConfigOptionInts({1}));
|
||||||
|
project.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard"}));
|
||||||
|
project.set_key_value("outer_wall_speed", new ConfigOptionFloats({100.}));
|
||||||
|
project.set_key_value("inner_wall_speed", new ConfigOptionFloats({150.}));
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"outer_wall_speed"},
|
||||||
|
base_finder(&base, calls));
|
||||||
|
CHECK(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
|
||||||
|
std::vector<std::string>{"Direct Drive Standard", "Direct Drive High Flow"});
|
||||||
|
// The listed key keeps the project's Standard value and takes High Flow from the base.
|
||||||
|
check_double_vector(config.option<ConfigOptionFloats>("outer_wall_speed")->values, {100., 300.});
|
||||||
|
check_double_vector(config.option<ConfigOptionFloats>("inner_wall_speed")->values, {250., 350.});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A project's different settings always keep the preset bookkeeping keys", "[Preset][ExternalPreset]")
|
||||||
|
{
|
||||||
|
const std::set<std::string> keys = PresetBundle::project_different_keys(escape_strings_cstyle({"wall_loops", "top_shell_layers"}));
|
||||||
|
for (const char *key : {"wall_loops", "top_shell_layers", "inherits", "print_settings_id", "filament_settings_id", "printer_settings_id"})
|
||||||
|
CHECK(keys.count(key) == 1);
|
||||||
|
CHECK(PresetBundle::project_different_keys(std::string()).count("inherits") == 1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -152,82 +152,3 @@ TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed uncha
|
|||||||
REQUIRE(resolve_cli_input_path("").empty());
|
REQUIRE(resolve_cli_input_path("").empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("is_path_within_root accepts a root given with a trailing separator", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const std::string root = tmp.path().string();
|
|
||||||
const std::string with_separator = GENERATE_COPY(root + "/", root + std::string(1, static_cast<char>(boost::filesystem::path::preferred_separator)));
|
|
||||||
|
|
||||||
CAPTURE(with_separator);
|
|
||||||
CHECK(is_path_within_root("vendor.json", with_separator));
|
|
||||||
CHECK(is_path_within_root("vendor/machine/printer.json", with_separator));
|
|
||||||
CHECK_FALSE(is_path_within_root("../vendor.json", with_separator));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_path_within_root treats Windows-specific name forms the same on every platform", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
|
|
||||||
SECTION("names ending in dots or spaces stay inside the root") {
|
|
||||||
const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("dir./file.json"), std::string("dir /file.json"));
|
|
||||||
CAPTURE(name);
|
|
||||||
CHECK(is_path_within_root(name, tmp.path()));
|
|
||||||
}
|
|
||||||
SECTION("drive-relative names are rejected") {
|
|
||||||
const std::string name = GENERATE(std::string("C:x"), std::string("c:x/y.json"), std::string("C:"));
|
|
||||||
CAPTURE(name);
|
|
||||||
CHECK_FALSE(is_path_within_root(name, tmp.path()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_path_within_root rejects a name with an embedded NUL", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
// The filesystem calls stop at the NUL, so they would act on a different path than the one checked.
|
|
||||||
const std::string name = GENERATE(std::string("..\0", 3), std::string("..\0x/file.json", 14), std::string("sub/..\0x", 8),
|
|
||||||
std::string("file.json\0", 10), std::string("\0file.json", 10));
|
|
||||||
CAPTURE(name.size());
|
|
||||||
CHECK_FALSE(is_path_within_root(name, tmp.path()));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_symlink_target_within_root accepts relative targets that stay inside the root", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const auto [link, target] = GENERATE(std::make_pair(std::string("Versions/Current"), std::string("A")),
|
|
||||||
std::make_pair(std::string("Foo.framework/Foo"), std::string("Versions/Current/Foo")),
|
|
||||||
std::make_pair(std::string("libfoo.so"), std::string("libfoo.so.1")),
|
|
||||||
std::make_pair(std::string("a/b/link"), std::string("c/d")));
|
|
||||||
CAPTURE(link, target);
|
|
||||||
CHECK(is_symlink_target_within_root(link, target, tmp.path()));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_symlink_target_within_root rejects absolute targets and targets that climb out", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const std::string outside = (tmp.path().parent_path() / "outside").generic_string();
|
|
||||||
const auto [link, target] = GENERATE_COPY(std::make_pair(std::string("sub/link"), outside),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("/etc/passwd")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("\\outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("C:/outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("C:outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("")),
|
|
||||||
std::make_pair(std::string("link"), std::string("..")),
|
|
||||||
std::make_pair(std::string("link"), std::string("../outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("../../outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("x/../../../outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("..\\..\\outside")),
|
|
||||||
// symlink() stops at the NUL, so this target would be created as "..".
|
|
||||||
std::make_pair(std::string("link"), std::string("..\0", 3)));
|
|
||||||
CAPTURE(link, target);
|
|
||||||
CHECK_FALSE(is_symlink_target_within_root(link, target, tmp.path()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
TEST_CASE("is_symlink_target_within_root rejects a target that passes through a symlink leading out", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const boost::filesystem::path root = tmp.path() / "root";
|
|
||||||
const boost::filesystem::path outside = tmp.path() / "outside";
|
|
||||||
boost::filesystem::create_directories(root);
|
|
||||||
boost::filesystem::create_directories(outside);
|
|
||||||
boost::filesystem::create_symlink(outside, root / "out");
|
|
||||||
|
|
||||||
CHECK_FALSE(is_symlink_target_within_root("link", "out/lib.so", root));
|
|
||||||
CHECK(is_symlink_target_within_root("link", "in/lib.so", root));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_plugin_install.cpp
|
test_plugin_install.cpp
|
||||||
test_plugin_lifecycle.cpp
|
test_plugin_lifecycle.cpp
|
||||||
test_plugin_printer_agent.cpp
|
test_plugin_printer_agent.cpp
|
||||||
|
test_printhost.cpp
|
||||||
test_slicing_pipeline_bindings.cpp
|
test_slicing_pipeline_bindings.cpp
|
||||||
test_slicing_pipeline_config.cpp
|
test_slicing_pipeline_config.cpp
|
||||||
test_plugin_sort.cpp
|
test_plugin_sort.cpp
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include "slic3r/Utils/PrintHost.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TestPrintHost : public PrintHost
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using PrintHost::format_error;
|
||||||
|
|
||||||
|
const char* get_name() const override { return "Test"; }
|
||||||
|
bool test(wxString&) const override { return true; }
|
||||||
|
wxString get_test_ok_msg() const override { return {}; }
|
||||||
|
wxString get_test_failed_msg(wxString&) const override { return {}; }
|
||||||
|
bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { return true; }
|
||||||
|
bool has_auto_discovery() const override { return false; }
|
||||||
|
bool can_test() const override { return false; }
|
||||||
|
PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
|
||||||
|
std::string get_host() const override { return {}; }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string format_error(const std::string& body, const std::string& error, unsigned status)
|
||||||
|
{
|
||||||
|
return TestPrintHost().format_error(body, error, status).ToStdString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string envelope(int code, const std::string& message, const std::string& traceback)
|
||||||
|
{
|
||||||
|
return nlohmann::json{{"error", {{"code", code}, {"message", message}, {"traceback", traceback}}}}.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string moonraker_error(int code, const std::string& message, const std::string& detail = {})
|
||||||
|
{
|
||||||
|
std::string line = "tornado.web.HTTPError: HTTP " + std::to_string(code) + ": " + message;
|
||||||
|
if (!detail.empty())
|
||||||
|
line += " (" + detail + ")";
|
||||||
|
return envelope(code, message, "Traceback (most recent call last):\n ...\n" + line + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A real Moonraker body for uploading a file that is being printed.
|
||||||
|
constexpr const char* k_busy_file_403 =
|
||||||
|
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
|
||||||
|
{
|
||||||
|
const std::string msg = format_error(k_busy_file_403, "", 403);
|
||||||
|
INFO("actual: " << msg);
|
||||||
|
|
||||||
|
CHECK(msg == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
||||||
|
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("Traceback"));
|
||||||
|
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("file_manager.py"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The specific cause is recovered from a file endpoint's traceback", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("a plain detail")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(403, "Forbidden", "File is loaded, upload not permitted");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail whose own parentheses nest (a filename)")
|
||||||
|
{
|
||||||
|
const std::string detail = "Directory does not exist (/home/pi/gcodes/plate (1).gcode)";
|
||||||
|
const std::string body = moonraker_error(400, "Bad Request", detail);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that contains the reason phrase")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(403, "Forbidden", "Forbidden zone: access denied");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (Forbidden zone: access denied)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that spans lines")
|
||||||
|
{
|
||||||
|
const std::string detail = "Move out of range\nX=250.000 Y=10.000";
|
||||||
|
const std::string body = moonraker_error(400, "Bad Request", detail);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that only repeats the reason phrase is dropped")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(401, "Unauthorized", "Unauthorized");
|
||||||
|
CHECK(format_error(body, "", 401) == "HTTP 401: Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An unhandled exception shows its type and message", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string frame = "Traceback (most recent call last):\n"
|
||||||
|
" File \"/home/pi/moonraker/moonraker/components/file_manager/file_manager.py\", line 1, in write\n"
|
||||||
|
" self._write(data)\n";
|
||||||
|
|
||||||
|
SECTION("a one-line message")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", frame + "OSError: [Errno 28] No space left on device\n");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 28] No space left on device)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a message that spans lines")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", frame + "ServerError: Klippy request failed\n see klippy.log\n");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (ServerError: Klippy request failed\n see klippy.log)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("raised while handling an HTTPError with the same code")
|
||||||
|
{
|
||||||
|
const std::string traceback = frame + "tornado.web.HTTPError: HTTP 500: Internal Server Error (Database locked)\n\n"
|
||||||
|
"During handling of the above exception, another exception occurred:\n\n" +
|
||||||
|
frame + "OSError: [Errno 5] Input/output error\n";
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", traceback);
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a traceback with no header")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", "OSError: [Errno 5] Input/output error");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A reason already complete in message is shown unchanged", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("message is the whole reason, no trailing detail")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(503, "Klippy is not ready");
|
||||||
|
CHECK(format_error(body, "", 503) == "HTTP 503: Klippy is not ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a message that itself contains parentheses is not duplicated")
|
||||||
|
{
|
||||||
|
const std::string reason = "Requested blocks (0-5) are unavailable";
|
||||||
|
const std::string body = moonraker_error(400, reason);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: " + reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A Moonraker error with no usable detail shows just the reason phrase", "[PrintHost]")
|
||||||
|
{
|
||||||
|
struct Case
|
||||||
|
{
|
||||||
|
const char* name;
|
||||||
|
const char* body;
|
||||||
|
unsigned status;
|
||||||
|
const char* expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto c = GENERATE(
|
||||||
|
Case{"an empty traceback", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": ""}})JSON", 500,
|
||||||
|
"HTTP 500: Internal Server Error"},
|
||||||
|
Case{"a traceback of only whitespace", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": "\n \n"}})JSON",
|
||||||
|
500, "HTTP 500: Internal Server Error"});
|
||||||
|
|
||||||
|
DYNAMIC_SECTION(c.name) { CHECK(format_error(c.body, "", c.status) == c.expected); }
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A percent sign in the reason is not a format specifier", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(507, "Insufficient Storage", "disk 100% full");
|
||||||
|
CHECK(format_error(body, "", 507) == "HTTP 507: Insufficient Storage (disk 100% full)");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("OctoPrint's string-valued error member")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": "File not found"})JSON";
|
||||||
|
CHECK(format_error(body, "", 404) == "HTTP 404: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("PrusaLink's top-level message, not under error")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"title": "Conflict", "message": "Printer is printing"})JSON";
|
||||||
|
CHECK(format_error(body, "", 409) == "HTTP 409: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a body that is not JSON")
|
||||||
|
{
|
||||||
|
const std::string html = "<html><head><title>502 Bad Gateway</title></head></html>";
|
||||||
|
CHECK(format_error(html, "", 502) == "HTTP 502: " + html);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an error object with no traceback")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error"}})JSON";
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an error object whose traceback is null")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": null}})JSON";
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an envelope whose reason phrase is empty")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(403, "", "Traceback (most recent call last):\nOSError: denied\n");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a transport error with no HTTP status")
|
||||||
|
{
|
||||||
|
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user