mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Merge branch 'main' into cad-mainline
This commit is contained in:
@@ -102,45 +102,6 @@ struct ZipUnicodePathExtraField
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that a relative file path does not escape the root directory via path traversal.
|
||||
static bool is_path_within_root(const std::string& file_path, const boost::filesystem::path& root)
|
||||
{
|
||||
if (file_path.empty())
|
||||
return false;
|
||||
|
||||
boost::filesystem::path p(file_path);
|
||||
if (p.is_absolute())
|
||||
return false;
|
||||
|
||||
// Reject any path component that is ".."
|
||||
for (const auto& component : p) {
|
||||
if (component == "..")
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve the full path and verify it starts with the canonical root (also catches symlink escapes)
|
||||
try {
|
||||
boost::filesystem::path full_path = root / p;
|
||||
boost::filesystem::path canonical_root = boost::filesystem::weakly_canonical(root);
|
||||
boost::filesystem::path canonical_full = boost::filesystem::weakly_canonical(full_path);
|
||||
|
||||
auto root_str = canonical_root.string();
|
||||
auto full_str = canonical_full.string();
|
||||
if (full_str.length() < root_str.length())
|
||||
return false;
|
||||
if (full_str.compare(0, root_str.length(), root_str) != 0)
|
||||
return false;
|
||||
// Ensure it's a proper prefix (not just a substring of a longer directory name)
|
||||
if (full_str.length() > root_str.length() &&
|
||||
full_str[root_str.length()] != boost::filesystem::path::preferred_separator)
|
||||
return false;
|
||||
} catch (const boost::filesystem::filesystem_error&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// VERSION NUMBERS
|
||||
// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them.
|
||||
// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files.
|
||||
@@ -685,6 +646,11 @@ bool bbs_is_valid_object_type(const std::string& type)
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool is_published_3mf_flag(const std::string &value)
|
||||
{
|
||||
return value == "1";
|
||||
}
|
||||
|
||||
void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
{
|
||||
if (!result) return;
|
||||
@@ -1221,6 +1187,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// add backup & restore logic
|
||||
bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector<Preset*>& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr,
|
||||
BBLProject* project = nullptr, int plate_id = 0);
|
||||
|
||||
// A minimal published 3MF carries no slicer tags (any tag would make old receivers show
|
||||
// a baked-in, wrong "old version" popup on their geometry-only fallback), so it
|
||||
// classifies as From_Other. It is still a fully structured OrcaSlicer file though:
|
||||
// identified by its own metadata, it keeps BBS-grade geometry handling (no instance
|
||||
// splitting, no transform baking, no renaming) in this build. Old receivers without the
|
||||
// publish feature don't know the metadata and take their third-party geometry path.
|
||||
// Reads the parse-time metadata: the model XML carries it before its resources, while
|
||||
// m_model->model_info is only filled in after the whole XML has been parsed.
|
||||
bool _is_published_3mf() const {
|
||||
const auto it = this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG);
|
||||
return it != this->model_info.metadata_items.end() && is_published_3mf_flag(it->second);
|
||||
}
|
||||
|
||||
bool _is_svg_shape_file(const std::string &filename) const;
|
||||
bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function<bool (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)>, bool restore = false);
|
||||
bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler);
|
||||
@@ -2054,7 +2034,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
lock.close();
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is more than one instance,
|
||||
// split the object in as many objects as instances
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance");
|
||||
@@ -3618,7 +3598,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_index_paths.insert({ object.first.second, object.first.first});
|
||||
}
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is only one object,
|
||||
// set the object name to match the filename
|
||||
if (m_model->objects.size() == 1)
|
||||
@@ -5341,7 +5321,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats);
|
||||
|
||||
if (!m_is_bbl_3mf) {
|
||||
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||
// if the 3mf was not produced by OrcaSlicer and there is only one instance,
|
||||
// bake the transformation into the geometry to allow the reload from disk command
|
||||
// to work properly
|
||||
@@ -5987,6 +5967,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
bool m_save_gcode { false }; // whether to save gcode for normal save
|
||||
bool m_skip_model { false }; // skip model when exporting .gcode.3mf
|
||||
bool m_skip_auxiliary { false }; // skip normal axuiliary files
|
||||
bool m_minimal_published { false }; // published 3MF: omit the project config, the embedded preset files and the slicer tags
|
||||
bool m_use_loaded_id { false }; // whether to use loaded id for identify_id
|
||||
bool m_share_mesh { false }; // whether to share mesh between objects
|
||||
std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE;
|
||||
@@ -6087,6 +6068,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_skip_auxiliary = store_params.strategy & SaveStrategy::SkipAuxiliary;
|
||||
m_share_mesh = store_params.strategy & SaveStrategy::ShareMesh;
|
||||
m_from_backup_save = store_params.strategy & SaveStrategy::Backup;
|
||||
m_minimal_published = store_params.strategy & SaveStrategy::MinimalPublished;
|
||||
|
||||
m_use_loaded_id = store_params.strategy & SaveStrategy::UseLoadedId;
|
||||
|
||||
@@ -6501,7 +6483,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
// Adds slic3r print config file ("Metadata/Slic3r_PE.config").
|
||||
// This file contains the content of FullPrintConfig / SLAFullPrintConfig.
|
||||
if (config != nullptr) {
|
||||
// Omitted for minimal published 3MF: OrcaSlicer versions without the publish feature
|
||||
// then fall back to importing the geometry only, and new versions read the published
|
||||
// payload from the model metadata instead.
|
||||
if (config != nullptr && !m_minimal_published) {
|
||||
// BBS: change to json format
|
||||
// if (!_add_print_config_file_to_archive(archive, *config)) {
|
||||
if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; }
|
||||
@@ -6514,8 +6499,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (cb_cancel) return false;
|
||||
}
|
||||
|
||||
// BBS: add project config
|
||||
if (project_presets.size() > 0) {
|
||||
// BBS: add project config (omitted for minimal published 3MF)
|
||||
if (!m_minimal_published && project_presets.size() > 0) {
|
||||
// BBS: add project embedded preset files
|
||||
_add_project_embedded_presets_to_archive(archive, model, project_presets);
|
||||
|
||||
@@ -6987,10 +6972,31 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// Orca: PRIVACY: do not store creation & modification date in 3mf
|
||||
metadata_item_map[BBL_CREATION_DATE_TAG] = "";
|
||||
metadata_item_map[BBL_MODIFICATION_TAG] = "";
|
||||
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION
|
||||
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
||||
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION.
|
||||
// A minimal published 3MF writes no slicer tags at all: any tag would route old
|
||||
// receivers onto a geometry-only fallback whose baked-in popup misreports the
|
||||
// file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify
|
||||
// as From_Other and import the geometry silently.
|
||||
if (m_minimal_published) {
|
||||
// metadata_item_map is seeded from the input file's metadata_items above, so a
|
||||
// project opened from a regular Orca/BBS 3MF still carries the slicer-identifying
|
||||
// tags it came with. Erase every one of them - not just the two most common -
|
||||
// so a published 3MF is fully tag-less: old receivers classify it as From_Other
|
||||
// and import the geometry silently instead of showing a baked-in "old version"
|
||||
// popup, and no version marker survives to seed a later re-save.
|
||||
metadata_item_map.erase(BBL_APPLICATION_TAG);
|
||||
metadata_item_map.erase(ORCASLICER_TAG);
|
||||
metadata_item_map.erase(BBS_3MF_VERSION);
|
||||
metadata_item_map.erase(BBS_3MF_VERSION1);
|
||||
} else {
|
||||
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
||||
}
|
||||
}
|
||||
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
||||
// The Bambu 3MF version marker is part of the slicer identity: omit it for a minimal
|
||||
// published file along with the tags erased above (skipping the overwrite alone would
|
||||
// leave the value the source file seeded into metadata_item_map).
|
||||
if (!m_minimal_published)
|
||||
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
||||
|
||||
if (!model.mk_name.empty()) {
|
||||
metadata_item_map[BBL_MAKERLAB_TAG] = xml_escape(model.mk_name);
|
||||
@@ -7013,7 +7019,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
BOOST_LOG_TRIVIAL(info) << "bbs_3mf: save key= " << item.first << ", value = " << item.second;
|
||||
stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">"
|
||||
<< xml_escape(item.second) << "</" << METADATA_TAG << ">\n";
|
||||
if (item.first == BBL_APPLICATION_TAG) {
|
||||
if (item.first == BBL_APPLICATION_TAG && !m_minimal_published) {
|
||||
// The OrcaSlicer tag is only written for files that carry the Application
|
||||
// tag, which a minimal published 3MF erases (see the map assignment above):
|
||||
// the explicit !m_minimal_published guard keeps the tag-less guarantee from
|
||||
// depending on that erase happening to run first.
|
||||
stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">"
|
||||
<< xml_escape(SoftFever_VERSION) << "</" << METADATA_TAG << ">\n";
|
||||
}
|
||||
@@ -8866,7 +8876,7 @@ public:
|
||||
auto model = object.get_model();
|
||||
auto o = m_temp_model.add_object(object);
|
||||
int backup_id = model->get_object_backup_id(object);
|
||||
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, 1 });
|
||||
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, { 1 } });
|
||||
}
|
||||
|
||||
void remove_object_mesh(ModelObject& object) {
|
||||
@@ -8876,7 +8886,7 @@ public:
|
||||
void backup_soon() {
|
||||
boost::lock_guard lock(m_mutex);
|
||||
m_other_changes_backup = true;
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
|
||||
m_cond.notify_all();
|
||||
}
|
||||
|
||||
@@ -8894,7 +8904,7 @@ public:
|
||||
m_ui_tasks.clear();
|
||||
m_tasks.clear();
|
||||
}
|
||||
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, removeAll });
|
||||
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, { removeAll } });
|
||||
++m_task_seq;
|
||||
if (model.is_need_backup()) {
|
||||
m_other_changes = false;
|
||||
@@ -9109,7 +9119,7 @@ public:
|
||||
else
|
||||
m_cond.wait(lock);
|
||||
if (m_interval > 0 && boost::get_system_time() > m_next_backup) {
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
|
||||
m_next_backup += boost::posix_time::seconds(m_interval);
|
||||
// Maybe wakeup from power sleep
|
||||
if (m_next_backup < boost::get_system_time())
|
||||
@@ -9205,6 +9215,126 @@ std::string bbs_3mf_get_thumbnail(const char *path)
|
||||
return data;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Parses just the model-file <metadata> elements, mirroring the importer's
|
||||
// _handle_start_metadata/_handle_end_metadata (attribute-order independent, entity-unescaped,
|
||||
// whitespace tolerant). Stops the parser as soon as the published flag node is read so the
|
||||
// geometry/resources that follow are skipped, which keeps the per-file cost small.
|
||||
struct PublishedXmlProbe
|
||||
{
|
||||
XML_Parser parser{nullptr};
|
||||
bool in_metadata{false};
|
||||
bool found{false};
|
||||
bool published{false};
|
||||
std::string curr_name;
|
||||
std::string curr_value;
|
||||
|
||||
static std::string attribute(const char** attrs, const char* key)
|
||||
{
|
||||
if (attrs == nullptr)
|
||||
return std::string();
|
||||
// expat hands the attrs as a NULL-terminated {name, value, ...} array.
|
||||
for (unsigned int a = 0; attrs[a] != nullptr; a += 2)
|
||||
if (::strcmp(attrs[a], key) == 0 && attrs[a + 1] != nullptr)
|
||||
return attrs[a + 1];
|
||||
return std::string();
|
||||
}
|
||||
|
||||
static void XMLCALL start(void* user_data, const char* name, const char** attrs)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (::strcmp(name, METADATA_TAG) == 0) {
|
||||
self->in_metadata = true;
|
||||
self->curr_name = attribute(attrs, NAME_ATTR);
|
||||
self->curr_value.clear();
|
||||
} else {
|
||||
self->in_metadata = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void XMLCALL characters(void* user_data, const XML_Char* s, int len)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (self->in_metadata)
|
||||
self->curr_value.append(s, len);
|
||||
}
|
||||
|
||||
static void XMLCALL end(void* user_data, const char* name)
|
||||
{
|
||||
auto* self = static_cast<PublishedXmlProbe*>(user_data);
|
||||
if (!self->in_metadata || ::strcmp(name, METADATA_TAG) != 0)
|
||||
return;
|
||||
self->in_metadata = false;
|
||||
if (self->curr_name == ORCA_PUBLISHED_TAG) {
|
||||
self->published = is_published_3mf_flag(xml_unescape(self->curr_value));
|
||||
self->found = true;
|
||||
if (self->parser != nullptr)
|
||||
XML_StopParser(self->parser, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bbs_3mf_is_published(const std::string &path)
|
||||
{
|
||||
mz_zip_archive archive;
|
||||
mz_zip_zero_struct(&archive);
|
||||
|
||||
struct close_lock
|
||||
{
|
||||
mz_zip_archive *archive;
|
||||
void close()
|
||||
{
|
||||
if (archive) {
|
||||
close_zip_reader(archive);
|
||||
archive = nullptr;
|
||||
}
|
||||
}
|
||||
~close_lock() { close(); }
|
||||
} lock{&archive};
|
||||
|
||||
if (!open_zip_reader(&archive, path))
|
||||
return false;
|
||||
|
||||
// Read just the model XML (the metadata node sits before the resources, so the probe below
|
||||
// stops early) rather than by a raw substring match; no geometry parsing.
|
||||
int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0);
|
||||
if (index < 0)
|
||||
return false;
|
||||
mz_zip_archive_file_stat stat;
|
||||
if (!mz_zip_reader_file_stat(&archive, index, &stat))
|
||||
return false;
|
||||
std::string xml(stat.m_uncomp_size, '\0');
|
||||
if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0))
|
||||
return false;
|
||||
|
||||
XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
if (parser == nullptr)
|
||||
return false;
|
||||
|
||||
PublishedXmlProbe probe;
|
||||
probe.parser = parser;
|
||||
XML_SetUserData(parser, &probe);
|
||||
XML_SetElementHandler(parser, PublishedXmlProbe::start, PublishedXmlProbe::end);
|
||||
XML_SetCharacterDataHandler(parser, PublishedXmlProbe::characters);
|
||||
// Never resolve external entities from a file we are only probing.
|
||||
XML_SetExternalEntityRefHandler(parser, nullptr);
|
||||
XML_SetEntityDeclHandler(parser, nullptr);
|
||||
|
||||
const XML_Status status = XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), 1);
|
||||
// XML_StopParser(parser, false) from the end handler makes XML_Parse return
|
||||
// XML_STATUS_ERROR with XML_ERROR_ABORTED - treat that as success (we stopped on the flag).
|
||||
const bool parse_ok = (status == XML_STATUS_OK) ||
|
||||
(XML_GetErrorCode(parser) == XML_ERROR_ABORTED && probe.found);
|
||||
XML_ParserFree(parser);
|
||||
|
||||
if (!parse_ok)
|
||||
return false;
|
||||
return probe.published;
|
||||
}
|
||||
|
||||
bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version)
|
||||
{
|
||||
CNumericLocalesSetter locales_setter;
|
||||
|
||||
@@ -159,12 +159,28 @@ enum class SaveStrategy
|
||||
SkipAuxiliary = 1 << 9,
|
||||
UseLoadedId = 1 << 10,
|
||||
ShareMesh = 1 << 11,
|
||||
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
|
||||
// production-extension value.
|
||||
MinimalPublished = 1 << 13,
|
||||
|
||||
SplitModel = 0x1000 | ProductionExt,
|
||||
Encrypted = SecureContentExt | SplitModel,
|
||||
Backup = 0x10000 | WithGcode | Silence | SkipStatic | SplitModel,
|
||||
};
|
||||
|
||||
// Model metadata keys of a "published" 3MF (see MinimalPublished): the flag marks a minimal,
|
||||
// tag-less publish export, the others carry the author-selected settings payload. Namespaced
|
||||
// with the "orca_published" prefix because metadata_items round-trips verbatim through other
|
||||
// slicers, where a bare "published" key could collide.
|
||||
inline constexpr const char *ORCA_PUBLISHED_TAG = "orca_published";
|
||||
inline constexpr const char *ORCA_PUBLISHED_KEYS_TAG = "orca_published_keys";
|
||||
inline constexpr const char *ORCA_PUBLISHED_MATERIAL_TAG = "orca_published_material_keys";
|
||||
inline constexpr const char *ORCA_PUBLISHED_CONFIG_TAG = "orca_published_config";
|
||||
|
||||
// Published files are produced with "1". The importer and the GUI loader both gate on this
|
||||
// exact value, so a "0"/"false"/unknown value is rejected consistently.
|
||||
bool is_published_3mf_flag(const std::string &value);
|
||||
|
||||
inline SaveStrategy operator | (SaveStrategy lhs, SaveStrategy rhs)
|
||||
{
|
||||
using T = std::underlying_type_t <SaveStrategy>;
|
||||
@@ -277,6 +293,9 @@ extern bool load_bbs_3mf(const char* path, DynamicPrintConfig* config, ConfigSub
|
||||
|
||||
extern std::string bbs_3mf_get_thumbnail(const char * path);
|
||||
|
||||
// Lightweight check: does this 3mf carry the "published" (orca_published == "1") marker? Only reads the 3D/3dmodel.model metadata node
|
||||
extern bool bbs_3mf_is_published(const std::string &path);
|
||||
|
||||
extern bool load_gcode_3mf_from_stream(std::istream & data, DynamicPrintConfig* config, Model* model, PlateDataPtrs* plate_data_list,
|
||||
Semver* file_version);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user