Merge branch 'main' into cad-mainline

This commit is contained in:
SoftFever
2026-09-18 14:01:23 +08:00
committed by GitHub
2596 changed files with 133083 additions and 118866 deletions
+183 -53
View File
@@ -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;
+19
View File
@@ -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);